/**
* Copyright 2010 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.box.consoleclient;
import static junit.framework.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.waveprotocol.box.common.DocumentConstants.MANIFEST_DOCUMENT_ID;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.waveprotocol.box.common.Snippets;
import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolSubmitResponse;
import org.waveprotocol.box.server.util.BlockingSuccessFailCallback;
import org.waveprotocol.box.server.util.WaveletDataUtil;
import org.waveprotocol.wave.client.common.util.ClientPercentEncoderDecoder;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.id.IdURIEncoderDecoder;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.wave.BlipContentOperation;
import org.waveprotocol.wave.model.operation.wave.BlipOperationVisitor;
import org.waveprotocol.wave.model.operation.wave.BlipOperationVisitorImpl;
import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
import org.waveprotocol.wave.model.util.Pair;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.version.HashedVersionFactory;
import org.waveprotocol.wave.model.version.HashedVersionZeroFactoryImpl;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.BlipData;
import org.waveprotocol.wave.model.wave.data.ReadableBlipData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import java.io.IOException;
import java.net.HttpCookie;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Utility class for tesing the client and related classes.
*
* @author mk.mateng@gmail.com (Michael Kuntzman)
*/
public class ClientTestingUtil {
/**
* Timeout, in milliseconds, for tests that may fail through abnormal
* behaviors such as deadlocks or infinite loops. Usually 1000-2000 ms should
* be enough. We give a little more to be safe.
*/
public static final long TEST_TIMEOUT = 5000;
public static final IdURIEncoderDecoder URI_CODEC =
new IdURIEncoderDecoder(new ClientPercentEncoderDecoder());
public static final HashedVersionFactory HASH_FACTORY =
new HashedVersionZeroFactoryImpl(URI_CODEC);
/**
* ClientBackend factory that creates a spy object on the backend and injects a fake RPC
* objects factory.
*/
public static final ClientBackend.Factory backendSpyFactory = new ClientBackend.Factory() {
@Override
public ClientBackend create(String userAtDomain, String server)
throws IOException {
ClientAuthenticator authenticator = mock(ClientAuthenticator.class);
HttpCookie cookie = new HttpCookie("anything", "token");
when(authenticator.authenticate(anyString(), any(char[].class))).thenReturn(cookie);
return spy(new ClientBackend(userAtDomain, server, new FakeRpcObjectFactory(),
HASH_FACTORY, authenticator));
}
};
/**
* @return a new mock renderer for the console client. The renderer skips rendering to speed up
* the tests and allows checking if the render method was called.
*/
// TODO(Michael): Add checks to the ConsoleClientTest to check that render is being called when
// appropriate.
public static ConsoleClient.Renderer getMockConsoleRenderer() {
return mock(ConsoleClient.Renderer.class);
}
/** The client backend on which this util instance acts. */
private final ClientBackend backend;
/** The user stored in the backend */
private final ParticipantId userId;
/**
* Constructs a {@code ClientTestingUtil} that acts on the given client backend.
*
* @param backend to act on.
*/
public ClientTestingUtil(ClientBackend backend) {
this.backend = backend;
userId = backend.getUserId();
}
/**
* Verifies that an operation completed without errors within the time set by the test timeout.
*
* @param callback the blocking callback that was used for the operation.
*/
public void assertOperationComplete(
BlockingSuccessFailCallback<ProtocolSubmitResponse, String> callback) {
// Make sure the test times out if something is wrong.
final long waitTimeout = TEST_TIMEOUT * 2;
final TimeUnit waitUnit = TimeUnit.MILLISECONDS;
Pair<ProtocolSubmitResponse, String> result = callback.await(waitTimeout, waitUnit);
// Process any incoming events that may have been generated.
backend.waitForAccumulatedEventsToProcess();
assertNotNull(result);
assertNotNull(result.getFirst());
}
/**
* Creates a new empty wavelet. The wavelet is not part of a {@code
* ClientWaveView} and not stored in the client backend.
*/
public WaveletData createWavelet() throws OperationException {
return createWavelet(WaveletName.of("example.com", "wave", "example.com", "wavelet"), userId);
}
/**
* Creates a new empty wavelet with an empty manifest document and the
* specified wavelet name. The wavelet is not part of a {@code ClientWaveView}
* and not stored in the client backend.
*
* @param waveletName of the new wavelet.
* @param creator the id of the wavelet creator
* @return the new wavelet
*/
public WaveletData createWavelet(WaveletName waveletName, ParticipantId creator)
throws OperationException {
long dummyCreationTime = System.currentTimeMillis();
WaveletData wavelet = WaveletDataUtil.createEmptyWavelet(waveletName, creator,
HashedVersion.unsigned(0), dummyCreationTime);
BlipData manifest = WaveletDataUtil.addEmptyBlip(wavelet, MANIFEST_DOCUMENT_ID, creator, 0L);
manifest.getContent().consume(ClientUtils.createManifest());
return wavelet;
}
/**
* Creates a valid wave (and wavelet) in the client backend.
*
* @return the new wave's conversation root wavelet.
*/
public WaveletData createWaveletInBackend() {
BlockingSuccessFailCallback<ProtocolSubmitResponse, String> callback =
BlockingSuccessFailCallback.create();
ClientWaveView view = backend.createConversationWave(callback);
// Make sure the wavelet creation completes successfully before returning the wavelet.
assertOperationComplete(callback);
Preconditions.checkNotNull(ClientUtils.getConversationRoot(view), "Wavelet creation failed");
return ClientUtils.getConversationRoot(view);
}
/**
* Returns all documents in the wave, aggregated from all the wavelets.
*
* @param wave to get the documents from.
* @return map of all documents in the wave, aggregated from all the wavelets, and keyed by their
* IDs.
*/
public Map<String, BlipData> getAllDocuments(ClientWaveView wave) {
return ClientUtils.getAllDocuments(wave);
}
/**
* Returns all documents in the wave, aggregated from all the wavelets. The wave is retrieved
* from the client backend using the given wave ID.
*
* @param waveId of the wave to get the documents from.
* @return map of all documents in the wave, aggregated from all the wavelets, and keyed by their
* IDs.
*/
public Map<String, BlipData> getAllDocuments(WaveId waveId) {
return getAllDocuments(backend.getWave(waveId));
}
/**
* Returns all participants in the wave, aggregated from all the wavelets.
*
* @param wave to get the participants from.
* @return all participants in the wave, aggregated from all the wavelets.
*/
public Set<ParticipantId> getAllParticipants(ClientWaveView wave) {
return ClientUtils.getAllParticipants(wave);
}
/**
* Returns all participants in the wave, aggregated from all the wavelets. The wave is retrieved
* from the client backend using the given wave ID.
*
* @param waveId of the wave to get the participants from.
* @return all participants in the wave, aggregated from all the wavelets.
*/
public Set<ParticipantId> getAllParticipants(WaveId waveId) {
return getAllParticipants(backend.getWave(waveId));
}
/**
* @return the first open wave in the client backend, not counting the index wave.
*/
public ClientWaveView getFirstWave() {
return backend.getWave(getFirstWaveId());
}
/**
* @return the WaveId of the first open wave in the client backend, not counting the index wave.
*/
public WaveId getFirstWaveId() {
return getOpenWaveId(0, false);
}
/**
* Returns the WaveId of the n-th open wave in the client backend.
*
* @param index of the open wave whose id to retreive (zero based).
* @param includingIndexWave should the index wave be included in the count of open waves?
* @return the WaveId, or null if not found.
*/
public WaveId getOpenWaveId(int index, boolean includingIndexWave) {
for (WaveId waveId : getOpenWaveIds(includingIndexWave)) {
if (index == 0) {
return waveId;
}
--index;
}
// Wave not found (index is out of range).
return null;
}
/**
* Returns the set of wave IDs of the waves that are currently open in the client backend,
* optionally including the index wave.
*
* @param includeIndexWave should the index wave be included in the returned set?
* @return the set of wave Ids of the open waves.
*/
public Set<WaveId> getOpenWaveIds(boolean includeIndexWave) {
return backend.getOpenWaveIds(includeIndexWave);
}
/**
* Counts the waves curretly open in the client backend.
*
* @param includeIndexWave should the index wave be included in the count?
* @return the number of waves currently open in the client backend.
*/
public int getOpenWavesCount(boolean includeIndexWave) {
return getOpenWaveIds(includeIndexWave).size();
}
/**
* Collects the text from the specified blip document.
*
* @param blip document to collect the text from.
* @return A string containing the characters from the blip.
*/
public static String getText(ReadableBlipData blip) {
return Snippets.collateTextForDocuments(Lists.newArrayList(blip));
}
/**
* Collates the specified document operations into a string equivalent to the resulting wavelet
* content.
*
* @param ops to collate.
* @return the resulting text content.
*/
public String getText(List<WaveletBlipOperation> ops) {
final List<DocOp> docOps = Lists.newArrayList();
BlipOperationVisitor opVisitor = new BlipOperationVisitorImpl() {
@Override
public void visitBlipContentOperation(BlipContentOperation op) {
docOps.add(op.getContentOp());
}
};
for (WaveletBlipOperation op : ops) {
// Skip changes to the manifest document since they may contain "retain"
// and other components that can't be collated by ClientUtils, and we
// don't really care about the manifest anyway.
if (!op.getBlipId().equals(MANIFEST_DOCUMENT_ID)) {
op.getBlipOp().acceptVisitor(opVisitor);
}
}
return Snippets.collateTextForOps(docOps);
}
/**
* Collects the text of all of the documents in a wave into a single String.
*
* @param wave wave to collect the text from.
* @return the collected text from the wave.
*/
public String getText(ClientWaveView wave) {
return ClientUtils.collateText(wave);
}
/**
* Collects the text of all of the documents in a wave into a single String. The wave is
* retrieved from the client backend using the given wave ID.
*
* @param waveId of the wave to collect the text from.
* @return the collected text from the wave.
*/
public String getText(WaveId waveId) {
return getText(backend.getWave(waveId));
}
}