/** * Creates and returns a new line. * * @param createAndUpdateSeparately if true, creates a line, then sets the attributes as a * separate operation. Otherwise, sets them all at once. We want to test both scenarios. */ Line create(int index, Type type, int indent, boolean createAndUpdateSeparately) { // info("Creating @" + index + " " + // type + " " + indent + " " + createAndUpdateSeparately); Point<ContentNode> loc = doc.locate(index * 2 + 1); Line l; if (createAndUpdateSeparately) { l = Line.fromLineElement(doc.createElement(loc, "line", Attributes.EMPTY_MAP)); update(index, type, indent); } else { l = Line.fromLineElement( doc.createElement(loc, "line", attributes(type, indent, false, true))); } assertNotNull(l); return l; }
/** Deletes the line at the specified index. */ Line delete(int index) { // info("Deleting @" + index); assert index != 0 : "Code doesn't (yet) support killing the initial line"; ContentElement e = getLineElement(index); Line line = Line.fromLineElement(e).next(); doc.deleteNode(e); return line; }
/** * Updates the attributes of the line at the specified index. * * @param alwaysSetRedundant if true, always set the listyle attribute even if it is not * necessary. For example, if the listyle attribute was "decimal", but the type is "HEADING", * the listyle attribute should normally be ignored and has no meaning. It won't make a * difference if it is set or not. We want to test both scenarios. */ void update(int index, Type type, int indent, boolean alwaysSetRedundant) { ContentElement e = getLineElement(index); // info("Making @" + ((doc.getLocation(e) - 1)/2) + " " + // type + " " + indent + " " + alwaysSetStyle); Map<String, String> updates = attributes(type, indent, alwaysSetRedundant, false); for (Map.Entry<String, String> pair : updates.entrySet()) { doc.setElementAttribute(e, pair.getKey(), pair.getValue()); } }
/** @return the first line object */ Line getFirstLine() { return Line.getFirstLineOfContainer(doc.getDocumentElement().getFirstChild().asElement()); }
/** @return the line element for the given index. */ ContentElement getLineElement(int index) { return doc.locate(index * 2 + 1).getNodeAfter().asElement(); }
/** @return index for the given line object (0 for the first line, etc). */ int index(Line line) { return (doc.getLocation(line.getLineElement()) - 1) / 2; }
/** * Performs a randomized test of renumbering logic. * * @param testIterations number of test iterations on the same document. Each iteration does a * substantial amount of work (depending on document size). * @param seed initial random seed. */ void doRandomTest(int testIterations, int seed) { ContentDocument.performExpensiveChecks = false; ContentDocument.validateLocalOps = false; IndexedDocumentImpl.performValidation = false; final int LEVELS = 4; final int MAX_RUN = 3; final int ITERS_PER_BATCH_RENDER = 6; final int DECIMALS_TO_OTHERS = 4; // ratio of decimal bullets to other stuff final int UPDATE_TO_ADD_REMOVE = 4; // ratio of updates to node adds/removals assertNull(scheduledTask); int maxRand = 5; Random r = new Random(seed); // For each iteration for (int iter = 0; iter < testIterations; iter++) { info("Iter: " + iter); // Repeat several times for a single batch render, to make sure we are // able to handle multiple overlapping, redundant updates. // Times two because we are alternating between two documents to test // the ability of the renumberer to handle more than one document // correctly. int innerIters = (r.nextInt(ITERS_PER_BATCH_RENDER) + 1) * 2; for (int inner = 0; inner < innerIters; inner++) { doc = doc1; // (inner % 2 == 0) ? doc1 : doc2; int totalLines = (doc.size() - 2) / 2; Line line = getFirstLine(); // Pick a random section of the document to perform a bunch of random // changes to int i = 0; int a = r.nextInt(totalLines); int b = r.nextInt(totalLines); int startSection = Math.min(a, b); int endSection = Math.max(a, b); while (i < startSection) { i++; line = line.next(); } while (i < endSection && line != null) { // Pick a random indentation to set int level = r.nextInt(LEVELS); // Length of run of elements to update int length; // Whether we are making them numbered items or doing something else boolean decimal; if (r.nextInt(DECIMALS_TO_OTHERS) == 0) { // No need making it a long run for non-numbered items. length = r.nextInt(2); decimal = false; } else { decimal = true; length = r.nextInt(MAX_RUN - 1) + 1; } while (length > 0 && i < endSection && line != null) { boolean fiftyFifty = i % 2 == 0; // If we're numbering these lines, then DECIMAL, otherwise choose a // random other type. Type type = decimal ? Type.DECIMAL : Type.values()[r.nextInt(Type.values().length - 1)]; // Randomly decide to add/remove, or to update if (r.nextInt(UPDATE_TO_ADD_REMOVE) == 0) { int index = index(line); // Randomly decide to add or remove. // Include some constraints to ensure the document doesn't get too small or too large. boolean add = index == 0 || totalLines < SIZE / 2 ? true : (totalLines > SIZE * 2 ? false : r.nextBoolean()); if (add) { line = create(index, type, level, r.nextBoolean()); } else { line = delete(index); if (line == null) { // We just deleted the last line. continue; } } assert line != null; } else { update(index(line), type, level, fiftyFifty); } length--; i++; line = line.next(); } } } check(iter); } }