http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java ---------------------------------------------------------------------- diff --git a/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java new file mode 100644 index 0000000..a2011bf --- /dev/null +++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpBasedWaveletFactory.java @@ -0,0 +1,194 @@ +/** + * 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.id.IdGenerator; +import org.waveprotocol.wave.model.id.WaveId; +import org.waveprotocol.wave.model.id.WaveletId; +import org.waveprotocol.wave.model.operation.OperationException; +import org.waveprotocol.wave.model.operation.OperationRuntimeException; +import org.waveprotocol.wave.model.operation.SilentOperationSink; +import org.waveprotocol.wave.model.operation.SilentOperationSink.Executor; +import org.waveprotocol.wave.model.operation.wave.WaveletOperation; +import org.waveprotocol.wave.model.schema.SchemaProvider; +import org.waveprotocol.wave.model.version.HashedVersion; +import org.waveprotocol.wave.model.wave.ParticipantId; +import org.waveprotocol.wave.model.wave.data.DocumentFactory; +import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; +import org.waveprotocol.wave.model.wave.data.WaveletData; +import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot; +import org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument; +import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl; +import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet; +import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl; + +/** + * Factory for creating {@link OpBasedWavelet} instances suitable for testing. + * + */ +public final class OpBasedWaveletFactory implements WaveViewImpl.WaveletFactory<OpBasedWavelet>, + Factory<OpBasedWavelet> { + + /** + * An operation sink that, on every operation it consumes, fires a version + * update operation back to the wavelet, then passes the operation along to + * the next sink. Wavelet versioning is specifically designed to be + * server-controlled. In a test context, this sink is used to simulate the + * behaviour of a wavelet server firing back acknowledgements with version + * updates, in order that tests that mutate wavelets also see version number + * increase. + */ + private final static class VersionIncrementingSink implements + SilentOperationSink<WaveletOperation> { + private final WaveletData data; + private final SilentOperationSink<? super WaveletOperation> output; + + public VersionIncrementingSink(WaveletData data, + SilentOperationSink<? super WaveletOperation> output) { + this.data = data; + this.output = output; + } + + @Override + public void consume(WaveletOperation op) { + // Update local version, simulating server response. + try { + op.createVersionUpdateOp(1, null).apply(data); + } catch (OperationException e) { + throw new OperationRuntimeException("test sink verison update failed", e); + } + + // Pass to output sink. + output.consume(op); + } + } + + /** + * Builder, through which a factory can be conveniently configured. + */ + public final static class Builder { + private final SchemaProvider schemas; + private ObservableWaveletData.Factory<?> holderFactory; + private SilentOperationSink<? super WaveletOperation> sink; + private ParticipantId author; + + public Builder(SchemaProvider schemas) { + this.schemas = schemas; + } + + public Builder with(SilentOperationSink<? super WaveletOperation> sink) { + this.sink = sink; + return this; + } + + public Builder with(ObservableWaveletData.Factory<?> holderFactory) { + this.holderFactory = holderFactory; + return this; + } + + public Builder with(ParticipantId author) { + this.author = author; + return this; + } + + public OpBasedWaveletFactory build() { + if (holderFactory == null) { + DocumentFactory<?> docFactory = ObservablePluggableMutableDocument.createFactory(schemas); + holderFactory = WaveletDataImpl.Factory.create(docFactory); + } + if (sink == null) { + sink = SilentOperationSink.VOID; + } + if (author == null) { + // Old tests expect this. + author = FAKE_PARTICIPANT; + } + return new OpBasedWaveletFactory(holderFactory, sink, author); + } + } + + private static final ParticipantId FAKE_PARTICIPANT = new ParticipantId("f...@example.com"); + + // Parameters with which to create the OpBasedWavelets. + private final ObservableWaveletData.Factory<?> holderFactory; + private final SilentOperationSink<? super WaveletOperation> sink; + private final ParticipantId author; + + // Testing hacks. + private MockWaveletOperationContextFactory lastContextFactory; + private MockParticipationHelper lastAuthoriser; + + /** + * Creates a factory, which creates op-based waves that adapt wave data + * holders provided by another factory, sending produced operations to a given + * sink. + * + * @param holderFactory factory for providing wave data holders + * @param sink sink to which produced operations are sent + * @param author id to which edits are to be attributed + */ + private OpBasedWaveletFactory(ObservableWaveletData.Factory<?> holderFactory, + SilentOperationSink<? super WaveletOperation> sink, + ParticipantId author) { + this.holderFactory = holderFactory; + this.sink = sink; + this.author = author; + } + + public static Builder builder(SchemaProvider schemas) { + return new Builder(schemas); + } + + @Override + public OpBasedWavelet create() { + IdGenerator gen = FakeIdGenerator.create(); + return create(gen.newWaveId(), gen.newConversationWaveletId(), FAKE_PARTICIPANT); + } + + @Override + public OpBasedWavelet create(WaveId waveId, WaveletId waveletId, ParticipantId creator) { + long now = System.currentTimeMillis(); + HashedVersion v0 = HashedVersion.unsigned(0); + ObservableWaveletData waveData = holderFactory + .create(new EmptyWaveletSnapshot(waveId, waveletId, creator, v0, now)); + lastContextFactory = new MockWaveletOperationContextFactory().setParticipantId(author); + lastAuthoriser = new MockParticipationHelper(); + SilentOperationSink<WaveletOperation> executor = + Executor.<WaveletOperation, WaveletData>build(waveData); + SilentOperationSink<WaveletOperation> out = new VersionIncrementingSink(waveData, sink); + return new OpBasedWavelet(waveId, waveData, lastContextFactory, lastAuthoriser, executor, out); + } + + /** + * Gets the authoriser provided to help the last {@link OpBasedWavelet} that + * was created. The result is undefined if no wavelets have been created. + */ + public MockParticipationHelper getLastAuthoriser() { + return lastAuthoriser; + } + + /** + * Gets the helper provided to the last {@link OpBasedWavelet} that was + * created. The result is undefined if no wavelets have been created. + */ + public MockWaveletOperationContextFactory getLastContextFactory() { + return lastContextFactory; + } +}
http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java ---------------------------------------------------------------------- diff --git a/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java new file mode 100644 index 0000000..8c17f5c --- /dev/null +++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/OpMatchers.java @@ -0,0 +1,94 @@ +/** + * 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.operation.wave.AddParticipant; +import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; +import org.waveprotocol.wave.model.operation.wave.WaveletOperation; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Hamcrest matchers for CWM operations. Many of these are for use in JMock + * tests as replacements for the deprecated and broken + * {@link org.jmock.Expectations#a(Class)} and non-typesafe alternative + * {@link org.hamcrest.Matchers#instanceOf(Class)}. + * + */ +public class OpMatchers { + /** + * Alternative to Matchers.a(AddParticipant.class) since JMock/Hamcrest's + * implementation is deprecated due to being broken in Java 5 and 6. + */ + public static Matcher<WaveletOperation> addParticipantOperation() { + return new BaseMatcher<WaveletOperation>() { + @Override + public boolean matches(Object obj) { + return obj instanceof AddParticipant; + } + + @Override + public void describeTo(Description description) { + description.appendText(" instanceof AddParticipant"); + } + }; + } + + /** Creates a matcher for operations created by the given author. */ + public static Matcher<WaveletOperation> opBy(final String author) { + return new TypeSafeMatcher<WaveletOperation>() { + @Override + public boolean matchesSafely(WaveletOperation op) { + return author.equals(op.getContext().getCreator().getAddress()); + } + + @Override + public void describeTo(Description description) { + description.appendText(" op created by " + author); + } + }; + } + + /** + * Alternative to Matchers.a(WaveletBlipOperation.class) since + * JMock/Hamcrest's implementation is deprecated due to being broken in Java 5 + * and 6. + */ + public static Matcher<WaveletOperation> waveletBlipOperation() { + return new BaseMatcher<WaveletOperation>() { + @Override + public boolean matches(Object obj) { + return obj instanceof WaveletBlipOperation; + } + + @Override + public void describeTo(Description description) { + description.appendText(" instanceof WaveletBlipOperation"); + } + }; + } + + /** Uninstantiable. */ + private OpMatchers() { + } +} http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java ---------------------------------------------------------------------- diff --git a/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java new file mode 100644 index 0000000..3db3715 --- /dev/null +++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomDocOpGenerator.java @@ -0,0 +1,1421 @@ +/** + * 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"); + } + } + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-wave/blob/d35211be/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java ---------------------------------------------------------------------- diff --git a/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java new file mode 100644 index 0000000..65d97b7 --- /dev/null +++ b/wave/src/test/java/org/waveprotocol/wave/model/testing/RandomNindoGenerator.java @@ -0,0 +1,776 @@ +/** + * 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.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 oh...@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; + @SuppressWarnings("rawtypes") + 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() + ); + + @SuppressWarnings("rawtypes") + 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); +// } + } + +}