/** * Copyright 2010 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 org.waveprotocol.wave.model.adt.ObservableSingletonTestBase; import org.waveprotocol.wave.model.document.Doc; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.operation.algorithm.Composer; import org.waveprotocol.wave.model.document.operation.algorithm.DocOpInverter; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.operation.impl.UncheckedDocOpBuffer; import org.waveprotocol.wave.model.document.util.DefaultDocEventRouter; import org.waveprotocol.wave.model.document.util.DocEventRouter; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.testing.BasicFactories; import org.waveprotocol.wave.model.util.CollectionUtils; /** * Tests for the document-based singleton. * * @author anorth@google.com (Alex North) */ public class ObservableSingletonWithDocumentBasedSingletonTest extends ObservableSingletonTestBase { private static final String TAG = "int"; private static final String ATTR = "v"; private static final Factory<Doc.E, Integer, String> FACTORY = TestUtil.newIntegerFactory(ATTR); private ObservableDocument doc; private DocumentBasedSingleton<Integer, String> target; private Listener listener; private DocEventRouter router; @Override protected DocumentBasedSingleton<Integer, String> createSingleton() { return createSingleton(DefaultDocEventRouter.create(BasicFactories.createDocument( DocumentSchema.NO_SCHEMA_CONSTRAINTS))); } /** * Creates a singleton backed by a document root. */ private static DocumentBasedSingleton<Integer, String> createSingleton(DocEventRouter router) { return DocumentBasedSingleton.create(router, router.getDocument().getDocumentElement(), TAG, FACTORY); } // Document-based implementation specific tests. @Override public void setUp() { super.setUp(); target = createSingleton(); doc = target.getDocument(); router = target.getEventRouter(); listener = new Listener(); } // Initialization. public void testInitialStateIsReadable() { remoteInsertCanonicalValue("42"); DocumentBasedSingleton<Integer, String> other = createSingleton(router); assertValue(other, 42); } // Concurrent behaviour and events. public void testRemoteChangesUpdateState() { remoteInsertCanonicalValue("42"); assertValue(target, 42); remoteInsertCanonicalValue("43"); assertValue(target, 43); remoteClearValue(); assertNoValue(target); } public void testRemoteChangesGenerateEvents() { target.addListener(listener); remoteInsertCanonicalValue("42"); listener.verifyValueChanged(null, 42); remoteInsertCanonicalValue("43"); listener.verifyValueChanged(42, 43); remoteClearValue(); listener.verifyValueChanged(43, null); } public void testRemoteChangeAfterInitializationGeneratesEvent() { remoteInsertCanonicalValue("42"); DocumentBasedSingleton<Integer, String> other = createSingleton(router); other.addListener(listener); remoteInsertCanonicalValue("43"); listener.verifyValueChanged(42, 43); } public void testAdditionOfNonCanonicalValueChangesNothing() { target.set("42"); target.addListener(listener); remoteInsertRedundantValue("43"); assertValue(target, 42); listener.verifyNoEvent(); } public void testRemovalOfNonCanonicalValueChangesNothing() { target.set("42"); remoteInsertRedundantValue("43"); target.addListener(listener); remoteRemoveRedundantValues(); assertValue(target, 42); listener.verifyNoEvent(); } public void testRemovalOfCanonicalValueSetsNewValue() { target.set("42"); remoteInsertRedundantValue("43"); target.addListener(listener); remoteRemoveCanonicalValue(); assertValue(target, 43); listener.verifyValueChanged(42, 43); } // Cleanup. public void testClearRemovesRedundantValues() { target.set("42"); remoteInsertRedundantValue("43"); target.addListener(listener); target.clear(); assertNoValue(target); listener.verifyValueChanged(42, null); } public void testSetClearsRedundantValues() { target.set("42"); remoteInsertRedundantValue("43"); target.addListener(listener); target.set("44"); listener.verifyValueChanged(42, 44); assertSame(DocHelper.getElementWithTagName(doc, TAG), DocHelper.getLastElementWithTagName(doc, TAG)); } public void testAtomicReplacementOfEquivalentFiresNoEvents() { target.set("42"); DocOp replacement = createReplaceOp(createErasureOp(), createRestoreOp()); target.addListener(listener); doc.hackConsume(Nindo.fromDocOp(replacement, false)); listener.verifyNoEvent(); } public void testAtomicReplacementFiresSingleEvent() { // Build "insert 42" state. target.set("42"); DocOp restore = createRestoreOp(); // Build "delete 43" state. target.set("43"); DocOp erasure = createErasureOp(); DocOp restoration = createReplaceOp(erasure, restore); target.addListener(listener); doc.hackConsume(Nindo.fromDocOp(restoration, false)); listener.verifyValueChanged(43, 42); } /** * Inserts a first-child element. */ private void remoteInsertCanonicalValue(String value) { Doc.N firstChild = doc.getFirstChild(doc.getDocumentElement()); Point<Doc.N> insertion = Point.<Doc.N>inElement(doc.getDocumentElement(), firstChild); doc.createElement(insertion, TAG, CollectionUtils.immutableMap(ATTR, value)); } /** * Inserts a last-child element. */ private void remoteInsertRedundantValue(String value) { doc.createChildElement(doc.getDocumentElement(), TAG, CollectionUtils.immutableMap(ATTR, value)); } private void remoteClearValue() { Doc.E toDelete = DocHelper.getLastElementWithTagName(doc, TAG, doc.getDocumentElement()); while (toDelete != null) { doc.deleteNode(toDelete); toDelete = DocHelper.getLastElementWithTagName(doc, TAG, doc.getDocumentElement()); } } private void remoteRemoveRedundantValues() { Doc.E canonical = DocHelper.getElementWithTagName(doc, TAG); Doc.E toDelete = DocHelper.getLastElementWithTagName(doc, TAG, doc.getDocumentElement()); while (toDelete != canonical) { doc.deleteNode(toDelete); toDelete = DocHelper.getLastElementWithTagName(doc, TAG, doc.getDocumentElement()); } } private void remoteRemoveCanonicalValue() { Doc.E canonical = DocHelper.getElementWithTagName(doc, TAG); doc.deleteNode(canonical); } /** Creates an op that restores the doc's current state. */ private DocOp createRestoreOp() { UncheckedDocOpBuffer builder = new UncheckedDocOpBuffer(); doc.toInitialization().apply(builder); return builder.finish(); } /** Creates an op that deletes the doc's current state. */ private DocOp createErasureOp() { UncheckedDocOpBuffer builder = new UncheckedDocOpBuffer(); doc.toInitialization().apply(builder); return DocOpInverter.invert(builder.finish()); } private static DocOp createReplaceOp(DocOp erase, DocOp restore) { try { return Composer.compose(erase, restore); } catch (OperationException e) { // If the code above fails, then there is a bug in the operation code, not // these tests. Fail with an exception, not with a JUnit fail(). throw new RuntimeException(e); } } }