/**
* 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.model.document.indexed;
import junit.framework.Assert;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.ModifiableDocument;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.operation.NindoSink;
import org.waveprotocol.wave.model.document.operation.algorithm.DocOpInverter;
import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
import org.waveprotocol.wave.model.operation.OperationException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Test cases for applying operations to documents.
*
*/
public class NindoTestCases {
/**
* A parser that can serialise documents to strings and deserialise documents from strings.
*
* @param <D> The type of the document.
*/
public interface DocumentParser<D extends NindoSink & ModifiableDocument> {
/**
* Deserialise the document from an XML string.
*
* @param documentString The XML string.
* @return The document.
*/
D parseDocument(String documentString);
/**
* @return a copy of the given document
*/
D copyDocument(D other);
/**
* Serialise the document to an XML string.
*
* @param document The document.
* @return The XML string.
*/
String asString(D document);
}
private static final class TestComponent {
final Nindo mutation;
final List<String> expectedResultAlternatives = new ArrayList<String>();
TestComponent(Nindo mutation, String... expectedResultAlternatives) {
this.mutation = mutation;
for (String expected : expectedResultAlternatives) {
this.expectedResultAlternatives.add(expected);
}
}
<D extends NindoSink & ModifiableDocument> void run(D document, DocumentParser<D> parser) {
String initialState = parser.asString(document);
D copy = parser.copyDocument(document);
DocOp docOp = null;
Assert.assertEquals("Copy did not work",
initialState, parser.asString(copy));
try {
docOp = document.consumeAndReturnInvertible(mutation);
copy.consume(docOp);
} catch (OperationException e) {
Assert.fail("An OperationException was thrown: " + e);
}
String computedDocument = parser.asString(document);
String message = "Computed: " + computedDocument + " Expected: "
+ expectedResultAlternatives + " Nindo: " + mutation
+ " Op: " + DocOpUtil.toConciseString(docOp);
Assert.assertTrue(message, expectedResultAlternatives.contains(computedDocument));
Assert.assertEquals("Generated invertible op not equivalent to non-invertible one",
parser.asString(document), parser.asString(copy));
try {
copy.consume(DocOpInverter.invert(docOp));
} catch (OperationException e) {
Assert.fail("An OperationException was thrown: " + e);
}
Assert.assertEquals("Inversion of generated invertible op is incorrect",
initialState, parser.asString(copy));
}
}
private static final class TestParameters {
private final String initialDocument;
private final List<TestComponent> testComponents;
TestParameters(Nindo documentMutation, String initialDocument,
String... finalDocuments) {
this.initialDocument = initialDocument;
testComponents =
Collections.singletonList(new TestComponent(documentMutation, finalDocuments));
}
TestParameters(String initialDocument, TestComponent... testComponents) {
this.initialDocument = initialDocument;
this.testComponents = Arrays.asList(testComponents);
}
<D extends NindoSink & ModifiableDocument> void run(DocumentParser<D> parser) {
D document = parser.parseDocument(initialDocument);
for (TestComponent component : testComponents) {
component.run(document, parser);
}
}
}
/**
* Tests for insertion of text.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> INSERT_TEXT_TESTS = Arrays.asList(
new TestParameters(
Nindo.insertCharacters(1, "ab"),
"<p></p>", "<p>ab</p>"
),
new TestParameters(
Nindo.insertCharacters(2, "12"),
"<p>abcde</p>", "<p>a12bcde</p>"
),
new TestParameters(
Nindo.insertCharacters(6, "12"),
"<p>abcde</p>", "<p>abcde12</p>"
),
new TestParameters(
Nindo.insertCharacters(5, "12"),
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"</p>",
"<p>" +
"<i>ab</i>" +
"12cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"</p>"
),
new TestParameters(
Nindo.insertCharacters(12, " 12 "),
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"</p>",
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>f 12 g</i>" +
"</b>" +
" h" +
"</p>"
)
);
/**
* Tests for deletion of text.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> DELETE_TEXT_TESTS = Arrays.asList(
new TestParameters(
createDeletion(1, 3),
"<p>ab</p>", "<p></p>", "<p/>"
),
new TestParameters(
createDeletion(2, 5),
"<p>abcde</p>", "<p>ae</p>"
),
new TestParameters(
createDeletion(4, 6),
"<p>ab<i>cd</i>ef</p>", "<p>ab<i></i>ef</p>", "<p>ab<i/>ef</p>"
),
new TestParameters(
createDeletion(2, 3, 4, 5),
"<p>ab<i>cd</i>ef</p>", "<p>a<i>d</i>ef</p>"
),
new TestParameters(
createDeletion(5, 6, 7, 8),
"<p>ab<i>cd</i>ef</p>", "<p>ab<i>c</i>f</p>"
),
new TestParameters(
createDeletion(3, 4, 6, 7),
"<p><i>ab</i><i>cd</i></p>", "<p><i>a</i><i>d</i></p>"
),
new TestParameters(
createDeletion(5, 6, 7, 8, 10, 11),
"<p><i>a<b>ef</b>b</i><i>cd</i></p>", "<p><i>a<b>e</b></i><i>d</i></p>"
),
new TestParameters(
createDeletion(3, 4, 6, 7, 8, 9),
"<p><i>ab</i><i>c<b>ef</b>d</i></p>", "<p><i>a</i><i><b>f</b>d</i></p>"
),
new TestParameters(
createDeletion(5, 6, 7, 8, 10, 11, 12, 13),
"<p><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i></p>", "<p><i>a<b>e</b></i><i><b>f</b>d</i></p>"
),
new TestParameters(
createDeletion(4, 6, 7, 8, 10, 11),
"<p><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i></p>",
"<p><i>a<b></b></i><i><b>ef</b>d</i></p>", "<p><i>a<b/></i><i><b>ef</b>d</i></p>"
),
new TestParameters(
createDeletion(7, 8, 10, 11, 12, 14),
"<p><i>a<b>ef</b>b</i><i>c<b>ef</b>d</i></p>",
"<p><i>a<b>ef</b></i><i><b></b>d</i></p>", "<p><i>a<b>ef</b></i><i><b/>d</i></p>"
)
);
/**
* Tests for insertion of elements.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> INSERT_ELEMENT_TESTS = Arrays.asList(
new TestParameters(
structuralSample1(1),
"<p>abc</p>", "<p>12<u>34</u>56abc</p>"
),
new TestParameters(
structuralSample1(4),
"<p>abc</p>", "<p>abc12<u>34</u>56</p>"
),
new TestParameters(
structuralSample1(3),
"<p>abc</p>", "<p>ab12<u>34</u>56c</p>"
),
new TestParameters(
structuralSample1(5),
"<p>a<b>b</b><i>c</i></p>", "<p>a<b>b</b>12<u>34</u>56<i>c</i></p>"
),
new TestParameters(
structuralSample2(3),
"<p>abc</p>", "<p>ab12<u>3<i>hello</i><b>world</b>4</u>56c</p>"
),
new TestParameters(
structuralSample3(3),
"<p>abc</p>",
"<p>ab12<u>3<i></i><b></b>4</u>56c</p>", "<p>ab12<u>3<i/><b/>4</u>56c</p>"
),
new TestParameters(
structuralSample4(3),
"<p>abc</p>", "<p>ab<i>hello</i><b>world</b>c</p>"
),
new TestParameters(
structuralSample5(3),
"<p>abc</p>", "<p>ab<a href=\"http://www.google.com/\">google</a>c</p>"
),
new TestParameters(
structuralSample1(12),
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"</p>",
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>f12<u>34</u>56g</i>" +
"</b>" +
" h" +
"</p>"
)
);
/**
* Tests for deletion of elements.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> DELETE_ELEMENT_TESTS = Arrays.asList(
new TestParameters(
structuralDeletionSample1(1),
"<p>12<u>34</u>56abc</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample1(4),
"<p>abc12<u>34</u>56</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample1(3),
"<p>ab12<u>34</u>56c</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample1(5),
"<p>a<b>b</b>12<u>34</u>56<i>c</i></p>", "<p>a<b>b</b><i>c</i></p>"
),
new TestParameters(
structuralDeletionSample2(3),
"<p>ab12<u>3<i>hello</i><b>world</b>4</u>56c</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample3(3),
"<p>ab12<u>3<i></i><b></b>4</u>56c</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample4(3),
"<p>ab<i>hello</i><b>world</b>c</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample5(3),
"<p>ab<a href=\"http://www.google.com/\">google</a>c</p>", "<p>abc</p>"
),
new TestParameters(
structuralDeletionSample1(12),
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>f12<u>34</u>56g</i>" +
"</b>" +
" h" +
"</p>",
"<p>" +
"<i>ab</i>" +
"cd " +
"<b>" +
"e" +
"<i>fg</i>" +
"</b>" +
" h" +
"</p>"
)
);
/**
* Tests for the setting and removal of attributes.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> ATTRIBUTE_TESTS = Arrays.asList(
new TestParameters(
Nindo.replaceAttributes(1, new AttributesImpl("href", "http://www.google.com/")),
"<p><a>ab</a></p>",
"<p><a href=\"http://www.google.com/\">ab</a></p>"
),
new TestParameters(
Nindo.replaceAttributes(1, new AttributesImpl("href", "http://www.google.com/")),
"<p><a name=\"thing\">ab</a></p>",
"<p><a href=\"http://www.google.com/\">ab</a></p>"
),
new TestParameters(
Nindo.setAttribute(1, "href", "http://www.google.com/"),
"<p><a>ab</a></p>",
"<p><a href=\"http://www.google.com/\">ab</a></p>"
),
new TestParameters(
Nindo.setAttribute(2, "href", "http://www.google.com/"),
"<p>a<a>b</a></p>",
"<p>a<a href=\"http://www.google.com/\">b</a></p>"
),
new TestParameters(
Nindo.setAttribute(2, "href", "http://www.google.com/"),
"<p>a<a href=\"http://www.yahoo.com/\">b</a></p>",
"<p>a<a href=\"http://www.google.com/\">b</a></p>"
),
new TestParameters(
Nindo.removeAttribute(2, "href"),
"<p>a<a href=\"http://www.google.com/\">b</a></p>",
"<p>a<a>b</a></p>"
)
);
/**
* Miscellaneous tests.
*/
// OFFSET ADJUSTED
private static final List<TestParameters> MISCELLANEOUS_TESTS = Arrays.asList(
new TestParameters(
"<p></p>",
new TestComponent(Nindo.insertCharacters(1, "a"), "<p>a</p>"),
new TestComponent(Nindo.insertCharacters(2, "b"), "<p>ab</p>"),
new TestComponent(Nindo.insertCharacters(3, "c"), "<p>abc</p>")
),
new TestParameters(
"",
new TestComponent(Nindo.insertElement(0, "p", Attributes.EMPTY_MAP),
"<p></p>", "<p/>"),
new TestComponent(Nindo.insertCharacters(1, "a"), "<p>a</p>"),
new TestComponent(Nindo.insertCharacters(2, "b"), "<p>ab</p>"),
new TestComponent(Nindo.insertCharacters(3, "c"), "<p>abc</p>")
),
new TestParameters(
"<p></p>",
new TestComponent(structuralSample1(1), "<p>12<u>34</u>56</p>"),
new TestComponent(structuralSample1(8), "<p>12<u>34</u>512<u>34</u>566</p>"),
new TestComponent(structuralSample1(15), "<p>12<u>34</u>512<u>34</u>512<u>34</u>5666</p>")
)
);
/**
* Runs the tests for the insertion of text.
*/
public static void runTextInsertionTests(DocumentParser<?> documentParser) {
processTests(INSERT_TEXT_TESTS, documentParser);
}
/**
* Runs the tests for the deletion of text.
*/
public static void runTextDeletionTests(DocumentParser<?> documentParser) {
processTests(DELETE_TEXT_TESTS, documentParser);
}
/**
* Runs the tests for the insertion of elements.
*/
public static void runElementInsertionTests(DocumentParser<?> documentParser) {
processTests(INSERT_ELEMENT_TESTS, documentParser);
}
/**
* Runs the tests for the deletion of elements.
*/
public static void runElementDeletionTests(DocumentParser<?> documentParser) {
processTests(DELETE_ELEMENT_TESTS, documentParser);
}
/**
* Runs the tests for the setting and removal of attributes.
*/
public static void runAttributeTests(DocumentParser<?> documentParser) {
processTests(ATTRIBUTE_TESTS, documentParser);
}
/**
* Runs a miscellany of tests.
*/
public static void runMiscellaneousTests(DocumentParser<?> documentParser) {
processTests(MISCELLANEOUS_TESTS, documentParser);
}
private static void processTests(List<TestParameters> tests, DocumentParser<?> documentParser) {
for (TestParameters parameters : tests) {
parameters.run(documentParser);
}
}
private static Nindo createDeletion(int... deletionBoundaries) {
Nindo.Builder builder = new Nindo.Builder();
int location = 0;
for (int i = 0; i < deletionBoundaries.length; i += 2) {
builder.skip(deletionBoundaries[i] - location);
int deletionSize = deletionBoundaries[i+1] - deletionBoundaries[i];
builder.deleteCharacters(deletionSize);
location = deletionBoundaries[i+1];
}
return builder.build();
}
/**
* Creates an operation to insert the XML fragment "12<u>34</u>56".
*
* @param location The location at which the insert the fragment.
* @return The operation.
*/
private static Nindo structuralSample1(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.characters("12");
builder.elementStart("u", Attributes.EMPTY_MAP);
builder.characters("34");
builder.elementEnd();
builder.characters("56");
return builder.build();
}
/**
* Creates an operation to insert the XML fragment
* "12<u>3<i>hello</i><b>world</b>4</u>56".
*
* @param location The location at which the insert the fragment.
* @return The operation.
*/
private static Nindo structuralSample2(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.characters("12");
builder.elementStart("u", Attributes.EMPTY_MAP);
builder.characters("3");
builder.elementStart("i", Attributes.EMPTY_MAP);
builder.characters("hello");
builder.elementEnd();
builder.elementStart("b", Attributes.EMPTY_MAP);
builder.characters("world");
builder.elementEnd();
builder.characters("4");
builder.elementEnd();
builder.characters("56");
return builder.build();
}
/**
* Creates an operation to insert the XML fragment "12<u>3<i/><b/>4</u>56".
*
* @param location The location at which the insert the fragment.
* @return The operation.
*/
private static Nindo structuralSample3(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.characters("12");
builder.elementStart("u", Attributes.EMPTY_MAP);
builder.characters("3");
builder.elementStart("i", Attributes.EMPTY_MAP);
builder.elementEnd();
builder.elementStart("b", Attributes.EMPTY_MAP);
builder.elementEnd();
builder.characters("4");
builder.elementEnd();
builder.characters("56");
return builder.build();
}
/**
* Creates an operation to insert the XML fragment "<i>hello</i><b>world</b>".
*
* @param location The location at which the insert the fragment.
* @return The operation.
*/
private static Nindo structuralSample4(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.elementStart("i", Attributes.EMPTY_MAP);
builder.characters("hello");
builder.elementEnd();
builder.elementStart("b", Attributes.EMPTY_MAP);
builder.characters("world");
builder.elementEnd();
return builder.build();
}
/**
* Creates an operation to insert the XML fragment
* "<a href=\"http://www.google.com/\">google</a>".
*
* @param location The location at which the insert the fragment.
* @return The operation.
*/
private static Nindo structuralSample5(int location) {
Attributes attributes = new AttributesImpl(
Collections.singletonMap("href", "http://www.google.com/"));
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.elementStart("a", new AttributesImpl(attributes));
builder.characters("google");
builder.elementEnd();
return builder.build();
}
/**
* Creates an operation to delete the XML fragment "12<u>34</u>56".
*
* @param location The start of the range where the fragment to be deleted
* resides.
* @return The operation.
*/
private static Nindo structuralDeletionSample1(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.deleteCharacters(2);
builder.deleteElementStart();
builder.deleteCharacters(2);
builder.deleteElementEnd();
builder.deleteCharacters(2);
return builder.build();
}
/**
* Creates an operation to delete the XML fragment
* "12<u>3<i>hello</i><b>world</b>4</u>56".
*
* @param location The start of the range where the fragment to be deleted
* resides.
* @return The operation.
*/
private static Nindo structuralDeletionSample2(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.deleteCharacters(2);
builder.deleteElementStart();
builder.deleteCharacters(1);
builder.deleteElementStart();
builder.deleteCharacters(5);
builder.deleteElementEnd();
builder.deleteElementStart();
builder.deleteCharacters(5);
builder.deleteElementEnd();
builder.deleteCharacters(1);
builder.deleteElementEnd();
builder.deleteCharacters(2);
return builder.build();
}
/**
* Creates an operation to delete the XML fragment "12<u>3<i/><b/>4</u>56".
*
* @param location The start of the range where the fragment to be deleted
* resides.
* @return The operation.
*/
private static Nindo structuralDeletionSample3(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.deleteCharacters(2);
builder.deleteElementStart();
builder.deleteCharacters(1);
builder.deleteElementStart();
builder.deleteElementEnd();
builder.deleteElementStart();
builder.deleteElementEnd();
builder.deleteCharacters(1);
builder.deleteElementEnd();
builder.deleteCharacters(2);
return builder.build();
}
/**
* Creates an operation to delete the XML fragment "<i>hello</i><b>world</b>".
*
* @param location The start of the range where the fragment to be deleted
* resides.
* @return The operation.
*/
private static Nindo structuralDeletionSample4(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.deleteElementStart();
builder.deleteCharacters(5);
builder.deleteElementEnd();
builder.deleteElementStart();
builder.deleteCharacters(5);
builder.deleteElementEnd();
return builder.build();
}
/**
* Creates an operation to delete the XML fragment
* "<a href=\"http://www.google.com/\">google</a>".
*
* @param location The start of the range where the fragment to be deleted
* resides.
* @return The operation.
*/
private static Nindo structuralDeletionSample5(int location) {
Nindo.Builder builder = new Nindo.Builder();
builder.skip(location);
builder.deleteElementStart();
builder.deleteCharacters(6);
builder.deleteElementEnd();
return builder.build();
}
}