One nice thing about Haskell is Quickcheck, if you're not familiar with Haskell or Quickcheck it works like this:
Prelude> let evens xs = [ x | x <- xs, x `mod` 2 == 0 ]
Prelude> evens [1..10]
[2,4,6,8,10]
Here we define a function that takes a list of numbers and returns only the even numbers. We can use Quickcheck to test this function:
Prelude> import Test.QuickCheck
Prelude Test.QuickCheck> let test_evens xs = [ x | x <- evens xs, x `mod` 2 /= 0 ] == []
Prelude Test.QuickCheck> quickCheck test_evens
+++ OK, passed 100 tests.
Here we define a test function that asserts that a list of even numbers
shouldn't contain any odd ones. Passing this function to Quickcheck
shows that the function passed 100 tests. Quickcheck sees that our
test_evens
function takes a list of numbers and returns a boolean, and
it tests our code by generating a set of random inputs and executing it
with those inputs.
Clearly this type of testing can be very useful, and not just for Haskell programs. Theft is a C library that brings Quickcheck style testing to C. This sort of testing is known as Property Testing.
Let's reimplement our Haskell code in C,
#include <stdio.h>
#include <stdlib.h>
#include <theft.h>
struct IntArray {
int len;
int arr[];
};
struct IntArray *evens(struct IntArray *input)
{
int nevens = 0;
struct IntArray *output;
if (input == NULL) {
return NULL;
}
output = malloc(sizeof (struct IntArray) + input->len * sizeof(int));
if (output == NULL) {
return NULL;
}
for (int i = 0; i < input->len; i++) {
if (input->arr[i] % 2 == 0) {
output->arr[nevens++] = input->arr[i];
}
}
output->len = nevens;
return output;
}
Here we define a function that takes an array of integers and outputs an array of even integers.
Now let's do some testing!
Since C is not strongly typed like Haskell we need to define a function that describes what a test input should look like. Providing this information to Theft will allow it to generate real test input data. The function should have the following prototype,
enum theft_alloc_res allocate_int_array(struct theft *, void *, void **)
Let's write it!
enum theft_alloc_res allocate_int_array(struct theft *t, void *data, void **result)
{
int SIZE_LIMIT = 100;
int size = theft_random_choice(t, SIZE_LIMIT);
struct IntArray *numbers = malloc(sizeof (struct IntArray) + size * sizeof(int));
if (numbers == NULL) {
return THEFT_ALLOC_ERROR;
}
for (int i = 0; i < size; i++) {
numbers->arr[i] = theft_random_choice(t, INT_MAX);
}
numbers->len = size;
*result = numbers;
return THEFT_ALLOC_OK;
}
theft_random_choice
is a function that will pick a random number
between 0 and some defined limit. The result is not truly random, but
instead based on the complexity of the input Theft requires. The
documentation for Theft points out that the main thing with this is to
ensure that wherever theft_random_choice
returns 0 our
alloc_int_array
function should return the simplest input possible, in
our case that would be an empty array.
Theft passes a reference pointer to the alloc_int_array
function, this
must be updated to point to the array we have allocated before the
function returns with THEFT_ALLOC_OK
. In the event of some kind of
error the function should return THEFT_ALLOC_ERROR
Next we write the property function, this function takes an input
array of integers generated by Theft, runs our evens
function over that input and
asserts that the resultant output doesn't contain any odd numbers.
enum theft_trial_res property_array_of_evens_has_no_odd_numbers(struct theft *t, void *test_input)
{
struct IntArray *test_array = test_input;
struct IntArray *result = evens(test_array);
// Array of even numbers should not contain any odd numbers
for (int i = 0; i < result->len; i++) {
if (result->arr[i] % 2 != 0) {
return THEFT_TRIAL_FAIL;
}
}
return THEFT_TRIAL_PASS;
}
Putting this together, we define some boiler plate to cover the various functions we just defined for generating test inputs,
struct theft_type_info random_array_info = {
.alloc = allocate_int_array,
.free = theft_generic_free_cb,
.autoshrink_config = {
.enable = true,
}
};
The alloc
member is updated to point to the function we just defined.
Since the test inputs are dynamically allocated with malloc
they will need
to be freed later on. Theft provides a generic function for freeing
which is sufficient for our purposes: theft_generic_free_cb
.
The last member of this structure needs more explanation. If Theft encounters an input which causes the test to fail, it will try to pare down the input to the smallest input that causes failure; this is called shrinking.
Theft lets you define a function that can provide some control over the
shrinking process, or it can use its own shrinking functions:
autoshrinking. If autoshrinking is used however, the function that
allocates test inputs must base the complexity of the input it generates
upon the result of one of the theft_random
functions, such as
theft_random_bits
, or theft_random_choice
. This is why our
alloc_int_array
function uses theft_random_choice
rather than
standard pseudo random number generating functions.
Finally we write a function to execute the tests,
int main(void)
{
theft_seed seed = theft_seed_of_time();
struct theft_run_config config = {
.name = __func__,
.prop1 = property_array_of_evens_has_no_odd_numbers,
.type_info = { &random_array_info },
.seed = seed
};
return (theft_run(&config) == THEFT_RUN_PASS) ? EXIT_SUCCESS : EXIT_FAILURE;
}
Compiling and running:
$ gcc -o test test.c -ltheft
$ ./test
== PROP 'main': 100 trials, seed 0x62a401b7fa52ac8b
.......................................................................
.............................
== PASS 'main': pass 100, fail 0, skip 0, dup 0
I hope this helps anyone looking to try out Property Testing in C. Another guide that might be useful can be found here, it has an example that uses a manually defined shrinking function, which may be useful for more complex situations.