/**
* 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.testing;
import org.waveprotocol.wave.model.document.bootstrap.BootstrapDocument;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.AttributesUpdate;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.DocOpCursor;
import org.waveprotocol.wave.model.document.operation.automaton.AutomatonDocument;
import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton;
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.automaton.DocumentSchema;
import org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl;
import org.waveprotocol.wave.model.document.operation.impl.AnnotationMap;
import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl;
import org.waveprotocol.wave.model.document.operation.impl.DocInitializationBuilder;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer;
import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil;
import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.Parameters.AnnotationOption;
import org.waveprotocol.wave.model.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Generates random document operations based on a document. They can be
* valid or invalid, depending on parameters.
*/
public final class RandomDocOpGenerator {
/**
* Random number generator interface, to avoid the dependency on java.util.Random,
* which would prevent the use of this class with GWT.
*/
public interface RandomProvider {
/** @returns a pseudorandom non-negative integer smaller than upperBound */
int nextInt(int upperBound);
/** @returns a pseudorandom boolean */
boolean nextBoolean();
}
private RandomDocOpGenerator() {}
/** Parameters for random DocOp generation. */
public static final class Parameters {
/**
* An annotation key with the corresponding list of value alternatives.
*/
public static final class AnnotationOption {
final String key;
final List<String> valueAlternatives;
public AnnotationOption(String key, List<String> valueAlternatives) {
Preconditions.checkNotNull(key, "key must not be null");
Preconditions.checkNotNull(valueAlternatives, "valueAlternatives must not be null");
this.key = key;
this.valueAlternatives = valueAlternatives;
}
public String getKey() {
return key;
}
public String randomValue(RandomProvider r) {
return randomElement(r, valueAlternatives);
}
}
int maxOpeningComponents = 16;
int maxInsertLength = 10;
int maxDeleteLength = 5;
boolean valid = true;
// only relevant when producing invalid ops.
int maxSkipAfterEnd = 5;
// We use lists here instead of sets to have an explicit fixed ordering,
// which helps reproducibility when generating pseudo-random operations.
// SortedSets would also work for this, but then we'd have to make
// AnnotationOptions comparable, which is more work.
List<String> elementTypes = Collections.unmodifiableList(Arrays.asList(
"body", "line", "input",
"image", "caption", "br"// "gadget",
));
List<String> attributeNames = Collections.unmodifiableList(Arrays.asList(
"_t", "t", "i", "attachment",
"style", "blipId", "state", "url", "fontWeight", "fontStyle", "invalid_dummy"));
// TODO: We should make attributeValues dependent on attributeNames (and perhaps on element
// types) so that we can randomly insert chess gadgets with a valid state and inline images
// with a proper attachment spec.
//
// updateAttributes will only generate attribute removals if null is in this list.
List<String> attributeValues = Collections.unmodifiableList(Arrays.asList(
null, "title", "li",
"h1", "h2", "h3", "h4", "",
"0", "1", "2", "3", "4", "5", "114", "9817"));
List<AnnotationOption> annotationOptions = Collections.unmodifiableList(
Arrays.asList(
new AnnotationOption("a", Arrays.asList(null, "1", "2")),
new AnnotationOption("b", Arrays.asList(null, "1")),
new AnnotationOption("c", Arrays.asList(null, "1"))
));
public static final List<AnnotationOption> RENDERABLE_ANNOTATION_OPTIONS =
Collections.unmodifiableList(Arrays.asList(
new AnnotationOption("link/auto",
Arrays.asList(null,
"http://www.youtube.com/watch?v=NBplLTBBmiA&feature=hd",
"http://code.google.com/p/wave-protocols/issues/entry")),
new AnnotationOption("style/fontWeight", Arrays.asList(null, "bold")),
new AnnotationOption("style/textDecoration", Arrays.asList(null, "underline"))
));
public List<String> attributeValues() {
return Collections.unmodifiableList(Arrays.asList("title", "li", "h1", "h2", "h3", "h4", "",
"0", "1", "2", "3", "4", "5", "114", "9817"));
}
public Parameters() {
}
public int getMaxOpeningComponents() {
return maxOpeningComponents;
}
/**
* @return the maxInsertLength
*/
public int getMaxInsertLength() {
return maxInsertLength;
}
/**
* @return the maxDeleteLength
*/
public int getMaxDeleteLength() {
return maxDeleteLength;
}
/**
* @return the annotationOptions
*/
public List<AnnotationOption> getAnnotationOptions() {
return Collections.unmodifiableList(annotationOptions);
}
public Parameters setMaxOpeningComponents(int maxOpeningComponents) {
this.maxOpeningComponents = maxOpeningComponents;
return this;
}
/**
* @param maxInsertLength the maxInsertLength to set
*/
public Parameters setMaxInsertLength(int maxInsertLength) {
this.maxInsertLength = maxInsertLength;
return this;
}
/**
* @param maxDeleteLength the maxDeleteLength to set
*/
public Parameters setMaxDeleteLength(int maxDeleteLength) {
this.maxDeleteLength = maxDeleteLength;
return this;
}
/**
* @param annotationOptions the annotationOptions to set
*/
public Parameters setAnnotationOptions(List<AnnotationOption> annotationOptions) {
this.annotationOptions = annotationOptions;
return this;
}
// Gotta love auto-generated javadoc.
/**
* @return the valid
*/
public boolean getValidity() {
return valid;
}
/**
* @param valid the valid to set
*/
public Parameters setValidity(boolean valid) {
this.valid = valid;
return this;
}
public int getMaxSkipAfterEnd() {
return maxSkipAfterEnd;
}
public Parameters setMaxSkipBeyondEnd(int maxSkipAfterEnd) {
this.maxSkipAfterEnd = maxSkipAfterEnd;
return this;
}
/**
* Returns the list of keys from annotationOptions.
*/
public List<String> getAnnotationKeys() {
List<String> keys = new ArrayList<String>(annotationOptions.size());
for (AnnotationOption o : annotationOptions) {
keys.add(o.key);
}
return Collections.unmodifiableList(keys);
}
public List<String> getElementTypes() {
return elementTypes;
}
public Parameters setElementTypes(List<String> elementTypes) {
this.elementTypes = elementTypes;
return this;
}
public List<String> getAttributeNames() {
return attributeNames;
}
public Parameters setAttributeNames(List<String> attributeNames) {
Preconditions.checkArgument(
new HashSet<String>(attributeNames).size() == attributeNames.size(),
"duplicate attribute name");
this.attributeNames = attributeNames;
return this;
}
public List<String> getAttributeValues() {
return attributeValues;
}
public Parameters setAttributeValues(List<String> attributeValues) {
this.attributeValues = attributeValues;
return this;
}
}
private static <T> T randomElement(RandomProvider r, List<T> l) {
return l.get(r.nextInt(l.size()));
}
private static int randomIntFromRange(RandomProvider r, int min, int limit) {
assert 0 <= min; // not really a precondition, but true in our case
assert min < limit;
int x = r.nextInt(limit - min) + min;
assert min <= x;
assert x < limit;
return x;
}
private static <T> void swap(ArrayList<T> a, int i, int j) {
T temp = a.get(i);
a.set(i, a.get(j));
a.set(j, temp);
}
private static void shuffle(RandomProvider r, ArrayList<?> a) {
int N = a.size();
for (int i = 0; i < N; i++) {
int j = randomIntFromRange(r, i, N);
swap(a, i, j);
}
}
private interface Mapper<I, O> {
O map(I in);
}
private static <I, O> O pickRandomNonNullMappedElement(RandomProvider r, List<I> in,
Mapper<I, O> mapper) {
List<I> list = new ArrayList<I>(in);
while (!list.isEmpty()) {
int index = randomIntFromRange(r, 0, list.size());
O value = mapper.map(list.get(index));
if (value != null) {
return value;
}
// Remove element efficiently by swapping in an element from the end.
list.set(index, list.get(list.size() - 1));
list.remove(list.size() - 1);
}
return null;
}
private static class Generator {
abstract class RandomizerOperationComponent {
abstract ValidationResult check(DocOpAutomaton a, ViolationCollector v);
abstract void apply(DocOpAutomaton a);
abstract void output(DocOpCursor c);
boolean isAnnotationBoundary() { return false; }
}
enum Stage {
// all components are permitted
S1_UNRESTRICTED,
// if deletion stack and insertion stack are empty, permit nothing (go to next stage).
// while deletion stack is nonempty, permit annotation boundaries, deleteCharacters,
// deleteElementStarts and deleteElementEnds. Must move on to next stage as soon as
// deletion stack becomes empty.
// while insertion stack is nonempty, permit elementEnds.
S2_CLOSE_STRUCTURE,
// if annotations are open, close them
S3_CLOSE_ANNOTATIONS,
// if not at end of document, assert invalidity and skip to end of document.
S4_SKIP_TO_END;
}
abstract class RandomOperationComponentGenerator {
// returns null if it couldn't generate a matching component
abstract RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage);
}
class SkipGenerator extends RandomOperationComponentGenerator {
@SuppressWarnings("fallthrough")
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
final int distance;
switch (stage) {
case S1_UNRESTRICTED:
int maxDistance = a.maxRetainItemCount();
if (maxDistance == 0) {
return null;
}
if (a.checkRetain(1, null).isIllFormed()) {
return null;
}
if (valid) {
if (!a.checkRetain(1, null).isValid()) {
return null;
}
int d = randomIntFromRange(r, 1, maxDistance + 1);
while (!a.checkRetain(d, null).isValid()) {
d--;
assert d > 0;
}
distance = d;
assert a.checkRetain(distance, null).isValid();
} else {
distance = randomIntFromRange(r, maxDistance + 1, maxDistance + p.getMaxSkipAfterEnd());
assert a.checkRetain(distance, null) == ValidationResult.INVALID_DOCUMENT;
}
break;
case S2_CLOSE_STRUCTURE:
case S3_CLOSE_ANNOTATIONS:
return null;
case S4_SKIP_TO_END:
if (!valid) {
throw new RuntimeException("Not implemented");
}
switch (a.checkRetain(1, null)) {
case INVALID_DOCUMENT:
assert a.checkFinish(null).isValid();
return null;
case VALID:
distance = a.maxRetainItemCount();
assert distance > 0;
assert !a.checkFinish(null).isValid();
break;
case INVALID_SCHEMA:
case ILL_FORMED: assert false;
default:
throw new RuntimeException("Unexpected validation result");
}
break;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
return new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkRetain(distance, v);
}
@Override
public void apply(DocOpAutomaton a) {
a.doRetain(distance);
}
@Override
public void output(DocOpCursor c) {
c.retain(distance);
}
@Override
public String toString() {
return "Skip(" + distance + ")";
}
};
}
}
class CharactersGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
if (stage != Stage.S1_UNRESTRICTED) {
return null;
}
ValidationResult v = a.checkCharacters("a", null);
if (v.isIllFormed()) {
return null;
}
int count;
if (valid) {
if (!v.isValid()) {
return null;
}
// TODO: implement this once we have size limits.
int max = p.getMaxInsertLength();
if (max == 0) {
return null;
}
count = randomIntFromRange(r, 1, max + 1);
} else {
if (v.isValid()) {
// Exceed length of document (if p.maxInsertLength allows it).
int max = p.getMaxInsertLength();
// TODO: implement this once we have size limits.
//count = randomIntFromRange(r, min, max + 1);
return null;
} else {
count = randomIntFromRange(r, 1, p.getMaxInsertLength());
}
}
StringBuilder sb = new StringBuilder();
assert count > 0;
char startChar = r.nextBoolean() ? 'a' : 'A';
for (int i = 0; i < count; i++) {
if (i <= 26) {
sb.append((char) (startChar + i));
} else {
sb.append('.');
}
}
final String s = sb.toString();
return new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkCharacters(s, v);
}
@Override
public void apply(DocOpAutomaton a) {
a.doCharacters(s);
}
@Override
public void output(DocOpCursor c) {
c.characters(s);
}
@Override
public String toString() {
return "Characters(" + s + ")";
}
};
}
}
class DeleteCharactersGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
if (stage != Stage.S1_UNRESTRICTED && (stage != Stage.S2_CLOSE_STRUCTURE || a.deletionStackComplexityMeasure() == 0)) {
return null;
}
// TODO: In stage 2, this should perhaps be less random about how many characters
// it deletes. Alternatively, skip in stage 4 could be more random.
int nextChar = a.nextChar(0);
if (nextChar == -1 ||
a.checkDeleteCharacters("" + ((char) nextChar), null).isIllFormed()) {
return null;
}
final int count;
if (valid) {
int max = Math.min(a.maxCharactersToDelete(), p.getMaxDeleteLength());
if (max == 0) {
return null;
}
count = randomIntFromRange(r, 1, max + 1);
} else {
int max = p.getMaxDeleteLength();
int min = a.maxCharactersToDelete() + 1;
if (min > max) {
return null;
}
count = randomIntFromRange(r, min, max + 1);
}
// TODO: implement invalid case, both by right char but wrong
// annotations (if possible) and wrong char.
StringBuilder b = new StringBuilder();
for (int i = 0; i < count; i++) {
int c = a.nextChar(i);
assert c != -1;
b.append((char) c);
if (valid && !a.checkDeleteCharacters(b.toString(), null).isValid()) {
b.deleteCharAt(b.length() - 1);
break;
}
}
if (b.length() == 0) {
// TODO: simplify this method
return null;
}
final String s = b.toString();
RandomizerOperationComponent c = new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkDeleteCharacters(s, v);
}
@Override
public void apply(DocOpAutomaton a) {
a.doDeleteCharacters(s);
}
@Override
public void output(DocOpCursor c) {
c.deleteCharacters(s);
}
@Override
public String toString() {
return "DeleteCharacters(" + s + ")";
}
};
if (c.check(a, null).isValid() != valid) {
return null;
} else {
return c;
}
}
}
interface AttributesUpdateChecker {
ValidationResult check(AttributesUpdate u);
}
// returns null on failure
AttributesUpdate generateRandomAttributesUpdate(final boolean valid,
final Attributes oldAttributes,
final AttributesUpdateChecker checker) {
AttributesUpdate accu = new AttributesUpdateImpl();
if (valid && !checker.check(accu).isValid()
|| !valid && checker.check(accu).isIllFormed()) {
return null;
}
if (!valid) {
// If we want an invalid component, and it's not already invalid without
// any attributes, make it invalid by adding an invalid attribute first.
if (checker.check(accu).isValid()) {
assert accu.changeSize() == 0;
accu = pickRandomNonNullMappedElement(r,
p.getAttributeNames(), new Mapper<String, AttributesUpdate>() {
@Override
public AttributesUpdate map(final String name) {
return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
new Mapper<String, AttributesUpdate> () {
@Override
public AttributesUpdate map(String value) {
AttributesUpdate b = new AttributesUpdateImpl(name,
oldAttributes.get(name), value);
switch (checker.check(b)) {
case ILL_FORMED:
return null;
case INVALID_DOCUMENT:
case INVALID_SCHEMA:
return b;
case VALID:
return null;
default:
throw new RuntimeException("Unexpected validation result");
}
}
});
}
});
if (accu == null) {
return null;
}
}
assert !checker.check(accu).isValid();
// Flip a coin and terminate if the number of attributes was really
// supposed to be zero.
if (r.nextBoolean()) {
return accu;
}
}
while (r.nextBoolean()) {
final AttributesUpdate finalAccu = accu;
AttributesUpdate newAccu = pickRandomNonNullMappedElement(r,
p.getAttributeNames(), new Mapper<String, AttributesUpdate>() {
@Override
public AttributesUpdate map(final String name) {
for (int i = 0; i < finalAccu.changeSize(); i++) {
if (finalAccu.getChangeKey(i).equals(name)) {
return null;
}
}
return pickRandomNonNullMappedElement(r, p.getAttributeValues(),
new Mapper<String, AttributesUpdate>() {
@Override
public AttributesUpdate map(String value) {
AttributesUpdate b = finalAccu.composeWith(new AttributesUpdateImpl(name,
oldAttributes.get(name), value));
assert b != finalAccu; // assert non-destructiveness
ValidationResult v = checker.check(b);
if (valid && !v.isValid() || !valid && v.isIllFormed()) {
return null;
} else {
return b;
}
}
});
}
});
if (newAccu == null) {
return accu;
}
accu = newAccu;
}
return accu;
}
class ElementStartGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
switch (stage) {
case S1_UNRESTRICTED:
return generate(a, valid);
case S2_CLOSE_STRUCTURE:
case S3_CLOSE_ANNOTATIONS:
case S4_SKIP_TO_END:
return null;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
}
RandomizerOperationComponent generateGivenTag(final DocOpAutomaton a, final boolean valid,
final String tag) {
{
ValidationResult v = a.checkElementStart(tag, Attributes.EMPTY_MAP, null);
if (valid && !v.isValid() || !valid && v.isIllFormed()) {
// Early exit if we can't build an element start with this tag.
return null;
}
}
AttributesUpdate u = generateRandomAttributesUpdate(valid, Attributes.EMPTY_MAP,
new AttributesUpdateChecker() {
@Override
public ValidationResult check(AttributesUpdate u) {
Attributes attrs = Attributes.EMPTY_MAP.updateWith(u);
return a.checkElementStart(tag, attrs, null);
}
});
if (u == null) {
return null;
} else {
final Attributes attributes = Attributes.EMPTY_MAP.updateWith(u);
return new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkElementStart(tag, attributes, v);
}
@Override
public void apply(DocOpAutomaton a) {
a.doElementStart(tag, attributes);
}
@Override
public void output(DocOpCursor c) {
c.elementStart(tag, attributes);
}
@Override
public String toString() {
return "ElementStart(" + tag + ", " + attributes + ")";
}
};
}
}
RandomizerOperationComponent generate(final DocOpAutomaton a, final boolean valid) {
return pickRandomNonNullMappedElement(r, p.getElementTypes(),
new Mapper<String, RandomizerOperationComponent>() {
@Override
public RandomizerOperationComponent map(final String tag) {
return generateGivenTag(a, valid, tag);
}
});
}
}
abstract class RandomConstantOperationComponentGenerator
extends RandomOperationComponentGenerator {
abstract ValidationResult check(DocOpAutomaton a, ViolationCollector v);
abstract void apply(DocOpAutomaton a);
abstract void output(DocOpCursor c);
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid) {
switch (check(a, null)) {
case ILL_FORMED:
return null;
case VALID:
if (!valid) {
return null;
}
break;
case INVALID_DOCUMENT:
if (valid) {
return null;
}
break;
case INVALID_SCHEMA:
if (valid) {
return null;
}
break;
default:
throw new RuntimeException("Unexpected validation result");
}
return new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return RandomConstantOperationComponentGenerator.this.check(a, v);
}
@Override
public void apply(DocOpAutomaton a) {
RandomConstantOperationComponentGenerator.this.apply(a);
}
@Override
public void output(DocOpCursor c) {
RandomConstantOperationComponentGenerator.this.output(c);
}
@Override
public String toString() {
return "Constant component from "
+ RandomConstantOperationComponentGenerator.this.getClass().getName();
}
};
}
}
class ElementEndGenerator extends RandomConstantOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
switch (stage) {
case S1_UNRESTRICTED:
return generate(a, valid);
case S2_CLOSE_STRUCTURE:
if (a.insertionStackComplexityMeasure() == 0) {
return null;
}
return generate(a, valid);
case S3_CLOSE_ANNOTATIONS:
case S4_SKIP_TO_END:
return null;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
}
@Override
ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkElementEnd(v);
}
@Override
void apply(DocOpAutomaton a) {
a.doElementEnd();
}
@Override
void output(DocOpCursor c) {
c.elementEnd();
}
}
class DeleteElementStartGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
switch (stage) {
case S1_UNRESTRICTED:
return generate(a, valid);
case S2_CLOSE_STRUCTURE:
if (a.deletionStackComplexityMeasure() == 0) {
return null;
}
return generate(a, valid);
case S3_CLOSE_ANNOTATIONS:
case S4_SKIP_TO_END:
return null;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
}
RandomizerOperationComponent generate(final DocOpAutomaton a, final boolean valid) {
final String tag = a.currentElementStartTag();
final Attributes oldAttrs = a.currentElementStartAttributes();
if (tag == null) {
assert oldAttrs == null;
return null;
}
assert oldAttrs != null;
switch (a.checkDeleteElementStart(tag, oldAttrs, null)) {
case ILL_FORMED:
case INVALID_DOCUMENT: // TODO: bring back generating invalid ops
case INVALID_SCHEMA:
return null;
case VALID:
return new RandomizerOperationComponent() {
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkDeleteElementStart(tag, oldAttrs, v);
}
@Override
public void apply(DocOpAutomaton a) {
a.doDeleteElementStart(tag, oldAttrs);
}
@Override
public void output(DocOpCursor c) {
c.deleteElementStart(tag, oldAttrs);
}
};
default:
throw new RuntimeException("Unexpected validation result");
}
}
}
class DeleteElementEndGenerator extends RandomConstantOperationComponentGenerator {
@Override
void apply(DocOpAutomaton a) {
a.doDeleteElementEnd();
}
@Override
ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkDeleteElementEnd(v);
}
@Override
void output(DocOpCursor c) {
c.deleteElementEnd();
}
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
switch (stage) {
case S1_UNRESTRICTED:
return generate(a, valid);
case S2_CLOSE_STRUCTURE:
if (a.deletionStackComplexityMeasure() == 0) {
return null;
}
return generate(a, valid);
case S3_CLOSE_ANNOTATIONS:
case S4_SKIP_TO_END:
return null;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
}
}
class ReplaceAttributesGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(final DocOpAutomaton a, boolean valid, Stage stage) {
if (stage != Stage.S1_UNRESTRICTED) {
return null;
}
final Attributes oldAttrs = a.currentElementStartAttributes();
if (oldAttrs == null) {
if (valid) {
return null;
}
}
if (!valid) {
// TODO: bring this back.
// several cases: invalid because of wrong old attributes, or invalid
// because of schema violation of new attributes, or because no
// element start here
throw new RuntimeException("Not implemented");
}
AttributesUpdate u = generateRandomAttributesUpdate(valid,
oldAttrs, new AttributesUpdateChecker() {
@Override
public ValidationResult check(AttributesUpdate u) {
return a.checkReplaceAttributes(oldAttrs, oldAttrs.updateWith(u), null);
}
});
if (u == null) {
return null;
}
final Attributes newAttrs = oldAttrs.updateWith(u);
return new RandomizerOperationComponent() {
@Override
public void apply(DocOpAutomaton a) {
a.doReplaceAttributes(oldAttrs, newAttrs);
}
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkReplaceAttributes(oldAttrs, newAttrs, v);
}
@Override
public void output(DocOpCursor c) {
c.replaceAttributes(oldAttrs, newAttrs);
}
@Override
public String toString() {
return "ReplaceAttributes(" + oldAttrs + ", " + newAttrs + ")";
}
};
}
}
class UpdateAttributesGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(final DocOpAutomaton a, boolean valid, Stage stage) {
if (stage != Stage.S1_UNRESTRICTED) {
return null;
}
final Attributes oldAttrs = a.currentElementStartAttributes();
if (oldAttrs == null) {
if (valid) {
return null;
}
}
if (!valid) {
// TODO: bring this back.
// several cases: invalid because of wrong old attributes, or invalid
// because of schema violation of new attributes, or because no
// element start here
throw new RuntimeException("Not implemented");
}
final AttributesUpdate update = generateRandomAttributesUpdate(valid,
oldAttrs, new AttributesUpdateChecker() {
@Override
public ValidationResult check(AttributesUpdate u) {
return a.checkUpdateAttributes(u, null);
}
});
if (update == null) {
return null;
}
return new RandomizerOperationComponent() {
@Override
public void apply(DocOpAutomaton a) {
a.doUpdateAttributes(update);
}
@Override
public ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkUpdateAttributes(update, v);
}
@Override
public void output(DocOpCursor c) {
c.updateAttributes(update);
}
@Override
public String toString() {
return "UpdateAttributes(" + update + ")";
}
};
}
}
interface RunnableWithException<E extends Throwable> {
void run() throws E;
}
class AnnotationBoundaryGenerator extends RandomOperationComponentGenerator {
@Override
RandomizerOperationComponent generate(DocOpAutomaton a, boolean valid, Stage stage) {
switch (stage) {
case S1_UNRESTRICTED:
case S2_CLOSE_STRUCTURE:
return generateWithLookahead(a, valid, stage);
case S3_CLOSE_ANNOTATIONS:
assert valid;
return generateClosing(a);
case S4_SKIP_TO_END:
return null;
default:
throw new RuntimeException("Unexpected stage: " + stage);
}
}
RandomizerOperationComponent generate(final AnnotationBoundaryMapImpl map) {
return new RandomizerOperationComponent() {
@Override
void apply(DocOpAutomaton a) {
a.doAnnotationBoundary(map);
}
@Override
ValidationResult check(DocOpAutomaton a, ViolationCollector v) {
return a.checkAnnotationBoundary(map, v);
}
@Override
void output(DocOpCursor c) {
c.annotationBoundary(map);
}
@Override
boolean isAnnotationBoundary() { return true; }
@Override
public String toString() {
return "AnnotationBoundary(" + map + ")";
}
};
}
String[] toArray(ArrayList<String> a) {
return a.toArray(new String[0]);
}
RandomizerOperationComponent generateClosing(DocOpAutomaton a) {
if (a.openAnnotations().isEmpty()) {
return null;
}
ArrayList<String> l = new ArrayList<String>(a.openAnnotations());
Collections.sort(l);
AnnotationBoundaryMapImpl map =
AnnotationBoundaryMapImpl.builder().initializationEnd(
toArray(l)).build();
assert !a.checkAnnotationBoundary(map, null).isIllFormed();
return generate(map);
}
class Result extends Exception {
final RandomizerOperationComponent component;
Result(RandomizerOperationComponent component) {
this.component = component;
}
}
class StringNullComparator implements Comparator<String> {
@Override
public int compare(String a, String b) {
if (a == b) {
return 0;
}
if (a == null) {
return -1;
}
if (b == null) {
return 1;
}
return a.compareTo(b);
}
}
RandomizerOperationComponent generateWithLookahead(final DocOpAutomaton a, boolean valid,
final Stage stage) {
{
ValidationResult r = a.checkAnnotationBoundary(
AnnotationBoundaryMapImpl.builder().updateValues("a", null, "1").build(), null);
assert r.isIllFormed() || r.isValid();
if (r.isIllFormed()) {
return null;
}
}
Set<String> keySet = new TreeSet<String>(new StringNullComparator());
for (AnnotationOption o : p.getAnnotationOptions()) {
keySet.add(o.key);
}
keySet.addAll(a.currentAnnotations().keySet());
keySet.addAll(a.inheritedAnnotations().keySet());
final ArrayList<String> keys = new ArrayList<String>(keySet);
Collections.sort(keys);
// For every key, either pick it, or don't (choice point, recursively
// explore both options).
// For each key, one option is to end that key if it currently is in
// openAnnotations().
// Another option is not to end that key: In that case, given the key,
// the valid old values are those from annotationOptions and
// those from currentAnnotations() (for deletions) and
// those from inheritedAnnotations() (for insertions);
// the valid new values are those from annotationOptions and
// those from inheritedAnnotations() (for deletion).
//
// Given the full map, we need to check if the component is valid, then
// temporarily apply it to find out if there is any valid component
// to follow up with.
final RunnableWithException<Result> chooseKeys = new RunnableWithException<Result>() {
ArrayList<String> keysToEnd = new ArrayList<String>();
ArrayList<String> changeKeys = new ArrayList<String>();
ArrayList<String> changeOldValues = new ArrayList<String>();
ArrayList<String> changeNewValues = new ArrayList<String>();
void tryThisOption() throws Result {
AnnotationBoundaryMapImpl map = AnnotationBoundaryMapImpl.builder()
.initializationEnd(toArray(keysToEnd))
.updateValues(toArray(changeKeys), toArray(changeOldValues),
toArray(changeNewValues)).build();
final RandomizerOperationComponent component = generate(map);
DocOpAutomaton temp = new DocOpAutomaton(a);
ViolationCollector v = new ViolationCollector();
component.check(temp, v);
assert !component.check(temp, null).isIllFormed();
component.apply(temp);
// System.err.println("begin lookahead for " + map);
RandomizerOperationComponent followup = pickComponent(temp, stage);
if (followup != null) {
// System.err.println("end lookahead, success");
throw new Result(component);
}
// System.err.println("end lookahead, failed");
}
void removeLastMaybe(ArrayList<String> l, int lastItemIndex) {
assert lastItemIndex == l.size() || lastItemIndex == l.size() - 1;
if (lastItemIndex == l.size() - 1) {
l.remove(lastItemIndex);
}
}
void take(int nextKeyIndex, String key) throws Result {
assert key != null;
if (a.openAnnotations().contains(key)) {
int oldSize = keysToEnd.size();
try {
keysToEnd.add(key);
nextKey(nextKeyIndex);
} finally {
removeLastMaybe(keysToEnd, oldSize);
}
}
Set<String> valueSet = new TreeSet<String>(new StringNullComparator());
for (AnnotationOption o : p.getAnnotationOptions()) {
if (key.equals(o.key)) {
valueSet.addAll(o.valueAlternatives);
}
}
AnnotationMap inheritedAnnotations = a.inheritedAnnotations();
if (inheritedAnnotations.containsKey(key)) {
valueSet.add(inheritedAnnotations.get(key));
} else {
valueSet.add(null);
}
ArrayList<String> newValues = new ArrayList<String>(valueSet);
AnnotationMap currentAnnotations = a.currentAnnotations();
if (currentAnnotations.containsKey(key)) {
valueSet.add(currentAnnotations.get(key));
} else {
valueSet.add(null);
}
ArrayList<String> oldValues = new ArrayList<String>(valueSet);
shuffle(r, oldValues);
shuffle(r, newValues);
for (String oldValue : oldValues) {
for (String newValue : newValues) {
assert changeKeys.size() == changeOldValues.size();
assert changeKeys.size() == changeNewValues.size();
int oldSize = changeKeys.size();
try {
changeKeys.add(key);
changeOldValues.add(oldValue);
changeNewValues.add(newValue);
nextKey(nextKeyIndex);
} finally {
removeLastMaybe(changeNewValues, oldSize);
removeLastMaybe(changeOldValues, oldSize);
removeLastMaybe(changeKeys, oldSize);
assert changeKeys.size() == changeOldValues.size();
assert changeKeys.size() == changeNewValues.size();
}
}
}
}
void nextKey(int nextKeyIndex) throws Result {
if (nextKeyIndex >= keys.size()) {
tryThisOption();
return;
}
String key = keys.get(nextKeyIndex);
boolean take = r.nextBoolean();
if (take) {
take(nextKeyIndex + 1, key);
nextKey(nextKeyIndex + 1);
} else {
nextKey(nextKeyIndex + 1);
take(nextKeyIndex + 1, key);
}
}
@Override
public void run() throws Result {
nextKey(0);
}
};
try {
chooseKeys.run();
} catch (Result e) {
return e.component;
}
return null;
}
}
private static boolean equal(Object a, Object b) {
return a == null ? b == null : a.equals(b);
}
final RandomProvider r;
final Parameters p;
final AutomatonDocument doc;
Generator(RandomProvider r, Parameters p, AutomatonDocument doc) {
this.r = r;
this.p = p;
this.doc = doc;
}
final List<RandomOperationComponentGenerator> componentGenerators =
Arrays.asList(
new AnnotationBoundaryGenerator(),
new CharactersGenerator(),
new ElementStartGenerator(),
new ElementEndGenerator(),
new SkipGenerator(),
new DeleteCharactersGenerator(),
new DeleteElementStartGenerator(),
new DeleteElementEndGenerator(),
new ReplaceAttributesGenerator(),
new UpdateAttributesGenerator()
);
DocOp generate() {
DocOpAutomaton a = new DocOpAutomaton(doc, DocumentSchema.NO_SCHEMA_CONSTRAINTS);
DocOpBuffer b = new DocOpBuffer();
generate1(a, b);
return b.finish();
}
RandomizerOperationComponent pickComponent(final DocOpAutomaton a, final Stage stage) {
// System.err.println("stage: " + stage);
RandomizerOperationComponent component = pickRandomNonNullMappedElement(r,
componentGenerators,
new Mapper<RandomOperationComponentGenerator, RandomizerOperationComponent>() {
@Override
public RandomizerOperationComponent map(RandomOperationComponentGenerator g) {
// System.err.println("trying generator " + g);
RandomizerOperationComponent c = g.generate(a, true, stage);
if (c != null) {
assert c.check(a, null).isValid();
}
return c;
}
});
// System.err.println("picked " + component);
return component;
}
RandomizerOperationComponent generate2(DocOpAutomaton a, DocOpCursor output, Stage stage) {
RandomizerOperationComponent component = pickComponent(a, stage);
assert component != null;
component.apply(a);
component.output(output);
return component;
}
void generate1(DocOpAutomaton a, DocOpCursor output) {
if (!p.getValidity()) {
throw new RuntimeException("generation of invalid operations not supported yet");
}
int desiredNumComponents = randomIntFromRange(r, 0, p.getMaxOpeningComponents());
int numComponentsPicked = 0;
while (numComponentsPicked < desiredNumComponents) {
RandomizerOperationComponent component = generate2(a, output, Stage.S1_UNRESTRICTED);
if (!component.isAnnotationBoundary()) {
numComponentsPicked++;
}
}
while (a.deletionStackComplexityMeasure() > 0) {
generate2(a, output, Stage.S2_CLOSE_STRUCTURE);
}
while (a.insertionStackComplexityMeasure() > 0) {
int before = a.insertionStackComplexityMeasure();
generate2(a, output, Stage.S2_CLOSE_STRUCTURE);
assert a.insertionStackComplexityMeasure() <= before;
}
if (!a.openAnnotations().isEmpty()) {
generate2(a, output, Stage.S3_CLOSE_ANNOTATIONS);
assert a.openAnnotations().isEmpty();
}
if (a.maxRetainItemCount() > 0) {
generate2(a, output, Stage.S4_SKIP_TO_END);
assert a.maxRetainItemCount() == 0;
}
}
}
/**
* Returns a randomly-generated document operation based on the given document,
* parameters, and schema.
*/
public static DocOp generate(RandomProvider r, Parameters p, AutomatonDocument doc) {
DocOp op = new Generator(r, p, doc).generate();
ViolationCollector v = new ViolationCollector();
DocOpValidator.validate(v, null, doc, op);
assert !v.isIllFormed();
assert p.getValidity() == v.isValid();
return op;
}
/**
* Stand-alone main() for quick experimentation.
*/
public static void main(String[] args) throws OperationException {
BootstrapDocument initialDoc = new BootstrapDocument();
initialDoc.consume(new DocInitializationBuilder()
.elementStart("blip", Attributes.EMPTY_MAP)
.elementStart("p", Attributes.EMPTY_MAP)
.characters("abc")
.elementEnd()
.elementEnd().build());
Parameters p = new Parameters();
p.setMaxOpeningComponents(10);
RandomProvider r = RandomProviderImpl.ofSeed(2538);
for (int i = 0; i < 200; i++) {
BootstrapDocument doc = new BootstrapDocument();
doc.consume(initialDoc.asOperation());
for (int j = 0; j < 20; j++) {
System.err.println("i=" + i + ", j=" + j);
System.err.println("old: " + DocOpUtil.toXmlString(doc.asOperation()));
System.err.println("old: " + DocOpUtil.toConciseString(doc.asOperation()));
DocOp op = generate(r, p, doc);
System.err.println("op: " + DocOpUtil.toConciseString(op));
doc.consume(op);
System.err.println("new: " + DocOpUtil.toConciseString(doc.asOperation()));
System.err.println("new: " + DocOpUtil.toXmlString(doc.asOperation()));
if (!DocOpValidator.validate(null, DocumentSchema.NO_SCHEMA_CONSTRAINTS,
doc.asOperation()).isValid()) {
throw new RuntimeException("doc not valid");
}
}
}
}
}