/** * 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.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.OperationChannelMultiplexerImpl.LoggerContext; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode; import org.waveprotocol.wave.concurrencycontrol.common.UnsavedDataListener; import org.waveprotocol.wave.concurrencycontrol.common.UnsavedDataListenerFactory; import org.waveprotocol.wave.concurrencycontrol.testing.FakeWaveViewServiceUpdate; import org.waveprotocol.wave.concurrencycontrol.testing.MockWaveViewService; 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.AddParticipant; import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext; import org.waveprotocol.wave.model.testing.BasicFactories; import org.waveprotocol.wave.model.testing.FakeHashedVersionFactory; import org.waveprotocol.wave.model.util.ImmediateExcecutionScheduler; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot; /** * Tests that the CC stack provides correct unsaved data info. * * @author jochen@google.com (Jochen Bekmann) * @author anorth@google.com (Alex North) */ public class UnsavedDataUpdateTest extends TestCase { private static final class FakeUnsavedDataListener implements UnsavedDataListener { public int unacknowledgedSize = 0; public int uncommittedSize; public int inFlight = 0; public int closeCalls = 0; private long lastAckVersion = 0; private long lastCommitVersion = 0; @Override public void onUpdate(UnsavedDataInfo unsavedDataInfo) { this.unacknowledgedSize = unsavedDataInfo.estimateUnacknowledgedSize(); this.uncommittedSize = unsavedDataInfo.estimateUncommittedSize(); this.inFlight = unsavedDataInfo.inFlightSize(); this.lastAckVersion = unsavedDataInfo.laskAckVersion(); this.lastCommitVersion = unsavedDataInfo.lastCommitVersion(); } @Override public synchronized void onClose(boolean everythingCommitted) { ++closeCalls; } } /** ID of wavelets for testing. */ private static final WaveletId WAVELET_ID_1 = WaveletId.of("example.com", "conv+1"); private static final WaveletId WAVELET_ID_2 = WaveletId.of("example.com", "conv+2"); private static final byte[] SIGNATURE1 = new byte[] { 1, 1, 1, 1 }; private static final byte[] SIGNATURE2 = new byte[] { 2, 2, 2, 2 }; private static final byte[] SIGNATURE3 = new byte[] { 3, 3, 3, 3 }; /** ID of wave for testing. */ private static final WaveId WAVE_ID = WaveId.of("example.com", "w+1"); /** Channel Id for testing. */ private static final String CHANNEL_ID = "channelId_1"; /** User name for testing. */ private static final ParticipantId USER_NAME = new ParticipantId("example@example.com"); private static final AbstractLogger logger = new PrintLogger(); private static final LoggerContext LOGGERS = new LoggerContext(logger, logger, logger, logger); private static final ObservableWaveletData.Factory<?> DATA_FACTORY = BasicFactories.waveletDataImplFactory(); private OperationChannelMultiplexerImpl mux; private ViewChannelFactory viewFactory; private MockWaveViewService waveViewService; private MockMuxListener muxListener; private FakeUnsavedDataListener fakeUnsavedDataListener; @Override protected void setUp() { ViewChannelImpl.setMaxViewChannelsPerWave(Integer.MAX_VALUE); waveViewService = new MockWaveViewService(); viewFactory = ViewChannelImpl.factory(waveViewService, logger); fakeUnsavedDataListener = new FakeUnsavedDataListener(); mux = new OperationChannelMultiplexerImpl(WAVE_ID, viewFactory, DATA_FACTORY, LOGGERS, new UnsavedDataListenerFactory() { public UnsavedDataListener create(WaveletId dummy) { return fakeUnsavedDataListener; } public void destroy(WaveletId waveletId) { } }, new ImmediateExcecutionScheduler(), FakeHashedVersionFactory.INSTANCE); muxListener = new MockMuxListener(); } /** * Create a wave, send Op which is unacked and expect unsaved data. */ public void testCreatingWave() throws ChannelException { // Connect to the server mux.open(muxListener, IdFilters.ALL_IDS); muxListener.verifyNoMoreInteractions(); OperationChannel channel = createOperationChannel(WAVELET_ID_1, USER_NAME); channel.send(createAddParticipantOp()); // Expect the delta channel to not be ready, so we expect an open call, but // no success on open and no submit call going through. assertEquals(1, waveViewService.opens.size()); assertEquals(0, waveViewService.submits.size()); muxListener.verifyNoMoreInteractions(); // We do expect unsaved data. assertUnsavedDataInfo(1, 1, 0, 0, 0); // Now bail out of the connection with an op pending, should close. mux.close(); assertEquals(1, fakeUnsavedDataListener.closeCalls); } /** * Open a wave; send an Op; wait for the ack; wait for the commit with no outstanding ops. */ public void testCommitNotification() throws ChannelException { // Connect to the server mux.open(muxListener, IdFilters.ALL_IDS); assertEquals(1, waveViewService.opens.size()); WaveViewService.OpenCallback openCallback = waveViewService.lastOpen().callback; // Pretend we got the initial snapshot at version 1 signature1. muxListener.verifyNoMoreInteractions(); openCallback.onUpdate(new FakeWaveViewServiceUpdate().setChannelId(CHANNEL_ID)); FakeWaveViewServiceUpdate update = new FakeWaveViewServiceUpdate() .setWaveletId(WAVELET_ID_1) .setWaveletSnapshot(WAVE_ID, USER_NAME, 0L, HashedVersion.of(1L, SIGNATURE1)) .setLastCommittedVersion(HashedVersion.unsigned(0)); openCallback.onUpdate(update); OperationChannel channel = muxListener.verifyOperationChannelCreated( update.getWaveletSnapshot(), Accessibility.READ_WRITE); openCallback.onUpdate(new FakeWaveViewServiceUpdate().setMarker(false)); muxListener.verifyOpenFinished(); channel.send(createAddParticipantOp()); assertEquals(1, waveViewService.submits.size()); assertUnsavedDataInfo(1, 1, 1, 0, 0); // Server ack's the previous add participant op. HashedVersion v2 = HashedVersion.of(2, SIGNATURE2); WaveViewService.SubmitCallback submitCallback = waveViewService.lastSubmit().callback; submitCallback.onSuccess(v2, 1, null, ResponseCode.OK); assertUnsavedDataInfo(0, 1, 0, 2, 0); // Server sends commit for the add participant op. openCallback.onUpdate( new FakeWaveViewServiceUpdate().setWaveletId(WAVELET_ID_1).setLastCommittedVersion(v2)); // Now we expect to be told everything is committed. assertUnsavedDataInfo(0, 0, 0, 2, 2); // Send another op, now it should be uncommitted. channel.send(createAddParticipantOp()); assertEquals(2, waveViewService.submits.size()); assertUnsavedDataInfo(1, 1, 1, 2, 2); // Now bail out of the connection with an op pending, should close. mux.close(); assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); assertEquals(1, fakeUnsavedDataListener.closeCalls); } /** * Open a wave; send an Op; wait for the ack; send another Op; get ack; get LCV for first ack; * get a LCV for second ack. */ public void testLastCommittedVersionUpdate() throws ChannelException { // Connect to the server mux.open(muxListener, IdFilters.ALL_IDS); assertEquals(1, waveViewService.opens.size()); WaveViewService.OpenCallback openCallback = waveViewService.lastOpen().callback; // Pretend we got the initial snapshot at version 1 signature1. muxListener.verifyNoMoreInteractions(); openCallback.onUpdate(new FakeWaveViewServiceUpdate().setChannelId(CHANNEL_ID)); FakeWaveViewServiceUpdate update = new FakeWaveViewServiceUpdate() .setWaveletId(WAVELET_ID_1) .setWaveletSnapshot(WAVE_ID, USER_NAME, 0L, HashedVersion.of(1L, SIGNATURE1)) .setLastCommittedVersion(HashedVersion.unsigned(0)); openCallback.onUpdate(update); OperationChannel channel = muxListener.verifyOperationChannelCreated( update.getWaveletSnapshot(), Accessibility.READ_WRITE); openCallback.onUpdate(new FakeWaveViewServiceUpdate().setMarker(false)); muxListener.verifyOpenFinished(); // Send Op, get ack. channel.send(createAddParticipantOp()); assertEquals(1, waveViewService.submits.size()); WaveViewService.SubmitCallback submitCallback1 = waveViewService.lastSubmit().callback; HashedVersion v2 = HashedVersion.of(2, SIGNATURE2); submitCallback1.onSuccess(v2, 1, null, ResponseCode.OK); assertUnsavedDataInfo(0, 1, 0, 2, 0); // Send another Op, get ack. channel.send(createAddParticipantOp()); assertEquals(2, waveViewService.submits.size()); WaveViewService.SubmitCallback submitCallback2 = waveViewService.lastSubmit().callback; HashedVersion v3 = HashedVersion.of(3, SIGNATURE3); submitCallback2.onSuccess(v3, 1, null, ResponseCode.OK); assertUnsavedDataInfo(0, 2, 0, 3, 0); // Server sends commit for the first addParticipant op. openCallback.onUpdate( new FakeWaveViewServiceUpdate().setWaveletId(WAVELET_ID_1).setLastCommittedVersion(v2)); // Now we expect to be told that NOT everything is committed. assertUnsavedDataInfo(0, 1, 0, 3, 2); // Server sends commit for the second addParticipant op. openCallback.onUpdate( new FakeWaveViewServiceUpdate().setWaveletId(WAVELET_ID_1).setLastCommittedVersion(v3)); // Now we expect to be told that everything is committed. assertUnsavedDataInfo(0, 0, 0, 3, 3); mux.close(); assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); assertEquals(1, fakeUnsavedDataListener.closeCalls); } public void testMuxCloseClosesAllUnsavedDataUpdaters() { mux.open(muxListener, IdFilters.ALL_IDS); MockOperationChannelListener listener1 = new MockOperationChannelListener(); MockOperationChannelListener listener2 = new MockOperationChannelListener(); OperationChannel ch1 = createOperationChannel(WAVELET_ID_1, USER_NAME); OperationChannel ch2 = createOperationChannel(WAVELET_ID_2, USER_NAME); ch1.setListener(listener1); ch2.setListener(listener2); mux.close(); assertEquals(2, fakeUnsavedDataListener.closeCalls); } private AddParticipant createAddParticipantOp(String participantName) { AddParticipant addPart = new AddParticipant(new WaveletOperationContext( USER_NAME, -1L, 0L), new ParticipantId(participantName)); return addPart; } private AddParticipant createAddParticipantOp() { return createAddParticipantOp("thedude@google.com"); } private OperationChannel createOperationChannel(WaveletId waveletId, ParticipantId address) { mux.createOperationChannel(waveletId, address); return muxListener.verifyOperationChannelCreated( DATA_FACTORY.create( new EmptyWaveletSnapshot(WAVE_ID, waveletId, address, HashedVersion.unsigned(0), 1273307837000L /* arbitrary time */)), Accessibility.READ_WRITE); } private void assertUnsavedDataInfo(int unacknowledged, int uncommitted, int inFlight, long lastAckVersion, long lastCommitVersion) { assertEquals(unacknowledged, fakeUnsavedDataListener.unacknowledgedSize); assertEquals(uncommitted, fakeUnsavedDataListener.uncommittedSize); assertEquals(inFlight, fakeUnsavedDataListener.inFlight); assertEquals(lastAckVersion, fakeUnsavedDataListener.lastAckVersion); assertEquals(lastCommitVersion, fakeUnsavedDataListener.lastCommitVersion); } }