/** * Copyright 2008 Google Inc. * * Licensed 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.impl.AttributesImpl; import org.waveprotocol.wave.model.document.util.DefaultDocumentEventRouter; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.Point; 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 DocumentBasedBasicMap}, focussing particularly on * interaction with the underlying document storage. * */ public class DocumentBasedBasicMapTest extends TestCase { private static final String KEY1 = "bEefFaCe*2"; private static final String KEY2 = "caFeBabE*9"; private static final String CONTAINER_TAG = "supplement"; private static final String ENTRY_TAG = "read"; private static final String KEY_ATTR = "blipId"; private static final 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 static final class FungeStack<N, E extends N, K, V> { private final ValueContext<N, E> context; private final DocumentBasedBasicMap<E, K, V> target; private final Serializer<K> keySerializer; private final Serializer<V> valueSerializer; public FungeStack(ValueContext<N, E> context, DocumentBasedBasicMap<E, K, V> target, Serializer<K> keySerializer, Serializer<V> valueSerializer) { this.context = context; this.target = target; this.keySerializer = keySerializer; this.valueSerializer = valueSerializer; } void addEntry(K key, V value) { Map<String, String> attrs = new HashMap<String, String>(); attrs.put(KEY_ATTR, keySerializer.toString(key)); attrs.put(VALUE_ATTR, valueSerializer.toString(value)); E child = context.doc.createElement(Point.start(context.doc, context.container), ENTRY_TAG, new AttributesImpl(attrs)); } void removeEntries(K key) { N curChild = context.doc.getFirstChild(context.container); String keyString = keySerializer.toString(key); E e = DocHelper.getFirstChildElement(context.doc, context.container); while (e != null) { if (ENTRY_TAG.equals(context.doc.getTagName(e)) && keyString.equals(context.doc.getAttribute(e, KEY_ATTR))) { context.doc.deleteNode(e); } e = DocHelper.getNextSiblingElement(context.doc, e); } } 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, V> implements Listener<K, V> { private final ListBuilder<K, V> entries = new ListBuilder<K, V>(); @Override public void onEntrySet(K key, V oldValue, V newValue) { entries.add(key, newValue); } public ListBuilder<K, V> 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) { DocumentBasedBasicMap<E, String, Integer> target = DocumentBasedBasicMap.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); } /** * Remove entries with the specified keys from the target map's underlying state. * This simulates a concurrent modification by some other agent. */ private void removeEntries(String key) { stack.removeEntries(key); } /** * @return the target map being tested. */ private DocumentBasedBasicMap<?, 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 testPutOfLaterValuesReplaceOld() { // 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(05), getTarget().get(KEY1)); assertSubstrateEquals(new ListBuilder<String, Integer>() .add(KEY1, 05) .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 testRemoteAddedLaterValueObviatesOldEntry() { // Set up the target with some initial state. createTargetOn(new ListBuilder<String, Integer>() .add(KEY1, 30) .add(KEY2, 20)); // Replace an entry remotely. removeEntries(KEY1); addEntry(KEY1, 10); assertEquals(new Integer(10), getTarget().get(KEY1)); assertSubstrateEquals(new ListBuilder<String, Integer>() .add(KEY1, 10) .add(KEY2, 20)); // Mutate locally, expect cleanup. getTarget().put(KEY2, 50); assertSubstrateEquals(new ListBuilder<String, Integer>() .add(KEY2, 50) .add(KEY1, 10)); } 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, 5); List<Entry<String, Integer>> receivedEntries = listener.getEntries().build(); assertEquals(1, receivedEntries.size()); } }