/******************************************************************************* * 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.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.LinkedList; import org.eclipse.gef.common.beans.binding.MultisetExpressionHelper; import org.eclipse.gef.common.beans.property.ReadOnlyMultisetWrapper; import org.eclipse.gef.common.beans.property.SimpleMultisetProperty; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.common.collections.MultisetChangeListener; import org.eclipse.gef.common.collections.ObservableMultiset; 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.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.google.inject.Provider; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; /** * Tests for correct behavior of {@link ObservableMultiset} implementations, * including respective {@link ObservableValue observable values}, as well as * related {@link MultisetChangeListener} and {@link MultisetExpressionHelper} * helper classes. Concrete implementations are tested by parameterizing the * test with a respective Provider, which is done for * {@link ObservableMultisetWrapper} as well as {@link SimpleMultisetProperty} * and {@link ReadOnlyMultisetWrapper}. * <p> * Ensures that correct behavior of the underlying {@link Multiset} is preserved * and that {@link InvalidationListener} and {@link MultisetChangeListener}, as * well as {@link ChangeListener} (in case of observable values) are notified * properly. * <p> * Test strategy is to use a backup {@link Multiset} on which to apply the same * operations as on the two be tested {@link ObservableMultiset}, so that same * behavior is ensured. * * @author anyssen * */ @RunWith(Parameterized.class) public class ObservableMultisetTests { 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 MultisetChangeExpector<E> implements MultisetChangeListener<E> { private ObservableMultiset<E> source; private LinkedList<LinkedList<E>> elementQueue = new LinkedList<>(); private LinkedList<LinkedList<Integer>> addedCountQueue = new LinkedList<>(); private LinkedList<LinkedList<Integer>> removedCountQueue = new LinkedList<>(); public MultisetChangeExpector(ObservableMultiset<E> source) { this.source = source; } public void addAtomicExpectation() { elementQueue.addFirst(new LinkedList<E>()); addedCountQueue.addFirst(new LinkedList<Integer>()); removedCountQueue.addFirst(new LinkedList<Integer>()); } public void addElementaryExpection(E element, int removedCount, int addedCount) { if (elementQueue.size() <= 0) { throw new IllegalArgumentException( "Add atomic expectation first."); } elementQueue.getFirst().addFirst(element); removedCountQueue.getFirst().addFirst(removedCount); addedCountQueue.getFirst().addFirst(addedCount); } public void check() { if (elementQueue.size() > 0) { fail("Did not receive " + elementQueue.size() + " expected changes."); } } @Override public void onChanged( org.eclipse.gef.common.collections.MultisetChangeListener.Change<? extends E> change) { if (elementQueue.size() <= 0) { fail("Received unexpected atomic change " + change); } LinkedList<E> elementaryElementsQueue = elementQueue.pollLast(); LinkedList<Integer> elementaryAddedCountQueue = addedCountQueue .pollLast(); LinkedList<Integer> elementaryRemovedCountQueue = removedCountQueue .pollLast(); assertEquals(source, change.getMultiset()); StringBuffer expectedString = new StringBuffer(); while (change.next()) { if (elementaryElementsQueue.size() <= 0) { fail("Did not expect another elementary change"); } // check element E expectedElement = elementaryElementsQueue.pollLast(); assertEquals(expectedElement, change.getElement()); // check added values int expectedAddCount = elementaryAddedCountQueue.pollLast(); assertEquals(expectedAddCount, change.getAddCount()); // check removed values int expectedRemoveCount = elementaryRemovedCountQueue .pollLast(); assertEquals(expectedRemoveCount, change.getRemoveCount()); // check string representation if (!expectedString.toString().isEmpty()) { expectedString.append(" "); } if (expectedAddCount > 0) { expectedString.append("Added " + expectedAddCount + " occurrences of " + expectedElement + "."); } else { expectedString.append("Removed " + expectedRemoveCount + " occurrences of " + expectedElement + "."); } } if (elementaryElementsQueue.size() > 0) { fail("Did not receive " + elementaryElementsQueue.size() + " expected elementary changes."); } assertEquals(expectedString.toString(), change.toString()); } } @Parameters public static Collection<Object[]> data() { return Arrays.asList( new Object[][] { { new Provider<ObservableMultiset<Integer>>() { @Override public ObservableMultiset<Integer> get() { // test ObservableMultisetWrapper as the 'default' // implementation of ObservableSetMultimap return CollectionUtils.observableHashMultiset(); } } }, { new Provider<ObservableMultiset<Integer>>() { @Override public ObservableMultiset<Integer> get() { // test SimpleMultisetProperty, which is the // 'default' implementation of the related // ObservableValue. return new SimpleMultisetProperty<>( CollectionUtils .<Integer> observableHashMultiset()); } } }, { new Provider<ObservableMultiset<Integer>>() { @Override public ObservableMultiset<Integer> get() { // test ReadOnlyMultisetWrapper, which is the // 'default' implementation of the related // read-only support. return new ReadOnlyMultisetWrapper<>( CollectionUtils .<Integer> observableHashMultiset()); } } } }); } private ObservableMultiset<Integer> observable; private Provider<ObservableMultiset<Integer>> observableProvider; private InvalidationExpector invalidationListener; private MultisetChangeExpector<Integer> multisetChangeListener; public ObservableMultisetTests( Provider<ObservableMultiset<Integer>> sourceProvider) { this.observableProvider = sourceProvider; } @Test public void add() { // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); check(observable, backupMultiset); registerListeners(); // add a single value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertEquals(backupMultiset.add(1), observable.add(1)); check(observable, backupMultiset); checkListeners(); // add a second occurrence of the same value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertEquals(backupMultiset.add(1), observable.add(1)); check(observable, backupMultiset); checkListeners(); // add a different value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(2, 0, 1); assertEquals(backupMultiset.add(2), observable.add(2)); check(observable, backupMultiset); checkListeners(); } @Test public void add_withCount() { // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); check(observable, backupMultiset); // register listeners registerListeners(); // add zero occurrences (no change expected) assertEquals(backupMultiset.add(1, 0), observable.remove(1, 0)); check(observable, backupMultiset); invalidationListener.check(); multisetChangeListener.check(); // add a single value multiple times invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(5, 0, 5); assertEquals(backupMultiset.add(5, 5), observable.add(5, 5)); check(observable, backupMultiset); checkListeners(); // add a value zero times (no events should occur) assertEquals(backupMultiset.add(1, 0), observable.add(1, 0)); check(observable, backupMultiset); checkListeners(); } @Test public void addAll() { // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); check(observable, backupMultiset); // register listeners registerListeners(); // add a collection with three values invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); multisetChangeListener.addElementaryExpection(2, 0, 2); multisetChangeListener.addElementaryExpection(3, 0, 3); Multiset<Integer> toAdd = HashMultiset.create(); toAdd.add(1); toAdd.add(2, 2); toAdd.add(3, 3); assertEquals(backupMultiset.addAll(toAdd), observable.addAll(toAdd)); check(observable, backupMultiset); checkListeners(); // add another collection with three values invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(2, 0, 2); multisetChangeListener.addElementaryExpection(4, 0, 3); toAdd = HashMultiset.create(); toAdd.add(2, 2); toAdd.add(4, 3); assertEquals(backupMultiset.addAll(toAdd), observable.addAll(toAdd)); check(observable, backupMultiset); checkListeners(); } @Before public void before() { observable = observableProvider.get(); } protected void check(ObservableMultiset<Integer> observable, Multiset<Integer> backupMultiset) { assertEquals(backupMultiset, observable); if (observable instanceof ReadOnlyMultisetWrapper) { assertEquals(backupMultiset, ((ReadOnlyMultisetWrapper<Integer>) observable) .getReadOnlyProperty().get()); } } protected void checkListeners() { invalidationListener.check(); multisetChangeListener.check(); } @Test public void clear() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); check(observable, backupMultiset); // register listeners registerListeners(); // clear invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 1, 0); multisetChangeListener.addElementaryExpection(2, 2, 0); multisetChangeListener.addElementaryExpection(3, 3, 0); observable.clear(); backupMultiset.clear(); check(observable, backupMultiset); checkListeners(); // clear again (while already empty) invalidationListener.expect(0); observable.clear(); check(observable, backupMultiset); checkListeners(); } @Test public void listenersNotProperlyIterating() { MultisetChangeListener<Integer> multisetChangeListener = new MultisetChangeListener<Integer>() { @Override public void onChanged( org.eclipse.gef.common.collections.MultisetChangeListener.Change<? extends Integer> change) { // initially cursor is left of first change try { // call getElement() without next change.getElement(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getElement() can be called.", e.getMessage()); } try { // call getAddCount() without next change.getAddCount(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getAddCount() can be called.", e.getMessage()); } try { // call getRemoveCount() without next change.getRemoveCount(); fail("Expect IllegalArgumentException, because next() has not been called."); } catch (IllegalStateException e) { assertEquals( "Need to call next() before getRemoveCount() can be called.", e.getMessage()); } // put cursor right of last change while (change.next()) { } change.next(); try { // call getElement() without next change.getElement(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getElement() if next() returned true.", e.getMessage()); } try { // call getAddCount() without next change.getAddCount(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getAddCount() if next() returned true.", e.getMessage()); } try { // call getRemoveCount() without next change.getRemoveCount(); fail("Expect IllegalArgumentException, because next() return value has not been respected."); } catch (IllegalStateException e) { assertEquals( "May only call getRemoveCount() if next() returned true.", e.getMessage()); } } }; observable.addListener(multisetChangeListener); // 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<ObservableMultiset<Integer>> observableValue = (ObservableValue<ObservableMultiset<Integer>>) observable; ChangeListener<ObservableMultiset<Integer>> changeListener = new ChangeListener<ObservableMultiset<Integer>>() { @Override public void changed( ObservableValue<? extends ObservableMultiset<Integer>> observable, ObservableMultiset<Integer> oldValue, ObservableMultiset<Integer> newValue) { // unregister ourselves observable.removeListener(this); // register ourselves (again) observable.addListener(this); } }; observableValue.addListener(changeListener); } MultisetChangeListener<Integer> multisetChangeListener = new MultisetChangeListener<Integer>() { @Override public void onChanged( org.eclipse.gef.common.collections.MultisetChangeListener.Change<? extends Integer> change) { // unregister ourselves change.getMultiset().removeListener(this); // register ourselves (again) change.getMultiset().addListener(this); } }; observable.addListener(multisetChangeListener); // ensure no concurrent modification exceptions result observable.add(1); } @Test public void listenersRegisteredMoreThanOnce() { // register listeners (twice) InvalidationExpector invalidationListener = new InvalidationExpector(); MultisetChangeExpector<Integer> multisetChangeListener = new MultisetChangeExpector<>( 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(multisetChangeListener); observable.addListener(multisetChangeListener); // add and remove should have no effect // add and remove should have no effect MultisetChangeListener<Integer> multisetChangeListener2 = new MultisetChangeListener<Integer>() { @Override public void onChanged( org.eclipse.gef.common.collections.MultisetChangeListener.Change<? extends Integer> change) { // ignore } }; observable.addListener(multisetChangeListener2); observable.removeListener(multisetChangeListener2); // perform add invalidationListener.expect(2); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertTrue(observable.add(1)); invalidationListener.check(); multisetChangeListener.check(); // remove single listener occurrence observable.removeListener(invalidationListener); observable.removeListener(multisetChangeListener); // perform another add invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertTrue(observable.add(1)); invalidationListener.check(); multisetChangeListener.check(); // remove listeners and ensure no notifications are received observable.removeListener(invalidationListener); observable.removeListener(multisetChangeListener); invalidationListener.check(); multisetChangeListener.check(); } protected void registerListeners() { invalidationListener = new InvalidationExpector(); multisetChangeListener = new MultisetChangeExpector<>(observable); observable.addListener(invalidationListener); observable.addListener(multisetChangeListener); } @Test public void remove() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); check(observable, backupMultiset); // register listeners registerListeners(); // remove (first occurrence of) value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(2, 1, 0); assertEquals(backupMultiset.remove(2), observable.remove(2)); check(observable, backupMultiset); checkListeners(); // remove (second occurrence of) value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(2, 1, 0); assertEquals(backupMultiset.remove(2), observable.remove(2)); check(observable, backupMultiset); checkListeners(); // remove not contained value (no change expected) assertEquals(backupMultiset.remove(2), observable.remove(2)); check(observable, backupMultiset); checkListeners(); } @Test public void remove_withCount() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); check(observable, backupMultiset); // register listeners registerListeners(); // remove zero occurrences (no change expected) assertEquals(backupMultiset.remove(3, 0), observable.remove(3, 0)); check(observable, backupMultiset); checkListeners(); // remove (two occurrences of) value invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(3, 2, 0); assertEquals(backupMultiset.remove(3, 2), observable.remove(3, 2)); check(observable, backupMultiset); checkListeners(); // remove more occurrences than contained (change contains fewer // occurrences) invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(3, 1, 0); assertEquals(backupMultiset.remove(3, 2), observable.remove(3, 2)); check(observable, backupMultiset); checkListeners(); // remove not contained value (no change expected) assertEquals(backupMultiset.remove(3, 1), observable.remove(3, 1)); check(observable, backupMultiset); checkListeners(); } @Test public void removeAll() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); check(observable, backupMultiset); // register listeners registerListeners(); // remove collection invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 1, 0); // all occurrences of 2 will be removed, even if toRemove contains fewer // occurrences. multisetChangeListener.addElementaryExpection(2, 2, 0); Multiset<Integer> toRemove = HashMultiset.create(); toRemove.add(1); toRemove.add(2, 1); toRemove.add(4, 4); assertEquals(backupMultiset.removeAll(toRemove), observable.removeAll(toRemove)); check(observable, backupMultiset); checkListeners(); } @Test public void replaceAll() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); observable.add(4, 4); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); backupMultiset.add(4, 4); check(observable, backupMultiset); // register listeners registerListeners(); // replaceAll invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(2, 1, 0); // decrease // count multisetChangeListener.addElementaryExpection(4, 4, 0); // remove multisetChangeListener.addElementaryExpection(3, 0, 3); // increase // count multisetChangeListener.addElementaryExpection(5, 0, 5); // add Multiset<Integer> toReplace = HashMultiset.create(); toReplace.add(1); toReplace.add(2, 1); toReplace.add(3, 6); toReplace.add(5, 5); observable.replaceAll(toReplace); backupMultiset.clear(); backupMultiset.addAll(toReplace); check(observable, backupMultiset); checkListeners(); // replace with same contents (should not have any effect) invalidationListener.expect(0); observable.replaceAll(toReplace); check(observable, backupMultiset); checkListeners(); } @Test public void retainAll() { // initialize multiset with some values observable.add(1, 1); observable.add(2, 2); observable.add(3, 3); // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); backupMultiset.add(1, 1); backupMultiset.add(2, 2); backupMultiset.add(3, 3); check(observable, backupMultiset); // register listeners registerListeners(); // remove collection invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(3, 3, 0); Multiset<Integer> toRetain = HashMultiset.create(); toRetain.add(1); toRetain.add(2, 1); toRetain.add(4, 4); assertEquals(backupMultiset.retainAll(toRetain), observable.retainAll(toRetain)); check(observable, backupMultiset); checkListeners(); } @Test public void setCount() { // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); check(observable, backupMultiset); // register listeners registerListeners(); // set count for non contained element invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertEquals(backupMultiset.setCount(1, 1), observable.setCount(1, 1)); check(observable, backupMultiset); checkListeners(); // increase count for already contained element invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 3); assertEquals(backupMultiset.setCount(1, 4), observable.setCount(1, 4)); check(observable, backupMultiset); checkListeners(); // decrease count for already contained element invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 2, 0); assertEquals(backupMultiset.setCount(1, 2), observable.setCount(1, 2)); check(observable, backupMultiset); checkListeners(); } @Test public void setCount_withOld() { // prepare backup multiset Multiset<Integer> backupMultiset = HashMultiset.create(); check(observable, backupMultiset); // register listeners registerListeners(); // set count for non contained element invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 2); assertEquals(backupMultiset.setCount(1, 0, 2), observable.setCount(1, 0, 2)); check(observable, backupMultiset); checkListeners(); // set count to increase occurrences invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 0, 1); assertEquals(backupMultiset.setCount(1, 2, 3), observable.setCount(1, 2, 3)); check(observable, backupMultiset); checkListeners(); // set count to decrease occurrences invalidationListener.expect(1); multisetChangeListener.addAtomicExpectation(); multisetChangeListener.addElementaryExpection(1, 1, 0); assertEquals(backupMultiset.setCount(1, 3, 2), observable.setCount(1, 3, 2)); check(observable, backupMultiset); checkListeners(); // set count where old value is not met (no change expected) assertEquals(backupMultiset.setCount(1, 4, 3), observable.setCount(1, 4, 3)); check(observable, backupMultiset); checkListeners(); } /** * Confirm {@link ObservableMultiset} 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(3, observable.count(1)); assertEquals(2, observable.count(2)); // clear observable.clear(); assertTrue(observable.isEmpty()); } }