/**
* 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.server.robots.passive;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.wave.api.data.converter.EventDataConverter;
import com.google.wave.api.data.converter.v22.EventDataConverterV22;
import com.google.wave.api.event.AnnotatedTextChangedEvent;
import com.google.wave.api.event.DocumentChangedEvent;
import com.google.wave.api.event.Event;
import com.google.wave.api.event.EventType;
import com.google.wave.api.event.WaveletBlipCreatedEvent;
import com.google.wave.api.event.WaveletBlipRemovedEvent;
import com.google.wave.api.event.WaveletParticipantsChangedEvent;
import com.google.wave.api.impl.EventMessageBundle;
import com.google.wave.api.robot.Capability;
import com.google.wave.api.robot.RobotName;
import org.waveprotocol.box.common.DeltaSequence;
import org.waveprotocol.box.server.robots.RobotsTestBase;
import org.waveprotocol.box.server.robots.util.ConversationUtil;
import org.waveprotocol.wave.model.conversation.ConversationBlip;
import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
import org.waveprotocol.wave.model.conversation.ObservableConversationThread;
import org.waveprotocol.wave.model.conversation.ObservableConversationView;
import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.id.IdURIEncoderDecoder;
import org.waveprotocol.wave.model.operation.CapturingOperationSink;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.SilentOperationSink;
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.operation.wave.WaveletOperationContext;
import org.waveprotocol.wave.model.schema.SchemaCollection;
import org.waveprotocol.wave.model.testing.FakeIdGenerator;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.version.HashedVersionFactory;
import org.waveprotocol.wave.model.version.HashedVersionFactoryImpl;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.ParticipationHelper;
import org.waveprotocol.wave.model.wave.data.DocumentFactory;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot;
import org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument;
import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl;
import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
import org.waveprotocol.wave.util.escapers.jvm.JavaUrlCodec;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Unit tests for the {@link EventGenerator}.
*
* This class constructs an {@link OpBasedWavelet} on which the operations
* performed are captured. These operations will later be used for in the
* {@link EventGenerator}.
*
* The wavelet will have {@code ALEX} as participant and an empty root blip in
* its conversation structure.
*
* @author ljvderijk@google.com (Lennard de Rijk)
*/
public class EventGeneratorTest extends RobotsTestBase {
private final static RobotName ROBOT_NAME = RobotName.fromAddress(ROBOT.getAddress());
private final static EventDataConverter CONVERTER = new EventDataConverterV22();
private static final IdURIEncoderDecoder URI_CODEC =
new IdURIEncoderDecoder(new JavaUrlCodec());
private static final HashedVersionFactory HASH_FACTORY = new HashedVersionFactoryImpl(URI_CODEC);
private static final DocumentFactory<?> DOCUMENT_FACTORY =
ObservablePluggableMutableDocument.createFactory(SchemaCollection.empty());
private static final WaveletOperationContext.Factory CONTEXT_FACTORY =
new WaveletOperationContext.Factory() {
@Override
public WaveletOperationContext createContext() {
return new WaveletOperationContext(ALEX, 0L, 1);
}
@Override
public WaveletOperationContext createContext(ParticipantId creator) {
throw new UnsupportedOperationException();
}
};
/** Map containing a subscription to all possible events */
private static final Map<EventType, Capability> ALL_CAPABILITIES;
static {
ImmutableMap.Builder<EventType, Capability> builder = ImmutableMap.builder();
for (EventType event : EventType.values()) {
if (!event.equals(EventType.UNKNOWN)) {
builder.put(event, new Capability(event));
}
}
ALL_CAPABILITIES = builder.build();
}
private EventGenerator eventGenerator;
private ObservableWaveletData waveletData;
private OpBasedWavelet wavelet;
private CapturingOperationSink<WaveletOperation> output;
private ConversationUtil conversationUtil;
@Override
protected void setUp() throws Exception {
conversationUtil = new ConversationUtil(FakeIdGenerator.create());
eventGenerator = new EventGenerator(ROBOT_NAME, conversationUtil);
waveletData = WaveletDataImpl.Factory.create(DOCUMENT_FACTORY).create(
new EmptyWaveletSnapshot(WAVELET_NAME.waveId, WAVELET_NAME.waveletId, ALEX,
HASH_FACTORY.createVersionZero(WAVELET_NAME), 0L));
// Robot should be participant in snapshot before deltas
// otherwise events will be filtered out.
waveletData.addParticipant(ROBOT);
waveletData.addParticipant(ALEX);
waveletData.setVersion(1);
SilentOperationSink<WaveletOperation> executor =
SilentOperationSink.Executor.<WaveletOperation, WaveletData> build(waveletData);
output = new CapturingOperationSink<WaveletOperation>();
wavelet =
new OpBasedWavelet(waveletData.getWaveId(), waveletData, CONTEXT_FACTORY,
ParticipationHelper.DEFAULT, executor, output);
// Make a conversation and clear the sink
WaveletBasedConversation.makeWaveletConversational(wavelet);
conversationUtil.buildConversation(wavelet).getRoot().getRootThread().appendBlip();
output.clear();
}
public void testGenerateWaveletParticipantsChangedEventOnAdd() throws Exception {
wavelet.addParticipant(BOB);
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_PARTICIPANTS_CHANGED);
assertTrue("Only expected one event", messages.getEvents().size() == 1);
WaveletParticipantsChangedEvent event =
WaveletParticipantsChangedEvent.as(messages.getEvents().get(0));
assertTrue("Bob should be added", event.getParticipantsAdded().contains(BOB.getAddress()));
}
public void testGenerateWaveletParticipantsChangedEventOnRemove() throws Exception {
wavelet.removeParticipant(ALEX);
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_PARTICIPANTS_CHANGED);
assertEquals("Expected one event", 1, messages.getEvents().size());
WaveletParticipantsChangedEvent event =
WaveletParticipantsChangedEvent.as(messages.getEvents().get(0));
assertTrue(
"Alex should be removed", event.getParticipantsRemoved().contains(ALEX.getAddress()));
}
public void testGenerateWaveletSelfAddedEvent() throws Exception {
waveletData.removeParticipant(ROBOT);
wavelet.addParticipant(ROBOT);
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_SELF_ADDED);
assertEquals("Expected two events", 2, messages.getEvents().size());
}
public void testGenerateWaveletSelfRemovedEvent() throws Exception {
wavelet.removeParticipant(ROBOT);
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_SELF_REMOVED);
// Participant changed event, after self removed event is filtered.
assertEquals("Expected only one event", 1, messages.getEvents().size());
}
/**
* Tests that events from a robot delta are filtered, after events from a
* human delta are received.
*/
public void testRobotSelfEventsFilteredAfterHuman() throws Exception {
// Robot receives two deltas, it is participant in wavelet before deltas.
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
// Delta1 start events: event #1.
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
// Delta1 event #2.
XmlStringBuilder builder = XmlStringBuilder.createText("some random content by alex");
LineContainers.appendToLastLine(newBlip.getContent(), builder);
List<WaveletOperation> ops1 = Lists.newArrayList(output.getOps());
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta1 = makeDeltaFromCapturedOps(ALEX, ops1, endVersion, 0L);
output.clear();
// Delta2 event #1.
conversation = conversationUtil.buildConversation(wavelet);
newBlip = conversation.getRoot().getRootThread().appendBlip();
// Delta2 event #2.
wavelet.addParticipant(BOB);
// Delta2 event #3.
builder = XmlStringBuilder.createText("some random content by robot");
LineContainers.appendToLastLine(newBlip.getContent(), builder);
List<WaveletOperation> ops2 = Lists.newArrayList(output.getOps());
HashedVersion endVersion2 = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta2 = makeDeltaFromCapturedOps(ROBOT, ops2, endVersion2, 0L);
output.clear();
assertTrue("Ops should not be empty", (!ops1.isEmpty()) && (!ops2.isEmpty()));
EventMessageBundle messages = generateEventsFromDeltas(delta1, delta2);
assertEquals("Expected two events", 2, messages.getEvents().size());
}
/**
* Tests that events from a robot delta are filtered, before events from a
* human delta are received.
*/
public void testRobotSelfEventsFilteredBeforeHuman() throws Exception {
// Robot receives two deltas, it is participant in wavelet before deltas.
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
// Delta1 start events: event #1.
ObservableConversationBlip rootBlip = conversation.getRoot().getRootThread().getFirstBlip();
// Delta1 event #2.
XmlStringBuilder builder = XmlStringBuilder.createText("some random content by robot");
LineContainers.appendToLastLine(rootBlip.getContent(), builder);
// Delta1 event #3.
wavelet.addParticipant(BOB);
List<WaveletOperation> ops1 = Lists.newArrayList(output.getOps());
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta1 = makeDeltaFromCapturedOps(ROBOT, ops1, endVersion, 0L);
output.clear();
// Delta2 event #1.
conversation = conversationUtil.buildConversation(wavelet);
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
// Delta2 event #2.
builder = XmlStringBuilder.createText("some random content by alex");
LineContainers.appendToLastLine(newBlip.getContent(), builder);
List<WaveletOperation> ops2 = Lists.newArrayList(output.getOps());
HashedVersion endVersion2 = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta2 = makeDeltaFromCapturedOps(ALEX, ops2, endVersion2, 0L);
output.clear();
EventMessageBundle messages = generateEventsFromDeltas(delta1, delta2);
assertEquals("Expected two events", 2, messages.getEvents().size());
}
public void testGenerateWaveletBlipCreatedEvent() throws Exception {
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_BLIP_CREATED);
assertEquals("Expected one event", 1, messages.getEvents().size());
WaveletBlipCreatedEvent event = WaveletBlipCreatedEvent.as(messages.getEvents().get(0));
assertEquals("Expected the same id as the new blip", newBlip.getId(), event.getNewBlipId());
}
public void testGenerateWaveletBlipRemovedEvent() throws Exception {
ObservableConversationThread rootThread =
conversationUtil.buildConversation(wavelet).getRoot().getRootThread();
ObservableConversationBlip newBlip = rootThread.appendBlip();
newBlip.delete();
EventMessageBundle messages = generateAndCheckEvents(EventType.WAVELET_BLIP_REMOVED);
assertEquals("Expected two events", 2, messages.getEvents().size());
// Blip removed should be the second event.
WaveletBlipRemovedEvent event = WaveletBlipRemovedEvent.as(messages.getEvents().get(1));
assertEquals("Expected the same id as the removed blip", newBlip.getId(),
event.getRemovedBlipId());
}
public void testGenerateDocumentChangedEvent() throws Exception {
ConversationBlip rootBlip =
conversationUtil.buildConversation(wavelet).getRoot().getRootThread().getFirstBlip();
XmlStringBuilder builder = XmlStringBuilder.createText("some random content");
LineContainers.appendToLastLine(rootBlip.getContent(), builder);
EventMessageBundle messages = generateAndCheckEvents(EventType.DOCUMENT_CHANGED);
assertEquals("Expected one event", 1, messages.getEvents().size());
// Can not check the blip id because it is not accessible, however the line
// here below will confirm that there was actually a real
// DocumentChangedEvent put into the message bundle.
DocumentChangedEvent event = DocumentChangedEvent.as(messages.getEvents().get(0));
assertEquals(ALEX.getAddress(), event.getModifiedBy());
}
public void testGenerateDocumentChangedEventOnlyOnce() throws Exception {
ConversationBlip rootBlip =
conversationUtil.buildConversation(wavelet).getRoot().getRootThread().getFirstBlip();
// Change the document twice
XmlStringBuilder builder = XmlStringBuilder.createText("some random content");
LineContainers.appendToLastLine(rootBlip.getContent(), builder);
LineContainers.appendToLastLine(rootBlip.getContent(), builder);
EventMessageBundle messages = generateAndCheckEvents(EventType.DOCUMENT_CHANGED);
assertEquals("Expected one event only", 1, messages.getEvents().size());
}
public void testGenerateAnnotatedTextChangedEvent() throws Exception {
ConversationBlip rootBlip =
conversationUtil.buildConversation(wavelet).getRoot().getRootThread().getFirstBlip();
String annotationKey = "key";
String annotationValue = "value";
rootBlip.getContent().setAnnotation(0, 1, annotationKey, annotationValue);
EventMessageBundle messages = generateAndCheckEvents(EventType.ANNOTATED_TEXT_CHANGED);
assertEquals("Expected one event only", 1, messages.getEvents().size());
AnnotatedTextChangedEvent event = AnnotatedTextChangedEvent.as(messages.getEvents().get(0));
assertEquals("Expected the key of the annotation", annotationKey, event.getName());
assertEquals("Expected the value of the annotation", annotationValue, event.getValue());
}
public void testSelfEventsAreFiltered() throws Exception {
// Robot receives two deltas, it is not participant in wavelet before deltas.
waveletData.removeParticipant(ROBOT);
// Delta1 event #1.
wavelet.addParticipant(ROBOT);
// Delta1 event #2.
wavelet.addParticipant(BOB);
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
List<WaveletOperation> ops1 = Lists.newArrayList(output.getOps());
TransformedWaveletDelta delta1 = makeDeltaFromCapturedOps(ALEX, ops1, endVersion, 0L);
output.clear();
// Delta2 event #1.
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
XmlStringBuilder builder = XmlStringBuilder.createText("some random content");
// Delta2 event #2.
LineContainers.appendToLastLine(newBlip.getContent(), builder);
// Delta2 event #3.
XmlStringBuilder.createText("some more random content by robot");
LineContainers.appendToLastLine(newBlip.getContent(), builder);
List<WaveletOperation> ops2 = Lists.newArrayList(output.getOps());
HashedVersion endVersion2 = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta2 = makeDeltaFromCapturedOps(ROBOT, ops2, endVersion2, 0L);
output.clear();
EventMessageBundle messages = generateEventsFromDeltas(delta1, delta2);
assertEquals("Expected two events", 2, messages.getEvents().size());
checkEventTypeWasGenerated(messages, EventType.WAVELET_SELF_ADDED,
EventType.WAVELET_PARTICIPANTS_CHANGED);
}
public void testEventsFromFirstDeltaAreFiltered() throws Exception {
// Robot receives two deltas, it is participant in wavelet before deltas.
wavelet.addParticipant(BOB);
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
List<WaveletOperation> ops1 = Lists.newArrayList(output.getOps());
TransformedWaveletDelta delta1 = makeDeltaFromCapturedOps(ROBOT, ops1, endVersion, 0L);
output.clear();
// Delta2 event #1.
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
XmlStringBuilder builder = XmlStringBuilder.createText("some random content");
// Delta2 event #2.
LineContainers.appendToLastLine(newBlip.getContent(), builder);
// Delta2 event #3.
wavelet.removeParticipant(BOB);
List<WaveletOperation> ops2 = Lists.newArrayList(output.getOps());
HashedVersion endVersion2 = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta2 = makeDeltaFromCapturedOps(ALEX, ops2, endVersion2, 0L);
output.clear();
EventMessageBundle messages = generateEventsFromDeltas(delta1, delta2);
assertEquals("Expected three events", 3, messages.getEvents().size());
checkEventTypeWasGenerated(messages, EventType.WAVELET_BLIP_CREATED,
EventType.DOCUMENT_CHANGED, EventType.WAVELET_PARTICIPANTS_CHANGED);
}
public void testEventsFromSecondDeltaAreFiltered() throws Exception {
// Robot receives two deltas, it is participant in wavelet before deltas
// Delta1 event #1 - should be delivered to robot.
wavelet.addParticipant(BOB);
List<WaveletOperation> ops = output.getOps();
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta1 = makeDeltaFromCapturedOps(ALEX, ops, endVersion, 0L);
output.clear();
ObservableConversationView conversation = conversationUtil.buildConversation(wavelet);
// Delta2 event #1.
ObservableConversationBlip newBlip = conversation.getRoot().getRootThread().appendBlip();
// Delta2 event #2.
wavelet.removeParticipant(ROBOT);
// Delta2 event #3 - should be filtered.
XmlStringBuilder builder = XmlStringBuilder.createText("some random content");
LineContainers.appendToLastLine(newBlip.getContent(), builder);
List<WaveletOperation> ops2 = output.getOps();
HashedVersion endVersion2 = HashedVersion.unsigned(waveletData.getVersion());
TransformedWaveletDelta delta2 = makeDeltaFromCapturedOps(ALEX, ops2, endVersion2, 0L);
output.clear();
EventMessageBundle messages = generateEventsFromDeltas(delta1, delta2);
assertEquals("Expected three events", 3, messages.getEvents().size());
checkEventTypeWasGenerated(messages, EventType.WAVELET_PARTICIPANTS_CHANGED,
EventType.WAVELET_BLIP_CREATED, EventType.WAVELET_SELF_REMOVED);
}
// Helper Methods.
/**
* Collects the ops applied to wavelet and creates a delta for processing in
* the event generator. The delta author is default human participantId
*
* @param eventType the type of event that should have been generated.
* @return the {@link EventMessageBundle} with the events generated when a
* robot is subscribed to all possible events.
*
* @see #generateAndCheckEvents(EventType, ParticipantId)
*/
private EventMessageBundle generateAndCheckEvents(EventType eventType) throws Exception {
EventMessageBundle eventMessageBundle = generateAndCheckEvents(eventType, ALEX);
return eventMessageBundle;
}
/**
* Collects the ops applied to wavelet and creates a delta for processing in
* the event generator.
*
* @param eventType the type of event that should have been generated.
* @param participantId the delta author (modifier)
* @return the {@link EventMessageBundle} with the events generated when a
* robot is subscribed to all possible events.
*
* @see #generateAndCheckEvents(EventType)
*/
private EventMessageBundle generateAndCheckEvents(EventType eventType,
ParticipantId participantId) throws Exception {
List<WaveletOperation> ops = output.getOps();
HashedVersion endVersion = HashedVersion.unsigned(waveletData.getVersion());
// Create the delta.
TransformedWaveletDelta delta = makeDeltaFromCapturedOps(participantId, ops, endVersion, 0L);
WaveletAndDeltas waveletAndDeltas =
WaveletAndDeltas.create(waveletData, DeltaSequence.of(delta));
// Put the wanted event in the capabilities map
Map<EventType, Capability> capabilities = Maps.newHashMap();
capabilities.put(eventType, new Capability(eventType));
// Generate the events
EventMessageBundle messages =
eventGenerator.generateEvents(waveletAndDeltas, capabilities, CONVERTER);
// Check that the event was generated and that no other types were generated
checkEventTypeWasGenerated(messages, eventType);
checkAllEventsAreInCapabilites(messages, capabilities);
// Generate events with all capabilities
messages = eventGenerator.generateEvents(waveletAndDeltas, ALL_CAPABILITIES, CONVERTER);
checkEventTypeWasGenerated(messages, eventType);
return messages;
}
/**
* Builds a "transformed" delta from client ops (no transformation happens).
*/
private TransformedWaveletDelta makeDeltaFromCapturedOps(ParticipantId author,
List<WaveletOperation> ops, HashedVersion endVersion, long timestamp) {
WaveletDelta clientDelta =
new WaveletDelta(author, HashedVersion.unsigned(endVersion.getVersion() - ops.size()), ops);
return TransformedWaveletDelta.cloneOperations(endVersion, timestamp, clientDelta);
}
/**
* Checks whether events of the given types were put in the bundle.
*/
private void checkEventTypeWasGenerated(EventMessageBundle messages, EventType... types) {
Set<EventType> eventsTypeSet = Sets.newHashSet();
for (EventType eventType : types) {
eventsTypeSet.add(eventType);
}
for (Event event : messages.getEvents()) {
if (eventsTypeSet.contains(event.getType())) {
eventsTypeSet.remove(event.getType());
}
}
if (eventsTypeSet.size() != 0) {
fail("Event of type " + eventsTypeSet.iterator().next() + " has not been generated");
}
}
/**
* Generate events from deltas
*/
private EventMessageBundle generateEventsFromDeltas(TransformedWaveletDelta... deltas)
throws OperationException {
WaveletAndDeltas waveletAndDeltas =
WaveletAndDeltas.create(waveletData, DeltaSequence.of(deltas));
Map<EventType, Capability> capabilities = ALL_CAPABILITIES;
// Generate the events
EventMessageBundle messages =
eventGenerator.generateEvents(waveletAndDeltas, capabilities, CONVERTER);
return messages;
}
/**
* Checks whether all events generated are in the capabilities map.
*/
private void checkAllEventsAreInCapabilites(EventMessageBundle messages,
Map<EventType, Capability> capabilities) {
for (Event event : messages.getEvents()) {
if (!capabilities.containsKey(event.getType())) {
fail("Generated event of type" + event.getType() + " which is not in the capabilities");
}
}
}
}