Package org.waveprotocol.wave.model.testing

Source Code of org.waveprotocol.wave.model.testing.RandomDocOpGenerator$Generator$CharactersGenerator

/**
* 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");
        }
      }
    }
  }

}
TOP

Related Classes of org.waveprotocol.wave.model.testing.RandomDocOpGenerator$Generator$CharactersGenerator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.