/** * 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.concurrencycontrol.channel; import junit.framework.TestCase; import org.waveprotocol.wave.common.logging.AbstractLogger; import org.waveprotocol.wave.common.logging.PrintLogger; import org.waveprotocol.wave.concurrencycontrol.channel.ViewChannel.Listener; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode; import org.waveprotocol.wave.concurrencycontrol.testing.FakeWaveViewServiceUpdate; import org.waveprotocol.wave.concurrencycontrol.testing.MockWaveViewService; import org.waveprotocol.wave.model.id.IdFilter; import org.waveprotocol.wave.model.id.IdFilters; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * Unit test for ViewChannelImpl. * * @author zdwang@google.com (David Wang) */ public class ViewChannelImplTest extends TestCase { /** * This is mock class to test that calls back from ViewChannel are as expected. */ private static class MockViewChannelListener implements Listener { public enum MethodCall { ON_CONNECTED, ON_CLOSED, ON_EXCEPTION, ON_OPEN_FINISHED, ON_UPDATE, ON_SNAPSHOT } public class MethodCallContext { final MethodCall method; final WaveletId waveletId; final ObservableWaveletData snapshot; final List<TransformedWaveletDelta> deltas; final HashedVersion lastCommittedVersion; final HashedVersion currentSignedVersion; public MethodCallContext(MethodCall method) { this.method = method; this.waveletId = null; this.snapshot = null; this.deltas = null; this.lastCommittedVersion = null; this.currentSignedVersion = null; } public MethodCallContext(MethodCall method, WaveletId waveletId, ObservableWaveletData snapshot, HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) { this.method = method; this.waveletId = waveletId; this.snapshot = snapshot; this.deltas = null; this.lastCommittedVersion = lastCommittedVersion; this.currentSignedVersion = currentSignedVersion; } public MethodCallContext(MethodCall method, WaveletId waveletId, List<TransformedWaveletDelta> deltas, HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) { this.method = method; this.waveletId = waveletId; this.snapshot = null; this.deltas = deltas; this.lastCommittedVersion = lastCommittedVersion; this.currentSignedVersion = currentSignedVersion; } public MethodCall method() { return method; } } private final ArrayList<MethodCallContext> methodCalls = new ArrayList<MethodCallContext>(); @Override public void onConnected() { methodCalls.add(new MethodCallContext(MethodCall.ON_CONNECTED)); } @Override public void onClosed() { methodCalls.add(new MethodCallContext(MethodCall.ON_CLOSED)); } @Override public void onException(ChannelException e) { methodCalls.add(new MethodCallContext(MethodCall.ON_EXCEPTION)); } @Override public void onOpenFinished() { methodCalls.add(new MethodCallContext(MethodCall.ON_OPEN_FINISHED)); } @Override public void onSnapshot(WaveletId waveletId, ObservableWaveletData wavelet, HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) { methodCalls.add(new MethodCallContext(MethodCall.ON_SNAPSHOT, waveletId, wavelet, lastCommittedVersion, currentSignedVersion)); } @Override public void onUpdate(WaveletId waveletId, List<TransformedWaveletDelta> deltas, HashedVersion lastCommittedVersion, HashedVersion currentSignedVersion) { methodCalls.add(new MethodCallContext(MethodCall.ON_UPDATE, waveletId, deltas, lastCommittedVersion, currentSignedVersion)); } public void expectedCall(MethodCall method) { assertEquals(method, methodCalls.get(0).method); methodCalls.remove(0); } public void expectedNothing() { assertEquals(0, methodCalls.size()); } /** * We don't test for container message as it's not important. */ public void expectedCall(MethodCall method, WaveletId waveletId) { assertFalse(methodCalls.isEmpty()); MethodCallContext context = methodCalls.get(0); assertEquals(method, context.method); assertEquals(waveletId, context.waveletId); methodCalls.remove(0); } public void clear() { methodCalls.clear(); } } /** * This is mock class to test that calls back from ViewChannel are as expected. */ private static class MockSubmitListener implements SubmitCallback { public enum MethodCall { ON_SUCCESS, ON_FAILURE } public class MethodCallContext { final MethodCall method; final int opsApplied; final HashedVersion version; final String error; public MethodCallContext(MethodCall method, int opsApplied, HashedVersion version) { this(method, null, opsApplied, version); } public MethodCallContext(MethodCall method, String reason, HashedVersion version) { this(method, reason, 0, version); } public MethodCallContext(MethodCall method, String reason) { this(method, reason, 0, HashedVersion.unsigned(0)); } public MethodCallContext(MethodCall method, String reason, int opsApplied, HashedVersion version) { this.method = method; this.error = reason; this.version = version; this.opsApplied = opsApplied; } } ArrayList<MethodCallContext> methodCalls = new ArrayList<MethodCallContext>(); @Override public void onSuccess(int opsApplied, HashedVersion version, ResponseCode responseCode, String errorMessage) { methodCalls.add(new MethodCallContext( MethodCall.ON_SUCCESS, errorMessage, opsApplied, version)); } @Override public void onFailure(String reason) { methodCalls.add(new MethodCallContext(MethodCall.ON_FAILURE, reason)); } public void expectedCall(MethodCall method, String error) { MethodCallContext context = methodCalls.get(0); assertEquals(method, context.method); assertEquals(error, context.error); methodCalls.remove(0); } public void expectedCall(MethodCall method, int opsApplied, HashedVersion version) { MethodCallContext context = methodCalls.get(0); assertEquals(method, context.method); assertEquals(opsApplied, context.opsApplied); assertEquals(version, context.version); methodCalls.remove(0); } } /** * Wavelet id to use in the tests. */ private static final WaveletId WAVELET_ID = WaveletId.of("example.com", "waveletId_1"); /** * Channel Id to be used in the tests. */ private static final String CHANNEL_ID = "channelId_1"; private static final AbstractLogger logger = new PrintLogger(); // // Fields used in most or all tests. // private ViewChannelImpl channel; private MockViewChannelListener viewOpenListener; private MockWaveViewService waveViewService; @Override protected void setUp() { WaveId waveId = WaveId.of("example.com", "waveid"); ViewChannelImpl.setMaxViewChannelsPerWave(Integer.MAX_VALUE); waveViewService = new MockWaveViewService(); viewOpenListener = new MockViewChannelListener(); channel = new ViewChannelImpl(waveId, waveViewService, logger); } /** * Opens the channel from the client side only. */ private void halfOpen() { Map<WaveletId, List<HashedVersion>> knownWavelets = Collections.emptyMap(); channel.open(viewOpenListener, IdFilters.ALL_IDS, knownWavelets); } /** * Simulates the server responding with the channel id. */ private void respondWithChannelId() { waveViewService.lastOpen().callback.onUpdate( new FakeWaveViewServiceUpdate().setChannelId(CHANNEL_ID)); } /** * Simulates the server responding with the open-finished marker. */ private void respondWithMarker(boolean waveEmpty) { waveViewService.lastOpen().callback.onUpdate( new FakeWaveViewServiceUpdate().setMarker(waveEmpty)); } /** * Simulates the server sending a streaming update. * * @param waveletId wavelet to update */ private void respondWithEmptyUpdate(WaveletId waveletId) { waveViewService.lastOpen().callback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(waveletId) .setLastCommittedVersion(HashedVersion.unsigned(0)) .addDelta(new TransformedWaveletDelta(null, HashedVersion.unsigned(0), 0L, Arrays.<WaveletOperation> asList()))); } private void respondToSubmit(HashedVersion version, int opsApplied, String error, ResponseCode response) { waveViewService.lastSubmit().callback.onSuccess(version, opsApplied, error, response); } private void respondToSubmitWithFailure() { waveViewService.lastSubmit().callback.onFailure("WAVE_SERVER_ERROR"); } /** * Opens the channel, and simulates the server responding with a channel and * a marker. */ private void open() { halfOpen(); respondWithChannelId(); respondWithMarker(false); } private void close() { channel.close(); } private void terminateOpenRpcWithSuccess() { waveViewService.lastOpen().callback.onSuccess(null); } private void terminateOpenRpcWithError() { waveViewService.lastOpen().callback.onSuccess("Server error for testing"); } private void terminateOpenRpcWithFailure(String status) { waveViewService.lastOpen().callback.onFailure(status); } private static WaveletDelta emptyDelta() { return new WaveletDelta(null, null, Arrays.<WaveletOperation> asList()); } /** * Test that when everything is ok, we can connect, submit, create wavelet and disconnect. * This is not supposed to be a thorough test. */ public void testSunnyDayScenario() { open(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CONNECTED); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED); // pretend an update with update wavelet. Note we don't add any data in the wavelet // because it's not really relevant for the test. respondWithEmptyUpdate(WAVELET_ID); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_UPDATE, WAVELET_ID); // Submit a delta and check that we have the right channel id remembered. MockSubmitListener submitListener = new MockSubmitListener(); channel.submitDelta(WAVELET_ID, emptyDelta(), submitListener); assertEquals(1, waveViewService.submits.size()); assertEquals(CHANNEL_ID, waveViewService.lastSubmit().channelId); // Return a success message on the submitted delta byte[] hash = new byte[] {1, 2, 3, 4}; respondToSubmit(HashedVersion.of(2, hash), 1, null, ResponseCode.OK); submitListener.expectedCall(MockSubmitListener.MethodCall.ON_SUCCESS, 1, HashedVersion.of(2, hash)); // Check disconnect channel.close(); assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); } /** * Tests that {@link ViewChannelImpl#open(Listener, IdFilter, Map)} * synchronously calls the ViewOpen rpc on its wave service. */ public void testOpenIssuesViewOpenRpc() { open(); assertEquals(1, waveViewService.opens.size()); } /** * Tests that a channel fails if it does not receive a channel id in the first * message. */ public void testInitialUpdateWithoutAChannelIdFails() { halfOpen(); // Receive an update with no channel id. waveViewService.lastOpen().callback.onUpdate(new FakeWaveViewServiceUpdate()); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_EXCEPTION); } /** * Tests that a channel fails if it receives a message before it is opened. */ public void testMessageBeforeOpenThrowsException() { try { channel.onUpdate(new FakeWaveViewServiceUpdate()); fail("Should not be able to receive update without open call"); } catch (IllegalStateException expected) { // Expected. } } public void testSuccessBeforeOpenThrowsException() { try { channel.onSuccess("for testing"); fail("Should not be able to receive onSuccess without open call"); } catch (IllegalStateException expected) { // Expected. } } public void testFailureBeforeOpenThrowsException() { try { channel.onFailure("for testing"); fail("Should not be able to receive onFailure without open call"); } catch (IllegalStateException expected) { // Expected. } } /** * Tests a channel fails if it receives success before channel id. */ public void testSuccessBeforeChannelIdFails() { halfOpen(); channel.onSuccess("for testing"); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_EXCEPTION); } /** * Tests a channel closes if it receives failure before channel id. */ public void testFailureBeforeChannelIdClosesChannel() { halfOpen(); channel.onFailure("for testing"); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); } /** * Tests that an update with the end-marker triggers the open-finished * callback. */ public void testMarkerTriggersOpenFinished() { halfOpen(); respondWithChannelId(); viewOpenListener.clear(); respondWithMarker(false); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED); } /** * Tests that an update with the end-marker triggers the open-finished * callback. */ public void testChannelIdTriggersConnectCallback() { halfOpen(); respondWithChannelId(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CONNECTED); } /** * Tests that updates that arrive before the end-marker are passed on as * updates, and that when the end-marker eventually arrives, open-finished * is triggered then. */ public void testUpdatesBeforeOpenFinishedStillTriggersOpenFinished() { halfOpen(); respondWithChannelId(); viewOpenListener.clear(); respondWithEmptyUpdate(WAVELET_ID); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_UPDATE, WAVELET_ID); respondWithMarker(false); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_OPEN_FINISHED); } /** * Tests that closing the channel after a full open issues a ViewClose rpc. */ public void testCloseAfterChannelIdCallsCloseRpc() { open(); close(); assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); } /** * Tests that closing the channel before the server has responded with a * channel id does not issue a ViewClose rpc. */ public void testCloseWithoutChannelIdDoesNotCallCloseRpc() { halfOpen(); close(); assertEquals(0, waveViewService.closes.size()); } /** * Tests that closing the channel before the server has responded with a * channel id causes a ViewClose rpc to be sent as soon as a channel id * arrives later. */ public void testCloseWithoutChannelIdCallsCloseRpcIfChannelIdArrives() { halfOpen(); close(); assertEquals(0, waveViewService.closes.size()); respondWithChannelId(); // Have got channel id, so we should now have issued a close. assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); } /** * Tests that closing the channel prevents updates that arrive later from * being passed to the channel listener. */ public void testCloseMasksFutureUpdatesFromOpenListener() { open(); close(); viewOpenListener.clear(); respondWithEmptyUpdate(WAVELET_ID); viewOpenListener.expectedNothing(); } public void testCloseTriggersCloseCallback() { open(); viewOpenListener.clear(); close(); // The underlying service is expected to have the following behaviour. terminateOpenRpcWithSuccess(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); } public void testOpenRpcTerminationAfterUpdatesAndCloseTriggersCloseCallback() { open(); respondWithEmptyUpdate(WAVELET_ID); viewOpenListener.clear(); close(); // The service should cause the open rpc to terminate successfully. terminateOpenRpcWithSuccess(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); } public void testOpenRpcTerminationWithoutCloseTriggersFailureAndClose() { open(); viewOpenListener.clear(); terminateOpenRpcWithSuccess(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); } public void testOpenRpcOnFailureAndThenTerminationWithoutCloseTriggersOneFailureAndClose() { open(); viewOpenListener.clear(); terminateOpenRpcWithFailure("WAVE_SERVER_ERROR"); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); terminateOpenRpcWithError(); viewOpenListener.expectedNothing(); } public void testMultipleClose() { open(); viewOpenListener.clear(); close(); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); close(); viewOpenListener.expectedNothing(); } public void testCloseWithoutOpen() { close(); viewOpenListener.expectedNothing(); } public void testOpenAfterOpenThrowsException() { open(); try { channel.open(null, null, null); fail("Should not be able to open again after open is called."); } catch (RuntimeException ex) { // Expected error. } } public void testSubmitDeltaWithErrorMessage() { open(); // submit a delta MockSubmitListener submitListener = new MockSubmitListener(); channel.submitDelta(WAVELET_ID, emptyDelta(), submitListener); // Success with error the submit delta. String errorMessage = "Bad things happened on the server"; respondToSubmit(HashedVersion.of(2, new byte[] {1, 2, 3, 4}), 1, errorMessage, ResponseCode.OK); submitListener.expectedCall(MockSubmitListener.MethodCall.ON_SUCCESS, errorMessage); } /** * Tests that a failure of a ViewSubmit rpc causes the channel to call the * failure callback registered on delta submission. */ public void testDeltaSubmissionFailureCallsSubmissionFailureCallback() { open(); // Submit a delta. MockSubmitListener listener = new MockSubmitListener(); channel.submitDelta(WAVELET_ID, emptyDelta(), listener); // Fail the submit delta, and expect that the submit listened got the error. respondToSubmitWithFailure(); listener.expectedCall(MockSubmitListener.MethodCall.ON_FAILURE, "WAVE_SERVER_ERROR"); } public void testSubmitDeltaOnClosedChannelThrowsIllegalStateException() { open(); close(); // submit a delta should fail try { channel.submitDelta(WAVELET_ID, emptyDelta(), new MockSubmitListener()); fail("Should not be able to submit on a closed channel"); } catch (IllegalStateException ex) { // expect exception } } public void testOpenAfterCloseThrowsIllegalStateException() { open(); close(); // Opening the channel again should fail. try { halfOpen(); fail("Should not be able to open a closed channel"); } catch (IllegalStateException ex) { // expect exception } } public void testFailedOpenCallsListenerFailure() { halfOpen(); // fail the open terminateOpenRpcWithFailure("WAVE_SERVER_ERROR"); viewOpenListener.expectedCall(MockViewChannelListener.MethodCall.ON_CLOSED); } public void testCannotCreateTooManyChannels() { ViewChannelImpl.setMaxViewChannelsPerWave(4); WaveId waveId = WaveId.of("example.com", "toomanywaveid"); for (int i = 0; i < 4; i++) { channel = new ViewChannelImpl(waveId, waveViewService, logger); } try { channel = new ViewChannelImpl(waveId, waveViewService, logger); fail("Should not be allowed to create any more view channels"); } catch (IllegalStateException ex) { // expected } } public void testClosingOneChannelMakesRoomForAnother() { WaveId waveId = WaveId.of("example.com", "makeroomwaveid"); ViewChannelImpl.setMaxViewChannelsPerWave(4); for (int i = 0; i < 4; i++) { channel = new ViewChannelImpl(waveId, waveViewService, logger); } channel.close(); // Close the last channel, making room for another. channel = new ViewChannelImpl(waveId, waveViewService, logger); } }