| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.waveprotocol.wave.model.adt.docbased; |
| |
| |
| import junit.framework.TestCase; |
| |
| import org.waveprotocol.wave.model.adt.ObservableBasicMap.Listener; |
| import org.waveprotocol.wave.model.adt.docbased.TestUtil.ValueContext; |
| import org.waveprotocol.wave.model.document.ObservableMutableDocument; |
| import org.waveprotocol.wave.model.document.operation.Attributes; |
| import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; |
| import org.waveprotocol.wave.model.document.util.DefaultDocumentEventRouter; |
| import org.waveprotocol.wave.model.testing.BasicFactories; |
| import org.waveprotocol.wave.model.testing.ExtraAsserts; |
| import org.waveprotocol.wave.model.util.Serializer; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Test cases for {@link DocumentBasedMonotonicMap}. |
| * |
| */ |
| |
| public class DocumentBasedMonotonicMapTest extends TestCase { |
| |
| private final static String KEY1 = "bEefFaCe*2"; |
| private final static String KEY2 = "caFeBabE*9"; |
| |
| private final static String CONTAINER_TAG = "supplement"; |
| private final static String ENTRY_TAG = "read"; |
| private final static String KEY_ATTR = "blipId"; |
| private final static String VALUE_ATTR = "version"; |
| |
| // |
| // The ADT used in these tests to represent underlying map state is just a list of entries. |
| // A Map can not be used, because one aspect to be tested is that the underlying document state |
| // can transitionally contain duplicate entries, and the expected-value type must be able to |
| // describe such states. |
| // |
| |
| /** |
| * A key-value pair. |
| */ |
| private static class Entry<K, V> { |
| final K key; |
| final V value; |
| |
| Entry(K key, V value) { |
| this.key = key; |
| this.value = value; |
| } |
| } |
| |
| /** |
| * Builder of a list of entries. |
| */ |
| private static class ListBuilder<K, V> { |
| private final List<Entry<K, V>> entries = new ArrayList<Entry<K, V>>(); |
| |
| ListBuilder <K, V> add(K key, V value) { |
| entries.add(new Entry<K, V>(key, value)); |
| return this; |
| } |
| |
| List<Entry<K, V>> build() { |
| return entries; |
| } |
| } |
| |
| /** |
| * Hack for simulating mutation events caused by a remote agent making |
| * modifications to the document underlying the map. |
| * |
| * Used to be used to simulate document events. Documents now broadcast |
| * events. |
| * |
| * TODO(user): delete all the scaffolding that's obviated by real events. |
| */ |
| private final static class FungeStack<N, E extends N, K, C extends Comparable<C>> { |
| private final ValueContext<N, E> context; |
| private final DocumentBasedMonotonicMap<E, K, C> target; |
| private final Serializer<K> keySerializer; |
| private final Serializer<C> valueSerializer; |
| |
| public FungeStack(ValueContext<N, E> context, |
| DocumentBasedMonotonicMap<E, K, C> target, |
| Serializer<K> keySerializer, Serializer<C> valueSerializer) { |
| this.context = context; |
| this.target = target; |
| this.keySerializer = keySerializer; |
| this.valueSerializer = valueSerializer; |
| } |
| |
| void addEntry(K key, C value) { |
| String keyString = keySerializer.toString(key); |
| String valueString = valueSerializer.toString(value); |
| Attributes attrs = new AttributesImpl(KEY_ATTR, keyString, VALUE_ATTR, valueString); |
| E child = context.doc.createChildElement(context.container, ENTRY_TAG, attrs); |
| } |
| |
| void assertSubstrateEquals(ListBuilder<String, Integer> expected) { |
| // Check that the monotonic map deleted the first entry. The simplest |
| // approach to check this is to check that the new substrate equals an |
| // expected document structure (done via XML comparison). |
| ExtraAsserts.assertStructureEquivalent(substrate(expected).doc, context.doc); |
| } |
| } |
| |
| /** |
| * Mock listener for map events. |
| */ |
| private static class MockListener<K, C> implements Listener<K, C> { |
| private final ListBuilder<K, C> entries = new ListBuilder<K, C>(); |
| |
| @Override |
| public void onEntrySet(K key, C oldValue, C newValue) { |
| entries.add(key, newValue); |
| } |
| |
| public ListBuilder<K, C> getEntries() { |
| return entries; |
| } |
| } |
| |
| /** Target state, containing the map being tested and its subtrate. */ |
| private FungeStack<?, ?, String, Integer> stack; |
| |
| // |
| // Test-specific setup helpers. |
| // |
| |
| /** |
| * Creates an target map based on an initial list of entries. The entries |
| * are used to build a substrate document, and the target map is instantiated |
| * on that substrate. |
| */ |
| private void createTargetOn(ListBuilder<String, Integer> builder) { |
| fungeCreateTargetOn(substrate(builder)); |
| } |
| |
| // Funge method to work around Sun JDK's laughably poor type inference. |
| private <N> void fungeCreateTargetOn(ValueContext<N, ?> context) { |
| createTargetOn(context); |
| } |
| |
| /** |
| * Creates a target map on a substrate. |
| */ |
| private <N, E extends N> void createTargetOn(ValueContext<N, E> context) { |
| DocumentBasedMonotonicMap<E, String, Integer> target = |
| DocumentBasedMonotonicMap.create(DefaultDocumentEventRouter.create(context.doc), |
| context.container, Serializer.STRING, Serializer.INTEGER, ENTRY_TAG, KEY_ATTR, |
| VALUE_ATTR); |
| |
| // Eventually, the target map and the substrate should be sufficient state for all tests. |
| // However, in order to simulate document events, the two need to be wrapped together in a |
| // FungeStack so that Java knows that the element type-parameters match. |
| stack = new FungeStack<N, E, String, Integer>( |
| context, target, Serializer.STRING, Serializer.INTEGER); |
| } |
| |
| /** |
| * Creates a substrate based on a list of entries. |
| * |
| * @param values list of entries to include in the document state |
| * @return a map-context view of the document state |
| */ |
| private static ValueContext<?, ?> substrate(ListBuilder<String, Integer> values) { |
| return substrate(BasicFactories.observableDocumentProvider().create("data", |
| Collections.<String, String>emptyMap()), values); |
| } |
| |
| /** |
| * Populates a document with an initial map state defined by an entry list. |
| * |
| * @return a map-context view of the document state. |
| */ |
| private static <N, E extends N> ValueContext<N, E> substrate( |
| ObservableMutableDocument<N, E, ?> doc, |
| ListBuilder<String, Integer> values) { |
| // Insert container element |
| E container = doc.createChildElement(doc.getDocumentElement(), CONTAINER_TAG, |
| Collections.<String,String>emptyMap()); |
| |
| // Insert entries |
| for (Entry<String, Integer> e : values.build()) { |
| Map<String, String> attrs = new HashMap<String, String>(); |
| attrs.put(KEY_ATTR, e.key); |
| attrs.put(VALUE_ATTR, Serializer.INTEGER.toString(e.value)); |
| doc.createChildElement(container, ENTRY_TAG, new AttributesImpl(attrs)); |
| } |
| |
| return new ValueContext<N, E>(doc, container); |
| } |
| |
| /** |
| * Asserts that the test-target's document substrate is in an expected state. |
| * |
| * @param expected list of entries describing the expected state |
| */ |
| private void assertSubstrateEquals(ListBuilder<String, Integer> expected) { |
| stack.assertSubstrateEquals(expected); |
| } |
| |
| /** |
| * Creates an empty map as the test target. |
| */ |
| private void createEmptyMap() { |
| createTargetOn(new ListBuilder<String, Integer>()); |
| } |
| |
| /** |
| * Adds an entry to the target map's underlying state. This simulates a |
| * concurrent modification by some other agent. |
| * |
| * @param key |
| * @param value |
| */ |
| private void addEntry(String key, Integer value) { |
| stack.addEntry(key, value); |
| } |
| |
| /** |
| * @return the target map being tested. |
| */ |
| private DocumentBasedMonotonicMap<?, String, Integer> getTarget() { |
| return stack.target; |
| } |
| |
| public void testPutOnEmptyMapIsReturnedByGet() { |
| createEmptyMap(); |
| getTarget().put(KEY1, 10); |
| assertEquals(new Integer(10), getTarget().get(KEY1)); |
| } |
| |
| public void testPutOnEmptyMapInsertsIntoSubstrate() { |
| createEmptyMap(); |
| getTarget().put(KEY1, 10); |
| assertSubstrateEquals(new ListBuilder<String, Integer>().add(KEY1, 10)); |
| } |
| |
| public void testLoadLeavesOverridenEntriesInSubstrateButCleansOnWrite() { |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20) |
| .add(KEY1, 30)); |
| |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20) |
| .add(KEY1, 30)); |
| |
| getTarget().put(KEY2, 50); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY2, 50) |
| .add(KEY1, 30)); |
| } |
| |
| public void testPutOfALesserValueDoesNothing() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| |
| getTarget().put(KEY1, 05); |
| |
| assertEquals(new Integer(10), getTarget().get(KEY1)); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| } |
| |
| public void testPutOfAnEqualValueDoesNothing() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| |
| getTarget().put(KEY2, 20); |
| |
| // If the substrate were to have been rewritten, we'd expect to find KEY2 at |
| // the start of the document as a newly written entry. Therefore, we test |
| // that it remains at its old location. |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| } |
| |
| public void testRemovePutWithLesserValues() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| |
| getTarget().remove(KEY1); |
| getTarget().put(KEY1, 5); |
| |
| assertEquals(new Integer(5), getTarget().get(KEY1)); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 5) |
| .add(KEY2, 20)); |
| } |
| |
| public void testPutOfAGreaterValueReplacesOld() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| |
| getTarget().put(KEY1, 30); |
| |
| assertEquals(new Integer(30), getTarget().get(KEY1)); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 30) |
| .add(KEY2, 20)); |
| } |
| |
| public void testRemoteAddedLesserEntriesGetScheduledForRemoval() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 30) |
| .add(KEY2, 20)); |
| |
| // Add an entry remotely. |
| addEntry(KEY1, 10); |
| |
| assertEquals(new Integer(30), getTarget().get(KEY1)); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 30) |
| .add(KEY2, 20) |
| .add(KEY1, 10)); |
| |
| getTarget().put(KEY2, 50); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY2, 50) |
| .add(KEY1, 30)); |
| } |
| |
| public void testRemoteAddedGreaterEntriesObviateOldEntry() { |
| // Set up the target with some initial state. |
| createTargetOn(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20)); |
| |
| // Add an entry remotely. |
| addEntry(KEY1, 30); |
| |
| assertEquals(new Integer(30), getTarget().get(KEY1)); |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 10) |
| .add(KEY2, 20) |
| .add(KEY1, 30)); |
| |
| // Mutate locally, expect cleanup |
| getTarget().put(KEY1, 50); |
| |
| assertSubstrateEquals(new ListBuilder<String, Integer>() |
| .add(KEY1, 50) |
| .add(KEY2, 20)); |
| } |
| |
| public void testPutOfNewEntryTriggersEvent() { |
| createEmptyMap(); |
| |
| MockListener<String, Integer> listener = new MockListener<String, Integer>(); |
| getTarget().addListener(listener); |
| getTarget().put(KEY1, 10); |
| |
| List<Entry<String, Integer>> receivedEntries = listener.getEntries().build(); |
| assertEquals(1, receivedEntries.size()); |
| } |
| |
| public void testReplacementEntryTriggersSingleEvent() { |
| createEmptyMap(); |
| getTarget().put(KEY1, 10); |
| |
| MockListener<String, Integer> listener = new MockListener<String, Integer>(); |
| getTarget().addListener(listener); |
| |
| getTarget().put(KEY1, 20); |
| List<Entry<String, Integer>> receivedEntries = listener.getEntries().build(); |
| assertEquals(1, receivedEntries.size()); |
| } |
| |
| } |