/** * 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.wave; import junit.framework.TestCase; import org.waveprotocol.wave.common.logging.PrintLogger; import org.waveprotocol.wave.concurrencycontrol.channel.OperationChannelMultiplexerImpl; import org.waveprotocol.wave.concurrencycontrol.channel.ViewChannelImpl; import org.waveprotocol.wave.concurrencycontrol.channel.WaveViewService; 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.document.Document; import org.waveprotocol.wave.model.document.operation.DocInitialization; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.id.IdFilters; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.id.IdGeneratorImpl; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.schema.SchemaCollection; import org.waveprotocol.wave.model.schema.SchemaProvider; import org.waveprotocol.wave.model.testing.DeltaTestUtil; 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.ObservableWavelet; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.ParticipationHelper; import org.waveprotocol.wave.model.wave.WaveViewListener; import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletConfigurator; import java.util.ArrayList; /** * Tests the CcBasedWaveView class. * * @author zdwang@google.com (David Wang) */ public class CcBasedWaveViewTest extends TestCase { private static final WaveId WAVE_ID = WaveId.of("example.com", "waveId_1"); private static final WaveletId GENERATED_WAVELET_ID = WaveletId.of("example.com", "some_id"); private static final WaveletId ROOT_WAVELET_ID = new IdGeneratorImpl("example.com", null).newConversationRootWaveletId(); private static final String GENERATED_BLIP_ID = "some blip id"; private static final ParticipantId USER_ID = new ParticipantId("userId_1@example.com"); private static final SchemaProvider SCHEMAS = SchemaCollection.empty(); private static class MockCcDocument implements CcDocument { public enum MethodCall { FLUSH, AS_OPERATION, CONSUME } public class MethodCallContext { final MethodCall method; /** Saved argument to flush() call */ final Runnable command; /** Saved argument to consume() call. */ final DocOp op; public MethodCallContext(MethodCall method) { this(method, null, null); } public MethodCallContext(MethodCall method, Runnable command) { this(method, command, null); } public MethodCallContext(MethodCall method, DocOp op) { this(method, null, op); } public MethodCallContext(MethodCall method, Runnable command, DocOp op) { this.method = method; this.command = command; this.op = op; } } ArrayList<MethodCallContext> methodCalls = new ArrayList<MethodCallContext>(); /** When flush() is called, the result we return */ boolean flush_return = true; @Override public boolean flush(Runnable resume) { methodCalls.add(new MethodCallContext(MethodCall.FLUSH, resume)); return flush_return; } @Override public DocInitialization asOperation() { methodCalls.add(new MethodCallContext(MethodCall.AS_OPERATION)); return null; } @Override public void consume(DocOp op){ methodCalls.add(new MethodCallContext(MethodCall.CONSUME, op)); } public MethodCallContext expectedCall(MethodCall method) { assertEquals(method, methodCalls.get(0).method); return methodCalls.remove(0); } public void expectedNothing() { assertEquals(0, methodCalls.size()); } public Document getMutableDocument() { return null; } public void init(SilentOperationSink<? super DocOp> sink) { } } private static class MockOpenListener implements CcBasedWaveView.OpenListener { private boolean opened = false; @Override public void onOpenFinished() { opened = true; } } private final MockCcDocument doc = new MockCcDocument(); private final CcBasedWaveViewImpl.CcDocumentFactory<?> docFactory = new CcBasedWaveViewImpl.CcDocumentFactory<CcDocument>() { @Override public CcDocument create(WaveletId waveletId, String blipId, DocInitialization content) { if (blipId.equals(GENERATED_BLIP_ID)) { return doc; } else { return new CcDataDocumentImpl(SCHEMAS.getSchemaForId(waveletId, blipId), content); } } @Override public CcDocument get(WaveletId waveletId, String blipId) { return doc; } }; private class MockWaveViewListener implements WaveViewListener { ObservableWavelet addedWavelet = null; @Override public void onWaveletAdded(ObservableWavelet wavelet) { this.addedWavelet = wavelet; } @Override public void onWaveletRemoved(ObservableWavelet wavelet) { reset(); } public void reset() { addedWavelet = null; } } private final IdGenerator idGenerator = new IdGenerator() { @Override public String newBlipId() { return GENERATED_BLIP_ID; } @Override @Deprecated public String peekBlipId() { return GENERATED_BLIP_ID; } @Override public WaveletId newConversationWaveletId() { return GENERATED_WAVELET_ID; } @Override public WaveletId newConversationRootWaveletId() { return ROOT_WAVELET_ID; } @Override public String newDataDocumentId() { throw new UnsupportedOperationException("Unsupported for test"); } @Override public WaveletId newUserDataWaveletId(String address) { return new IdGeneratorImpl("", null).newUserDataWaveletId(address); } @Override public WaveId newWaveId() { throw new UnsupportedOperationException("Unsupported for test"); } @Override public String newId(String namespace) { throw new UnsupportedOperationException("Unsupported for test"); } @Override public String newUniqueToken() { throw new UnsupportedOperationException("Unsupported for test"); } @Override public String getDefaultDomain() { throw new UnsupportedOperationException("Unsupported for test"); } }; private final PrintLogger logger = new PrintLogger(); /** * {@inheritDoc} */ @Override protected void tearDown() { System.out.println(logger); } /** * Because most of CcBasedWaveView is wiring up the various components, we simply test * that all the wiring are correct here rather than going to exhaustively testing every case. */ public void testSimpleSunnyDayCase() { // This mock accepts all service calls. This is the wrong testing // layer to be placing expectations on the mechanisms of the 3 // layers between the CcBasedWaveView and the underlying WaveViewService. MockWaveViewService waveViewService = new MockWaveViewService(); OperationChannelMultiplexerImpl.LoggerContext loggers = new OperationChannelMultiplexerImpl.LoggerContext(logger, logger, logger, logger); OperationChannelMultiplexerImpl mux = new OperationChannelMultiplexerImpl(WAVE_ID, ViewChannelImpl.factory(waveViewService, logger), WaveletDataImpl.Factory.create(docFactory), loggers, null, new ImmediateExcecutionScheduler(), FakeHashedVersionFactory.INSTANCE); CcBasedWaveView waveView = CcBasedWaveViewImpl.create(docFactory, SCHEMAS, WAVE_ID, USER_ID, mux, IdFilters.ALL_IDS, idGenerator, logger, new BasicWaveletOperationContextFactory(USER_ID), ParticipationHelper.IGNORANT, null, WaveletConfigurator.ADD_CREATOR, DuplexOpSinkFactory.PASS_THROUGH); MockWaveViewListener viewListener = new MockWaveViewListener(); waveView.addListener(viewListener); // Open the view and expect the open call on the service. MockOpenListener openListener = new MockOpenListener(); waveView.open(openListener); assertEquals(1, waveViewService.opens.size()); WaveViewService.OpenCallback openCallback = waveViewService.lastOpen().callback; // Locally create a wavelet. CcBasedWavelet createdWavelet = waveView.createWavelet(); assertEquals(GENERATED_WAVELET_ID, createdWavelet.getId()); assertNotNull(viewListener.addedWavelet); viewListener.reset(); // Prime with channel id. openCallback.onUpdate(new FakeWaveViewServiceUpdate().setChannelId("1")); // Receive a new wavelet from the server. openCallback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(WaveletId.of("example.com", "newServerWavelet_1")) .setWaveletSnapshot(WAVE_ID, USER_ID, 0L, HashedVersion.of(1L, sig(123))) .setLastCommittedVersion(HashedVersion.unsigned(0))); assertNotNull(viewListener.addedWavelet); openCallback.onUpdate(new FakeWaveViewServiceUpdate().setMarker(false)); assertTrue(openListener.opened); // Receive version-zero snapshot for locally generated wavelet. openCallback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(GENERATED_WAVELET_ID) .setWaveletSnapshot(WAVE_ID, USER_ID, 0L, HashedVersion.unsigned(0)) .setLastCommittedVersion(HashedVersion.unsigned(0))); // Ack the configurator's add-participant assertEquals(1, waveViewService.submits.size()); waveViewService.lastSubmit().callback .onSuccess(HashedVersion.of(1L, sig(111)), 1, null, ResponseCode.OK); DeltaTestUtil util = new DeltaTestUtil(USER_ID); // Receive an operation from the server that doesn't affect the document. TransformedWaveletDelta delta1 = util.delta(1L, util.addParticipant(new ParticipantId("reuben@example.com"))); openCallback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(GENERATED_WAVELET_ID).addDelta(delta1)); assertEquals(2, createdWavelet.getParticipantIds().size()); // create an empty blip edit (i.e. implicit creation) and expect flush TransformedWaveletDelta delta2 = util.delta(2L, util.noOpDocOp(GENERATED_BLIP_ID)); openCallback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(GENERATED_WAVELET_ID).addDelta(delta2)); doc.expectedCall(MockCcDocument.MethodCall.FLUSH); doc.expectedCall(MockCcDocument.MethodCall.CONSUME); doc.expectedNothing(); // now hold back flush on another server op doc.flush_return = false; TransformedWaveletDelta delta3 = util.delta(3L, util.noOpDocOp(GENERATED_BLIP_ID)); openCallback.onUpdate(new FakeWaveViewServiceUpdate() .setWaveletId(GENERATED_WAVELET_ID).addDelta(delta3)); MockCcDocument.MethodCallContext callContext = doc.expectedCall(MockCcDocument.MethodCall.FLUSH); doc.expectedNothing(); // now we are ready to get the ops doc.flush_return = true; callContext.command.run(); doc.expectedCall(MockCcDocument.MethodCall.FLUSH); doc.expectedCall(MockCcDocument.MethodCall.CONSUME); doc.expectedNothing(); // close the view waveView.close(); assertEquals(1, waveViewService.closes.size()); waveViewService.lastClose().callback.onSuccess(); } private static byte[] sig(int s) { return new byte[] { (byte) s, (byte) s, (byte) s, (byte) s }; } }