Package org.waveprotocol.wave.concurrencycontrol.channel

Source Code of org.waveprotocol.wave.concurrencycontrol.channel.OperationChannelMultiplexerImplTest

/**
* 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.OperationChannelMultiplexer.KnownWavelet;
import org.waveprotocol.wave.concurrencycontrol.channel.OperationChannelMultiplexerImpl.LoggerContext;
import org.waveprotocol.wave.concurrencycontrol.common.ChannelException;
import org.waveprotocol.wave.concurrencycontrol.common.CorruptionDetail;
import org.waveprotocol.wave.concurrencycontrol.common.Recoverable;
import org.waveprotocol.wave.concurrencycontrol.common.UnsavedDataListener;
import org.waveprotocol.wave.concurrencycontrol.common.UnsavedDataListenerFactory;
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.NoOp;
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.testing.BasicFactories;
import org.waveprotocol.wave.model.testing.DeltaTestUtil;
import org.waveprotocol.wave.model.testing.FakeHashedVersionFactory;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.ImmediateExcecutionScheduler;
import org.waveprotocol.wave.model.util.Scheduler;
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;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* Test for the multiplexer.
*
* @author zdwang@google.com (David Wang)
* @author anorth@google.com (Alex North)
*/

public class OperationChannelMultiplexerImplTest extends TestCase {

  // TODO(anorth): this really tests the whole client end of the stack.
  // Make some simpler tests just for the mux.

  /**
   * Holds information about a single channel from a mux.
   */
  private static final class ConnectionInfo {
    public final WaveletId waveletId;
    public final long initialVersion;
    public final byte[] initialSignature;
    public final ObservableWaveletData snapshot;
    public final HashedVersion initialHashedVersion;

    public ConnectionInfo(WaveletId waveletId, long initialVersion, byte[] initialSignature) {
      this.waveletId = waveletId;
      this.initialVersion = initialVersion;
      this.initialSignature = initialSignature;
      this.snapshot = createSnapshot(waveletId, initialVersion, initialSignature);
      this.initialHashedVersion = HashedVersion.of(initialVersion, initialSignature);
    }
  }

  private static final class ConnectedChannel {
    public final OperationChannel channel;
    public final MockOperationChannelListener listener;

    public ConnectedChannel(OperationChannel channel, MockOperationChannelListener listener) {
      this.channel = channel;
      this.listener = listener;
    }
  }

  private static class FakeScheduler implements Scheduler {
    Scheduler.Command command;

    @Override
    public void reset() {
      // Do nothing
    }

    @Override
    public boolean schedule(Command command) {
      this.command = command;
      return true;
    }
  }

  /** IDs of wavelets for testing. */
  private static final WaveletId WAVELET_ID_1 = WaveletId.of("example.com","w+1234");
  private static final WaveletId WAVELET_ID_2 = WaveletId.of("example.com","w+5678");

  /** ID of wave for testing. */
  private static final WaveId WAVE_ID = WaveId.of("example.com", "waveId_1");

  /** User name for testing. */
  private static final ParticipantId USER_ID = ParticipantId.ofUnsafe("fred@example.com");

  private static final byte[] NOSIG = new byte[0];
  private static final byte[] SIG1 = new byte[] { 1, 1, 1, 1 };
  private static final byte[] SIG2 = new byte[] { 2, 2, 2, 2 };
  private static final byte[] SIG3 = new byte[] { 3, 3, 3, 3 };
  private static final byte[] SIG4 = new byte[] { 4, 4, 4, 4 };
  private static final byte[] SIG5 = new byte[] { 5, 5, 5, 5 };
  private static final byte[] SIG6 = new byte[] { 6, 6, 6, 6 };
  private static final byte[] SIG7 = new byte[] { 7, 7, 7, 7 };


  private static final Map<WaveletId, List<HashedVersion>> NO_KNOWN_WAVELETS =
    Collections.<WaveletId, List<HashedVersion>>emptyMap();

  private static final AbstractLogger logger = new PrintLogger();

  private static final LoggerContext LOGGERS = new LoggerContext(logger, logger, logger, logger);
  private static final DeltaTestUtil testUtil = new DeltaTestUtil(USER_ID);
  private static final ObservableWaveletData.Factory<?> DATA_FACTORY =
      BasicFactories.waveletDataImplFactory();

  private OperationChannelMultiplexerImpl mux;
  private MockViewChannel.Factory viewFactory;
  private MockMuxListener muxListener;

  @Override
  public void setUp() {
    ViewChannelImpl.setMaxViewChannelsPerWave(Integer.MAX_VALUE);
    viewFactory = new MockViewChannel.Factory();
    UnsavedDataListenerFactory fakeListenerFactory = new UnsavedDataListenerFactory() {

      @Override
      public UnsavedDataListener create(WaveletId waveletId) {
        return null;
      }

      @Override
      public void destroy(WaveletId waveletId) {
      }
    };
    mux = new OperationChannelMultiplexerImpl(WAVE_ID, viewFactory, DATA_FACTORY, LOGGERS,
        fakeListenerFactory, new ImmediateExcecutionScheduler(), FakeHashedVersionFactory.INSTANCE);
    muxListener = new MockMuxListener();
  }

  public void testMuxOpenOpensView() {
    // Connect to the server.
    MockViewChannel viewChannel = viewFactory.expectCreate();
    viewChannel.expectOpen(IdFilters.ALL_IDS, NO_KNOWN_WAVELETS);
    mux.open(muxListener, IdFilters.ALL_IDS);
    viewChannel.takeListener().onConnected();
    viewChannel.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testReceivedSnapshotOpensChannel() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    muxListener.verifyNoMoreInteractions();
    viewListener.onConnected();

    ConnectedChannel opChannel = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);
    viewChannel.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
    // Also proves that an expected (initial) snapshot doesn't clobber the channel
  }

  public void testReceivedSnapshotClobbersExistingChannel() throws ChannelException {
    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    muxListener.verifyNoMoreInteractions();
    viewListener.onConnected();

    ObservableWaveletData snapshotUpdate = createSnapshot(WAVELET_ID_1, 1, SIG1);
    HashedVersion committed = HashedVersion.unsigned(0);

    viewListener.onSnapshot(WAVELET_ID_1, snapshotUpdate, committed, null);
    OperationChannel ch = muxListener.verifyOperationChannelCreated(snapshotUpdate,
        Accessibility.READ_WRITE);

    // The second snapshot may have a non-zero version.
    committed = HashedVersion.unsigned(1);
    ch = checkSendClobberingSnapshot(viewListener, ch, snapshotUpdate, committed);

    // Repeat the clobbering with a higher version.
    committed = HashedVersion.unsigned(2000);
    checkSendClobberingSnapshot(viewListener, ch, snapshotUpdate, committed);
  }

  public void testReceivedSnapshotClobbersOnlyAppropriateChannel() throws ChannelException {
    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    muxListener.verifyNoMoreInteractions();
    viewListener.onConnected();

    ObservableWaveletData snapshotUpdate = createSnapshot(WAVELET_ID_1, 1, SIG1);
    HashedVersion committed = HashedVersion.unsigned(0);

    viewListener.onSnapshot(WAVELET_ID_1, snapshotUpdate, committed, null);
    OperationChannel ch =
        muxListener.verifyOperationChannelCreated(snapshotUpdate, Accessibility.READ_WRITE);

    // This snapshot should clobber wavelet 1's channel
    committed = HashedVersion.of(1, SIG1);
    checkSendClobberingSnapshot(viewListener, ch, snapshotUpdate, committed);
    muxListener.verifyNoMoreInteractions();

    // This snapshot should only create a new channel and nothing else
    viewListener.onSnapshot(WAVELET_ID_2, snapshotUpdate, committed, null);
    muxListener.verifyOperationChannelCreated(snapshotUpdate, Accessibility.READ_WRITE);
    muxListener.verifyNoMoreInteractions();
  }

  public void testOpsReceivedAndChannelClobbered() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final int serverOps = 1;
    final byte[] finalSignature = SIG2;

    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    viewListener.onConnected();
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);

    // Receive a delta.
    checkReceiveDelta(viewListener, ch.channel, ch.listener, WAVELET_ID_1,
        chInfo.initialVersion, serverOps, finalSignature);
    viewChannel.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();

    // Now receive a snapshot and it should clobber the existing channel
    HashedVersion committed = HashedVersion.of(1000000L, SIG3);
    ObservableWaveletData update = createSnapshot(WAVELET_ID_1, committed.getVersion(),
        committed.getHistoryHash());
    checkSendClobberingSnapshot(viewListener, ch.channel, update, committed);
  }

  public void testOpReceivedOnChannel() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final int serverOps = 1;
    final byte[] finalSignature = SIG2;

    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    viewListener.onConnected();
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);

    // Receive a delta.
    checkReceiveDelta(viewListener, ch.channel, ch.listener, WAVELET_ID_1,
        chInfo.initialVersion, serverOps, finalSignature);
    viewChannel.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testChannelSendSubmitsToView() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel viewChannel = openMux();
    ViewChannel.Listener viewListener = viewChannel.takeListener();
    viewListener.onConnected();

    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);

    // Send an operation and check view submission
    WaveletOperation op = createOp();
    WaveletDelta delta = createDelta(chInfo.initialHashedVersion, op);
    viewChannel.expectSubmitDelta(WAVELET_ID_1, delta);
    ch.channel.send(op);
    viewChannel.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testAckResultsInChannelOp() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final byte[] finalSignature = SIG2;

    MockViewChannel view = openMux();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);

    checkSendDelta(view, ch.channel, chInfo.initialHashedVersion, WAVELET_ID_1);
    checkAckDelta(view, ch.channel, ch.listener, 1, 2, finalSignature);
    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testMultipleChannelsAreIndependent() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final ConnectionInfo chInfo2 = new ConnectionInfo(WAVELET_ID_2, 20, SIG2);

    MockViewChannel view = openMux();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshot.
    ConnectedChannel ch1 = connectChannelSnapshot(viewListener, chInfo1);
    ConnectedChannel ch2 = connectChannelSnapshot(viewListener, chInfo2);
    triggerAndCheckOpenFinished(viewListener);

    // Check channels receive ops independently.
    final int serverOps1 = 5;
    checkReceiveDelta(viewListener, ch1.channel, ch1.listener, WAVELET_ID_1, chInfo1.initialVersion,
        serverOps1, SIG4);
    ch2.listener.checkOpsReceived(0);

    final int serverOps2 = 7;
    checkReceiveDelta(viewListener, ch2.channel, ch2.listener, WAVELET_ID_2, chInfo2.initialVersion,
        serverOps2, SIG5);
    ch1.listener.checkOpsReceived(0);

    // Check channels send ops independently.
    checkSendDelta(view, ch1.channel, HashedVersion.of(chInfo1.initialVersion + serverOps1, SIG4),
        WAVELET_ID_1);
    checkSendDelta(view, ch2.channel, HashedVersion.of(chInfo2.initialVersion + serverOps2, SIG5),
        WAVELET_ID_2);

    // Check acks are received independently.
    final byte[] ackSignature1 = SIG6;
    final byte[] ackSignature2 = SIG7;
    final long ackVersion1 = chInfo1.initialVersion + serverOps1 + 1;
    final long ackVersion2 = chInfo2.initialVersion + serverOps2 + 1;
    checkAckDelta(view, ch1.channel, ch1.listener, 1, ackVersion1, ackSignature1);
    checkAckDelta(view, ch2.channel, ch2.listener, 1, ackVersion2, ackSignature2);
    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testMuxCloseClosesViewAndChannels() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final ConnectionInfo chInfo2 = new ConnectionInfo(WAVELET_ID_2, 20, SIG2);

    MockViewChannel view = openMux();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch1 = connectChannelSnapshot(viewListener, chInfo1);
    ConnectedChannel ch2 = connectChannelSnapshot(viewListener, chInfo2);
    triggerAndCheckOpenFinished(viewListener);

    view.expectClose();
    mux.close();

    // Receive lagging delta from view channel, expect nothing.
    final List<TransformedWaveletDelta> update = createServerDeltaList(1, 1, SIG4);
    viewListener.onUpdate(chInfo1.waveletId, update, null, null);
    ch1.listener.checkOpsReceived(0);

    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testMuxFailCallbackThrowsException() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    OperationChannelMultiplexer.Listener closingListener =
        new OperationChannelMultiplexer.Listener() {
          public void onFailed(CorruptionDetail detail) {
            // Attempt to close the mux *during* the failure callback.
            mux.close();
          }

          public void onOpenFinished() {
          }

          public void onOperationChannelCreated(OperationChannel channel,
              ObservableWaveletData snapshotMetadata,
              Accessibility accessibility) {
          }

          public void onOperationChannelRemoved(
              OperationChannel channel, WaveletId waveletId) {
          }
        };

    MockViewChannel view = viewFactory.expectCreate();
    view.expectOpen(IdFilters.ALL_IDS, NO_KNOWN_WAVELETS);
    mux.open(muxListener, IdFilters.ALL_IDS);

    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshot.
    ConnectedChannel ch1 = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // Fail a channel, which will fail the mux.
    ObservableWaveletData update = createSnapshot(WAVELET_ID_1, 1, SIG1);
    try {
      viewListener.onSnapshot(WAVELET_ID_1, update, null, null);
      fail("Expected exception on bad first message");
    } catch (ChannelException ex) {
      // Expected
    }
  }

  /**
   * Tests that the mux ignores known wavelets that don't match the
   * wavelet filter, hence will never receive updates from the server.
   */
  public void testOpenWithKnownWaveletsIgnoresFilteredWavelets() {
    long knownVersion = 40;
    byte[] knownSig = SIG1;
    IdFilter onlyWavelet1 = IdFilter.ofPrefixes("w+1");

    ObservableWaveletData knownSnapshot1 = createSnapshot(WAVELET_ID_1, knownVersion, knownSig);
    ObservableWaveletData knownSnapshot2 = createSnapshot(WAVELET_ID_2, 0, NOSIG);

    MockViewChannel view = viewFactory.expectCreate();
    Map<WaveletId, List<HashedVersion>> expectedSigs = createKnownVersions(WAVELET_ID_1,
        knownVersion, knownSig);
    view.expectOpen(onlyWavelet1, expectedSigs);
    mux.open(muxListener, onlyWavelet1, Arrays.asList(
        createKnownWavelet(knownSnapshot1, knownVersion, knownSig, Accessibility.READ_WRITE),
        createKnownWavelet(knownSnapshot2, 0, NOSIG, Accessibility.READ_WRITE)));
    view.checkExpectationsSatisified();
  }

  public void testOpenWithKnownWaveletWaitsForReconnection() throws ChannelException {
    long knownVersion = 40;
    byte[] knownSig = SIG1;

    ObservableWaveletData knownSnapshot = createSnapshot(WAVELET_ID_1, knownVersion, knownSig);
    MockViewChannel view = openMuxWithKnownWavelet(knownSnapshot);

    // The channel is "connected" though the underlying view isn't.
    ConnectedChannel ch = expectConnectedChannel(knownSnapshot, Accessibility.READ_WRITE);
    checkOpenFinished();

    // Attempt to send a client op. Submission should be held until the view
    // connects and the channel receives the empty reconnection delta.
    WaveletOperation clientOp = createOp();
    WaveletDelta delta = createDelta(HashedVersion.of(knownVersion, knownSig), clientOp);
    ch.channel.send(clientOp);
    ch.listener.checkOpsReceived(0);

    // Connect the underlying view.
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive the reconnection delta, expect the client delta submission.
    view.expectSubmitDelta(WAVELET_ID_1, delta);
    reconnectChannel(viewListener, WAVELET_ID_1, knownVersion, knownSig);
    // Don't expect this empty delta from the channel.
    ch.listener.checkOpsReceived(0);
    assertNull(ch.channel.receive());

    checkAckDelta(view, ch.channel, ch.listener, 1, knownVersion + 1, SIG2);

    // No snapshot, but check we can receive and send deltas on the channel.
    checkReceiveAndSend(viewListener, view, ch, WAVELET_ID_1, knownVersion + 1);
    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  /**
   * Tests that the mux dies if the server doesn't respond with a reconnection
   * version for every reconnecting wavelet.
   */
  public void testOpenWithKnownWaveletsFailsIfServerDoesntKnow() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final ConnectionInfo chInfo2 = new ConnectionInfo(WAVELET_ID_2, 20, SIG2);

    Map<WaveletId, List<HashedVersion>> expectedSigs = createKnownVersions(WAVELET_ID_1,
        chInfo1.initialVersion, chInfo1.initialSignature, WAVELET_ID_2, chInfo2.initialVersion,
        chInfo2.initialSignature);
    Collection<KnownWavelet> knownWavelets = Arrays.asList(
        createKnownWavelet(chInfo1.snapshot, chInfo1.initialVersion, chInfo1.initialSignature,
            Accessibility.READ_WRITE),
        createKnownWavelet(chInfo2.snapshot, chInfo2.initialVersion, chInfo2.initialSignature,
            Accessibility.READ_WRITE));

    // Connect to the server.
    MockViewChannel view = viewFactory.expectCreate();
    view.expectOpen(IdFilters.ALL_IDS, expectedSigs);
    mux.open(muxListener, IdFilters.ALL_IDS, knownWavelets);

    // The mux appears "connected" though the underlying view isn't.
    ConnectedChannel ch1 = expectConnectedChannel(chInfo1.snapshot, Accessibility.READ_WRITE);
    ConnectedChannel ch2 = expectConnectedChannel(chInfo2.snapshot, Accessibility.READ_WRITE);
    checkOpenFinished();

    // Connect the underlying view.
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive one reconnection delta.
    final List<TransformedWaveletDelta> ch1reconnect = createServerDeltaList(
        chInfo1.initialVersion, 0, chInfo1.initialSignature);
    viewListener.onUpdate(WAVELET_ID_1, ch1reconnect,
        HashedVersion.of(chInfo1.initialVersion, chInfo1.initialSignature), null);

    // Receive openFinished before reconnection delta for second wavelet,
    // expect turbulence.
    try {
      viewListener.onOpenFinished();
      fail("Expected a channel exception");
    } catch (ChannelException ex) {
      // Expected.
    }
  }

  /**
   * Tests that the mux survives if the server doesn't respond with a reconnection
   * version for an inaccessible wavelet.
   */
  public void testOpenWithKnownWaveletsSucceedsIfServerDoesntKnowInaccessibleWavelet()
      throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final ConnectionInfo chInfo2 = new ConnectionInfo(WAVELET_ID_2, 20, SIG2);

    // Only the accessible wavelet will resync.
    Map<WaveletId, List<HashedVersion>> expectedSigs = createKnownVersions(WAVELET_ID_1,
        chInfo1.initialVersion, chInfo1.initialSignature);
    Collection<KnownWavelet> knownWavelets = Arrays.asList(
        createKnownWavelet(chInfo1.snapshot, chInfo1.initialVersion, chInfo1.initialSignature,
            Accessibility.READ_WRITE),
        createKnownWavelet(chInfo2.snapshot, chInfo2.initialVersion, chInfo2.initialSignature,
            Accessibility.INACCESSIBLE));

    // Connect to the server.
    MockViewChannel view = viewFactory.expectCreate();
    view.expectOpen(IdFilters.ALL_IDS, expectedSigs);
    mux.open(muxListener, IdFilters.ALL_IDS, knownWavelets);

    // The mux appears "connected" though the underlying view isn't.
    ConnectedChannel ch1 = expectConnectedChannel(chInfo1.snapshot, Accessibility.READ_WRITE);
    ConnectedChannel ch2 = expectConnectedChannel(chInfo2.snapshot, Accessibility.INACCESSIBLE);
    checkOpenFinished();

    // Connect the underlying view.
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive a reconnection delta for the accessible wavelet.
    final List<TransformedWaveletDelta> ch1reconnect = createServerDeltaList(
        chInfo1.initialVersion, 0, chInfo1.initialSignature);
    viewListener.onUpdate(WAVELET_ID_1, ch1reconnect,
        HashedVersion.of(chInfo1.initialVersion, chInfo1.initialSignature), null);

    // Receive openFinished in the absence of a reconnection delta for the
    // inaccessible wavelet, expect bliss.
    viewListener.onOpenFinished();

    // Check sending ops to the disconnected wavelet fails.
    WaveletOperation op = createOp();
    try {
      ch2.channel.send(op);
      fail("Expected a channel exception");
    } catch (ChannelException expected) {
    }

    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testNewWaveletSuppressesSnapshot() throws ChannelException {
    MockViewChannel view = openMux();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();
    triggerAndCheckOpenFinished(viewListener);

    ConnectedChannel ch = createOperationChannel(WAVELET_ID_1, USER_ID);

    // Send and ack first op.
    checkSendDelta(view, ch.channel, HashedVersion.unsigned(0), WAVELET_ID_1);
    view.checkExpectationsSatisified();
    checkAckDelta(view, ch.channel, ch.listener, 1, 1, SIG1);

    // Drop the empty snapshot sent by the server.
    ObservableWaveletData snapshot = createSnapshot(WAVELET_ID_1, 0, NOSIG);
    HashedVersion committed = HashedVersion.unsigned(0);
    viewListener.onSnapshot(snapshot.getWaveletId(), snapshot, committed, null);
    muxListener.verifyNoMoreInteractions();

    // Now CC should have sent the first client delta so it's acked.
    view.checkExpectationsSatisified();

    checkReceiveAndSend(viewListener, view, ch, WAVELET_ID_1, 1);
    view.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  public void testMuxReconnectsAfterDisconnect() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    final ConnectionInfo chInfo2 = new ConnectionInfo(WAVELET_ID_2, 20, SIG2);

    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch1 = connectChannelSnapshot(viewListener, chInfo1);
    ConnectedChannel ch2 = connectChannelSnapshot(viewListener, chInfo2);
    triggerAndCheckOpenFinished(viewListener);

    view.expectClose();
    MockViewChannel view2 = viewFactory.expectCreate();
    view2.expectOpen(IdFilters.ALL_IDS, createKnownVersions(WAVELET_ID_1, 1, SIG1,
        WAVELET_ID_2, 20, SIG2));
    viewListener.onException(new ChannelException("failed for testing", Recoverable.RECOVERABLE));

    ViewChannel.Listener viewListener2 = reconnectView(view2, chInfo1, chInfo2);
    // Don't expect the mux nor any channel to know about reconnection.
    muxListener.verifyNoMoreInteractions();
    ch1.listener.checkOpsReceived(0);
    ch2.listener.checkOpsReceived(0);

    // Check we can still receive and send deltas.
    checkReceiveAndSend(viewListener2, view2, ch1, WAVELET_ID_1, chInfo1.initialVersion);
    checkReceiveAndSend(viewListener2, view2, ch2, WAVELET_ID_2, chInfo2.initialVersion);

    // If a message is received on the old view it should be ignored
    viewListener.onUpdate(WAVELET_ID_1, createServerDeltaList(1, 1, SIG3), null,
        null);

    view.checkExpectationsSatisified();
    view2.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  private static enum KnownWaveletDisconnectWhen {
    BEFORE_VIEW_CONNECTED,
    AFTER_VIEW_CONNECTED,
    AFTER_RESYNC,
  }

  /**
   * Tests that a mux reconnects with known wavelets if the view channel fails
   * before the channel id message is received.
   */
  public void testMuxReconnectsKnownWaveletBeforeViewConnected() throws ChannelException {
    doTestMuxReconnectsKnownWavelet(KnownWaveletDisconnectWhen.BEFORE_VIEW_CONNECTED);
  }

  /**
   * Tests that a mux reconnects with known wavelets if the view channel fails
   * after the channel id but before the reconnection delta is received.
   */
  public void testMuxReconnectsKnownWaveletAfterViewConnected() throws ChannelException {
    doTestMuxReconnectsKnownWavelet(KnownWaveletDisconnectWhen.AFTER_VIEW_CONNECTED);
  }

  /**
   * Tests that a mux reconnects with known wavelets if the view channel fails
   * after the reconnection delta is received, but before any other deltas.
   */
  public void testMuxReconnectsKnownWaveletAfterResync() throws ChannelException {
    doTestMuxReconnectsKnownWavelet(KnownWaveletDisconnectWhen.AFTER_RESYNC);
  }

  /**
   * Helps test that a mux reconnects with known wavelets if the view channel
   * fails during reconnection.
   */
  private void doTestMuxReconnectsKnownWavelet(KnownWaveletDisconnectWhen when)
      throws ChannelException {
    ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);
    MockViewChannel view = openMuxWithKnownWavelet(chInfo.snapshot);
    ConnectedChannel ch = expectConnectedChannel(chInfo.snapshot, Accessibility.READ_WRITE);
    checkOpenFinished();

    ViewChannel.Listener viewListener = view.takeListener();
    if (when.compareTo(KnownWaveletDisconnectWhen.AFTER_VIEW_CONNECTED) >= 0) {
      viewListener.onConnected();
    }

    if (when.compareTo(KnownWaveletDisconnectWhen.AFTER_RESYNC) >= 0) {
    // Receive reconnection delta and open finished.
      reconnectChannel(viewListener, WAVELET_ID_1, chInfo.initialVersion, chInfo.initialSignature);
      viewListener.onOpenFinished();
      view.checkExpectationsSatisified();
    }

    // Fail view, expect reconnection.
    MockViewChannel view2 = failViewAndExpectReconnection(viewListener, view,
        "View failed after resync message", createKnownVersions(WAVELET_ID_1, 1, SIG1));
    reconnectViewAndCheckEverythingStillWorks(view2, chInfo, ch);
    muxListener.verifyNoMoreInteractions();
  }

  private static enum NewWaveletDisconnectWhen {
    BEFORE_VIEW_CONNECTED,
    AFTER_VIEW_CONNECTED,
    AFTER_SEND_DELTA,
    AFTER_ACK_DELTA,
  }

  /**
   * Tests that a mux reconnects with a new version-zero channel if the view
   * channel fails before it opens.
   */
  public void testMuxReconnectsNewWaveletBeforeViewConnected() throws ChannelException {
    doTestMuxReconnectsNewWavelet(NewWaveletDisconnectWhen.BEFORE_VIEW_CONNECTED);
  }

  /**
   * Tests that the mux reconnects at version zero when the server fails before
   * the client sends any ops on a wavelet.
   */
  public void testMuxReconnectsNewWaveletAfterViewConnected() throws ChannelException {
    doTestMuxReconnectsNewWavelet(NewWaveletDisconnectWhen.AFTER_VIEW_CONNECTED);
  }

  /**
   * Tests that the mux reconnects at version zero when the server fails and
   * loses the client's version-zero delta before acknowledging it, forgetting
   * the wavelet entirely.
   */
  public void testMuxReconnectsNewWaveletAfterFirstSend() throws ChannelException {
    doTestMuxReconnectsNewWavelet(NewWaveletDisconnectWhen.AFTER_SEND_DELTA);
  }

  /**
   * Tests that the mux reconnects at version zero when the server fails and
   * loses the client's version-zero delta, forgetting the wavelet entirely.
   */
  public void testMuxReconnectsNewWaveletWhenServerLosesFirstDelta() throws ChannelException {
    doTestMuxReconnectsNewWavelet(NewWaveletDisconnectWhen.AFTER_ACK_DELTA);
  }

  /**
   * Helps test that the mux reconnects at version zero for a locally-created
   * channel.
   */
  private void doTestMuxReconnectsNewWavelet(NewWaveletDisconnectWhen when)
      throws ChannelException {
    ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 0, NOSIG);
    byte[] deltaSig = SIG1;
    MockViewChannel view = openMux();
    ViewChannel.Listener viewListener = view.takeListener();

    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_VIEW_CONNECTED) >= 0) {
      viewListener.onConnected();
      triggerAndCheckOpenFinished(viewListener);
    }

    ConnectedChannel ch = createOperationChannel(WAVELET_ID_1, USER_ID);

    WaveletDelta clientDelta = null;
    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_SEND_DELTA) >= 0) {
      clientDelta = checkSendDelta(view, ch.channel, chInfo.initialHashedVersion, chInfo.waveletId);
    }

    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_ACK_DELTA) >= 0) {
      checkAckDelta(view,  ch.channel, ch.listener, 1, chInfo.initialVersion + 1, deltaSig);
    }

    // Fail the view.
    Map<WaveletId, List<HashedVersion>> expectedReconnectVersions = CollectionUtils.newHashMap();
    // Zero is always a resync version, even if no delta was submitted.
    expectedReconnectVersions.put(chInfo.waveletId, CollectionUtils.newArrayList(
        HashedVersion.unsigned(0)));
    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_ACK_DELTA) >= 0) {
      expectedReconnectVersions.get(chInfo.waveletId).add(HashedVersion.of(1, deltaSig));
    }
    MockViewChannel view2 = failViewAndExpectReconnection(viewListener, view,
        "View failed for testing", expectedReconnectVersions);

    // Reconnect the view with no channels - the server never knew about or
    // lost the wavelet.
    ViewChannel.Listener viewListener2 = view2.takeListener();
    viewListener2.onConnected();

    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_SEND_DELTA) >= 0) {
      // Expect the client delta to be resubmitted.
      view2.expectSubmitDelta(chInfo.waveletId, clientDelta);
    }
    viewListener2.onOpenFinished();
    view2.checkExpectationsSatisified();

    if (when.compareTo(NewWaveletDisconnectWhen.AFTER_SEND_DELTA) >= 0) {
      // (Re-)ack the resubmitted delta and check the channel works.
      checkAckDelta(view2, ch.channel, ch.listener, 1, chInfo.initialVersion + 1, deltaSig);
      checkReceiveAndSend(viewListener2, view2, ch, chInfo.waveletId, chInfo.initialVersion + 1);
    } else {
      // Check we can send on then channel.
//      checkSendDelta(view2, ch.channel, chInfo.initialVersion, chInfo.waveletId);
      checkReceiveAndSend(viewListener2, view2, ch, chInfo.waveletId, chInfo.initialVersion);
    }
  }

  public void testMuxReconnectUsingScheduler() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    FakeScheduler scheduler = new FakeScheduler();
    mux = new OperationChannelMultiplexerImpl(WAVE_ID, viewFactory, DATA_FACTORY,
        LOGGERS, null, scheduler, FakeHashedVersionFactory.INSTANCE);
    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // Send but don't ack delta.
    WaveletDelta delta =
        checkSendDelta(view, ch.channel, chInfo1.initialHashedVersion, WAVELET_ID_1);

    // No schedule yet
    assertNull(scheduler.command);

    // Reconnect channel.
    view.expectClose();
    viewListener.onClosed();

    // Check we've scheduled something.
    assertNotNull(scheduler.command);

    // scheduler call back
    MockViewChannel view2 = viewFactory.expectCreate();
    view2.expectOpen(IdFilters.ALL_IDS, createKnownVersions(WAVELET_ID_1, 1, SIG1));
    scheduler.command.execute();

    // Signal reconnected
    ViewChannel.Listener viewListener2 = view2.takeListener();
    viewListener2.onConnected();

    // Expect retransmit of the sent delta after reconnect.
    view2.expectSubmitDelta(WAVELET_ID_1, delta);
    reconnectChannel(viewListener2, WAVELET_ID_1, chInfo1.initialVersion, chInfo1.initialSignature);
    viewListener2.onOpenFinished();

  }

  public void testMuxReconnectUsingScheduleResetWithDelta() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    FakeScheduler scheduler = new FakeScheduler();
    mux = new OperationChannelMultiplexerImpl(WAVE_ID, viewFactory, DATA_FACTORY,
        LOGGERS, null, scheduler, FakeHashedVersionFactory.INSTANCE);
    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // No schedule yet
    assertNull(scheduler.command);

    // Reconnect 20 times.
    for (int i = 0; i < 20; i++) {
      // Reconnect channel.
      view.expectClose();
      viewListener.onClosed();

      // scheduler call back
      view = viewFactory.expectCreate();
      view.expectOpen(IdFilters.ALL_IDS, createKnownVersions(WAVELET_ID_1, 1, SIG1));
      assertNotNull(scheduler.command);
      scheduler.command.execute();
      scheduler.command = null;

      viewListener = view.takeListener();
      viewListener.onConnected();

      // Reconnection message
      reconnectChannel(viewListener, WAVELET_ID_1, chInfo1.initialVersion,
          chInfo1.initialSignature);
      viewListener.onOpenFinished();
    }
  }

  public void testMuxReconnectsAfterDisconnectWithOutstandingSubmit() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // Send but don't ack delta.
    WaveletDelta delta =
        checkSendDelta(view, ch.channel, chInfo1.initialHashedVersion, WAVELET_ID_1);

    // Reconnect channel.
    MockViewChannel view2 = failViewAndExpectReconnection(viewListener, view,
        "View failed with outstanding submit", createKnownVersions(WAVELET_ID_1, 1, SIG1));

    // Expect retransmit of the sent delta after reconnect.
    view2.expectSubmitDelta(WAVELET_ID_1, delta);
    ViewChannel.Listener viewListener2 = reconnectView(view2, chInfo1);
    muxListener.verifyNoMoreInteractions(); // No callback on reconnection.

    checkAckDelta(view2, ch.channel, ch.listener, 1, chInfo1.initialVersion + 1, SIG1);
    checkReceiveAndSend(viewListener2, view2, ch, WAVELET_ID_1, chInfo1.initialVersion + 1);

    // If the submit is then acked, it should be ignored.
    view.ackSubmit(1, chInfo1.initialVersion + 1, SIG5);

    view.checkExpectationsSatisified();
    view2.checkExpectationsSatisified();
    ch.listener.checkOpsReceived(0);
    muxListener.verifyNoMoreInteractions();
  }

  public void testMuxReconnectsAfterSubmitFailure() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // Send but don't ack delta.
    WaveletDelta delta =
        checkSendDelta(view, ch.channel, chInfo1.initialHashedVersion, WAVELET_ID_1);

    // Fail the submission, expecting reconnection.
    MockViewChannel view2 = failViewAndExpectReconnection(viewListener, view,
        "View failed with outstanding submit", createKnownVersions(WAVELET_ID_1, 1, SIG1));

    // Expect retransmit of the sent delta after reconnect.
    view2.expectSubmitDelta(WAVELET_ID_1, delta);
    ViewChannel.Listener viewListener2 = reconnectView(view2, chInfo1);
    muxListener.verifyNoMoreInteractions(); // No callback on reconnection.

    checkAckDelta(view2, ch.channel, ch.listener, 1, chInfo1.initialVersion + 1, SIG2);

    // If the first view later disconnects, it should be ignored
    viewListener.onException(new ChannelException("failed for testing", Recoverable.RECOVERABLE));

    checkReceiveAndSend(viewListener2, view2, ch, WAVELET_ID_1, chInfo1.initialVersion + 1);
    view.checkExpectationsSatisified();
    view2.checkExpectationsSatisified();
    ch.listener.checkOpsReceived(0);
    muxListener.verifyNoMoreInteractions();
  }

  public void testMuxFailsAfterChannelCorrupt() throws ChannelException {
    final ConnectionInfo chInfo1 = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo1);
    triggerAndCheckOpenFinished(viewListener);

    // Receive a message that should not be received.
    ObservableWaveletData update = createSnapshot(WAVELET_ID_1, 1, SIG1);
    view.expectClose();

    try {
      viewListener.onSnapshot(WAVELET_ID_1, update, null, null);
      fail("Expected exception corruption");
    } catch (ChannelException ex) {
      // Expected
    }
  }

  public void testMuxReconnectsAgainAfterReconnectFailure() throws ChannelException {
    final ConnectionInfo chInfo = new ConnectionInfo(WAVELET_ID_1, 1, SIG1);

    MockViewChannel view = openMux();
    muxListener.verifyNoMoreInteractions();
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    // Receive initial snapshots.
    ConnectedChannel ch = connectChannelSnapshot(viewListener, chInfo);
    triggerAndCheckOpenFinished(viewListener);

    // Disconnect, expect reconnection.
    view.expectClose();
    MockViewChannel view2 = viewFactory.expectCreate();
    view2.expectOpen(IdFilters.ALL_IDS, createKnownVersions(WAVELET_ID_1, 1, SIG1));
    viewListener.onClosed();
    ViewChannel.Listener viewListener2 = view2.takeListener();

    // Disconnect, expect reconnection again.
    view2.expectClose();
    MockViewChannel view3 = viewFactory.expectCreate();
    view3.expectOpen(IdFilters.ALL_IDS, createKnownVersions(WAVELET_ID_1, 1, SIG1));
    viewListener2.onClosed();

    view2.checkExpectationsSatisified();
    view3.checkExpectationsSatisified();
    muxListener.verifyNoMoreInteractions();
  }

  private static ObservableWaveletData createSnapshot(WaveletId waveletId, final long version,
      final byte[] signature) {
    final HashedVersion hv = HashedVersion.of(version, signature);
    return DATA_FACTORY.create(new EmptyWaveletSnapshot(WAVE_ID, waveletId, USER_ID, hv,
        1273307837000L));
  }

  private static TransformedWaveletDelta createReconnect(WaveId waveId, long connectVersion,
      byte[] connectSignature) {
    TransformedWaveletDelta delta = new TransformedWaveletDelta(USER_ID,
        HashedVersion.of(connectVersion, connectSignature), 0L,
        Collections.<WaveletOperation>emptyList());
    return delta;
  }

  private static KnownWavelet createKnownWavelet(ObservableWaveletData snapshot, long version,
      byte[] signature, Accessibility accessibility) {
    HashedVersion commitVersion = HashedVersion.of(version, signature);
    return new KnownWavelet(snapshot, commitVersion, accessibility);
  }

  private static List<TransformedWaveletDelta> createServerDeltaList(long version, int numOps,
      byte[] signature) {
    TransformedWaveletDelta delta =
        testUtil.makeTransformedDelta(0L, HashedVersion.of(version + numOps, signature), numOps);
    return Collections.singletonList(delta);
  }

  private static NoOp createOp() {
    return testUtil.noOp();
  }

  private static WaveletDelta createDelta(HashedVersion targetVersion, WaveletOperation... ops) {
    return new WaveletDelta(USER_ID, targetVersion, Arrays.asList(ops));
  }

  private static Map<WaveletId, List<HashedVersion>> createKnownVersions(
      WaveletId waveletId, long version, byte[] hash) {
    return CollectionUtils.immutableMap(waveletId, Collections.singletonList(
        HashedVersion.of(version, hash)));
  }

  private static Map<WaveletId, List<HashedVersion>> createKnownVersions(
      WaveletId waveletId1, long version1, byte[] hash1, WaveletId waveletId2,
      long version2, byte[] hash2) {
    assertFalse(waveletId1.equals(waveletId2));
    return CollectionUtils.immutableMap(
        waveletId1, Collections.singletonList(HashedVersion.of(version1, hash1)),
        waveletId2, Collections.singletonList(HashedVersion.of(version2, hash2)));
  }

  private OperationChannel checkSendClobberingSnapshot(ViewChannel.Listener viewListener,
      OperationChannel channelToClobber, ObservableWaveletData snapshotUpdate,
      HashedVersion committed) throws ChannelException {
    viewListener.onSnapshot(WAVELET_ID_1, snapshotUpdate, committed, null);
    muxListener.verifyOperationChannelRemoved(channelToClobber);
    return muxListener.verifyOperationChannelCreated(snapshotUpdate, Accessibility.READ_WRITE);
  }

  private static void checkReceiveDelta(ViewChannel.Listener viewListener,
      OperationChannel opChannel, MockOperationChannelListener opChannelListener,
      WaveletId waveletId, long version, int numOps, byte[] signature) throws ChannelException {
    // Receive delta from view channel, expect ops on op channel.
    final List<TransformedWaveletDelta> update = createServerDeltaList(version, numOps, signature);
    viewListener.onUpdate(waveletId, update, null, null);
    opChannelListener.checkOpsReceived(1);
    opChannelListener.clear();
    for (int i = 0; i < numOps; ++i) {
      assertNotNull(opChannel.receive());
    }
    assertNull(opChannel.receive());
  }

  /**
   * Sends an operation on an operation channel and expects the delta to
   * be submitted to the view.
   */
  private static WaveletDelta checkSendDelta(MockViewChannel viewChannel,
      OperationChannel opChannel, HashedVersion initialVersion, WaveletId expectedWaveletId)
      throws ChannelException {
    WaveletOperation op = createOp();
    WaveletDelta delta = createDelta(initialVersion, op);
    viewChannel.expectSubmitDelta(expectedWaveletId, delta);
    opChannel.send(op);
    return delta;
  }

  /**
   * Acks a delta and checks that the fake version-incrementing op is received
   * from the operation channel.
   */
  private static void checkAckDelta(MockViewChannel viewChannel, OperationChannel opChannel,
      MockOperationChannelListener opChannelListener, int ackedOps, long version, byte[] signature)
      throws ChannelException {
    viewChannel.ackSubmit(ackedOps, version, signature);
    opChannelListener.checkOpsReceived(1);
    opChannelListener.clear();
    assertNotNull(opChannel.receive());
    opChannelListener.checkOpsReceived(0);
    opChannelListener.clear();
  }

  /**
   * Receives a one-op delta and sends/acks another, checking expectations.
   */
  private static void checkReceiveAndSend(ViewChannel.Listener viewListener, MockViewChannel
      viewChannel, ConnectedChannel ch, WaveletId waveletId, long version)
      throws ChannelException {
    checkReceiveDelta(viewListener, ch.channel, ch.listener, waveletId, version, 1, SIG2);
    checkSendDelta(viewChannel, ch.channel, HashedVersion.of(version + 1, SIG2), waveletId);
    checkAckDelta(viewChannel, ch.channel, ch.listener, 1, version + 2, SIG3);
  }

  /**
   * Opens a new mux and returns the created view mock.
   */
  private MockViewChannel openMux() {
    return openMux(IdFilters.ALL_IDS);
  }

  private MockViewChannel openMux(IdFilter idFilter) {
    MockViewChannel viewChannel = viewFactory.expectCreate();
    viewChannel.expectOpen(idFilter, NO_KNOWN_WAVELETS);
    mux.open(muxListener, idFilter);
    return viewChannel;
  }

  /**
   * Opens a new mux with a known wavelet and returns the created view mock.
   */
  private MockViewChannel openMuxWithKnownWavelet(ObservableWaveletData knownSnapshot) {
    long version = knownSnapshot.getVersion();
    byte[] signature = knownSnapshot.getHashedVersion().getHistoryHash();
    MockViewChannel view = viewFactory.expectCreate();
    Map<WaveletId, List<HashedVersion>> expectedSigs = createKnownVersions(WAVELET_ID_1,
        version, signature);
    view.expectOpen(IdFilters.ALL_IDS, expectedSigs);
    mux.open(muxListener, IdFilters.ALL_IDS, Collections.singletonList(createKnownWavelet(
        knownSnapshot, version, signature, Accessibility.READ_WRITE)));
    return view;
  }

  private ConnectedChannel createOperationChannel(WaveletId waveletId, ParticipantId address) {
    mux.createOperationChannel(waveletId, address);
    OperationChannel channel = muxListener.verifyOperationChannelCreated(createSnapshot(
        WAVELET_ID_1, 0, NOSIG), Accessibility.READ_WRITE);
    MockOperationChannelListener channelListener = new MockOperationChannelListener();
    channel.setListener(channelListener);
    return new ConnectedChannel(channel, channelListener);
  }

  private ConnectedChannel connectChannelSnapshot(ViewChannel.Listener viewListener,
      ConnectionInfo info) throws ChannelException {
    return connectChannelSnapshot(viewListener, info.snapshot, info.initialHashedVersion);
  }

  private ConnectedChannel connectChannelSnapshot(ViewChannel.Listener viewListener,
      ObservableWaveletData snapshot, HashedVersion committed)
      throws ChannelException {
    viewListener.onSnapshot(snapshot.getWaveletId(), snapshot, committed, null);
    OperationChannel channel = muxListener.verifyOperationChannelCreated(snapshot,
        Accessibility.READ_WRITE);
    MockOperationChannelListener listener = new MockOperationChannelListener();
    channel.setListener(listener);
    return new ConnectedChannel(channel, listener);
  }

  private ConnectedChannel expectConnectedChannel(ObservableWaveletData knownSnapshot,
      Accessibility accessibility) {
    OperationChannel channel = muxListener.verifyOperationChannelCreated(knownSnapshot,
        accessibility);
    MockOperationChannelListener listener = new MockOperationChannelListener();
    channel.setListener(listener);
    return new ConnectedChannel(channel, listener);
  }

  private void triggerAndCheckOpenFinished(ViewChannel.Listener viewListener)
      throws ChannelException {
    viewListener.onOpenFinished();
    checkOpenFinished();
  }

  private void checkOpenFinished() {
    muxListener.verifyOpenFinished();
  }

  /**
   * Sends a ChannelException to a view listener and expects a new view to be opened
   * to reconnect it.
   *
   * @return the new mock view
   */
  private MockViewChannel failViewAndExpectReconnection(ViewChannel.Listener viewListenerToFail,
      MockViewChannel failingView, String failureReason,
      Map<WaveletId, List<HashedVersion>> expectedReconnectionSigs) {
    failingView.expectClose();
    MockViewChannel newView = viewFactory.expectCreate();
    newView.expectOpen(IdFilters.ALL_IDS, expectedReconnectionSigs);
    viewListenerToFail.onException(new ChannelException(failureReason, Recoverable.RECOVERABLE));
    viewListenerToFail.onClosed();
    failingView.checkExpectationsSatisified();
    return newView;
  }

  /**
   * Reconnects a mux on a view, returning the reconnected view's listener.
   */
  private static ViewChannel.Listener reconnectView(MockViewChannel view,
      ConnectionInfo... channels) throws ChannelException {
    ViewChannel.Listener viewListener = view.takeListener();
    viewListener.onConnected();

    for (ConnectionInfo chInfo : channels) {
      reconnectChannel(viewListener, chInfo.waveletId, chInfo.initialVersion,
          chInfo.initialSignature);
    }
    viewListener.onOpenFinished();
    view.checkExpectationsSatisified();
    return viewListener;
  }

  /**
   * Sends a reconnect delta with current version the same as the reconnection
   * version.
   */
  private static void reconnectChannel(ViewChannel.Listener viewListener, WaveletId waveletId,
      long version, byte[] signature) throws ChannelException {
    reconnectChannel(viewListener, waveletId, version, signature, version, signature);
  }

  private static void reconnectChannel(ViewChannel.Listener viewListener, WaveletId waveletId,
      long connectVersion, byte[] connectSignature, long currentVersion, byte[] currentSignature)
      throws ChannelException {
    TransformedWaveletDelta reconnect = createReconnect(null, connectVersion, connectSignature);
    HashedVersion distinctVersion = HashedVersion.of(currentVersion, currentSignature);
    viewListener.onUpdate(waveletId, Collections.singletonList(reconnect),
        distinctVersion, distinctVersion);
  }

  /**
   * Reconnects a view with a single operation channel and checks that the channel
   * is usable and expectations are satisfied.
   */
  private void reconnectViewAndCheckEverythingStillWorks(MockViewChannel view,
      ConnectionInfo chInfo, ConnectedChannel ch) throws ChannelException {
    // Perform the reconnect.
    ViewChannel.Listener viewListener2 = reconnectView(view, chInfo);
    ch.listener.checkOpsReceived(0);

    // Check everything still works.
    checkReceiveAndSend(viewListener2, view, ch, WAVELET_ID_1, chInfo.initialVersion);
    view.checkExpectationsSatisified();
  }
}
TOP

Related Classes of org.waveprotocol.wave.concurrencycontrol.channel.OperationChannelMultiplexerImplTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.