/** * 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.testing; import org.waveprotocol.wave.model.document.indexed.IndexedDocument; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.operation.Nindo.NindoCursor; import org.waveprotocol.wave.model.document.operation.NindoAutomaton; import org.waveprotocol.wave.model.document.operation.NindoValidator; 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.AttributesImpl; import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl; import org.waveprotocol.wave.model.document.raw.impl.Element; import org.waveprotocol.wave.model.document.raw.impl.Node; import org.waveprotocol.wave.model.document.raw.impl.Text; import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.Parameters; import org.waveprotocol.wave.model.testing.RandomDocOpGenerator.RandomProvider; import org.waveprotocol.wave.model.util.Pair; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; /** * Generates random document operations based on a document. They can be * valid or invalid, depending on parameters. * * @author ohler@google.com (Christian Ohler) */ @SuppressWarnings("unchecked") // TODO(ohler, danilatos): declare generics properly public final class RandomNindoGenerator { private RandomNindoGenerator() {} private static <T> T randomElement(RandomProvider r, List<T> l) { return l.get(r.nextInt(l.size())); } private static <T> T randomElement(RandomProvider r, Set<T> s) { int n = randomIntFromRange(r, 0, s.size()); for (T e : s) { if (n == 0) { return e; } n--; } assert false; throw new RuntimeException("fell off end of loop"); } 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 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 { interface RandomizerMutationComponent { ValidationResult check(ViolationCollector v); void apply(); } abstract class RandomMutationComponentGenerator { abstract RandomizerMutationComponent generate(boolean valid); // 0 means this transition will never be needed to complete an operation // (e.g., skip or setAttributes) // -1 means this transition may be needed to complete an operation but // increases the size of the structural stack (e.g. deleteElementStart) // -2 means this transition may be needed to complete an operation but // does not change the size of the structural stack (e.g. deleteCharacters) // -3 means this transition may be needed to complete an operation and // decreases the size of the structural stack (e.g. deleteElementEnd) abstract int potential(); } class SkipGenerator extends RandomMutationComponentGenerator { @Override public int potential() { return 0; } @Override RandomizerMutationComponent generate(boolean valid) { int maxDistance = a.maxSkipDistance(); if (maxDistance == 0) { return null; } if (a.checkSkip(1, null).isIllFormed()) { return null; } final int distance; if (valid) { distance = randomIntFromRange(r, 1, maxDistance + 1); assert a.checkSkip(distance, null).isValid(); } else { distance = randomIntFromRange(r, maxDistance + 1, maxDistance + p.getMaxSkipAfterEnd()); assert a.checkSkip(distance, null) == ValidationResult.INVALID_DOCUMENT; } return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkSkip(distance, v); } @Override public void apply() { a.doSkip(distance); targetDoc.skip(distance); } }; } } class CharactersGenerator extends RandomMutationComponentGenerator { @Override public int potential() { return 0; } @Override RandomizerMutationComponent generate(boolean valid) { ValidationResult v = a.checkCharacters("a", null); if (v.isIllFormed()) { return null; } int count; if (valid) { if (!v.isValid()) { return null; } int max = Math.min(a.maxLengthIncrease(), 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(); int min = a.maxLengthIncrease() + 1; if (min > max) { return null; } count = randomIntFromRange(r, min, max + 1); } else { count = randomIntFromRange(r, 1, p.getMaxInsertLength()); } } StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { if (i <= 26) { sb.append((char) ('a' + i)); } else { sb.append('.'); } } final String s = sb.toString(); return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkCharacters(s, v); } @Override public void apply() { a.doCharacters(s); targetDoc.characters(s); } }; } } class DeleteCharactersGenerator extends RandomMutationComponentGenerator { @Override RandomizerMutationComponent generate(boolean valid) { if (a.checkDeleteCharacters(1, 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); } return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkDeleteCharacters(count, v); } @Override public void apply() { a.doDeleteCharacters(count); targetDoc.deleteCharacters(count); } }; } @Override public int potential() { return -2; } } interface AttributesChecker { ValidationResult check(Attributes attrs); } Attributes generateRandomAttributes(final boolean valid, final AttributesChecker checker) { Attributes attrAccu = Attributes.EMPTY_MAP; if (valid && !checker.check(Attributes.EMPTY_MAP).isValid() || !valid && checker.check(Attributes.EMPTY_MAP).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(attrAccu).isValid()) { assert attrAccu.isEmpty(); attrAccu = pickRandomNonNullMappedElement(r, p.getAttributeNames(), new Mapper<String, Attributes>() { @Override public Attributes map(final String name) { return pickRandomNonNullMappedElement(r, p.getAttributeValues(), new Mapper<String, Attributes> () { @Override public Attributes map(String value) { Attributes b = new AttributesImpl(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 (attrAccu == null) { return null; } } assert !checker.check(attrAccu).isValid(); // Flip a coin and terminate if the number of attributes was really // supposed to be zero. if (r.nextBoolean()) { return attrAccu; } } while (r.nextBoolean()) { final Attributes finalAttrAccu = attrAccu; Pair<String, String> newAttr = pickRandomNonNullMappedElement(r, p.getAttributeNames(), new Mapper<String, Pair<String, String>>() { @Override public Pair<String, String> map(final String name) { if (finalAttrAccu.containsKey(name)) { return null; } return pickRandomNonNullMappedElement(r, p.getAttributeValues(), new Mapper<String, Pair<String, String>>() { @Override public Pair<String, String> map(String value) { Attributes b = finalAttrAccu.updateWith( new AttributesUpdateImpl(name, null, value)); assert b != finalAttrAccu; // assert non-destructiveness ValidationResult v = checker.check(b); if (valid && !v.isValid() || !valid && v.isIllFormed()) { return null; } else { return Pair.of(name, value); } } }); } }); if (newAttr == null) { return attrAccu; } attrAccu = attrAccu.updateWith( new AttributesUpdateImpl(newAttr.getFirst(), null, newAttr.getSecond())); } return attrAccu; } class ElementStartGenerator extends RandomMutationComponentGenerator { @Override public int potential() { return 0; } @Override RandomizerMutationComponent generate(final boolean valid) { Pair<String, Attributes> args = pickRandomNonNullMappedElement(r, p.getElementTypes(), new Mapper<String, Pair<String, Attributes>>() { @Override public Pair<String, Attributes> map(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; } } Attributes attrs = generateRandomAttributes(valid, new AttributesChecker() { @Override public ValidationResult check(Attributes attrs) { return a.checkElementStart(tag, attrs, null); } }); if (attrs == null) { return null; } else { return Pair.of(tag, attrs); } } }); if (args == null) { return null; } final String tag = args.getFirst(); final Attributes attributes = args.getSecond(); return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkElementStart(tag, attributes, v); } @Override public void apply() { a.doElementStart(tag, attributes); targetDoc.elementStart(tag, attributes); } }; } } abstract class RandomConstantMutationComponentGenerator extends RandomMutationComponentGenerator { abstract ValidationResult check(ViolationCollector v); abstract void apply(); @Override RandomizerMutationComponent generate(boolean valid) { switch (check(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 RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return RandomConstantMutationComponentGenerator.this.check(v); } @Override public void apply() { RandomConstantMutationComponentGenerator.this.apply(); } @Override public String toString() { return this.getClass().getName() + " from " + RandomConstantMutationComponentGenerator.this.getClass().getName(); } }; } } class ElementEndGenerator extends RandomConstantMutationComponentGenerator { @Override int potential() { return -3; } @Override ValidationResult check(ViolationCollector v) { return a.checkElementEnd(v); } @Override void apply() { a.doElementEnd(); targetDoc.elementEnd(); } } class DeleteElementStartGenerator extends RandomConstantMutationComponentGenerator { @Override int potential() { return -1; } @Override ValidationResult check(ViolationCollector v) { return a.checkDeleteElementStart(v); } @Override void apply() { a.doDeleteElementStart(); targetDoc.deleteElementStart(); } } class DeleteElementEndGenerator extends RandomConstantMutationComponentGenerator { @Override int potential() { return -3; } @Override ValidationResult check(ViolationCollector v) { return a.checkDeleteElementEnd(v); } @Override void apply() { a.doDeleteElementEnd(); targetDoc.deleteElementEnd(); } } abstract class AttributesOnlyRandomMutationComponentGenerator extends RandomMutationComponentGenerator { abstract ValidationResult check(Attributes attrs, ViolationCollector v); abstract void apply(Attributes attrs); @Override RandomizerMutationComponent generate(boolean valid) { final Attributes attrs = generateRandomAttributes(valid, new AttributesChecker() { @Override public ValidationResult check(Attributes attrs) { return AttributesOnlyRandomMutationComponentGenerator.this.check(attrs, null); } }); if (attrs == null) { return null; } return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return AttributesOnlyRandomMutationComponentGenerator.this.check(attrs, v); } @Override public void apply() { AttributesOnlyRandomMutationComponentGenerator.this.apply(attrs); } @Override public String toString() { return this.getClass().getName() + " from " + AttributesOnlyRandomMutationComponentGenerator.this.getClass().getName() + " " + attrs; } }; } } class SetAttributesGenerator extends AttributesOnlyRandomMutationComponentGenerator { @Override int potential() { return 0; } @Override ValidationResult check(Attributes attrs, ViolationCollector v) { return a.checkSetAttributes(attrs, v); } @Override void apply(Attributes attrs) { a.doSetAttributes(attrs); targetDoc.replaceAttributes(attrs); } } class UpdateAttributesGenerator extends AttributesOnlyRandomMutationComponentGenerator { @Override int potential() { return 0; } @Override ValidationResult check(Attributes attrs, ViolationCollector v) { return a.checkUpdateAttributes(attrs, v); } @Override void apply(Attributes attrs) { a.doUpdateAttributes(attrs); targetDoc.updateAttributes(attrs); } } class StartAnnotationGenerator extends RandomMutationComponentGenerator { @Override int potential() { return 0; } @Override RandomizerMutationComponent generate(boolean valid) { if (!valid) { return null; } if (p.getAnnotationOptions().isEmpty()) { return null; } Parameters.AnnotationOption option = randomElement(r, p.getAnnotationOptions()); final String key = option.getKey(); final String value = option.randomValue(r); return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkStartAnnotation(key, value, v); } @Override public void apply() { a.doStartAnnotation(key, value); targetDoc.startAnnotation(key, value); } }; } } class EndAnnotationGenerator extends RandomMutationComponentGenerator { @Override int potential() { return -3; } @Override RandomizerMutationComponent generate(boolean valid) { if (!valid) { return null; } return pickRandomNonNullMappedElement(r, p.getAnnotationKeys(), new Mapper<String, RandomizerMutationComponent>() { @Override public RandomizerMutationComponent map(final String key) { switch (a.checkEndAnnotation(key, null)) { case ILL_FORMED: return null; case VALID: return new RandomizerMutationComponent() { @Override public ValidationResult check(ViolationCollector v) { return a.checkEndAnnotation(key, v); } @Override public void apply() { a.doEndAnnotation(key); targetDoc.endAnnotation(key); } }; case INVALID_DOCUMENT: case INVALID_SCHEMA: default: throw new RuntimeException("unexpected validation result"); } } } ); } } final RandomProvider r; final Parameters p; final DocumentSchema schemaConstraints; NindoAutomaton a; NindoCursor targetDoc; final IndexedDocument<Node, Element, Text> doc; Generator(RandomProvider r, Parameters p, DocumentSchema s, IndexedDocument<Node, Element, Text> doc) { this.r = r; this.p = p; this.doc = doc; this.schemaConstraints = s; } final List<RandomMutationComponentGenerator> componentGenerators = Arrays.asList(new SkipGenerator(), new CharactersGenerator(), new DeleteCharactersGenerator(), new ElementStartGenerator(), new ElementEndGenerator(), new DeleteElementStartGenerator(), new DeleteElementEndGenerator(), new SetAttributesGenerator(), new UpdateAttributesGenerator(), new StartAnnotationGenerator(), new EndAnnotationGenerator() ); Nindo generate() { while (true) { this.a = new NindoAutomaton(schemaConstraints, doc); Nindo.Builder b = new Nindo.Builder(); targetDoc = b; boolean ok = generate1(); if (ok) { return b.build(); } } } boolean generate1() { int desiredNumComponents = randomIntFromRange(r, 0, p.getMaxOpeningComponents()); for (int i = 0; i < desiredNumComponents; i++) { RandomizerMutationComponent component = pickRandomNonNullMappedElement(r, componentGenerators, new Mapper<RandomMutationComponentGenerator, RandomizerMutationComponent>() { @Override public RandomizerMutationComponent map(RandomMutationComponentGenerator g) { return g.generate(p.getValidity()); } }); if (component == null) { // This can happen e.g. if we have skipped to the end of the document, and valid // may be true, and there may not be any annotation options. break; } component.apply(); } // Close all open components. while (a.checkFinish(null) == ValidationResult.ILL_FORMED) { int potential = -3 - 1; RandomizerMutationComponent component; do { potential++; final int finalPotential = potential; component = pickRandomNonNullMappedElement(r, componentGenerators, new Mapper<RandomMutationComponentGenerator, RandomizerMutationComponent>() { @Override public RandomizerMutationComponent map(RandomMutationComponentGenerator g) { if (g.potential() >= finalPotential) { return null; } return g.generate(p.getValidity()); } }); } while (potential < 0 && component == null); if (component == null) { // This can happen e.g. if we did an deleteAntiElementStart on the // final </p> of the blip, where there is nothing to join with. return false; } component.apply(); } return true; } } /** * Returns a randomly-generated document mutation based on the given document, * parameters, and schema. */ public static Nindo generate(RandomProvider r, Parameters p, DocumentSchema s, IndexedDocument<Node, Element, Text> doc) { Nindo m = new Generator(r, p, s, doc).generate(); ViolationCollector v = NindoValidator.validate(doc, m, s); assert !v.isIllFormed(); assert p.getValidity() == v.isValid(); return m; } /** * Stand-alone main() for quick experimentation. */ public static void main(String[] args) { // IndexedDocument<Node, Element, Text> doc = // DocProviders.POJO.parse("<body><line></line>a</body>"); // // Parameters p = new Parameters(); // // p.setMaxOpeningComponents(10); // // for (int i = 0; i < 200; i++) { // System.out.println("i=" + i); // RandomProvider r = RandomProviderImpl.ofSeed(i); // Nindo m = generate(r, p, // NindoValidator.DEFAULT_BLIP_SCHEMA_CONSTRAINTS, doc); // System.out.print(m); // } } }