/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.waveprotocol.wave.model.operation.testing;
import org.waveprotocol.wave.model.operation.Domain;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationPair;
import org.waveprotocol.wave.model.operation.TransformException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* @author ohler@google.com (Christian Ohler)
*/
public class DomainRandomTester<D, O> {
public static class FailureException extends RuntimeException {
}
interface Log {
void info(String ... lines);
void inconsistent(String ... lines);
void fatal(Throwable exception, String ... lines);
}
private final int INITIAL_MUTATION_COUNT = 3;
private final int FEATURE_ITERATION_COUNT = 20;
private final Domain<D, O> domain;
private final RandomOpGenerator<D, O> generator;
private final Log log;
public DomainRandomTester(Log log, Domain<D, O> domain, RandomOpGenerator<D, O> generator) {
this.domain = domain;
this.generator = generator;
this.log = log;
}
/**
* Test that applying data as an operation to get back a copy of the data
* works correctly.
*
* The other tests assume that this test always passes.
*
* @param numIterations
*/
public void testDataOperationEquivalence(int numIterations) {
log.info("TESTING testDataOperationEquivalence");
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
D d1 = domain.initialState();
try {
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
D d2 = domain.initialState();
domain.apply(domain.asOperation(d1), d2);
if (!domain.equivalent(d1, d2)) {
log.inconsistent(
"DATA-AS-OPERATION BUG",
"Subiteration: " + i,
"Op from data: " + domain.asOperation(d1),
"Data: " + d1,
"Result of op on fresh state: " + d2
);
}
}
} catch (OperationException e) {
logException("DATA-AS-OPERATION BUG? Operation exception", e);
} catch (RuntimeException e) {
logException("DATA-AS-OPERATION BUG? Runtime exception", e);
}
}
}
public void testOperationInversion(int numIterations) {
log.info("TESTING testOperationInversion");
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
D d1 = domain.initialState();
try {
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
}
for (int i = 0; i < FEATURE_ITERATION_COUNT; i++) {
log.info("X " + i);
D backup = copy(d1);
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
D afterOp = copy(d1);
O reverse = domain.invert(op);
domain.apply(reverse, d1);
if (!domain.equivalent(d1, backup)) {
log.inconsistent(
"INVERSION BUG",
"Subiteration: " + i,
"Op: " + op,
"Reverse: " + reverse,
"Initial state: " + backup,
"State after op: " + afterOp,
"State after inverse: " + d1
);
}
}
} catch (OperationException e) {
logException("COMPOSE BUG? Operation exception", e);
} catch (RuntimeException e) {
logException("COMPOSE BUG? Runtime exception", e);
}
}
}
public void testCompositionOnInitialState(int numIterations) {
log.info("TESTING testCompositionOnInitialState");
testSimpleComposition(numIterations, true);
}
/**
* Test that (a.b).c = a.(b.c)
*
*
*/
public void testCompositionAssociativity(int numIterations) {
log.info("TESTING testCompositionAssociativity");
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
try {
D d1 = domain.initialState();
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
}
D d2 = copy(d1);
D d3 = copy(d1);
for (int i = 0; i < FEATURE_ITERATION_COUNT; i++) {
D backup = copy(d1);
O op1 = generator.randomOperation(d1, r);
domain.apply(op1, d1);
O op2 = generator.randomOperation(d1, r);
domain.apply(op2, d1);
O op3 = generator.randomOperation(d1, r);
domain.apply(op3, d1);
O op12 = domain.compose(op2, op1);
O op23 = domain.compose(op3, op2);
domain.apply(op1, d2);
domain.apply(op23, d2);
domain.apply(op12, d3);
domain.apply(op3, d3);
if (!domain.equivalent(d2, d3)) {
log.inconsistent(
"COMPOSE ASSOCIATIVITY BUG",
"Subiteration: " + i,
"Op1: " + op1,
"Op2: " + op2,
"Op3: " + op2,
"Op2.Op1: " + op12,
"Op3.Op2: " + op23,
"Initial state: " + backup,
"State after Op3.(Op2.Op1): " + d3,
"State after (Op3.Op2).Op1: " + d2
);
}
}
} catch (OperationException e) {
logException("COMPOSE BUG? Operation exception", e);
} catch (RuntimeException e) {
logException("COMPOSE BUG? Runtime exception", e);
}
}
}
/**
* This test is not strictly required, but provides good redundancy.
*
*
*
* @param numIterations
*/
public void testSimpleComposition(int numIterations) {
log.info("TESTING testSimpleComposition");
testSimpleComposition(numIterations, false);
}
private void testSimpleComposition(int numIterations, boolean emptyInitialState) {
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
D d1 = domain.initialState();
try {
if (!emptyInitialState) {
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
}
}
D d2 = copy(d1);
for (int i = 0; i < FEATURE_ITERATION_COUNT; i++) {
D backup = copy(d1);
O op1 = generator.randomOperation(d1, r);
domain.apply(op1, d1);
D after1 = copy(d1);
O op2 = generator.randomOperation(d1, r);
domain.apply(op2, d1);
O composedOp = domain.compose(op2, op1);
domain.apply(composedOp, d2);
if (!domain.equivalent(d1, d2)) {
log.inconsistent(
"COMPOSE ASSOCIATIVITY BUG",
"Subiteration: " + i,
"Op1: " + op1,
"Op2: " + op2,
"Composed: " + composedOp,
"Initial state: " + backup,
"State after first: " + after1,
"State after first then second: " + d1,
"State after composed: " + d2
);
}
}
} catch (OperationException e) {
logException("COMPOSE BUG? Operation exception", e);
} catch (RuntimeException e) {
logException("COMPOSE BUG? Runtime exception", e);
}
}
}
public void testTransformDiamondProperty(int numIterations) {
log.info("TESTING testTransformDiamondProperty");
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
D d1 = domain.initialState();
try {
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(d1, r);
domain.apply(op, d1);
}
D d2 = copy(d1);
for (int i = 0; i < FEATURE_ITERATION_COUNT; i++) {
D original = copy(d1);
O op1 = generator.randomOperation(original, r);
O op2 = generator.randomOperation(original, r);
domain.apply(op1, d1);
domain.apply(op2, d2);
D client = copy(d1);
D server = copy(d2);
OperationPair<O> pair = domain.transform(op1, op2);
domain.apply(pair.serverOp(), d1);
domain.apply(pair.clientOp(), d2);
if (!domain.equivalent(d1, d2)) {
log.inconsistent(
"TRANSFORM BUG",
"Subiteration: " + i,
"Client: " + op1,
"Server: " + op2,
"Client': " + pair.clientOp(),
"Server': " + pair.serverOp(),
"Initial state: " + original,
"Client state 1:" + client,
"Client state 2:" + d1,
"Server state 1:" + server,
"Server state 2:" + d2
);
}
}
} catch (OperationException e) {
logException("TRANSFORM BUG? Operation exception", e);
} catch (TransformException e) {
logException("TRANSFORM BUG? Transform exception", e);
} catch (RuntimeException e) {
logException("TRANSFORM BUG? Runtime exception", e);
}
}
}
/**
* Tests that transformation and composition are compatible
*
* Assumes the diamond property of transformation
*
* NOTE: This should be rewritten to do proper comparison of operations.
*
* @param numIterations
*/
public void testTransformationCompositionCompatible(int numIterations) {
log.info("TESTING testTransformationCompositionCompatible");
Random r = new Random(0);
for (int iteration = 0; iteration < numIterations; iteration++) {
log.info("Iteration: " + iteration);
D server = domain.initialState();
try {
for (int i = 0; i < INITIAL_MUTATION_COUNT; i++) {
O op = generator.randomOperation(server, r);
domain.apply(op, server);
}
D client = copy(server);
for (int i = 0; i < FEATURE_ITERATION_COUNT; i++) {
D original = copy(server);
if (!domain.equivalent(client, server)) {
log.inconsistent("Sanity check failed: client and server not the same at start of test");
}
// Client is on the left for the first pass, but this
// is reversed in the second pass (the meaning of the
// variables "client" and "server" also reverses).
//
// original (o)
// / \
// client a b server
// / \ /
// c d
// \ /
// end (e)
//
O oa = generator.randomOperation(client, r);
O ob = generator.randomOperation(server, r);
domain.apply(oa, client);
domain.apply(ob, server);
D a = copy(client);
D b = copy(server);
OperationPair<O> pair1 = domain.transform(oa, ob);
O bd = pair1.clientOp();
O ad = pair1.serverOp();
O ac = generator.randomOperation(a, r);
domain.apply(ac, client);
D c = copy(client);
domain.apply(bd, server);
D d = copy(server);
D test = copy(a);
domain.apply(ad, test);
OperationPair<O> pair2 = domain.transform(ac, ad);
O ce = pair2.serverOp();
O de = pair2.clientOp();
domain.apply(de, server);
domain.apply(ce, client);
D end = copy(client);
O oc = domain.compose(ac, oa);
O be = domain.compose(de, bd);
// The property we want to test is that ce = ce2 and be = be2
//
OperationPair<O> pair3 = domain.transform(oc, ob);
O ce2 = pair3.serverOp();
O be2 = pair3.clientOp();
D d1 = copy(c);
domain.apply(ce2, d1);
D d2 = copy(b);
domain.apply(be2, d2);
boolean ceOK = domain.equivalent(end, d1);
boolean beOK = domain.equivalent(end, d2);
if (!ceOK || !beOK) {
log.inconsistent(
"TRANSFORM AND COMPOSITION NOT COMPATIBLE",
"Subiteration: " + i,
ceOK ? "GOOD:" : "BAD:",
"ce: " + ce,
"ce2: " + ce2,
beOK ? "GOOD:" : "BAD:",
"be: " + be,
"be2: " + be2,
"-- States without compose: ",
" original (o)",
" / \\",
" client a b server",
" / \\ /",
" c d",
" \\ /",
" end (e)",
"o: " + original,
"a: " + a,
"b: " + b,
"c: " + c,
"d: " + d,
"e: " + end
);
}
}
} catch (OperationException e) {
logException("TRANSFORM BUG? Operation exception", e);
} catch (TransformException e) {
logException("TRANSFORM BUG? Transform exception", e);
} catch (RuntimeException e) {
logException("TRANSFORM BUG? Runtime exception", e);
}
}
}
private void logException(String message, Exception e) {
if (e instanceof FailureException) {
throw (FailureException) e;
}
// TODO: use record exception stuff below
log.fatal(e, message, e + "");
}
private D copy(D state) throws OperationException {
D copy = domain.initialState();
domain.apply(domain.asOperation(state), copy);
return copy;
}
final List<Throwable> exceptions = new ArrayList<Throwable>();
String exceptionToStringForComparison(Throwable e) {
StringWriter w = new StringWriter();
e.printStackTrace(new PrintWriter(w));
String s = w.toString();
// Remove the explanatory string of the exception to eliminate any
// distinction of different instances of the same problem.
int firstLineEnd = s.indexOf('\n');
assert firstLineEnd != -1;
return e.getClass().getName() + '\n' + s.substring(firstLineEnd + 1);
}
boolean exceptionsEqual(Throwable e, Throwable known) {
return exceptionToStringForComparison(e).equals(exceptionToStringForComparison(known));
}
boolean isExceptionKnown(Throwable e) {
for (Throwable known : exceptions) {
if (exceptionsEqual(e, known)) {
return true;
}
}
return false;
}
void recordException(Throwable e) {
if (!isExceptionKnown(e)) {
exceptions.add(e);
System.err.println("new exception " + exceptions.size() + ":");
e.printStackTrace(System.err);
System.err.println("*** exceptions so far: " + exceptions.size());
} else {
System.err.println("*** exceptions so far: still " + exceptions.size());
}
}
}