/** * 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.concurrencycontrol.client; import junit.framework.TestCase; import org.waveprotocol.wave.common.logging.PrintLogger; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.model.document.operation.DocInitialization; import org.waveprotocol.wave.model.document.operation.SuperSink; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder; import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.operation.OpComparators; import org.waveprotocol.wave.model.operation.OperationException; 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.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; 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.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * This is a thorough test of client CC by emitting fake events and checking the outputs from CC. * The operations used in the tests are secondary, we are more focused on on the CC producing the * right version numbers etc. * * @author zdwang@google.com (David Wang) */ public class OT3Test extends TestCase { private static final ParticipantId DEFAULT_CREATOR = new ParticipantId("test@example.com"); private static final DeltaTestUtil CLIENT_UTIL = new DeltaTestUtil(DEFAULT_CREATOR); private static final DeltaTestUtil EXTRA_UTIL = new DeltaTestUtil("actasme@example.com"); /** * NOTE(zdwang): Different user id to DEFAULT_CREATOR so that comparison test in CC for * reconnection works. */ private static final DeltaTestUtil SERVER_UTIL = new DeltaTestUtil("someoneelse@example.com"); /** * Test Config to pretend editor did something or we got something on the wire from the server. * @author zdwang@google.com (David Wang) */ private static final class TestConfig { private final PrintLogger logger = new PrintLogger(); ClientMock clientMock; ServerConnectionMock serverConnectionMock = new ServerConnectionMock(); public void init(int version) { init(version, null); } public void init(int version, String intialBlipXml) { HashedVersion signature = genSignature(version); serverConnectionMock = new ServerConnectionMock(); ConcurrencyControl clientCC = new ConcurrencyControl(logger, signature); serverConnectionMock.setListener(clientCC); clientMock = new ClientMock( clientCC, intialBlipXml != null ? parse(intialBlipXml) : null, DEFAULT_CREATOR, serverConnectionMock); clientCC.initialise(serverConnectionMock, clientMock); try { clientCC.onOpen(signature, signature); } catch (ChannelException e) { fail("onOpen failed: " + e); } } /** * Pretend client did some insert at the given op versions. We don't need a version * number as it's inferred in client CC. * * Flush at the end. * @throws OperationException * @throws TransformException */ public TestConfig clientDoInsert(int insertionPoint, String content) throws OperationException, TransformException { return clientDoInsert(insertionPoint, content, true); } /** * Pretend client did some insert at the given op versions. We don't need a version * number as it's inferred in client CC. * * @throws OperationException * @throws TransformException */ public TestConfig clientDoInsert(int insertionPoint, String content, boolean flush) throws OperationException, TransformException { clientMock.doInsert(insertionPoint, content); if (flush) { clientMock.flush(); } return this; } /** * Pretend client did some ops at the given op versions. * @throws OperationException * @throws TransformException */ private TestConfig clientDoOps(WaveletOperation ... ops) throws OperationException, TransformException { for (WaveletOperation op : ops) { clientMock.addClientOperation(op); } clientMock.flush(); return this; } /** * Pretend client did some ops. * * Assumption: Noops are not merged in OperationMergingDelta. If they are, then tests * will break as they count the number of ops sent from the client to the server. * * @throws TransformException */ public TestConfig clientDoOps(int numOps) throws OperationException, TransformException { for (int i = 0; i < numOps; i++) { clientMock.addClientOperation(new NoOp(new WaveletOperationContext( clientMock.getParticipantId(), 0L, 1L))); } clientMock.flush(); return this; } private WaveletOperation noOpDocOp(String blipId) { WaveletOperationContext context = new WaveletOperationContext( clientMock.getParticipantId(), 0L, 1L); BlipContentOperation blipOp = new BlipContentOperation(context, (new DocOpBuilder()).build()); return new WaveletBlipOperation(blipId, blipOp); } /** * Pretend client did some doc ops. * * @throws TransformException */ public TestConfig clientDoDocOps(String... blipIds) throws OperationException, TransformException { for (String b : blipIds) { clientMock.addClientOperation(noOpDocOp(b)); } clientMock.flush(); return this; } /** * Pretend client did some insert at the given op versions. * @throws OperationException * @throws TransformException */ public TestConfig serverDoInsert(int startVersion, int insertionPoint, String content, int remaining) throws OperationException, TransformException { TransformedWaveletDelta d = TransformedWaveletDelta.cloneOperations(SERVER_UTIL.getAuthor(), genSignature(startVersion + 1), 0L, Arrays.asList( SERVER_UTIL.insert(insertionPoint, content, remaining, null))); serverConnectionMock.triggerServerDeltas(Collections.singletonList(d)); return this; } /** * Pretend server did some ops at the given op version using different deltas. * @throws OperationException * @throws TransformException */ public TestConfig serverDoOps(int version) throws TransformException, OperationException { return serverDoOps(version, 1); } /** * Pretend server did some ops at the given op version using different deltas. * * We generate a predictable signature here for testing later. Each signature is the * version on the server after the ops. */ public TestConfig serverDoOps(int startVersion, int numOps) throws TransformException, OperationException { ArrayList<TransformedWaveletDelta> deltas = CollectionUtils.newArrayList(); for (int i = 0; i < numOps; i++) { TransformedWaveletDelta d = TransformedWaveletDelta.cloneOperations( SERVER_UTIL.getAuthor(), genSignature(startVersion + i + 1), 0L, Arrays.asList(SERVER_UTIL.noOp())); deltas.add(d); } serverConnectionMock.triggerServerDeltas(deltas); return this; } /** * Pretend the server echos back client's operation. Timestamp default to 0L. */ public TestConfig serverDoEchoBack(int startVersion) throws TransformException, OperationException { return serverDoEchoBack(startVersion, 0L); } /** * Pretend the server echos back client's operation. Timestamp default to 0L. */ public TestConfig serverDoEchoBackDocOp(int startVersion, String blipId) throws TransformException, OperationException { TransformedWaveletDelta d = TransformedWaveletDelta.cloneOperations( clientMock.getParticipantId(), genSignature(startVersion + 1), 0L, Arrays.asList(noOpDocOp(blipId))); serverConnectionMock.triggerServerDeltas(Collections.singletonList(d)); return this; } /** * Pretend the server echos back client's operation. */ public TestConfig serverDoEchoBack(int startVersion, long timestamp) throws TransformException, OperationException { TransformedWaveletDelta d = TransformedWaveletDelta.cloneOperations( clientMock.getParticipantId(), genSignature(startVersion + 1), timestamp, Arrays.asList(SERVER_UTIL.noOp())); serverConnectionMock.triggerServerDeltas(Collections.singletonList(d)); return this; } /** * Pretend the server acked one op the given version * @throws OperationException * @throws TransformException */ public TestConfig serverAck(int version) throws TransformException, OperationException { return serverAck(version, 1); } /** * Pretend the server acked the given version. * * We generate a predictable signature here for testing later. Which is the version on the * server after the op. * * @param version The new version after the operations from the client is applied. * @param numApplied Number of operations applied on the server. * @throws OperationException * @throws TransformException */ public TestConfig serverAck(int version, int numApplied) throws TransformException, OperationException { serverConnectionMock.triggerServerSuccess(numApplied, genSignature(version)); return this; } /** * Pretend the server send a commit notification for the given version. * * @param version The new version after the operations from the client is applied. * @throws OperationException * @throws TransformException */ public TestConfig serverCommit(int version) throws TransformException, OperationException { serverConnectionMock.triggerServerCommit(version); return this; } /** * Pretend we are reconnecting to the server. * @throws OperationException * @throws TransformException */ public TestConfig reconnectToServer() throws Exception { serverConnectionMock.setOpen(true); serverConnectionMock.reconnect(clientMock.getReconnectionVersions()); return this; } /** * Pretend we are disconnected to the server. */ public TestConfig disconnectFromServer() { serverConnectionMock.setOpen(false); return this; } /** * Predictable signature at given version. */ private HashedVersion genSignature(int version) { return HashedVersion.of(version, new byte[] {(byte) version}); } /** * Server open the connection at the given version and tells of the last version. */ public TestConfig serverDoOpen(int startVersion, int endVersion) throws ChannelException { serverConnectionMock.triggerOnOpen(genSignature(startVersion), genSignature(endVersion)); return this; } private void clientReceiveOpFromCC(boolean expectOps) { if (expectOps) { assertTrue(clientMock.getNumOpsReceived() > 0); } clientMock.clearEvents(); clientMock.receiveServerOperations(); } /** * Check client got nothing from the server. */ public TestConfig checkClientGotOps() { clientReceiveOpFromCC(false); ArrayList<WaveletOperation> clientGot = clientMock.getServerOperations(); assertEquals(0, clientGot.size()); return this; } /** * Check client got operations from the server with the version before the first op. */ public TestConfig checkClientGotOps(int startVersion) { return checkClientGotOps(startVersion, 1); } /** * Check client got operations from the server with the version before the first op and * the given number of ops. */ public TestConfig checkClientGotOps(int startVersion, int numOps) { clientReceiveOpFromCC(numOps != 0); ArrayList<WaveletOperation> clientGot = clientMock.getServerOperations(); assertEquals(numOps, clientGot.size()); for (int i = 0; i < numOps; i++) { assertEquals(1, clientGot.get(i).getContext().getVersionIncrement()); } // Clear the client recieved ops in clientMock clientGot.clear(); return this; } /** * Check client didn't send anything. */ public TestConfig checkClientSentOps() { List<WaveletDelta> serverGot = serverConnectionMock.getSentDeltas(); assertEquals(0, serverGot.size()); return this; } /** * Check server got operations from the client with the version before the first op as * the argument. */ public TestConfig checkClientSentOps(int startVersion) { return checkClientSentOps(startVersion, 1); } /** * Check server got operations from the client with the given target version * and the given number of ops. */ public TestConfig checkClientSentOps(int startVersion, int numOps) { List<WaveletDelta> serverGot = serverConnectionMock.getSentDeltas(); // Should have only sent 1 delta assertEquals(1, serverGot.size()); // Delta has the version number assertEquals(startVersion, serverGot.get(0).getTargetVersion().getVersion()); assertEquals(numOps, serverGot.get(0).size()); // Clear the sent ops inside serverConnectionMock serverGot.clear(); return this; } /** * Check client sent signatures from the given version. * @param versions Each version number is after the operation as applied. */ public TestConfig checkClientSentOpen(int ... versions) { List<HashedVersion> signatures = serverConnectionMock.getReconnectSignatures(); assertEquals(versions.length, signatures.size()); for (int i = 0; i < versions.length; i++) { assertEquals(genSignature(versions[i]), signatures.get(i)); } signatures.clear(); return this; } private static SuperSink parse(String xml) { return DocProviders.POJO.parse(xml); } /** * Check client has a document that looks like the following. */ public TestConfig checkClientDoc(String xml) { DocInitialization expected = parse(xml).asOperation(); DocInitialization actual = clientMock.getDoc().asOperation(); assertTrue(OpComparators.SYNTACTIC_IDENTITY.equal(expected, actual)); return this; } } /** * Test the various divergence to check we transform the operations correctly. It's not * an exhaustive test of transformation, but to test CC call transformation properly. * * @throws OperationException * @throws TransformException */ public void testTransformOperations() throws TransformException, OperationException { TestConfig t = new TestConfig(); // Empty Case t.init(0); t.checkClientGotOps().checkClientSentOps(); // Simple insert from client t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X"); t.checkClientGotOps().checkClientSentOps(0).checkClientDoc("<blip><p>Xabc</p></blip>"); // Simple insert from server t.init(0, "<blip><p>abc</p></blip>"); t.serverDoInsert(0, 2, "X", 5); t.checkClientGotOps(0).checkClientSentOps().checkClientDoc("<blip><p>Xabc</p></blip>"); // Simple conflict // c1 /\ s1 t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").serverDoInsert(0, 3, "Y", 4); t.checkClientGotOps(0).checkClientSentOps(0).checkClientDoc("<blip><p>XaYbc</p></blip>"); // Conflict // c1 /\ s1 // c2 / t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").clientDoInsert(3, "Y"); t.serverDoInsert(0, 3, "A", 4); t.checkClientGotOps(0).checkClientSentOps(0).checkClientDoc("<blip><p>XYaAbc</p></blip>"); // Conflict // c1, c2 /\ s1 // c3 / t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X", false).clientDoInsert(4, "Y").clientDoInsert(6, "Z"); t.serverDoInsert(0, 3, "A", 5); t.checkClientGotOps(0).checkClientSentOps(0, 1).checkClientDoc("<blip><p>XaYAbZc</p></blip>"); // Conflict // c1 /\ s1 // \ s2 t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").serverDoInsert(0, 3, "Y", 4).serverDoInsert(1, 4, "Z", 4); t.checkClientGotOps(0, 2).checkClientSentOps(0).checkClientDoc("<blip><p>XaYZbc</p></blip>"); // Conflict // c1 /\ s1 // c2 / \ s2 t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").clientDoInsert(3, "Y"); t.serverDoInsert(0, 3, "Z", 4).serverDoInsert(1, 4, "A", 4); t.checkClientGotOps(0, 2).checkClientSentOps(0).checkClientDoc("<blip><p>XYaZAbc</p></blip>"); // Empty Server Delta t.init(0); t.serverConnectionMock.triggerServerDeltas(Collections.singletonList( new TransformedWaveletDelta(null, HashedVersion.unsigned(0), 0L, Arrays.<WaveletOperation> asList()))); t.checkClientGotOps().checkClientSentOps(); t.clientDoOps(1); t.checkClientGotOps().checkClientSentOps(0); } /** * Test the CC caches the operations from the server correct when the client is not * ready receive them yet. * * @throws OperationException * @throws TransformException */ public void testServerOperationCache() throws TransformException, OperationException { TestConfig t = new TestConfig(); // c1 /\ s1 // c2 (not given to cc) / t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").clientDoInsert(3, "Y", false).checkClientSentOps(0); t.checkClientDoc("<blip><p>XYabc</p></blip>"); t.serverDoInsert(0, 3, "A", 4).serverDoInsert(1, 4, "B", 4); t.clientMock.flush(); t.checkClientGotOps(0, 2).checkClientSentOps().checkClientDoc("<blip><p>XYaABbc</p></blip>"); // Check that if we don't flush the operations cached in the client, bad things happen // c1 /\ s1 // c2 (not given to cc) / t.init(0, "<blip><p>abc</p></blip>"); t.clientDoInsert(2, "X").clientDoInsert(3, "Y", false).checkClientSentOps(0); t.checkClientDoc("<blip><p>XYabc</p></blip>"); t.serverDoInsert(0, 3, "A", 4); try { t.checkClientGotOps(0, 2).checkClientSentOps().checkClientDoc("<blip><p>XYABabc</p></blip>"); fail("Expected a runtime exception"); } catch (RuntimeException expected) { // Expect an exception because the client ops didn't get transformed // so the expected length of the document is incorrect when applying // server ops. assertTrue(expected.getCause() instanceof OperationException); } } /** * Tests bundling of operations into deltas and queuing of operations while * others are in flight. */ public void testClientOperationQueuing() throws TransformException, OperationException { TestConfig t = new TestConfig(); // Tests queuing of a delta until one which is in flight has been acked, at // which point the waiting delta should be sent. t.init(1); t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); t.serverAck(2).checkClientSentOps(2).checkClientGotOps(1); t.serverAck(3).checkClientSentOps().checkClientGotOps(2); // Tests bundling of operations by the one author into a single delta. t.init(1); t.clientDoOps(new WaveletOperation[] {CLIENT_UTIL.noOp(), CLIENT_UTIL.noOp()}); t.checkClientSentOps(1, 2).checkClientGotOps(); t.serverAck(3, 2).checkClientSentOps().checkClientGotOps(1, 2); // Tests that two operations from differing creators are sent as separate // deltas. In the process, checks again that only one delta is in flight at // a time and that the acking of the first causes the second to be sent. t.init(1); t.clientDoOps(new WaveletOperation[] {CLIENT_UTIL.noOp(), EXTRA_UTIL.noOp()}); t.checkClientSentOps(1, 1).checkClientGotOps(); t.serverAck(2, 1).checkClientSentOps(2, 1).checkClientGotOps(1, 1); t.serverAck(3, 1).checkClientSentOps().checkClientGotOps(2, 1); } /** * Test various ways the operations are interleaved. We don't test they are transformed * as they are tested elsewhere. * @throws OperationException * @throws TransformException */ public void testConcurrencySimulation() throws OperationException, TransformException { TestConfig t = new TestConfig(); // Simple insert from client t.init(0); t.clientDoOps(1).checkClientSentOps(0); t.serverAck(1).checkClientSentOps(); // Simple insert from server t.init(0); t.serverDoOps(0).checkClientGotOps(0).checkClientSentOps(); // No Conflict. Left is client action. Right is server action. // c1 / ack c1 // c2 /\ s1 // c3 (cached) / / ack c2, causes c3' to be sent // s1' \/ ack c3 // \ s2 t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); // Expect a version update op here. t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); t.serverAck(3).checkClientSentOps(3).checkClientGotOps(2); t.serverAck(4).checkClientSentOps().checkClientGotOps(3); t.serverDoOps(4).checkClientSentOps().checkClientGotOps(4); // Conflict. Left is client action. Right is server action. // c1 / ack c1 // \ s1 // c2 /\ s2 // c3 (cached) / / ack c2, causes c3', c4' to be sent // c4 (cached) / // s2''' \ // c5 / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).clientDoOps(1).clientDoOps(1).serverDoOps(2) .checkClientSentOps(2).checkClientGotOps(2); t.serverAck(4).checkClientSentOps(4, 2).checkClientGotOps(3); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); } /** * Test errors in the protocol. */ public void testErrorConditions() throws TransformException, OperationException { TestConfig t = new TestConfig(); // Missing ack // c1 /\ s1 // s1' \ // \ s2 t.init(0); t.clientDoOps(1).serverDoOps(0).checkClientSentOps(0).checkClientGotOps(0); try { t.serverDoOps(2); fail("Suppose to fail with unexpected version"); } catch (TransformException ex) { } // Wrong ack // c1 / // // | ack c2 t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); try { t.serverAck(2); fail("Suppose to fail with unexpected version"); } catch (TransformException ex) { } } /** * test reconnecting to the server. * @throws OperationException * @throws TransformException */ public void testRecovery() throws Exception { TestConfig t = new TestConfig(); // Simple case, but also test breaking connection. // c1 / ack c1 <-- recover from here // c2, c3 / ack c2, c3 // c4 (in flight)/ <-- connection broken // c5 (cached) / // c6 (cached) / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(2).checkClientSentOps(1, 2).checkClientGotOps(); t.serverAck(3, 2).checkClientSentOps().checkClientGotOps(1, 2); t.clientDoOps(1).checkClientSentOps(3).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Break connection t.disconnectFromServer(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 3); // Resend delta containing c2, c3 t.serverDoOpen(1, 3).checkClientSentOps(1, 2); // Simple case // c1 / ack c1 // c2 / ack c2 <-- recover from here // c4 (in flight) / <-- connection broken // c5 (cached) / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 2); // Resend delta containing c4 t.serverDoOpen(2, 2).checkClientSentOps(2, 1); // Simple case // c1 / ack c1 <-- recover from here // c2 / ack c2 // c3 (ack lost) / ack c3 <-- connection broken // c4 (cached) / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 2); t.serverDoOpen(2, 3).checkClientSentOps(); // Resend delta containing c2 t.serverDoEchoBack(2).checkClientSentOps(3); // Server ops, no transformation needed at recovery // c1 / ack c1 // U \ s1 // c2 / ack c2 <-- recover from here (test we chop the inferred server path) // c3 (in flight) / <-- connection broken t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); t.serverAck(3).checkClientSentOps().checkClientGotOps(2); t.clientDoOps(1).checkClientSentOps(3).checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(2, 3); // Resend delta containing c3 t.serverDoOpen(2, 2).checkClientSentOps(2, 1); // Server ops, transformation needed at recovery and no operation comparison need. // c1 / ack c1 <-- recover from here <-- point of resend // c2 (cached) /\ s2 <-- connection broken // \ s3 t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); // Break connection t.disconnectFromServer(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1); t.serverDoOpen(1, 4).checkClientSentOps(1); // Now send back ops on server, need to test comparison here. t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.serverDoOps(2).checkClientSentOps().checkClientGotOps(2); // Server ops, transformation needed at recovery and operation comparison need. // c1 / ack c1 <-- recover from here // c2 (ack lost) /\ s2 <-- connection broken // c3 (cached) / / ack c2 <-- point of resend // \ s3 t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1); t.serverDoOpen(1, 4).checkClientSentOps(); // Now send back ops on server, need to test comparison here. t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.serverDoEchoBack(2).checkClientSentOps(3).checkClientGotOps(2); t.serverDoOps(3).checkClientSentOps().checkClientGotOps(3); // Server ops, transformation needed at recovery and operation comparison need. // c1 / ack c1 <-- recover from here // c2 (in flight) /\ s2 <-- connection broken // c3 (cached) / \ s3 <-- point of resend t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1); t.serverDoOpen(1, 3).checkClientSentOps(); // Now send back ops on server, need to test comparison here. t.serverDoOps(1).checkClientSentOps().checkClientGotOps(1); t.serverDoOps(2).checkClientSentOps(3, 1).checkClientGotOps(2); // Not recoverable, no signatures recognised. // <-- server restarted before inferred server path // c1 / ack c1 // c2 (ack lost) / <-- connection broken t.init(1); // start at version 1 t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(1, 2); // Server doesn't recognize any signature, so it sends the latest signature it knows try { t.serverDoOpen(0, 0); fail("ConnectionFailedException expected"); } catch (ChannelException expected) { } } /** * Mainly to test that we are doing comparison for doc op. */ public void testEchoBackDocumentEquality() throws Exception { TestConfig t = new TestConfig(); // Simple case // c1 / ack c1 // c2 / ack c2 // c3 (ack lost) / ack c3 <-- connection broken, <-- recover from here // c4 (cached) / t.init(0); // Use different blip ids so that we don't merge ops t.clientDoDocOps("blip1").checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoDocOps("blip2").checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.clientDoDocOps("blip3").checkClientSentOps(2).checkClientGotOps(); t.clientDoDocOps("blip4").checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 2); t.serverDoOpen(2, 3).checkClientSentOps(); // Client interprets the echo-back as an ack, which we test by // seeing that it goes on to send c4 t.serverDoEchoBackDocOp(2, "blip3").checkClientSentOps(3); } /** * Test being disconnected several times. */ public void testRecoveryMultipleTimes() throws Exception { TestConfig t = new TestConfig(); // Simple case, but also test breaking connection. // c1 / ack c1 <-- (2) recover from here, (4) recover from here again // c2, c3 / ack c2, c3 // c4 (in flight)/ <-- (1) connection broken, (3) rebroken again // c5 (cached) / // c6 (cached) / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(2).checkClientSentOps(1, 2).checkClientGotOps(); t.serverAck(3, 2).checkClientSentOps().checkClientGotOps(1, 2); t.clientDoOps(1).checkClientSentOps(3).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Break connection t.disconnectFromServer(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 3); // Resend delta containing c1 t.serverDoOpen(1, 1).checkClientSentOps(1, 2); // Break connection again t.disconnectFromServer(); // Reconnect again t.reconnectToServer().checkClientSentOpen(0, 1); // Resend delta containing c1 t.serverDoOpen(1, 1).checkClientSentOps(1, 2); } public void testRecoveryWithCommit() throws Exception { TestConfig t = new TestConfig(); // Reconnect after server commit // c1 / ack c1 <-- server commit, reconnect here. // c2 / ack c2 t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.serverCommit(1); // Reconnect t.reconnectToServer().checkClientSentOpen(1, 2); } /** * Test that the client ignores the timestamp in the echo back op from the server. */ public void testEchobackWithDifferentTimeStamp() throws Exception { TestConfig t = new TestConfig(); // Simple case // c1 / ack c1 <-- recover from here // c2 / ack c2 // c3 (ack lost) / ack c3 <-- connection broken // c4 (cached) / t.init(0); t.clientDoOps(1).checkClientSentOps(0).checkClientGotOps(); t.serverAck(1).checkClientSentOps().checkClientGotOps(0); t.clientDoOps(1).checkClientSentOps(1).checkClientGotOps(); t.serverAck(2).checkClientSentOps().checkClientGotOps(1); t.clientDoOps(1).checkClientSentOps(2).checkClientGotOps(); t.clientDoOps(1).checkClientSentOps().checkClientGotOps(); // Reconnect t.reconnectToServer().checkClientSentOpen(0, 1, 2); t.serverDoOpen(2, 3).checkClientSentOps(); // Using a different timestamp. The client should not care about the timestamp. t.serverDoEchoBack(2, 12345L).checkClientSentOps(3); } /** * Test that a double submit from the same user is nullified. */ public void testDoubleSubmit() throws Exception { TestConfig t = new TestConfig(); // Simple double submit. After recovery, server sends client's own op back then ack // c1 /\ c1 <-- connection broken, <-- recover before here <--- got c1 then ack c1 // c2 (cached) / t.init(0); t.clientDoDocOps("blip1").checkClientSentOps(0).checkClientGotOps(); t.clientDoDocOps("blip2").checkClientSentOps().checkClientGotOps(); // Reconnect before c1 t.reconnectToServer().checkClientSentOpen(0); // Client resending c1 t.serverDoOpen(0, 0).checkClientSentOps(0); // Should not send c2 on echo back, just nullify c1 t.serverDoEchoBackDocOp(0, "blip1").checkClientSentOps().checkClientGotOps(0); // Send c2, once we get an ack on c1 t.serverAck(1, 0).checkClientSentOps(1).checkClientGotOps(); // Slightly more complicated double submit. // c1 / ack <-- (2) recover from here // c2 /\ c2 <-- (1) connection broken <--- (3) got c2 then ack c2 <-- (4) broken again // c3 (cached) / t.init(0); t.clientDoDocOps("blip1").checkClientSentOps(0).checkClientGotOps(); t.clientDoDocOps("blip2").checkClientSentOps().checkClientGotOps(); t.serverAck(1).checkClientSentOps(1).checkClientGotOps(0); t.clientDoDocOps("blip3").checkClientSentOps().checkClientGotOps(); // Reconnect after c1 t.reconnectToServer().checkClientSentOpen(0, 1); // Client resending c2 t.serverDoOpen(1, 1).checkClientSentOps(1); // Should not send c2 on echo back, just nullify c2 t.serverDoEchoBackDocOp(1, "blip2").checkClientSentOps().checkClientGotOps(1); // Send c3, once we get an ack on c2 t.serverAck(2, 0).checkClientSentOps(2).checkClientGotOps(); // Connection broken again at c2. There should be just one signature sent out in open. t.reconnectToServer().checkClientSentOpen(2); } }