/******************************************************************************* * Copyright (c) 2011 Centrum Wiskunde en Informatica (CWI) * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Anya Helene Bagge (anya@ii.uib.no) - initial API and implementation *******************************************************************************/ package org.rascalmpl.value; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.rascalmpl.value.IInteger; import org.rascalmpl.value.INumber; import org.rascalmpl.value.IRational; import org.rascalmpl.value.IReal; import org.rascalmpl.value.IValue; import org.rascalmpl.value.IValueFactory; import org.rascalmpl.value.io.BinaryValueReader; import org.rascalmpl.value.io.BinaryValueWriter; import org.rascalmpl.value.io.IValueBinaryReader; import org.rascalmpl.value.io.IValueBinaryWriter; import org.rascalmpl.value.io.IValueTextReader; import org.rascalmpl.value.io.IValueTextWriter; import org.rascalmpl.value.io.StandardTextReader; import org.rascalmpl.value.io.StandardTextWriter; import org.rascalmpl.value.random.DataGenerator; import org.rascalmpl.value.random.RandomIntegerGenerator; import org.rascalmpl.value.random.RandomNumberGenerator; import org.rascalmpl.value.random.RandomRationalGenerator; import org.rascalmpl.value.random.RandomRealGenerator; import org.rascalmpl.value.type.Type; import junit.framework.AssertionFailedError; import junit.framework.TestCase; /** * Implements random testing of algebraic properties of the PDB values numeric * types (aka axiom-based testing or property-based testing). * * Data is generated using the random data genererator in the random subpackage. * @author anya * */ abstract public class BaseTestRandomValues extends TestCase { protected IValueFactory vf; protected IInteger INT_ONE; protected IInteger INT_ZERO; protected IRational RAT_ONE; protected IRational RAT_ZERO; protected IReal REAL_ONE; protected IReal REAL_ZERO; protected IReal MAX_ERROR_RATIO; protected double DOUBLE_MAX_ERROR_RATIO; protected IReal EPSILON; protected double DOUBLE_EPSILON; protected static final int PRECISION = 100; // number of iterations per axiom protected int N = 500; // TODO add more test cases protected List<IInteger> intTestSet; protected List<IRational> ratTestSet; protected List<IReal> realTestSet; private DataGenerator generator; protected List<INumber> mixedTestSet; protected static final boolean noisy = true; protected void setUp(IValueFactory factory) throws Exception { super.setUp(); vf = factory; vf.setPrecision(PRECISION); INT_ONE = vf.integer(1); INT_ZERO = vf.integer(0); RAT_ONE = vf.rational(1,1); RAT_ZERO = vf.rational(0,1); REAL_ONE = vf.real(1.0); REAL_ZERO = vf.real(0.0); // For approximate equality of reals a ~= b: // this is the max allowable ratio of (a-b) to max(a,b) MAX_ERROR_RATIO = vf.real(1e-15); DOUBLE_MAX_ERROR_RATIO = 1e-10; // this is the max allowed difference between a and b EPSILON = vf.real(1e-10); DOUBLE_EPSILON = 1e-10; intTestSet = Arrays.asList(vf.integer(0), vf.integer(1), vf.integer(-1), vf.integer(2), vf.integer(-2), vf.integer(Long.MAX_VALUE), vf.integer(Long.MIN_VALUE), vf.integer(Long.MAX_VALUE).multiply(vf.integer(Long.MAX_VALUE)), vf.integer(Long.MIN_VALUE).multiply(vf.integer(Long.MAX_VALUE))); ratTestSet = Arrays.asList(vf.rational(0,1), vf.rational(1,1), vf.rational(-1,1), vf.rational(1,2), vf.rational(2,1), vf.rational(-1,2), vf.rational(-2,1), vf.rational(Long.MAX_VALUE,Long.MIN_VALUE)); realTestSet = new ArrayList<>(); mixedTestSet = new ArrayList<>(); for(IInteger i : intTestSet) { if(!ratTestSet.contains(i.toRational())) { realTestSet.add(i.toReal(PRECISION)); } } for(IRational r : ratTestSet) { realTestSet.add(r.toReal(PRECISION)); } realTestSet.addAll(Arrays.asList(vf.real(Float.MAX_VALUE), vf.real(Float.MIN_VALUE))); mixedTestSet.addAll(intTestSet); mixedTestSet.addAll(ratTestSet); mixedTestSet.addAll(realTestSet); generator = new DataGenerator(); generator.addGenerator(IInteger.class, intTestSet, new RandomIntegerGenerator(vf)); generator.addGenerator(IRational.class, ratTestSet, new RandomRationalGenerator(vf)); generator.addGenerator(IReal.class, realTestSet, new RandomRealGenerator(vf)); } protected void assertEqual(IValue l, IValue r) { assertTrue("Expected " + l + " got " + r, l.isEqual(r)); } protected void assertEqualNumber(INumber l, INumber r) { assertTrue("Expected " + l + " got " + r, l.equal(r).getValue()); } protected void assertEqual(String message, IValue l, IValue r) { assertTrue(message + ": Expected " + l + " got " + r, l.isEqual(r)); } protected void assertEqualNumber(String message, INumber l, INumber r) { assertTrue(message + ": Expected " + l + " got " + r, l.equal(r).getValue()); } /** * Test that the difference between two reals is insignificant. */ protected void assertApprox(IReal l, IReal r) { assertTrue("Expected ~" + l + " got " + r + " (diff magnitude " + ((IReal)l.subtract(r).abs()).scale() + ")", approxEqual(l, r)); } protected void assertApprox(double l, double r) { assertTrue("Expected ~" + l + " got " + r, approxEqual(l, r)); } protected void assertApprox(String message, IReal l, IReal r) { assertTrue(message + ": Expected ~" + l + " got " + r + " (diff magnitude " + ((IReal)l.subtract(r).abs()).scale() + ")", approxEqual(l, r)); } protected void assertApprox(String message, double l, double r) { assertTrue(message + ": Expected ~" + l + " got " + r, approxEqual(l, r)); } /** * @return true if the two arguments are approximately equal */ protected boolean approxEqual(IReal l, IReal r) { if(l.equals(r)) return true; // really equal IReal max = (IReal) l.abs(); if(((IReal)r.abs()).greater(max).getValue()) max = (IReal) r.abs(); IReal diff = (IReal) l.subtract(r).abs(); if(diff.less(EPSILON).getValue()) return true; // absolute difference is very small IReal relativeDiff = diff.divide(max, PRECISION); if(!relativeDiff.less(MAX_ERROR_RATIO).getValue()) System.out.println(""); // otherwise test relative difference return relativeDiff.less(MAX_ERROR_RATIO).getValue(); } /** * @return true if the two arguments are approximately equal */ protected boolean approxEqual(double l, double r) { if(l == r) return true; // really equal double max = Math.abs(l); if(Math.abs(r) > max) max = Math.abs(r); double diff = Math.abs(l - r); if(diff < DOUBLE_EPSILON) return true; // absolute difference is very small double relativeDiff = diff / max; // otherwise test relative difference return relativeDiff < DOUBLE_MAX_ERROR_RATIO; } protected void assertEqual(Type l, Type r) { assertTrue("Expected " + l + " got " + r, l.equivalent(r)); } public void testIO() throws IOException { if(noisy) System.out.println("Test I/O: " + "(" + getClass().getPackage().getName() + ")"); ioHelperBin("PBF", new BinaryValueReader(), new BinaryValueWriter()); ioHelperText("Text", new StandardTextReader(), new StandardTextWriter()); } private void ioHelperText(String io, IValueTextReader reader, IValueTextWriter writer) throws IOException { ioHelperText2(io + " Integers", reader, writer, new DataGenerator(generator, INumber.class, intTestSet, new RandomIntegerGenerator(vf))); ioHelperText2(io + " Rationals", reader, writer, new DataGenerator(generator, INumber.class, ratTestSet, new RandomRationalGenerator(vf))); ioHelperText2(io + " Reals", reader, writer, new DataGenerator(generator, INumber.class, realTestSet, new RandomRealGenerator(vf))); } private void ioHelperBin(String io, IValueBinaryReader reader, IValueBinaryWriter writer) throws IOException { ioHelperBin2(io + " Integers", reader, writer, new DataGenerator(generator, INumber.class, intTestSet, new RandomIntegerGenerator(vf))); ioHelperBin2(io + " Rationals", reader, writer, new DataGenerator(generator, INumber.class, ratTestSet, new RandomRationalGenerator(vf))); ioHelperBin2(io + " Reals", reader, writer, new DataGenerator(generator, INumber.class, realTestSet, new RandomRealGenerator(vf))); } private void ioHelperText2(String typeName, IValueTextReader reader, IValueTextWriter writer, DataGenerator g) throws IOException { if(noisy) System.out.printf(" %-16s ", typeName + ":"); int count = 0; for(INumber n : g.generate(INumber.class, N*10)) { ioHelperText3(reader, writer, n); count++; } if(noisy) System.out.println("" + count + " values"); } private void ioHelperBin2(String typeName, IValueBinaryReader reader, IValueBinaryWriter writer, DataGenerator g) throws IOException { if(noisy) System.out.printf(" %-16s ", typeName + ":"); int count = 0; for(INumber n : g.generate(INumber.class, N*10)) { ioHelperBin3(reader, writer, n); count++; } if(noisy) System.out.println("" + count + " values"); } private void ioHelperText3(IValueTextReader reader, IValueTextWriter writer, INumber n) throws IOException, AssertionFailedError { StringWriter output = new StringWriter(); writer.write(n, output); output.close(); StringReader input = new StringReader(output.toString()); IValue v = reader.read(vf, input); assertEqual(n, v); } private void ioHelperBin3(IValueBinaryReader reader, IValueBinaryWriter writer, INumber n) throws IOException, AssertionFailedError { ByteArrayOutputStream output = new ByteArrayOutputStream(); writer.write(n, output); output.close(); ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); IValue v = reader.read(vf, input); assertEqual(n, v); } /** * Run all axioms in the current class, i.e. all *public* methos * with names starting with "axiom". * * Constructs argument lists using the data generator, and calls * the axioms using reflection. * * @throws Throwable */ public void testAxioms() throws Throwable { if(noisy) System.out.println("Test Axioms: " + "(" + getClass().getPackage().getName() + ")"); Method[] methods = getClass().getMethods(); long millis = System.currentTimeMillis(); for(Method m : methods) { if(m.getName().startsWith("axiom")) { Class<?>[] params = m.getParameterTypes(); // if at least one argument is an INumber, we want to // test the axiom for all numeric types if(hasINumber(params)) { if(noisy) System.out.print(m.getName() + "\n Integers: "); callAxiom(m, params, new Object[params.length], 0, new DataGenerator(generator, INumber.class, intTestSet, new RandomIntegerGenerator(vf))); if(noisy) System.out.print(" " + count + " calls\n" + m.getName() + "\n Rationals: "); callAxiom(m, params, new Object[params.length], 0, new DataGenerator(generator, INumber.class, ratTestSet, new RandomRationalGenerator(vf))); if(noisy) System.out.print(" " + count + " calls\n" + m.getName() + "\n Reals: "); callAxiom(m, params, new Object[params.length], 0, new DataGenerator(generator, INumber.class, realTestSet, new RandomRealGenerator(vf))); if(noisy) System.out.print(" " + count + " calls\n" + m.getName() + "\n Mixed: "); callAxiom(m, params, new Object[params.length], 0, new DataGenerator(generator, INumber.class, mixedTestSet, new RandomNumberGenerator(vf))); } else { if(noisy) System.out.print(m.getName() + "\n : "); callAxiom(m, params, new Object[params.length], 0, generator); } if(noisy) System.out.println(" " + count + " calls"); } } System.out.println("Axiom tests done in " + (System.currentTimeMillis()-millis) + " ms " + "(" + getClass().getPackage().getName() + ")"); } private boolean hasINumber(Class<?>[] params) { for(Class<?> p : params) { if(p.isAssignableFrom(INumber.class)) { return true; } } return false; } /** * Keeps track of the number of times an axiom has been called. */ private int count = 0; /** * @param m The axiom method * @param params The list of parameter types * @param args The argument list we've built so far * @param k The number of arguments we've added so far * @param g The data generator * @throws Throwable if anything went wrong */ private <T> void callAxiom(Method m, Class<?>[] params, Object[] args, int k, DataGenerator g) throws Throwable { if(k == 0) count = 0; if(params.length == k) { // we have a complete argument list try { m.invoke(this, args); } catch(InvocationTargetException e) { if (noisy) { System.err.println("FAIL: " + m.getName() + "(" + Arrays.toString(args) + ")"); } if(e.getCause() != null) throw e.getCause(); else throw e; } count ++; if(noisy) if(count % 1000 == 0) System.out.print("."); if(noisy) if(count % 80000 == 0) System.out.print("\n "); } else { // try with all possible values from the data generator for // this argument for(Object t : g.generate(params[k], numberOfValuesFor(params.length))) { args[k] = t; callAxiom(m, params, args, k+1, g); } } } /** * Restrict the number of random values for long argument lists, * or we'll end up with billions of calls. * * @return the number of random values we should generate for an argument * list of the given length. */ private int numberOfValuesFor(int length) { if(length >= 3) return N/70; else if(length == 2) return N/10; else return N*10; } /** * Relationship between compare() and the comparison functions, * and between the various comparisons. */ public void axiomCompare(INumber a, INumber b) { int cmp = a.compare(b); assertEquals(cmp == 0, b.compare(a) == 0); // negating and comparing directly isn't safe assertEquals(cmp < 0, b.compare(a) > 0); assertEquals(cmp > 0, b.compare(a) < 0); assertEquals(cmp < 0, a.less(b).getValue()); assertEquals(cmp > 0, a.greater(b).getValue()); assertEquals(cmp == 0, a.equal(b).getValue()); assertEquals(cmp <= 0, a.less(b).getValue() || a.equal(b).getValue()); assertEquals(cmp >= 0, a.greater(b).getValue() || a.equal(b).getValue()); assertEquals(a.less(b), b.greater(a)); assertEquals(a.greaterEqual(b), b.lessEqual(a)); assertEquals(a.lessEqual(b).getValue(), a.less(b).getValue() || a.equal(b).getValue()); assertEquals(a.greaterEqual(b).getValue(), a.greater(b).getValue() || a.equal(b).getValue()); assertEquals(a.less(b).getValue() || a.greater(b).getValue(), !a.equal(b).getValue()); assertEquals(a.equal(b).getValue(), b.equal(a).getValue()); assertTrue(a.equal(a).getValue()); if(a.equals(b) && a.getType() == b.getType()) { assertEquals("" + a + ".hashCode() != " + b + ".hashCode()", a.hashCode(), b.hashCode()); if(!(a instanceof IReal || b instanceof IReal) && a.getType().equivalent(b.getType())) { assertEquals("" + a + ".toString() != " + b + ".toString()", a.toString(), b.toString()); } } if(a.getType().equivalent(b.getType())) { INumber c = b.abs(); // add/subtract a non-negative number gives a greater/smaller or equal result assertTrue("" + a + " + " + c + " >= " + a, a.add(c).greaterEqual(a).getValue()); assertTrue("" + a + " + -" + c + " >= " + a, a.add(c.negate()).lessEqual(a).getValue()); } } /** * Closure: These operations should yield a result of the same type. */ public void axiomClosure(INumber a, INumber b) { if(a.signum() == 0 && b.signum() == 0) a.signum(); if(a.getType().equivalent(b.getType())) { assertEqual(a.getType(), a.add(b).getType()); assertEqual(a.getType(), a.multiply(b).getType()); assertEqual(a.getType(), a.subtract(b).getType()); assertEqual(a.getType(), a.abs().getType()); assertEqual(a.getType(), a.negate().getType()); } } /** * Associativity: addition and multiplication * * (Possibly not strictly true for reals.) */ public void axiomAssociativity(INumber a, INumber b, INumber c) { if(!(a instanceof IReal || b instanceof IReal || c instanceof IReal)) { assertEqualNumber(a.add(b.add(c)), a.add(b).add(c)); assertEqualNumber(a.multiply(b.multiply(c)), a.multiply(b).multiply(c)); } } /** * Commutativity: addition and multiplication */ public void axiomCommutativity(INumber a, INumber b) { assertEqualNumber(a.toString() + " + " + b.toString(), a.add(b), b.add(a)); assertEqualNumber(a.toString() + " * " + b.toString(), a.multiply(b), b.multiply(a)); } /** * 0 or 1 are identities for all the binary ops */ public void axiomIdentity(INumber a) { assertEqualNumber(a, a.add(INT_ZERO)); assertEqualNumber(a, a.multiply(INT_ONE)); assertEqualNumber(a, a.subtract(INT_ZERO)); if(a instanceof IInteger) assertEqualNumber(a, ((IInteger)a).divide(INT_ONE)); if(a instanceof IRational) assertEqualNumber(a, ((IRational)a).divide(RAT_ONE)); if(a instanceof IReal) assertEqualNumber(a, ((IReal)a).divide(REAL_ONE, ((IReal)a).precision())); } /** * Subtraction is inverse of addition. * Division is inverse of non-integer multiplication. */ public void axiomInverse(INumber a) { if(a instanceof IInteger) { IInteger i = (IInteger)a; assertEqualNumber(INT_ZERO, i.add(i.negate())); assertEqualNumber(INT_ZERO, i.subtract(i)); if(i.signum() != 0) { assertEqualNumber(INT_ONE, i.divide(i)); } } if(a instanceof IRational) { IRational r = (IRational)a; assertEqualNumber(RAT_ZERO, r.add(r.negate())); assertEqualNumber(RAT_ZERO, r.subtract(r)); if(r.signum() != 0) { assertEqualNumber(RAT_ONE, r.divide(r)); assertEqualNumber(RAT_ONE, r.multiply(RAT_ONE.divide(r))); } } if(a instanceof IReal) { IReal r = (IReal)a; // this should hold: assertEqualNumber(REAL_ZERO, r.add(r.negate())); // this one only approximately try { assertApprox(REAL_ONE, r.divide(r, 80)); assertApprox(REAL_ONE, r.multiply(REAL_ONE.divide(r, 80))); } catch(ArithmeticException e) { // ignore division by zero } } } /** * Multiplication distributes over addition. * * (Possibly not strictly true for reals.) */ public void axiomDistributivity(INumber a, INumber b, INumber c) { if(!(a instanceof IReal || b instanceof IReal || c instanceof IReal)) { assertEqualNumber(String.format("a=%s, b=%s, c=%s", a.toString(), b.toString(), c.toString()), a.multiply(b.add(c)), a.multiply(b).add(a.multiply(c))); } else { //assertApprox(String.format("a=%s, b=%s, c=%s", a.toString(), b.toString(), c.toString()), // a.multiply(b.add(c)).toReal(), a.multiply(b).add(a.multiply(c)).toReal()); } } /** * This may not be strictly true for reals. */ public void axiomTransitivity(INumber a, INumber b, INumber c) { if(a.equal(b).getValue() && b.equal(c).getValue()) assertTrue("" + a + " == " + b + " == " + c, a.equal(c).getValue()); if(a.lessEqual(b).getValue() && b.lessEqual(c).getValue()) assertTrue("" + a + " <= " + b + " <= " + c, a.lessEqual(c).getValue()); } public void axiomNoEqualInt(IInteger i) { assertFalse(i.toReal(PRECISION).equals(i)); assertTrue(i.toReal(PRECISION).equal(i).getValue()); assertFalse(i.toRational().equals(i)); assertTrue(i.toRational().equal(i).getValue()); } public void axiomNoEqualRat(IRational i) { assertFalse(i.toReal(PRECISION).equals(i)); assertTrue(i.toReal(PRECISION).equal(i).getValue()); assertFalse(i.toInteger().equals(i)); } public void axiomNoEqualReal(IReal i) { assertFalse(i.toInteger().equals(i)); } /** * Check that behavour of add/subtract/multiply/divide of integers is * approximately the same as for reals **/ public void axiomRationalBehavior(IRational a, IRational b) { assertEqualNumber(a, a.add(b).subtract(b)); assertEqualNumber(a, a.subtract(b).add(b)); if(b.signum() != 0) { assertEqualNumber(a, a.divide(b).multiply(b)); assertEqualNumber(a, a.multiply(b).divide(b)); } assertEqualNumber(a, a.negate().negate()); assertEqualNumber(a, a.abs().multiply(vf.integer(a.signum()))); assertEqualNumber(a, a.numerator().toRational().divide(a.denominator().toRational())); assertApprox(a.doubleValue() + b.doubleValue(), a.add(b).doubleValue()); assertApprox(a.doubleValue() - b.doubleValue(), a.subtract(b).doubleValue()); assertApprox(a.doubleValue() * b.doubleValue(), a.multiply(b).doubleValue()); try { assertApprox(a.doubleValue() / b.doubleValue(), a.divide(b).doubleValue()); } catch(ArithmeticException e) { } } /** * Check various behaviour + * Check that behavour of add/subtract/multiply of rationals is * the same as that for reals and rationals. **/ public void axiomIntegerBehavior(IInteger a, IInteger b) { assertEqualNumber(a, a.add(b).subtract(b)); assertEqualNumber(a, a.subtract(b).add(b)); if(b.signum() != 0) { assertEqualNumber(a, a.divide(b).multiply(b).add(a.remainder(b))); assertEqualNumber(a, a.multiply(b).divide(b)); } assertEqualNumber(a, a.negate().negate()); assertEqualNumber(a, a.abs().multiply(vf.integer(a.signum()))); if(b.signum() != 0) assertTrue(a.mod(b.abs()).less(b.abs()).getValue()); // check vs. rational assertEqualNumber(a.toRational().add(b.toRational()).toInteger(), a.add(b)); assertEqualNumber(a.toRational().subtract(b.toRational()).toInteger(), a.subtract(b)); assertEqualNumber(a.toRational().multiply(b.toRational()).toInteger(), a.multiply(b)); } public void axiomRealBehavior(IReal a, IReal b) { assertApprox(a, a.add(b).subtract(b)); assertApprox(a, a.subtract(b).add(b)); try { assertApprox(a, a.divide(b, PRECISION).multiply(b)); assertApprox(a, a.multiply(b).divide(b, PRECISION)); } catch(ArithmeticException e) { } assertEqualNumber(a, a.negate().negate()); } }