package org.swellrt.server.box.events; import junit.framework.TestCase; import org.swellrt.model.generic.ListType; import org.swellrt.model.generic.MapType; import org.swellrt.model.generic.Model; import org.swellrt.model.generic.TextType; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.id.InvalidIdException; import org.waveprotocol.wave.model.id.ModernIdSerialiser; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.testing.BasicFactories; import org.waveprotocol.wave.model.testing.FakeIdGenerator; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.DocumentFactory; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.model.wave.data.impl.WaveViewDataImpl; import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl; import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView; import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletConfigurator; import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletFactory; import org.waveprotocol.wave.util.logging.Log; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class DeltaBasedEventSourceTest extends TestCase { private static final Log LOG = Log.get(DeltaBasedEventSourceTest.class); static String fakeDomain = "example.com"; static ParticipantId fakeAuthor = ParticipantId.ofUnsafe("nobody@example.com"); static IdGenerator fakeIdGenerator = FakeIdGenerator.create(); static ObservableWaveView fakeWave = null; static Model fakeDataModel = null; public static interface EventAssertionChecker { public void checkAssertions(Event e); } /** * A sink to collect wavelet operations after data model changes. It keeps ops * in a buffer until the #getTransformedWaveletDelta() is called. */ private static class BufferedSilentOperationSink implements SilentOperationSink<WaveletOperation> { private List<WaveletOperation> opBuffer = new ArrayList<WaveletOperation>(); private int version = 0; /** * Get buffered ops as a transformed wavelet delta and reset the buffer. * * @return */ public TransformedWaveletDelta getTransformedWaveletDelta() { TransformedWaveletDelta deltas = TransformedWaveletDelta.cloneOperations(fakeAuthor, HashedVersion.unsigned(++version), System.currentTimeMillis(), opBuffer); opBuffer.clear(); return deltas; } @Override public void consume(WaveletOperation op) { opBuffer.add(op); } } /** * Create an sample wave view with a op-based wavelet factory * * @return */ private static ObservableWaveView createSampleWave( SilentOperationSink<? super WaveletOperation> sink) { final WaveViewDataImpl waveData = WaveViewDataImpl.create(fakeIdGenerator.newWaveId()); final DocumentFactory<?> docFactory = BasicFactories.fakeDocumentFactory(); final ObservableWaveletData.Factory<?> waveletDataFactory = new ObservableWaveletData.Factory<WaveletDataImpl>() { private final ObservableWaveletData.Factory<WaveletDataImpl> inner = WaveletDataImpl.Factory.create(docFactory); @Override public WaveletDataImpl create(ReadableWaveletData data) { WaveletDataImpl wavelet = inner.create(data); waveData.addWavelet(wavelet); return wavelet; } }; WaveletFactory<OpBasedWavelet> waveletFactory = BasicFactories.opBasedWaveletFactoryBuilder().with(fakeAuthor).with(waveletDataFactory) .with(sink) .build(); WaveViewImpl<?> wave = WaveViewImpl.create(waveletFactory, waveData.getWaveId(), fakeIdGenerator, fakeAuthor, WaveletConfigurator.ADD_CREATOR); return wave; } /** * A sink to collect wavelet operations after data model changes. It keeps ops * in a buffer until the #getTransformedWaveletDelta() is called. */ private static BufferedSilentOperationSink fakeSink = new BufferedSilentOperationSink(); /** * Initialize a shared fake wave for all tests. Data model init data should be * added here. */ static { fakeWave = createSampleWave(fakeSink); fakeDataModel = Model.create(fakeWave, fakeDomain, fakeAuthor, Boolean.TRUE, fakeIdGenerator); fakeDataModel.getRoot().put("one", fakeDataModel.createString("String One")); ListType list = (ListType) fakeDataModel.getRoot().put("list", fakeDataModel.createList()).asList(); list.add(fakeDataModel.createString("list-zero")); list.add(fakeDataModel.createString("list-one")); list.add(fakeDataModel.createString("list-two")); MapType map = (MapType) list.add(fakeDataModel.createMap()).asMap(); map.put("l4-k1", fakeDataModel.createString("value 1")); map.put("l4-k2", fakeDataModel.createString("value 2")); fakeDataModel.getRoot().put("two", fakeDataModel.createString("String Two")); fakeDataModel.getRoot().put("map", fakeDataModel.createMap()); fakeDataModel.getRoot().put("text", fakeDataModel.createText()); LOG.fine("Created fake Wave and Data Model"); } // root.list.?.<property> protected void setUp() throws Exception { super.setUp(); // Emtpy the buffer of operations. fakeSink.getTransformedWaveletDelta(); LOG.fine("Emtpy sink buffered operations"); } /** * An utility method wiring all componentes of these tests: EventQueue, * EventSource, EventSourceConfigurator... * * @param pathsToExtract * @param _app * @param _dataType * @param assertions * @throws InvalidIdException */ private void testWaveletUpdate(final ArrayList<String> pathsToExtract, final String _app, final String _dataType, final EventAssertionChecker assertions) throws InvalidIdException { EventQueue eventQueue = new EventQueue() { @Override public void add(Event event) { assertions.checkAssertions(event); } @Override public boolean hasEventsFor(String app, String dataType) { return (_app == null && _dataType == null) || (_app.equals(app) && _dataType.equals(dataType)); } @Override public Set<String> getExpressionPaths(String app, String dataType) { return new HashSet<String>(pathsToExtract); } @Override public void registerListener(EventQueueListener listener) { // TODO Auto-generated method stub } @Override public void registerConfigurator(EventQueueConfigurator configurator) { // TODO Auto-generated method stub } }; DeltaBasedEventSource eventSource = new DeltaBasedEventSource(eventQueue); eventSource.waveletUpdate( fakeWave.getWavelet( ModernIdSerialiser.INSTANCE.deserialiseWaveletId(fakeDomain + "/" + Model.WAVELET_SWELL_ROOT)) .getWaveletData(), DeltaSequence.of(fakeSink.getTransformedWaveletDelta())); } public void testEventListItemAddedString() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. ListType list = (ListType) fakeDataModel.getRoot().get("list").asList(); list.add(fakeDataModel.createString("Hello World")); // Generate event and test testWaveletUpdate(CollectionUtils.<String> newArrayList(), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { assertEquals(Event.Type.LIST_ITEM_ADDED, e.getType()); assertEquals("Hello World", e.getContextData().get("root.list.?")); } }); } // // LIST_ITEM_ADDED // public void testEventListItemAddedMap() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. ListType list = (ListType) fakeDataModel.getRoot().get("list").asList(); // Operation (Step 0) MapType map = (MapType) list.add(fakeDataModel.createMap()); map.put("field1", "foo"); map.put("field2", "bar"); MapType map_map = (MapType) map.put("map", fakeDataModel.createMap()); map_map.put("field1", "boom"); map_map.put("field2", "flash"); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList( "root.one", "root.two", "root.list.?.field1", "root.list.?.field2", "root.list.?.map.field1", "root.list.?.map.field2"), "default", "default", new EventAssertionChecker() { int opStep = 0; @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); // We test only first model change if (opStep == 0) { assertEquals(Event.Type.LIST_ITEM_ADDED, e.getType()); // Chech event-specific context data // Simple values in the list assertEquals("foo", e.getContextData().get("root.list.?.field1")); assertEquals("bar", e.getContextData().get("root.list.?.field2")); // Map values in the list item assertEquals("boom", e.getContextData().get("root.list.?.map.field1")); assertEquals("flash", e.getContextData().get("root.list.?.map.field2")); opStep++; } // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // LIST_ITEM_REMOVED // public void testEventListItemRemoved() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. ListType list = (ListType) fakeDataModel.getRoot().get("list").asList(); list.add(fakeDataModel.createString("item one")); list.add(fakeDataModel.createString("item two")); list.add(fakeDataModel.createMap()); list.add(fakeDataModel.createList()); // Clear sink operations so far fakeSink.getTransformedWaveletDelta(); // Delete these items list.remove(list.size() - 4); list.remove(list.size() - 3); list.remove(list.size() - 2); list.remove(list.size() - 1); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList("root.one", "root.two"), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); assertEquals(Event.Type.LIST_ITEM_REMOVED, e.getType()); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // MAP_ENTRY_UPDATED // public void testEventMapEntryUpdated() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. MapType map = (MapType) fakeDataModel.getRoot().get("map").asMap(); map.put("field0", "hello world"); // Reset operations sink fakeSink.getTransformedWaveletDelta(); // Three updates in the same delta, all of them will share the same last // state = "ro" map.put("field1", "foo"); map.put("field1", "bar"); map.put("field1", "ro"); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList( "root.one", "root.two", "root.map.field0", "root.map.field1", "root.list.?.field1") , "default", "default", new EventAssertionChecker() { int eventCount = 0; @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); // All updates has the same value (the lastest) assertEquals(Event.Type.MAP_ENTRY_UPDATED, e.getType()); if (eventCount == 0) assertEquals("foo", e.getContextData().get("root.map.field1")); else if (eventCount == 1) assertEquals("bar", e.getContextData().get("root.map.field1")); else if (eventCount == 2) assertEquals("ro", e.getContextData().get("root.map.field1")); eventCount++; assertEquals("hello world", e.getContextData().get("root.map.field0")); assertNull(e.getContextData().get("root.list.?.field1")); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // MAP_ENTRY_UPDATED inside a LIST // public void testEventMapEntryUpdatedInsideList() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. MapType map = ((MapType) fakeDataModel.getRoot().get("list").asList().get(3)).asMap(); map.put("l4-k1", "hello world"); // Reset operations sink fakeSink.getTransformedWaveletDelta(); // Three updates in the same delta, all of them will share the same last // state = "ro" map.put("l4-k2", "foo"); map.put("l4-k2", "bar"); map.put("l4-k2", "ro"); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList( "root.one", "root.two", "root.map.field0", "root.map.field1", "root.list.?.l4-k1", "root.list.?.l4-k2"), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); // All updates has the same value (the lastest) assertEquals(Event.Type.MAP_ENTRY_UPDATED, e.getType()); assertEquals("ro", e.getContextData().get("root.list.?.l4-k2")); assertEquals("hello world", e.getContextData().get("root.list.?.l4-k1")); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // MAP_ENTRY_REMOVED // public void testEventMapEntryRemoved() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. MapType map = (MapType) fakeDataModel.getRoot().get("map").asMap(); map.put("field2", "delete me"); map.put("field3", "update me"); // Reset operations sink fakeSink.getTransformedWaveletDelta(); // Mix updates and removes to check right doc op processing map.put("field2", "please, delete me"); map.put("field3", "i am updated"); map.remove("field2"); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList( "root.one", "root.two", "root.map.field2", "root.map.field3"), "default", "default", new EventAssertionChecker() { int step = 0; @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); if (step == 0) { assertEquals(Event.Type.MAP_ENTRY_UPDATED, e.getType()); assertEquals("please, delete me", e.getContextData().get("root.map.field2")); step++; } if (step == 1) { assertEquals(Event.Type.MAP_ENTRY_UPDATED, e.getType()); assertEquals("i am updated", e.getContextData().get("root.map.field3")); step++; } if (step == 3) { assertEquals(Event.Type.MAP_ENTRY_REMOVED, e.getType()); assertNull(e.getContextData().get("root.map.field2")); step++; } // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // ADD_PARTICIPANT // public void testEventAddParticipant() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. fakeDataModel.addParticipant("peter@" + fakeDomain); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList("root.one", "root.two"), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); assertEquals(Event.Type.ADD_PARTICIPANT, e.getType()); assertEquals("peter@" + fakeDomain, e.getParticipant()); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // REMOVE_PARTICIPANT // public void testEventRemoveParticipant() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. fakeDataModel.removeParticipant("peter@" + fakeDomain); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList( "root.one", "root.two"), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); assertEquals(Event.Type.REMOVE_PARTICIPANT, e.getType()); assertEquals("peter@" + fakeDomain, e.getParticipant()); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } // // DOC_CHANGE // public void testEventDocChange() throws InvalidIdException { // Mutate the fake data model. Changes reach the sink as doc ops. TextType text = (TextType) fakeDataModel.fromPath("root.text"); text.insertText(text.getSize() - 1, "document fever"); // Test generated events testWaveletUpdate(CollectionUtils.<String> newArrayList("root.one", "root.two"), "default", "default", new EventAssertionChecker() { @Override public void checkAssertions(Event e) { LOG.fine(e.getType().toString()); assertEquals(Event.Type.DOC_CHANGE, e.getType()); assertEquals("document fever", e.getCharacters()); // Check generic context data, it applies for all events assertEquals("String One", e.getContextData().get("root.one")); assertEquals("String Two", e.getContextData().get("root.two")); } }); } }