/******************************************************************************* * Copyright (c) 2016 itemis AG and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (itemis AG) - initial API & implementation * *******************************************************************************/ package org.eclipse.gef.common.tests; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.LinkedList; import java.util.List; import org.eclipse.gef.common.beans.property.ReadOnlyListWrapperEx; import org.eclipse.gef.common.beans.property.SimpleListPropertyEx; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.common.collections.ListListenerHelperEx.AtomicChange; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import com.google.inject.Provider; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.SimpleListProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; /** * Tests for correct behavior of {@link ObservableList} implementations, * including respective {@link ObservableValue observable values}and * {@link ListChangeListenerEx} helper classes. Concrete implementations are * tested by parameterizing the test with a respective Provider, which is done * for {@link ObservableListWrapperEx} as well as {@link SimpleListPropertyEx} * and {@link ReadOnlyListWrapperEx}. * <p> * Ensures that correct behavior of the underlying {@link List} is preserved and * that {@link InvalidationListener} and {@link ListChangeListener}, as well as * {@link ChangeListener} (in case of observable values) are notified properly. * <p> * Test strategy is to use a backup {@link List} on which to apply the same * operations as on the two be tested {@link ObservableList}, so that same * behavior is ensured. * * @author anyssen * */ @RunWith(Parameterized.class) public class ObservableListTests { protected static class InvalidationExpector implements InvalidationListener { int expect = 0; public void check() { if (expect > 0) { fail("Did not receive " + expect + " expected invalidation event."); } } public void expect(int expext) { this.expect = expext; } @Override public void invalidated(Observable observable) { if (expect-- <= 0) { fail("Did not expect an invalidation event."); } } } protected static class ListChangeExpector<E> implements ListChangeListener<E> { private ObservableList<E> source; private LinkedList<LinkedList<List<E>>> addedElementsQueue = new LinkedList<>(); private LinkedList<LinkedList<List<E>>> removedElementsQueue = new LinkedList<>(); private LinkedList<LinkedList<int[]>> permutationsQueue = new LinkedList<>(); private LinkedList<LinkedList<Integer>> fromQueue = new LinkedList<>(); private LinkedList<LinkedList<Integer>> toQueue = new LinkedList<>(); private ArrayList<E> previousValue; public ListChangeExpector(ObservableList<E> source) { this.source = source; } public void addAtomicExpectation() { addedElementsQueue.addFirst(new LinkedList<List<E>>()); removedElementsQueue.addFirst(new LinkedList<List<E>>()); permutationsQueue.addFirst(new LinkedList<int[]>()); fromQueue.addFirst(new LinkedList<Integer>()); toQueue.addFirst(new LinkedList<Integer>()); // capture previous value this.previousValue = new ArrayList<>(source); } public void addElementaryExpectation(List<E> removedElements, List<E> addedElements, int[] permutations, int from, int to) { if (addedElementsQueue.size() <= 0) { throw new IllegalArgumentException( "Add atomic expectation first."); } removedElementsQueue.getFirst().addFirst(removedElements); addedElementsQueue.getFirst().addFirst(addedElements); permutationsQueue.getFirst().addFirst(permutations); fromQueue.getFirst().addFirst(from); toQueue.getFirst().addFirst(to); } public void check() { if (addedElementsQueue.size() > 0) { fail("Did not receive " + addedElementsQueue.size() + " expected changes."); } } @Override public void onChanged(ListChangeListener.Change<? extends E> change) { if (addedElementsQueue.size() <= 0) { fail("Received unexpected atomic change " + change); } LinkedList<List<E>> elementaryRemovedElementsQueue = removedElementsQueue .pollLast(); LinkedList<List<E>> elementaryAddedElementsQueue = addedElementsQueue .pollLast(); LinkedList<int[]> elementaryPermutationsQueue = permutationsQueue .pollLast(); LinkedList<Integer> elementaryFrom = fromQueue.pollLast(); LinkedList<Integer> elementaryTo = toQueue.pollLast(); assertEquals(source, change.getList()); StringBuffer expectedString = new StringBuffer(); while (change.next()) { if (elementaryAddedElementsQueue.size() <= 0) { fail("Did not expect another elementary change, but received " + change); } // check removed List<E> expectedRemovedElements = elementaryRemovedElementsQueue .pollLast(); if (expectedRemovedElements != null) { assertTrue(change.wasRemoved()); assertEquals(expectedRemovedElements, change.getRemoved()); assertEquals(expectedRemovedElements.size(), change.getRemovedSize()); } else { assertFalse(change.wasRemoved()); } // check added List<E> expectedAddedElements = elementaryAddedElementsQueue .pollLast(); if (expectedAddedElements != null) { assertTrue(change.wasAdded()); if (expectedRemovedElements != null) { assertTrue(change.wasReplaced()); } assertEquals(expectedAddedElements, change.getAddedSubList()); assertEquals(expectedAddedElements.size(), change.getAddedSize()); } else { assertFalse(change.wasAdded()); } // check permutations int[] expectedPermutations = elementaryPermutationsQueue .pollLast(); if (expectedPermutations != null) { assertTrue(change.wasPermutated()); assertArrayEquals(expectedPermutations, CollectionUtils.getPermutation(change)); for (int i = 0; i < expectedPermutations.length; i++) { assertEquals(expectedPermutations[i], change.getPermutation(i)); } } else { assertFalse(change.wasPermutated()); } // check from int from = elementaryFrom.pollLast(); assertEquals(from, change.getFrom()); // check to int to = elementaryTo.pollLast(); assertEquals(to, change.getTo()); // check string representation if (!expectedString.toString().isEmpty()) { expectedString.append(" "); } if (expectedAddedElements != null && expectedRemovedElements != null) { expectedString.append("Replaced" + expectedRemovedElements + " by " + expectedAddedElements + " at " + change.getFrom() + "."); } else if (expectedAddedElements != null) { expectedString.append("Added" + expectedAddedElements + " at " + change.getFrom() + "."); } else if (expectedRemovedElements != null) { expectedString.append("Removed" + expectedRemovedElements + " at " + change.getFrom() + "."); } else { expectedString.append("Permutated by " + Arrays.toString( CollectionUtils.getPermutation(change)) + "."); } } if (elementaryAddedElementsQueue.size() > 0) { fail("Did not receive " + elementaryAddedElementsQueue.size() + " expected elementary changes."); } // check string representation of change (only in case of atomic // change) if (change instanceof AtomicChange) { assertEquals(expectedString.toString(), change.toString()); } // check previous value of change (only in case of JavaFX change) if (!(change instanceof AtomicChange)) { assertEquals(previousValue, CollectionUtils.getPreviousContents(change)); } } } @Parameters public static Collection<Object[]> data() { return Arrays.asList( new Object[][] { { new Provider<ObservableList<Integer>>() { @Override public ObservableList<Integer> get() { return CollectionUtils .observableList(new ArrayList<Integer>()); } } }, { new Provider<ObservableList<Integer>>() { @Override public ObservableList<Integer> get() { // test JavaFX behavior return FXCollections.observableArrayList(); } } }, { new Provider<ObservableList<Integer>>() { @Override public ObservableList<Integer> get() { // test SimpleListPropertyEx, which is the // 'default' implementation of the related // ObservableValue. return new SimpleListPropertyEx<>(CollectionUtils .observableList(new ArrayList<Integer>())); } } }, { new Provider<ObservableList<Integer>>() { @Override public ObservableList<Integer> get() { return new SimpleListProperty<>(FXCollections .<Integer> observableArrayList()); } } }, { new Provider<ObservableList<Integer>>() { @Override public ObservableList<Integer> get() { // test ReadOnlyListWrapperEx, which is the // 'default' implementation of the related // read-only support. return new ReadOnlyListWrapperEx<>( CollectionUtils.observableList( new ArrayList<Integer>())); } } }/* * , { new Provider<ObservableList<Integer>>() { * * @Override public ObservableList<Integer> get() { * return new ReadOnlyListWrapper<>( * FXCollections.<Integer> observableArrayList()); } * } } */ }); } private ObservableList<Integer> observable; private Provider<ObservableList<Integer>> observableProvider; private InvalidationExpector invalidationListener; private ListChangeExpector<Integer> listChangeListener; public ObservableListTests( Provider<ObservableList<Integer>> sourceProvider) { this.observableProvider = sourceProvider; } @Test public void add() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); registerListeners(); // add a single value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.<Integer> singletonList(1), null, 0, 1); assertEquals(backupList.add(1), observable.add(1)); check(observable, backupList); checkListeners(); // add a different value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.<Integer> singletonList(2), null, 1, 2); assertEquals(backupList.add(2), observable.add(2)); check(observable, backupList); checkListeners(); } @Test public void add_with_index() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); observable.addAll(Arrays.asList(1, 2, 3)); backupList.addAll(Arrays.asList(1, 2, 3)); // register listeners registerListeners(); // add a single value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.<Integer> singletonList(4), null, 2, 3); backupList.add(2, 4); observable.add(2, 4); check(observable, backupList); checkListeners(); } @Test public void addAll() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); // register listeners registerListeners(); // add a single value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Arrays.asList(1, 2, 3, 4, 5), null, 0, 5); backupList.addAll(Arrays.asList(1, 2, 3, 4, 5)); observable.addAll(Arrays.asList(1, 2, 3, 4, 5)); check(observable, backupList); checkListeners(); } @Test public void addAll_varargs() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); // register listeners registerListeners(); // add a single value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Arrays.asList(1, 2, 3, 4, 5), null, 0, 5); backupList.addAll(Arrays.asList(1, 2, 3, 4, 5)); observable.addAll(1, 2, 3, 4, 5); check(observable, backupList); checkListeners(); } @Test public void addAll_with_index() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); observable.addAll(Arrays.asList(1, 2, 3)); backupList.addAll(Arrays.asList(1, 2, 3)); // register listeners registerListeners(); // add a single value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Arrays.asList(1, 2, 3, 4, 5), null, 3, 8); backupList.addAll(3, Arrays.asList(1, 2, 3, 4, 5)); observable.addAll(3, Arrays.asList(1, 2, 3, 4, 5)); check(observable, backupList); checkListeners(); } @Before public void before() { observable = observableProvider.get(); } protected void check(ObservableList<Integer> observable, List<Integer> backupList) { assertEquals(backupList, observable); if (observable instanceof ReadOnlyListWrapperEx) { assertEquals(backupList, ((ReadOnlyListWrapperEx<Integer>) observable) .getReadOnlyProperty().get()); } } protected void checkListeners() { invalidationListener.check(); listChangeListener.check(); } @Test public void clear() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); check(observable, backupList); // register listeners registerListeners(); // clear invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(1, 2, 3), null, null, 0, 0); observable.clear(); backupList.clear(); check(observable, backupList); checkListeners(); // clear again (while already empty) observable.clear(); check(observable, backupList); checkListeners(); } /** * Tests that exceptions, which are thrown in the context of listener * notification are captured and forwarded to the UncaughtExceptionHandler * registered for the Thread. */ @Test public void exceptionHandling() throws InterruptedException { // invalidation listeners final boolean[] caughtException = new boolean[] { false }; final boolean[] uncaughtException = new boolean[] { false }; // XXX: Run test in own thread as otherwise junit will already catch the // uncaught exception before the registered handler. Thread testThread = new Thread() { @Override public void run() { InvalidationListener listener = new InvalidationListener() { @Override public void invalidated(Observable arg0) { throw new IllegalArgumentException( "expected invalidation"); } }; observable.addListener(listener); Thread.currentThread().setUncaughtExceptionHandler( new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { if (e.getMessage() .equals("expected invalidation")) { caughtException[0] = true; } } }); try { observable.add(1); } catch (IllegalArgumentException e) { if (e.getMessage().equals("exptected invalidation")) { uncaughtException[0] = true; } } observable.removeListener(listener); } }; testThread.start(); testThread.join(); assertTrue(caughtException[0]); assertFalse(uncaughtException[0]); // list change listeners caughtException[0] = false; uncaughtException[0] = false; testThread = new Thread() { @Override public void run() { ListChangeListener<Integer> listener = new ListChangeListener<Integer>() { @Override public void onChanged( ListChangeListener.Change<? extends Integer> c) { throw new IllegalArgumentException( "expected list change"); } }; observable.addListener(listener); Thread.currentThread().setUncaughtExceptionHandler( new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { if (e.getMessage() .equals("expected list change")) { caughtException[0] = true; } } }); try { observable.add(1); } catch (IllegalArgumentException e) { if (e.getMessage().equals("exptected list change")) { uncaughtException[0] = true; } } observable.removeListener(listener); } }; testThread.start(); testThread.join(); assertTrue(caughtException[0]); assertFalse(uncaughtException[0]); // change listeners // TODO: change listener notifications are not "guarded" by try/catch. // if (observable instanceof ObservableValue) { // caughtException[0] = false; // uncaughtException[0] = false; // testThread = new Thread() { // @SuppressWarnings("unchecked") // @Override // public void run() { // ObservableValue<ObservableList<Integer>> observableValue = // (ObservableValue<ObservableList<Integer>>) observable; // ChangeListener<ObservableList<Integer>> listener = new // ChangeListener<ObservableList<Integer>>() { // // @Override // public void changed( // ObservableValue<? extends ObservableList<Integer>> observable, // ObservableList<Integer> oldValue, // ObservableList<Integer> newValue) { // throw new IllegalArgumentException( // "expected change"); // } // }; // observableValue.addListener(listener); // Thread.currentThread().setUncaughtExceptionHandler( // new UncaughtExceptionHandler() { // @Override // public void uncaughtException(Thread t, Throwable e) { // if (e.getMessage().equals("expected change")) { // caughtException[0] = true; // } // } // }); // try { // ((ListPropertyBase<Integer>) observableValue) // .set(CollectionUtils // .observableList(Arrays.asList(3))); // } catch (IllegalArgumentException e) { // if (e.getMessage().equals("expected change")) { // uncaughtException[0] = true; // } // } // observableValue.removeListener(listener); // } // }; // testThread.start(); // testThread.join(); // assertTrue(caughtException[0]); // assertFalse(uncaughtException[0]); // } } @Test public void listenersNotProperlyIterating() { // ensure assumption exceptions can be properly handled by JUnit Thread.currentThread() .setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } } }); ListChangeListener<Integer> listChangeListener = new ListChangeListener<Integer>() { @Override public void onChanged( ListChangeListener.Change<? extends Integer> change) { assumeTrue("Skip for all except ObservableListWrapperEx", observable.getClass().getSimpleName() .equals("ObservableListWrapperEx")); // initially cursor is left of first change try { // call wasReplaced() without next change.wasReplaced(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before wasReplaced() can be called.", e.getMessage()); } try { // call wasAdded() without next change.wasAdded(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before wasAdded() can be called.", e.getMessage()); } try { // call wasRemoved() without next change.wasRemoved(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before wasRemoved() can be called.", e.getMessage()); } try { // call wasPermutated() without next change.wasPermutated(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before wasPermutated() can be called.", e.getMessage()); } try { // call wasUpdated() without next change.wasUpdated(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before wasUpdated() can be called.", e.getMessage()); } try { // call getAddedSubList() without next change.getAddedSubList(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getAddedSubList() can be called.", e.getMessage()); } try { // call getAddedSize() without next change.getAddedSize(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getAddedSize() can be called.", e.getMessage()); } try { // call getRemoved() without next change.getRemoved(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getRemoved() can be called.", e.getMessage()); } try { // call getRemovedSize() without next change.getRemovedSize(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getRemovedSize() can be called.", e.getMessage()); } try { // call getPermutation(int) without next change.getPermutation(0); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getPermutation(int) can be called.", e.getMessage()); } try { // call getFrom() without next change.getFrom(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getFrom() can be called.", e.getMessage()); } try { // call getTo() without next change.getTo(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getTo() can be called.", e.getMessage()); } // put cursor right of last change while (change.next()) { } change.next(); try { // call wasReplaced() without next change.wasReplaced(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call wasReplaced() if next() returned true.", e.getMessage()); } try { // call wasAdded() without next change.wasAdded(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call wasAdded() if next() returned true.", e.getMessage()); } try { // call wasRemoved() without next change.wasRemoved(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call wasRemoved() if next() returned true.", e.getMessage()); } try { // call wasPermutated() without next change.wasPermutated(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call wasPermutated() if next() returned true.", e.getMessage()); } try { // call wasUpdated() without next change.wasUpdated(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call wasUpdated() if next() returned true.", e.getMessage()); } try { // call getAddedSubList() without next change.getAddedSubList(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getAddedSubList() if next() returned true.", e.getMessage()); } try { // call getAddedSize() without next change.getAddedSize(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getAddedSize() if next() returned true.", e.getMessage()); } try { // call getRemoved() without next change.getRemoved(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getRemoved() if next() returned true.", e.getMessage()); } try { // call getRemovedSize() without next change.getRemovedSize(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getRemovedSize() if next() returned true.", e.getMessage()); } try { // call getPermutation(int) without next change.getPermutation(0); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getPermutation(int) if next() returned true.", e.getMessage()); } try { // call getFrom() without next change.getFrom(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getFrom() if next() returned true.", e.getMessage()); } try { // call getTo() without next change.getTo(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getTo() if next() returned true.", e.getMessage()); } } }; observable.addListener(listChangeListener); // ensure no concurrent modification exceptions result observable.add(1); } /** * Checks that its safe (and does not lead to a * {@link ConcurrentModificationException} if a listener registers or * unregisters itself as the result of a notification. */ @Test public void listenersProvokingConcurrentModifications() { // add listeners InvalidationListener invalidationListener = new InvalidationListener() { @Override public void invalidated(Observable observable) { // unregister ourselves observable.removeListener(this); // register ourselves (again) observable.addListener(this); } }; observable.addListener(invalidationListener); if (observable instanceof ObservableValue) { // register change listener as well @SuppressWarnings("unchecked") ObservableValue<ObservableList<Integer>> observableValue = (ObservableValue<ObservableList<Integer>>) observable; ChangeListener<ObservableList<Integer>> changeListener = new ChangeListener<ObservableList<Integer>>() { @Override public void changed( ObservableValue<? extends ObservableList<Integer>> observable, ObservableList<Integer> oldValue, ObservableList<Integer> newValue) { // unregister ourselves observable.removeListener(this); // register ourselves (again) observable.addListener(this); } }; observableValue.addListener(changeListener); } ListChangeListener<Integer> listChangeListener = new ListChangeListener<Integer>() { @Override public void onChanged( ListChangeListener.Change<? extends Integer> change) { // unregister ourselves change.getList().removeListener(this); // register ourselves (again) change.getList().addListener(this); } }; observable.addListener(listChangeListener); // ensure no concurrent modification exceptions result observable.add(1); } @Test public void listenersRegisteredMoreThanOnce() { // register listeners (twice) InvalidationExpector invalidationListener = new InvalidationExpector(); ListChangeExpector<Integer> listChangeListener = new ListChangeExpector<>( observable); observable.addListener(invalidationListener); observable.addListener(invalidationListener); // add and remove should have no effect InvalidationListener invalidationListener2 = new InvalidationListener() { @Override public void invalidated(Observable observable) { // ignore } }; observable.addListener(invalidationListener2); observable.removeListener(invalidationListener2); observable.addListener(listChangeListener); observable.addListener(listChangeListener); // add and remove should have no effect // add and remove should have no effect ListChangeListener<Integer> listChangeListener2 = new ListChangeListener<Integer>() { @Override public void onChanged( ListChangeListener.Change<? extends Integer> change) { // ignore } }; observable.addListener(listChangeListener2); observable.removeListener(listChangeListener2); // perform add invalidationListener.expect(2); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.singletonList(1), null, 0, 1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.singletonList(1), null, 0, 1); assertTrue(observable.add(1)); invalidationListener.check(); listChangeListener.check(); // remove single listener occurrence observable.removeListener(invalidationListener); observable.removeListener(listChangeListener); // perform another add invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, Collections.singletonList(1), null, 1, 2); assertTrue(observable.add(1)); invalidationListener.check(); listChangeListener.check(); // remove listeners and ensure no notifications are received observable.removeListener(invalidationListener); observable.removeListener(listChangeListener); invalidationListener.check(); listChangeListener.check(); } protected void registerListeners() { invalidationListener = new InvalidationExpector(); listChangeListener = new ListChangeExpector<>(observable); observable.addListener(invalidationListener); observable.addListener(listChangeListener); } @Test public void remove() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); check(observable, backupList); // register listeners registerListeners(); // clear invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2), null, null, 1, 1); observable.remove(1); backupList.remove(1); check(observable, backupList); checkListeners(); } @Test public void remove_from_to() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); observable.add(4); observable.add(5); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); backupList.add(4); backupList.add(5); check(observable, backupList); // register listeners registerListeners(); // clear invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2, 3), null, null, 1, 1); observable.remove(1, 3); backupList.remove(2); backupList.remove(1); check(observable, backupList); checkListeners(); } @Test public void remove_object() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); check(observable, backupList); // register listeners registerListeners(); // clear invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(1), null, null, 0, 0); observable.remove((Object) 1); backupList.remove((Object) 1); check(observable, backupList); checkListeners(); // remove not contained element invalidationListener.expect(0); observable.remove((Object) 5); check(observable, backupList); checkListeners(); } @Test public void removeAll() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); observable.add(4); observable.add(5); observable.add(6); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); backupList.add(4); backupList.add(5); backupList.add(6); check(observable, backupList); // register listeners registerListeners(); // remove all (elements are not continuous) invalidationListener.expect(1); // we expect two changes, as the deleted elements are not 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2), null, null, 1, 1); // after the initial delete, 4 is now located at index 2 listChangeListener.addElementaryExpectation(Arrays.asList(4), null, null, 2, 2); observable.removeAll(Arrays.asList(4, 2)); backupList.removeAll(Arrays.asList(4, 2)); check(observable, backupList); checkListeners(); // remove all (elements are continuous) invalidationListener.expect(1); // we expect a single change, as the deleted elements are 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(3, 5), null, null, 1, 1); observable.removeAll(Arrays.asList(5, 3)); backupList.removeAll(Arrays.asList(5, 3)); check(observable, backupList); checkListeners(); // remove all (no effect, elements were already removed) observable.removeAll(Arrays.asList(5, 3)); backupList.removeAll(Arrays.asList(5, 3)); check(observable, backupList); checkListeners(); } @Test public void removeAll_varargs() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); observable.add(4); observable.add(5); observable.add(6); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); backupList.add(4); backupList.add(5); backupList.add(6); check(observable, backupList); // register listeners registerListeners(); // remove all (elements are not continuous) invalidationListener.expect(1); // we expect two changes, as the deleted elements are not 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2), null, null, 1, 1); // after the initial delete, 4 is now located at index 2 listChangeListener.addElementaryExpectation(Arrays.asList(4), null, null, 2, 2); observable.removeAll(4, 2); backupList.removeAll(Arrays.asList(4, 2)); check(observable, backupList); checkListeners(); // remove all (elements are continuous) invalidationListener.expect(1); // we expect a single change, as the deleted elements are 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(3, 5), null, null, 1, 1); observable.removeAll(5, 3); backupList.removeAll(Arrays.asList(5, 3)); check(observable, backupList); checkListeners(); } @Test public void retainAll() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); observable.add(4); observable.add(5); observable.add(6); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); backupList.add(4); backupList.add(5); backupList.add(6); check(observable, backupList); // register listeners registerListeners(); // remove all (elements are not continuous) invalidationListener.expect(1); // we expect two changes, as the deleted elements are not 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2), null, null, 1, 1); // after the initial delete, 4 is now located at index 2 listChangeListener.addElementaryExpectation(Arrays.asList(4), null, null, 2, 2); observable.retainAll(Arrays.asList(1, 3, 5, 6)); backupList.retainAll(Arrays.asList(1, 3, 5, 6)); check(observable, backupList); checkListeners(); // remove all (elements are continuous) invalidationListener.expect(1); // we expect a single change, as the deleted elements are 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(3, 5), null, null, 1, 1); observable.retainAll(Arrays.asList(1, 6)); backupList.retainAll(Arrays.asList(1, 6)); check(observable, backupList); checkListeners(); // no change expected as no further elements are removed observable.retainAll(Arrays.asList(1, 6)); backupList.retainAll(Arrays.asList(1, 6)); check(observable, backupList); checkListeners(); } @Test public void retainAll_varargs() { // initialize list with some values observable.add(1); observable.add(2); observable.add(3); observable.add(4); observable.add(5); observable.add(6); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(1); backupList.add(2); backupList.add(3); backupList.add(4); backupList.add(5); backupList.add(6); check(observable, backupList); // register listeners registerListeners(); // remove all (elements are not continuous) invalidationListener.expect(1); // we expect two changes, as the deleted elements are not 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(2), null, null, 1, 1); // after the initial delete, 4 is now located at index 2 listChangeListener.addElementaryExpectation(Arrays.asList(4), null, null, 2, 2); observable.retainAll(1, 3, 5, 6); backupList.retainAll(Arrays.asList(1, 3, 5, 6)); check(observable, backupList); checkListeners(); // remove all (elements are continuous) invalidationListener.expect(1); // we expect a single change, as the deleted elements are 'continuous' listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(3, 5), null, null, 1, 1); observable.retainAll(1, 6); backupList.retainAll(Arrays.asList(1, 6)); check(observable, backupList); checkListeners(); } @Test public void set_with_index() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); observable.addAll(Arrays.asList(1, 2, 3)); backupList.addAll(Arrays.asList(1, 2, 3)); // register listeners registerListeners(); // set different value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation( Collections.<Integer> singletonList(2), Collections.<Integer> singletonList(4), null, 1, 2); backupList.set(1, 4); observable.set(1, 4); check(observable, backupList); checkListeners(); assumeTrue( "Skip for all except ObservableListWrapperEx, SimpleListPropertyEx, or ReadOnlyListWrapperEx", observable.getClass().getSimpleName() .equals("ObservableListWrapperEx") || observable instanceof SimpleListPropertyEx || observable instanceof ReadOnlyListWrapperEx); // set same value (no notifications expected) backupList.set(1, 4); observable.set(1, 4); check(observable, backupList); checkListeners(); } @Test public void setAll() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); observable.addAll(Arrays.asList(1, 2, 3)); backupList.addAll(Arrays.asList(1, 2, 3)); // register listeners registerListeners(); // set different value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(1, 2, 3), Arrays.asList(3, 4, 5), null, 0, 3); backupList.clear(); backupList.addAll(Arrays.asList(3, 4, 5)); observable.setAll(Arrays.asList(3, 4, 5)); check(observable, backupList); checkListeners(); assumeTrue( "Skip for all except ObservableListWrapperEx, SimpleListPropertyEx, or ReadOnlyListWrapperEx", observable.getClass().getSimpleName() .equals("ObservableListWrapperEx") || observable instanceof SimpleListPropertyEx || observable instanceof ReadOnlyListWrapperEx); // set same value (no notifications expected) observable.setAll(Arrays.asList(3, 4, 5)); check(observable, backupList); checkListeners(); } @Test public void setAll_varargs() { // prepare backup list List<Integer> backupList = new ArrayList<>(); check(observable, backupList); observable.addAll(Arrays.asList(1, 2, 3)); backupList.addAll(Arrays.asList(1, 2, 3)); // register listeners registerListeners(); // set different value invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(Arrays.asList(1, 2, 3), Arrays.asList(3, 4, 5), null, 0, 3); backupList.clear(); backupList.addAll(Arrays.asList(3, 4, 5)); observable.setAll(3, 4, 5); check(observable, backupList); checkListeners(); assumeTrue( "Skip for all except ObservableListWrapperEx, SimpleListPropertyEx, or ReadOnlyListWrapperEx", observable.getClass().getSimpleName() .equals("ObservableListWrapperEx") || observable instanceof SimpleListPropertyEx || observable instanceof ReadOnlyListWrapperEx); // set same value (no notifications expected) observable.setAll(3, 4, 5); check(observable, backupList); checkListeners(); } @Test public void sort() { assumeTrue("Skip for all except ObservableListWrapperEx", observable .getClass().getSimpleName().equals("ObservableListWrapperEx")); // initialize list with some values observable.add(3); observable.add(1); observable.add(2); // prepare backup list List<Integer> backupList = new ArrayList<>(); backupList.add(3); backupList.add(1); backupList.add(2); check(observable, backupList); // register listeners registerListeners(); // sort invalidationListener.expect(1); listChangeListener.addAtomicExpectation(); listChangeListener.addElementaryExpectation(null, null, new int[] { 2, 0, 1 }, 0, 3); Collections.sort(observable); Collections.sort(backupList); check(observable, backupList); checkListeners(); // sort again (while already empty) invalidationListener.expect(0); Collections.sort(observable); check(observable, backupList); checkListeners(); } /** * Confirm {@link ObservableList} works as expected even if no listeners are * registered. */ @Test public void withoutListeners() { // add assertTrue(observable.add(1)); assertTrue(observable.add(1)); assertTrue(observable.add(1)); assertTrue(observable.add(2)); assertTrue(observable.add(2)); assertEquals(5, observable.size()); // clear observable.clear(); assertTrue(observable.isEmpty()); } }