Skip to content

Instantly share code, notes, and snippets.

@sybrandy
Created April 10, 2013 00:10
Show Gist options
  • Save sybrandy/5350590 to your computer and use it in GitHub Desktop.
Save sybrandy/5350590 to your computer and use it in GitHub Desktop.
This is an initial attempt to create a quickcheck library for D. It's not ready for distribution as I haven't used it much, but I wanted to get it out there for people to use if they want to.
/**
DQC - D QuickCheck
This is an attempt to create a version of QuickCheck for the D Programming
Language. This is an improvement over dashcheck as it has the following
features :
<ol>
<li>Better randomization of values.</li>
<li>Support for all basic D data types.</li>
<li>Able to specify length of arrays.</li>
<li>Able to specify the number of times a method is tested with random
data.</li>
<li>Does not stop on first failure.</li>
<ul>
<li>This allows the user to see various failed cases at the same
time to discover possible similarities between them.</li>
<li>The specific seed and the values that failed are displayed on
the screen.</li>
</ul>
<li>Can execute individual tests.</li>
<li>Tests can be executed with a specific seed value.</li>
<ul>
<li>This allows specific scenrios to be replayed with ease.</li>
</ul>
<li>Tests are executed in parallel to improve performance.</li>
</ol>
Output from executing multiple runs against a single test will look
similar to this :
----------------------------------------------------------
==== Executing 10 tests against [isEven]
Percentage of tests passed: 40
Percentage of tests failed: 60
Errors:
Test failed for seed: 1919803915
Arguments:
0: -1215718437
Test failed for seed: 387316912
Arguments:
0: -1430208935
----------------------------------------------------------
The tests themselves can take any number of parameters, but must return a
boolean. If the return type of the test is not a boolean, then an
exception is thrown.
Custom generators must implement the same interface as genData.
----------------------------------------------------------
T genData(uint seed, size_t length);
----------------------------------------------------------
*/
// TODO: Perhaps see if we can add timings to the checkMany calls so that
// methods can be benchmarked to show performance regressions.
module test.dqc;
import std.ascii;
import std.array;
import std.format;
import std.parallelism;
import std.random;
import std.stdio;
import std.traits;
private T genScalar(T)(uint seed)
{
Random gen = Random(seed);
// Only generating ASCII characters for strings to ensure we don't get
// garbage UTF which causes issues with some methods.
static if (isSomeChar!T)
{
T temp = uniform(T.min, T.max, gen);
while (!isASCII(temp))
{
temp = uniform(T.min, T.max, gen);
}
return temp;
}
else
{
return uniform(T.min, T.max, gen);
}
}
private bool genBool(uint seed)
{
Random gen = Random(seed);
return uniform(0, 2, gen) == 0;
}
private T[] genArray(T)(uint seed, size_t length)
{
Random gen = Random(seed);
T[] data;
foreach (i; 0..length)
{
uint currSeed = gen.front;
gen.popFront;
static if (isBoolean!T)
{
data ~= genBool(currSeed);
}
else static if (isSomeString!T)
{
data ~= genString(currSeed, length);
}
else
{
data ~= genScalar!(T)(currSeed);
}
}
return data;
}
private string genString(uint seed, size_t length)
{
return genArray!(char)(seed, length).idup;
}
/**
Generate randomized data.
This method takes a data type, a seed, and an optional length, for arrays
and strings, and returns a new random value.
Params:
T = The type of the data to generate.
seed = The seed to be used by the random number generator.
length = The size of the array to generate. Default is 0.
Returns: A new random value for type "T".
Examples:
----------------------------------------------------------
// Using a randomized seed:
auto foo = genData!(char)(unpredictableSeed);
// Using a known seed, such as for unit testing:
auto foo = genData!(int)(5);
// Specifying a seed and length:
auto foo = genData!(long[])(42, 100);
----------------------------------------------------------
*/
T genData(T)(uint seed, size_t length = 0)
{
static if (isBoolean!(T))
{
return genBool(seed);
}
else static if (isSomeString!T)
{
return genString(seed, length);
}
else static if (isArray!T)
{
// Funny hack here to get the type of the data that's in the array.
return genArray!(ForeachType!T)(seed, length);
}
else static if (isScalarType!(T))
{
return genScalar!(T)(seed);
}
}
/**
Executes a test many times.
This method takes a test and one or more generators and executes the test
100 times with, if specified, arrays of length up to 1000.
Params:
test = The test to be executed.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
checkMany!(isASCII)(genData!(char)());
----------------------------------------------------------
*/
void checkMany(alias test, G...)(G gs)
{
checkMany!(test, 100, 1000)(gs);
}
/**
Executes a test many times.
This method takes a test and one or more generators and executes the test
a specified number of times times with, if specified, arrays of length up
to 1000.
Params:
test = The test to be executed.
count = The number of times to execute the test.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
checkMany!(isASCII, 57)(genData!(char)());
----------------------------------------------------------
*/
void checkMany(alias test, alias count, G...)(G gs)
{
checkMany!(test, count, 1000)(gs);
}
/**
Executes a test many times.
This method takes a test and one or more generators and executes the test
a specified number of times times with, if specified, arrays of length up
to a specified maximum.
Params:
test = The test to be executed.
count = The number of times to execute the test.
maxlen = The maximum length of any generated arrays.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
checkMany!(isAllASCII, 57, 42)(genData!(char[])());
----------------------------------------------------------
*/
void checkMany(alias test, alias count, alias maxlen, G...)(G gs)
{
writefln("\n==== Executing %d tests against [%s]", count, __traits(identifier, test));
static if (isBoolean!(ReturnType!test))
{
auto messages = appender!(string[]);
uint failures;
Random gen = Random(unpredictableSeed);
Counter c = Counter(count);
// Executing individual runsin parallel to speed up the testing.
// Unfortunately, this required the use of a range to work properly,
// so one had to be created for this task.
foreach (i; taskPool.parallel(c))
{
uint currSeed = gen.front;
gen.popFront;
size_t len = uniform(0, maxlen, gen);
alias ParameterTypeTuple!test TP;
TP args;
foreach (j, g; gs)
{
args[j] = g(currSeed, len);
}
if (!test(args))
{
synchronized
{
auto msg = appender!(string);
formattedWrite(msg, "Test failed for seed: %d\n\tArguments:\n", currSeed);
foreach (k, a; args)
{
formattedWrite(msg, "\t\t%d: %s\n", k, a);
}
messages.put(msg.data);
failures++;
}
}
}
writeln("Percentage of tests passed: ",
(cast(double)(count - failures) / cast(double)count) * 100);
writeln("Percentage of tests failed: ",
(cast(double)failures / cast(double)count) * 100);
if (messages.data.length > 0)
{
writeln("Errors:");
foreach (m; messages.data)
{
writefln("\t%s", m);
}
}
}
else
{
throw new Exception("The test function must return a boolean!");
}
}
/*
For the parallel foreach loop, needed to create this simple range to act
as a counter.
*/
struct Counter
{
size_t max;
size_t currVal;
this(size_t c)
{
this.max = c;
}
@property size_t front()
{
return currVal;
}
@property void popFront()
{
currVal++;
}
@property bool empty()
{
return max < currVal;
}
}
/**
Execute an individual test.
Params:
test = The test to be executed.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
check!(isAllASCII)(genData!(char[])());
----------------------------------------------------------
*/
bool check(alias test, G...)(G gs)
{
Random gen = Random();
size_t len = uniform(size_t.min, size_t.max, gen);
return check!(test, unpredictableSeed, len)(gs);
}
/**
Execute an individual test.
Params:
test = The test to be executed.
s = The seed to use for the test.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
check!(isAllASCII, 57)(genData!(char[])());
----------------------------------------------------------
*/
bool check(alias test, alias s, G...)(G gs)
{
Random gen = Random(s);
size_t len = uniform(size_t.min, size_t.max, gen);
return check!(test, s, len)(gs);
}
/**
Execute an individual test.
Params:
test = The test to be executed.
s = The seed to use for the test.
l = The maximum length of any generated arrays.
gs = The generators to be used. For built-in types, this should be
genData!(...).
Examples:
----------------------------------------------------------
// Using a randomized seed:
check!(isAllASCII, 57, 42)(genData!(char[])());
----------------------------------------------------------
*/
bool check(alias test, alias s, alias l, G...)(G gs)
{
static if (isBoolean!(ReturnType!test))
{
alias ParameterTypeTuple!test TP;
TP args;
foreach (i, g; gs)
{
args[i] = g(s, l);
}
return test(args);
}
else
{
throw new Exception("The test function must return a boolean!");
}
}
debug(test)
{
import std.algorithm;
void main()
{
writeln("Scalar types.");
writeln("Generated byte: ", genScalar!(byte)(1));
writeln("Generated ubyte: ", genScalar!(ubyte)(1));
writeln("Generated short: ", genScalar!(short)(1));
writeln("Generated ushort: ", genScalar!(ushort)(1));
writeln("Generated int: ", genScalar!(int)(1));
writeln("Generated uint: ", genScalar!(uint)(1));
writeln("Generated long: ", genScalar!(long)(1));
writeln("Generated ulong: ", genScalar!(ulong)(1));
writeln("Generated float: ", genScalar!(float)(1));
writeln("Generated double: ", genScalar!(double)(1));
writeln("Generated char: ", genScalar!(char)(1));
writeln("Generated wchar: ", genScalar!(wchar)(1));
writeln("Generated dchar: ", genScalar!(dchar)(1));
writeln("Generated bool: ", genBool(1));
writeln("\nArray types.");
writeln("Generated byte: ", genArray!(byte)(1, 10));
writeln("Generated ubyte: ", genArray!(ubyte)(1, 10));
writeln("Generated short: ", genArray!(short)(1, 10));
writeln("Generated ushort: ", genArray!(ushort)(1, 10));
writeln("Generated int: ", genArray!(int)(1, 10));
writeln("Generated uint: ", genArray!(uint)(1, 10));
writeln("Generated long: ", genArray!(long)(1, 10));
writeln("Generated ulong: ", genArray!(ulong)(1, 10));
writeln("Generated float: ", genArray!(float)(1, 10));
writeln("Generated double: ", genArray!(double)(1, 10));
writeln("Generated char: ", genArray!(char)(1, 10));
writeln("Generated wchar: ", genArray!(wchar)(1, 10));
writeln("Generated dchar: ", genArray!(dchar)(1, 10));
writeln("Generated bool: ", genArray!(bool)(1, 10));
writeln("Generated string: ", genString(1, 10));
writeln("Generated empty bool: ", genArray!(bool)(1, 0));
writeln("\nTesting general purpose method.");
writeln("Generated dchar: ", genData!(dchar)(1));
writeln("Generated bool: ", genData!(bool)(1));
writeln("Generated ulong: ", genData!(ulong[])(1, 10));
writeln("\nTests.");
writeln("Single test passed: ", check!(inverseTest, 1, 1)(&genData!(int)));
writeln("Single test passed: ", check!(inverseTest, 1)(&genData!(int)));
writeln("Single test passed: ", check!(inverseTest)(&genData!(int)));
writeln("\nMultiple tests.");
checkMany!(inverseTest, 100)(&genData!(int));
checkMany!(isEven, 10)(&genData!(int));
checkMany!(checkSorted, 10, 20)(&genData!(char[]));
checkMany!(checkSimilar, 10, 10)(&genData!(int[]), &genData!(int[]));
writeln("\nNon-boolean return values.");
check!(inverse)(&genData!(int));
// TODO: Test cases with multiple data types.
}
int inverse(int i)
{
return i ^ int.max;
}
bool inverseTest(int i)
{
return inverse(i) == (i ^ int.max);
}
bool isEven(int i)
{
return (i & 1) == 0;
}
bool checkSorted(char[] data)
{
return isSorted(data);
}
bool checkSimilar(int[] a, int[] b)
{
return setIntersection(a, b).array.length > 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment