/**
* Copyright 2009 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.wave.model.document.operation.impl;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.waveprotocol.wave.model.document.bootstrap.BootstrapDocument;
import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.DocInitializationCursor;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ValidationResult;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector;
import org.waveprotocol.wave.model.document.operation.util.ImmutableStateMap.Attribute;
import org.waveprotocol.wave.model.operation.OperationException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @author ohler@google.com (Christian Ohler)
*/
public final class DocOpValidatorTest extends TestCase {
public static final DocumentSchema TEST_CONSTRAINTS =
new DocumentSchema() {
@Override
public boolean permitsAttribute(String type, String attributeName) {
return false;
}
@Override
public boolean permitsAttribute(String type, String attributeName, String attributeValue) {
return false;
}
@Override
public boolean permitsChild(String parent, String child) {
return null == parent && "body".equals(child)
|| "body".equals(parent) && "line".equals(child)
|| "body".equals(parent) && "otherchildofbody".equals(child);
}
@Override
public PermittedCharacters permittedCharacters(String type) {
if ("body".equals(type)) {
return PermittedCharacters.BLIP_TEXT;
}
return PermittedCharacters.NONE;
}
@Override
public List<String> getRequiredInitialChildren(String typeOrNull) {
if ("body".equals(typeOrNull)) {
return Collections.singletonList("line");
}
return Collections.emptyList();
}
};
// TODO(ohler): These tests are by far not enough. Need more coverage.
abstract class TestData {
abstract boolean build(DocInitializationCursor d, DocOpCursor m);
DocumentSchema getSchemaConstraints() {
return DocumentSchema.NO_SCHEMA_CONSTRAINTS;
}
}
public void test1() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
return true;
}
});
}
public void test2() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("<", Attributes.EMPTY_MAP);
m.elementEnd();
return false;
}
});
}
public void test3() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("blip", Attributes.EMPTY_MAP);
m.elementEnd();
return true;
}
});
}
public void test4() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("p", Attributes.EMPTY_MAP);
m.elementEnd();
return false;
}
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
});
}
public void test5() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("blip", Attributes.EMPTY_MAP);
m.elementEnd();
return true;
}
});
}
public void testMaxSkipDistanceDoesntAssert() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.elementStart("blip", Attributes.EMPTY_MAP);
d.elementEnd();
m.retain(3);
m.retain(1);
return false;
}
});
}
public void testUnsortedAttributes() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("blip",
AttributesImpl.fromSortedAttributesUnchecked(
Arrays.asList(new Attribute[] {
new Attribute("a", "1"),
new Attribute("b", "1")
})));
m.elementEnd();
return true;
}
});
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("blip",
AttributesImpl.fromSortedAttributesUnchecked(
Arrays.asList(new Attribute[] {
new Attribute("b", "1"),
new Attribute("a", "1")
})));
m.elementEnd();
return false;
}
});
}
// We want to be able to test with AnnotationBoundaryMaps that
// AnnotationBoundaryMapImpl's builder doesn't let us construct
// because of its error checking. So we need our own implementation.
private static class DumbAnnotationBoundaryMap implements AnnotationBoundaryMap {
final String[] endKeys;
final String[] changeTriplets;
DumbAnnotationBoundaryMap(String[] endKeys, String[] changeTriplets) {
this.endKeys = endKeys;
this.changeTriplets = changeTriplets;
}
@Override
public int endSize() {
return endKeys.length;
}
@Override
public int changeSize() {
return changeTriplets.length / 3;
}
@Override
public String getEndKey(int endIndex) {
return endKeys[endIndex];
}
@Override
public String getChangeKey(int changeIndex) {
return changeTriplets[changeIndex * 3];
}
@Override
public String getOldValue(int changeIndex) {
return changeTriplets[changeIndex * 3 + 1];
}
@Override
public String getNewValue(int changeIndex) {
return changeTriplets[changeIndex * 3 + 2];
}
}
public void testDuplicateAnnotationKeys() throws OperationException {
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("ab");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.updateValues("a", null, "1")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("a")
.updateValues("b", null, "2")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("b")
.build());
return true;
}
});
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("ab");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.updateValues("a", null, "1")
.build());
m.retain(1);
m.annotationBoundary(new DumbAnnotationBoundaryMap(
new String[] { "a" },
new String[] { "a", null, "2" }));
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("a")
.build());
return false;
}
});
}
public void testUnsortedAnnotationKeys() throws OperationException {
// all ok
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("ab");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.updateValues("a", null, "1", "b", null, "2")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("a")
.updateValues("b", null, "2", "c", null, "3")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("b", "c")
.build());
return true;
}
});
// change keys in wrong order
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("ab");
m.annotationBoundary(new DumbAnnotationBoundaryMap(
new String[] {},
new String[] { "b", null, "2", "a", null, "1" }));
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("a")
.updateValues("b", null, "2", "c", null, "3")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("b", "c")
.build());
return false;
}
});
// end keys in wrong order
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("ab");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.updateValues("a", null, "1", "b", null, "2")
.build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder()
.initializationEnd("a")
.updateValues("b", null, "2", "c", null, "3")
.build());
m.retain(1);
m.annotationBoundary(new DumbAnnotationBoundaryMap(
new String[] { "c", "b" },
new String[] {}));
return false;
}
});
}
public void testDeletionAnnotationsAreRelative() throws OperationException {
// annotations are not needed if previous character has the same annotations
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.characters("b");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.retain(1);
m.deleteCharacters("b");
return true;
}
});
// but may be specified
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.characters("b");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", "1", "1").build());
m.deleteCharacters("b");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
return true;
}
});
// even for null values
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.characters("a");
d.characters("b");
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", null).build());
m.deleteCharacters("b");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
return true;
}
});
}
public void testDeletionAnnotationsAreRelative2() throws OperationException {
// annotations are needed if previous character has different annotations
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.deleteCharacters("a");
return false;
}
});
// annotations are needed if previous character has different annotations
// (positive case)
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", "1", null).build());
m.deleteCharacters("a");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
return true;
}
});
// this is even true when deleting multiple items in sequence
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("ab");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", "1", null).build());
m.deleteCharacters("a");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.deleteCharacters("b");
return false;
}
});
// this is even true when deleting multiple items in sequence
// (positive case)
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("ab");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", "1", null).build());
m.deleteCharacters("a");
m.deleteCharacters("b");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
return true;
}
});
}
public void testDeletionAnnotationsAreRelative3() throws OperationException {
// annotations are needed if previous character has more annotations
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
d.characters("b");
m.retain(1);
m.deleteCharacters("b");
return false;
}
});
// annotations are needed if previous character has more annotations
doTest(new TestData() {
@Override
public boolean build(DocInitializationCursor d, DocOpCursor m) {
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationValues("a", "1").build());
d.characters("a");
d.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
d.characters("b");
m.retain(1);
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", null, "1").build());
m.deleteCharacters("b");
m.annotationBoundary(
AnnotationBoundaryMapImpl.builder().initializationEnd("a").build());
return true;
}
});
}
public void testRequiredTag() throws OperationException {
// ok
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("body", Attributes.EMPTY_MAP);
m.elementStart("line", Attributes.EMPTY_MAP);
m.elementEnd();
m.elementEnd();
return true;
}});
// missing required element
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("body", Attributes.EMPTY_MAP);
m.elementEnd();
return false;
}});
// characters before required element
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("body", Attributes.EMPTY_MAP);
m.characters("a");
m.elementStart("line", Attributes.EMPTY_MAP);
m.elementEnd();
m.elementEnd();
return false;
}});
// different element before required element
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
m.elementStart("body", Attributes.EMPTY_MAP);
m.elementStart("otherchildofbody", Attributes.EMPTY_MAP);
m.elementEnd();
m.elementStart("line", Attributes.EMPTY_MAP);
m.elementEnd();
m.elementEnd();
return false;
}});
}
public void testDeletingRequiredTag() throws OperationException {
// ok
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
d.elementStart("body", Attributes.EMPTY_MAP);
d.elementStart("line", Attributes.EMPTY_MAP);
d.elementEnd();
d.elementEnd();
m.deleteElementStart("body", Attributes.EMPTY_MAP);
m.deleteElementStart("line", Attributes.EMPTY_MAP);
m.deleteElementEnd();
m.deleteElementEnd();
return true;
}});
// missing required element
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
d.elementStart("body", Attributes.EMPTY_MAP);
d.elementStart("line", Attributes.EMPTY_MAP);
d.elementEnd();
d.elementEnd();
m.retain(1);
m.deleteElementStart("line", Attributes.EMPTY_MAP);
m.deleteElementEnd();
m.retain(1);
return false;
}});
}
public void testInsertingAroundRequiredTag() throws OperationException {
// ok to insert after
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
d.elementStart("body", Attributes.EMPTY_MAP);
d.elementStart("line", Attributes.EMPTY_MAP);
d.elementEnd();
d.elementEnd();
m.retain(3);
m.elementStart("line", Attributes.EMPTY_MAP);
m.elementEnd();
m.retain(1);
return true;
}});
// not ok to insert before
doTest(new TestData() {
@Override
DocumentSchema getSchemaConstraints() {
return TEST_CONSTRAINTS;
}
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
d.elementStart("body", Attributes.EMPTY_MAP);
d.elementStart("line", Attributes.EMPTY_MAP);
d.elementEnd();
d.elementEnd();
m.retain(1);
m.characters("a");
m.retain(3);
return false;
}});
}
public void testCharAtPastEnd() throws OperationException {
// this should not crash
doTest(new TestData() {
@Override
boolean build(DocInitializationCursor d, DocOpCursor m) {
m.retain(1);
m.deleteCharacters("ab");
return false;
}});
}
void doTest(TestData t) throws OperationException {
DocOpBuffer d = new DocOpBuffer();
DocOpBuffer m = new DocOpBuffer();
boolean expected = t.build(d, m);
BootstrapDocument doc = new BootstrapDocument();
// initialize document
doc.consume(d.finishUnchecked());
// check whether m would apply
ViolationCollector v = new ViolationCollector();
ValidationResult result =
DocOpValidator.validate(v, t.getSchemaConstraints(), doc, m.finishUnchecked());
try {
assertEquals(expected, v.isValid());
assertEquals(result, v.getValidationResult());
} catch (AssertionFailedError e) {
System.err.println("test data:");
System.err.println(DocOpUtil.toConciseString(d.finish()));
System.err.println(DocOpUtil.toConciseString(m.finish()));
System.err.println("violations:");
v.printDescriptions(System.err);
throw e;
}
}
}