/** * Copyright 2009 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.concurrencycontrol.client; import junit.framework.TestCase; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder; import org.waveprotocol.wave.model.operation.TransformException; import org.waveprotocol.wave.model.operation.wave.BlipContentOperation; import org.waveprotocol.wave.model.operation.wave.NoOp; import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext; import org.waveprotocol.wave.model.testing.DeltaTestUtil; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; /** * Tests {@link OperationQueue}. * */ public class OperationQueueTest extends TestCase { private static final ParticipantId BOB = new ParticipantId("bob@example.com"); private static final WaveletOperation BOB_A = new NoOp(makeContext(BOB, 1000)); private static final WaveletOperation BOB_B = new NoOp(makeContext(BOB, 2000)); private static final WaveletOperation BOB_C = new NoOp(makeContext(BOB, 3000)); private static final ParticipantId JIM = new ParticipantId("jim@example.com"); private static final WaveletOperation JIM_A = new NoOp(makeContext(JIM, 1000)); private static final WaveletOperation JIM_B = new NoOp(makeContext(JIM, 2000)); private static final WaveletOperation JIM_C = new NoOp(makeContext(JIM, 3000)); private static final ParticipantId TOM = new ParticipantId("tom@example.com"); private static final WaveletOperation TOM_B = new NoOp(makeContext(TOM, 2000)); private OperationQueue queue; private MockDeltaTransformer transformer; @Override protected void setUp() { transformer = new MockDeltaTransformer(); queue = new OperationQueue(transformer); } private static WaveletOperationContext makeContext(ParticipantId participant, long timestamp) { return new WaveletOperationContext(participant, timestamp, 1); } /** Tests that newly created queues are empty. */ public void testQueueStartsEmpty() { assertQueueIsEmpty(); } /** * Tests that adding operations to an empty queue results in it being * non-empty until those operations are taken out again. */ public void testQueueNonemptyAfterOperationsAddedUntilTaken() { queue.add(BOB_A); assertQueueSize(1); queue.take(); assertQueueIsEmpty(); } /** * Tests that inserting a new head delta into an empty queue makes the queue * non-empty if and only if the delta has ops in it. */ public void testQueueNonemptyAfterInsertingNonemptyHead() { queue.insertHead(new MergingSequence()); assertQueueIsEmpty(); MergingSequence head = new MergingSequence(); head.add(BOB_A); queue.insertHead(head); assertQueueSize(1); } /** * Tests that attempting to take a delta from an empty queue results in a * {@link NoSuchElementException} being thrown. */ public void testThrowsExceptionTakingFromEmptyQueue() { try { queue.take(); fail("take() from empty queue should fail"); } catch (NoSuchElementException expected) { } } /** * Tests that adding multiple consecutive operations from a single author * results in them being merged into the one delta. */ public void testMergesConsecutiveOperationsWithSameCreator() { queue.add(BOB_A); queue.add(BOB_B); assertQueueSizeBetween(1, 2); assertEquals(list(BOB_A, BOB_B), copyList(queue.take())); // Queue should now be empty, even though we did two adds and only one take assertQueueIsEmpty(); } /** Tests that operations by different authors are not merged. */ public void testDoesNotMergeOperationsDividedByOtherAuthor() { queue.add(BOB_A); queue.add(JIM_B); assertQueueSizeBetween(1, 2); assertEquals(list(BOB_A), copyList(queue.take())); assertEquals(list(JIM_B), copyList(queue.take())); } /** * Tests that two operations from a single author that are added one after * another are not merged if the calls to add() are separated by a take() * which extracts the first operation. */ public void testOperationsNotMergedOverQueueEmptyPoints() { queue.add(BOB_A); assertEquals(list(BOB_A), copyList(queue.take())); assertQueueIsEmpty(); queue.add(BOB_B); assertEquals(list(BOB_B), copyList(queue.take())); } /** * Tests that two operations from a single author that are added one after * another are still merge if the calls to add() are separated by a take() * which extracts previously added operations rather than either of the two * aforementioned operations. */ public void testOperationsMergeDespiteTakeIfQueueNotMadeEmpty() { queue.add(JIM_A); queue.add(BOB_B); assertEquals(list(JIM_A), copyList(queue.take())); assertQueueSize(1); assertQueueSizeBetween(1, 2); queue.add(BOB_C); assertEquals(list(BOB_B, BOB_C), copyList(queue.take())); assertQueueIsEmpty(); } /** * Tests that a new head delta can be inserted in at the start of an empty * queue. Future adds of ops by the same creator do not merge with it. */ public void testHeadInsertedIntoEmptyQueueAllowsFutureMerging() { MergingSequence head = new MergingSequence(); head.add(BOB_A); head.add(BOB_B); queue.insertHead(head); queue.add(BOB_C); assertQueueSizeBetween(1, 3); // Delta pushed to head is not mergable to a single delta. assertEquals(list(BOB_A, BOB_B), copyList(queue.take())); assertEquals(list(BOB_C), copyList(queue.take())); assertTrue(queue.isEmpty()); } /** * Tests that a new head delta can be inserted in a non-empty queue. * It does not merge with the existing head. */ public void testInsertHeadMergesIfSameCreatorAsExistingHead() { queue.add(BOB_C); MergingSequence head = new MergingSequence(); head.add(BOB_A); head.add(BOB_B); queue.insertHead(head); assertQueueSizeBetween(1, 3); // Delta pushed to head is not mergable to a single delta. assertEquals(list(BOB_A, BOB_B), copyList(queue.take())); assertEquals(list(BOB_C), copyList(queue.take())); assertTrue(queue.isEmpty()); } /** * Tests that a new head delta which is being inserted does not merge with the * existing head if the creators do not match, hence the old head is moved to * be the second delta but otherwise remains unmodified. */ public void testInsertHeadMakesNewDeltaIfCreatorDiffersFromExistingHead() { queue.add(BOB_C); MergingSequence head = new MergingSequence(); head.add(JIM_A); head.add(JIM_B); queue.insertHead(head); assertEquals(list(JIM_A, JIM_B), copyList(queue.take())); assertEquals(list(BOB_C), copyList(queue.take())); assertTrue(queue.isEmpty()); } /** * Tests that the deltas provided by take() are optimised. Note that this test * makes assumptions about what type of deltas are used by * {@link OperationQueue} and what operations those deltas merge. */ public void testProducesOptimisedDeltas() { queue.add(new WaveletBlipOperation("a", new BlipContentOperation(BOB_A.getContext(), new DocOpBuilder().retain(1).characters("hi").retain(1).build()))); queue.add(new WaveletBlipOperation("a", new BlipContentOperation(BOB_B.getContext(), new DocOpBuilder().retain(1).characters("hi").retain(3).build()))); assertQueueSizeBetween(1, 2); assertEquals(1, queue.take().size()); assertQueueIsEmpty(); } /** * Tests that deltas which are transformed make their way back into the queue, * replacing the untransformed versions and being sent out of take(). */ public void testTransformedDeltasReplaceOriginals() throws TransformException { queue.add(BOB_A); queue.add(BOB_B); queue.add(JIM_C); transformer.expect(BOB_A, BOB_B).transformTo(BOB_A); transformer.expect(JIM_C).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); transformer.checkDone(); assertQueueSizeBetween(1, 2); assertEquals(list(BOB_A), copyList(queue.take())); assertEquals(list(JIM_C), copyList(queue.take())); assertQueueIsEmpty(); } /** * Tests that deltas which become empty due to a transform are discarded from * the queue. */ public void testDeltasEmptyAfterTransformAreDiscarded() throws TransformException { queue.add(BOB_A); queue.add(JIM_A); queue.add(BOB_B); queue.add(TOM_B); queue.add(BOB_C); transformer.expect(BOB_A).kill(); transformer.expect(JIM_A).echo(); transformer.expect(BOB_B).kill(); transformer.expect(TOM_B).echo(); transformer.expect(BOB_C).kill(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); transformer.checkDone(); assertQueueSizeBetween(1, 2); assertEquals(list(JIM_A), copyList(queue.take())); assertEquals(list(TOM_B), copyList(queue.take())); assertQueueIsEmpty(); } /** * Tests that deltas with the same author are merged if they end up being * consecutive after deltas which were previously dividing them are discarded * in a transform. */ public void testAdjacentDeltasBySameAuthorAfterTransformDiscardsAreMerged() throws TransformException { queue.add(BOB_A); queue.add(JIM_A); queue.add(BOB_B); queue.add(TOM_B); queue.add(BOB_C); transformer.expect(BOB_A).echo(); transformer.expect(JIM_A).kill(); transformer.expect(BOB_B).echo(); transformer.expect(TOM_B).kill(); transformer.expect(BOB_C).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); transformer.checkDone(); assertQueueSizeBetween(1, 3); assertEquals(list(BOB_A, BOB_B, BOB_C), copyList(queue.take())); assertQueueIsEmpty(); } /** * Test we still compose operations after we've transformed. */ public void testComposistionAfterTransform() throws TransformException { DeltaTestUtil util = new DeltaTestUtil(BOB); // 2 ops merged into one. queue.add(util.noOpDocOp("blipA")); queue.add(util.noOpDocOp("blipA")); // Get a server op transformer.expect(util.noOpDocOp("blipA")).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); // Adding one more operation after that should not merge into previously // transformed client op queue.add(util.noOpDocOp("blipA")); // This operation should merge into the previous op as the previous op is not // yet transformed queue.add(util.noOpDocOp("blipA")); // Should get 1 op, the result of merging and composing the two deltas. assertQueueSizeBetween(1, 3); assertEquals(list(util.noOpDocOp("blipA")), copyList(queue.take())); assertQueueIsEmpty(); } public void testCompositionBeforeTransform() throws TransformException { DeltaTestUtil util = new DeltaTestUtil(BOB); // 2 ops merged into one. queue.add(util.noOpDocOp("blipA")); queue.add(util.noOpDocOp("blipA")); // Get a server op transformer.expect(util.noOpDocOp("blipA")).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); // Should get only 1 op assertQueueSize(1); assertEquals(list(util.noOpDocOp("blipA")), copyList(queue.take())); assertQueueIsEmpty(); } /** * Test we compose operations after we've transformed. However, we should get several * transformed client operations out in the same delta if they don't compose. */ public void testGettingSeveralOpsInOneDelta() throws TransformException { DeltaTestUtil util = new DeltaTestUtil(BOB); // Do 1 client op queue.add(util.noOpDocOp("blipA")); // Get a server op transformer.expect(util.noOpDocOp("blipA")).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); // Do another client op queue.add(util.noOpDocOp("blipB")); // Get a server op transformer.expect(util.noOpDocOp("blipA"), util.noOpDocOp("blipB")).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); // Should get 2 ops in the same delta assertQueueSizeBetween(1, 2); assertEquals(list(util.noOpDocOp("blipA"), util.noOpDocOp("blipB")), copyList(queue.take())); assertQueueIsEmpty(); } /** * Since transform and compose doesn't commute, test we don't compose operations after * we've sent the operation. So when we push to head, we shouldn't merge sent deltas. */ public void testNoCompositionWithInsertHead() throws TransformException { DeltaTestUtil util = new DeltaTestUtil(BOB); // Do 1 client op queue.add(util.noOpDocOp("blipA")); // Pushing an op to the head should never merge queue.insertHead(Arrays.asList(util.noOpDocOp("blipA"))); // Get a server op transformer.expect(util.noOpDocOp("blipA")).echo(); transformer.expect(util.noOpDocOp("blipA")).echo(); assertEquals(transformer.getOutputServerDelta(), // \u2620 queue.transform(transformer.getInputServerDelta())); // Should be 2 deltas a the first one is not mergable assertQueueSizeBetween(1, 2); assertEquals(list(util.noOpDocOp("blipA")), copyList(queue.take())); assertEquals(list(util.noOpDocOp("blipA")), copyList(queue.take())); assertQueueIsEmpty(); } /** * Test operations are actually transformed. */ public void testOpsAreTransformed() throws TransformException { queue = new OperationQueue(); DeltaTestUtil bob = new DeltaTestUtil(BOB); DeltaTestUtil jim = new DeltaTestUtil(JIM); // Do bob client op 1 queue.add(bob.insert(1, "a", 1, null)); // Get delta 1 from jim queue.transform(Arrays.asList(jim.insert(1, "j", 1, null))); // Do bob client op 2 queue.add(bob.insert(1, "b", 3, null)); // Get delta 2 from jim queue.transform(Arrays.asList(jim.insert(1, "i", 2, null))); // check ops are transformed assertEquals(list(bob.insert(1, "ba", 3, null)), copyList(queue.take())); assertQueueIsEmpty(); } /** * Tests that the queue's size estimate is at least one per un-mergable delta. * Since it's an underestimate it must be exactly correct if each delta * contains one op. */ public void testQueueSizeEstimateIsAtLeastDeltaSize() { queue.add(BOB_A); queue.add(JIM_A); queue.add(BOB_B); queue.add(JIM_B); queue.add(BOB_C); queue.add(JIM_C); assertEquals(6, queue.estimateSize()); } /** Asserts that the queue is empty and size estimate is zero. */ private void assertQueueIsEmpty() { assertTrue("Expected empty queue", queue.isEmpty()); assertEquals("Expected queue size zero", 0, queue.estimateSize()); } /** * Asserts that the queue is not empty and the size estimate is as expected. */ private void assertQueueSize(int size) { assertFalse("Expected non-empty queue", queue.isEmpty()); int estimate = queue.estimateSize(); assertEquals("Expected queue size " + size + ", was " + estimate, size, estimate); } /** * Asserts that the queue is not empty and the size estimate is within * expected bounds (inclusive). */ private void assertQueueSizeBetween(int minSize, int maxSize) { assertFalse("Expected non-empty queue", queue.isEmpty()); int estimate = queue.estimateSize(); assertTrue("Expected queue size >= " + minSize + ", was " + estimate, estimate >= minSize); assertTrue("Expected queue size <= " + maxSize + ", was " + estimate, estimate <= maxSize); } private static <T> List<T> list(T... es) { return Collections.unmodifiableList(CollectionUtils.newArrayList(es)); } private static <T> List<T> copyList(Iterable<T> es) { return Collections.unmodifiableList(CollectionUtils.newArrayList(es)); } }