Created
April 10, 2013 00:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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