blob: 9c81375818e3a0b49642d034de0266e5ef063939 [file] [log] [blame]
/*
* 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 accord.utils;
import accord.utils.async.AsyncChains;
import accord.utils.async.AsyncResult;
import accord.utils.async.AsyncResults;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
public class Property
{
public static abstract class Common<T extends Common<T>>
{
protected long seed = ThreadLocalRandom.current().nextLong();
protected int examples = 1000;
protected boolean pure = true;
@Nullable
protected Duration timeout = null;
protected Common() {
}
protected Common(Common<?> other) {
this.seed = other.seed;
this.examples = other.examples;
this.pure = other.pure;
this.timeout = other.timeout;
}
public T withSeed(long seed)
{
this.seed = seed;
return (T) this;
}
public T withExamples(int examples)
{
if (examples <= 0)
throw new IllegalArgumentException("Examples must be positive");
this.examples = examples;
return (T) this;
}
public T withPure(boolean pure)
{
this.pure = pure;
return (T) this;
}
public T withTimeout(Duration timeout)
{
this.timeout = timeout;
return (T) this;
}
protected void checkWithTimeout(Runnable fn)
{
AsyncResult.Settable<?> promise = AsyncResults.settable();
Thread t = new Thread(() -> {
try
{
fn.run();
promise.setSuccess(null);
}
catch (Throwable e)
{
promise.setFailure(e);
}
});
t.setName("property with timeout");
t.setDaemon(true);
try
{
t.start();
AsyncChains.getBlocking(promise, timeout.toNanos(), TimeUnit.NANOSECONDS);
}
catch (ExecutionException e)
{
throw new PropertyError(propertyError(this, e.getCause()));
}
catch (InterruptedException e)
{
t.interrupt();
throw new PropertyError(propertyError(this, e));
}
catch (TimeoutException e)
{
t.interrupt();
TimeoutException override = new TimeoutException("property test did not complete within " + this.timeout);
override.setStackTrace(new StackTraceElement[0]);
throw new PropertyError(propertyError(this, override));
}
}
}
public static class ForBuilder extends Common<ForBuilder>
{
public void check(FailingConsumer<RandomSource> fn)
{
forAll(Gens.random()).check(fn);
}
public <T> SingleBuilder<T> forAll(Gen<T> gen)
{
return new SingleBuilder<>(gen, this);
}
public <A, B> DoubleBuilder<A, B> forAll(Gen<A> a, Gen<B> b)
{
return new DoubleBuilder<>(a, b, this);
}
public <A, B, C> TrippleBuilder<A, B, C> forAll(Gen<A> a, Gen<B> b, Gen<C> c)
{
return new TrippleBuilder<>(a, b, c, this);
}
}
private static Object normalizeValue(Object value)
{
if (value == null)
return null;
// one day java arrays will have a useful toString... one day...
if (value.getClass().isArray())
{
Class<?> subType = value.getClass().getComponentType();
if (!subType.isPrimitive())
return Arrays.asList((Object[]) value);
if (Byte.TYPE == subType)
return Arrays.toString((byte[]) value);
if (Character.TYPE == subType)
return Arrays.toString((char[]) value);
if (Short.TYPE == subType)
return Arrays.toString((short[]) value);
if (Integer.TYPE == subType)
return Arrays.toString((int[]) value);
if (Long.TYPE == subType)
return Arrays.toString((long[]) value);
if (Float.TYPE == subType)
return Arrays.toString((float[]) value);
if (Double.TYPE == subType)
return Arrays.toString((double[]) value);
}
try
{
return value.toString();
}
catch (Throwable t)
{
return "Object.toString failed: " + t.getClass().getCanonicalName() + ": " + t.getMessage();
}
}
private static String propertyError(Common<?> input, Throwable cause, Object... values)
{
StringBuilder sb = new StringBuilder();
// return "Seed=" + seed + "\nExamples=" + examples;
sb.append("Property error detected:\nSeed = ").append(input.seed).append('\n');
sb.append("Examples = ").append(input.examples).append('\n');
sb.append("Pure = ").append(input.pure).append('\n');
if (cause != null)
{
String msg = cause.getMessage();
sb.append("Error: ");
// to improve readability, if a newline is detected move the error msg to the next line
if (msg != null && msg.contains("\n"))
msg = "\n\t" + msg.replace("\n", "\n\t");
if (msg == null)
msg = cause.getClass().getCanonicalName();
sb.append(msg).append('\n');
}
if (values != null)
{
sb.append("Values:\n");
for (int i = 0; i < values.length; i++)
sb.append('\t').append(i).append(" = ").append(normalizeValue(values[i])).append('\n');
}
return sb.toString();
}
public interface FailingConsumer<A>
{
void accept(A value) throws Exception;
}
public static class SingleBuilder<T> extends Common<SingleBuilder<T>>
{
private final Gen<T> gen;
private SingleBuilder(Gen<T> gen, Common<?> other) {
super(other);
this.gen = Objects.requireNonNull(gen);
}
public void check(FailingConsumer<T> fn)
{
if (timeout != null)
{
checkWithTimeout(() -> checkInternal(fn));
return;
}
checkInternal(fn);
}
private void checkInternal(FailingConsumer<T> fn)
{
RandomSource random = new DefaultRandom(seed);
for (int i = 0; i < examples; i++)
{
T value = null;
try
{
checkInterrupted();
fn.accept(value = gen.next(random));
}
catch (Throwable t)
{
throw new PropertyError(propertyError(this, t, value), t);
}
if (pure)
{
seed = random.nextLong();
random.setSeed(seed);
}
}
}
}
public interface FailingBiConsumer<A, B>
{
void accept(A a, B b) throws Exception;
}
public static class DoubleBuilder<A, B> extends Common<DoubleBuilder<A, B>>
{
private final Gen<A> aGen;
private final Gen<B> bGen;
private DoubleBuilder(Gen<A> aGen, Gen<B> bGen, Common<?> other) {
super(other);
this.aGen = Objects.requireNonNull(aGen);
this.bGen = Objects.requireNonNull(bGen);
}
public void check(FailingBiConsumer<A, B> fn)
{
if (timeout != null)
{
checkWithTimeout(() -> checkInternal(fn));
return;
}
checkInternal(fn);
}
private void checkInternal(FailingBiConsumer<A, B> fn)
{
RandomSource random = new DefaultRandom(seed);
for (int i = 0; i < examples; i++)
{
A a = null;
B b = null;
try
{
checkInterrupted();
fn.accept(a = aGen.next(random), b = bGen.next(random));
}
catch (Throwable t)
{
throw new PropertyError(propertyError(this, t, a, b), t);
}
if (pure)
{
seed = random.nextLong();
random.setSeed(seed);
}
}
}
}
public interface FailingTriConsumer<A, B, C>
{
void accept(A a, B b, C c) throws Exception;
}
public static class TrippleBuilder<A, B, C> extends Common<TrippleBuilder<A, B, C>>
{
private final Gen<A> as;
private final Gen<B> bs;
private final Gen<C> cs;
public TrippleBuilder(Gen<A> as, Gen<B> bs, Gen<C> cs, Common<?> other)
{
super(other);
this.as = as;
this.bs = bs;
this.cs = cs;
}
public void check(FailingTriConsumer<A, B, C> fn)
{
if (timeout != null)
{
checkWithTimeout(() -> checkInternal(fn));
return;
}
checkInternal(fn);
}
private void checkInternal(FailingTriConsumer<A, B, C> fn)
{
RandomSource random = new DefaultRandom(seed);
for (int i = 0; i < examples; i++)
{
A a = null;
B b = null;
C c = null;
try
{
checkInterrupted();
fn.accept(a = as.next(random), b = bs.next(random), c = cs.next(random));
}
catch (Throwable t)
{
throw new PropertyError(propertyError(this, t, a, b, c), t);
}
if (pure)
{
seed = random.nextLong();
random.setSeed(seed);
}
}
}
}
private static void checkInterrupted() throws InterruptedException
{
if (Thread.currentThread().isInterrupted())
throw new InterruptedException();
}
public static class PropertyError extends AssertionError
{
public PropertyError(String message, Throwable cause)
{
super(message, cause);
}
public PropertyError(String message)
{
super(message);
}
}
public static ForBuilder qt()
{
return new ForBuilder();
}
}