Added eqcmini quickcheck library to lib/

This commit is contained in:
Kallfaktorn 2011-05-05 02:46:21 +02:00
parent 8a574db360
commit 73194d7d28
21 changed files with 2075 additions and 0 deletions

View file

@ -0,0 +1,227 @@
%% This file tests that generators produce the right kind of data. It
%% also illustrates how to write a QuickSpec specification of a
%% datatype (such as the generator datatype).
-module(generators_eqc).
-include_lib("eqc/include/eqc.hrl").
-compile(export_all).
%% Generate most combinations of generator functions, as a symbolic
%% generator--for example:
%% {call,oneof,[{{call,bitstring,14},{call,elements,[15]}}]}
%% We control the size of the symbolic generator, by limiting the
%% nesting depth.
generator() ->
?SIZED(N,generator(N)).
generator(0) ->
% base case: no nested generators.
oneof([constant(),
binary,{call,binary,nat()},
bitstring,{call,bitstring,nat()},
bool,char,
?SUCHTHAT({call,choose,Lo,Hi},
{call,choose,int(),int()},
Lo=<Hi),
{call,elements,non_empty_list(constant())},
int,largeint,nat,real]);
generator(N) ->
% recursive case: reduce the size of nested compound generators.
N1 = N div 4,
?LAZY(oneof([generator(0),compound_generator(N1)])).
constant() ->
oneof([int(),atom()]).
atom() ->
elements([a,b,c,d]).
term() ->
?LET(G,generator(),generated_by(G)).
compound_generator(N) ->
Smaller = generator(N),
oneof([?LETSHRINK([Sm],[Smaller],?LET(Def,term(),{call,default,Sm,Def})),
?LETSHRINK(Gs,non_empty_list(Smaller),
{call,frequency,[{positive(),G} || G <- Gs]}),
?LETSHRINK([Sm],[Smaller],{call,list,Sm}),
?LETSHRINK([Sm],[Smaller],{call,orderedlist,Sm}),
?LETSHRINK([Sm],[?SUCHTHAT(Sm,Smaller,not often_empty(Sm))],
{call,non_empty,Sm}),
?LETSHRINK([Sm],[Smaller],{call,noshrink,Sm}),
?LETSHRINK(Gs,non_empty_list(Smaller),
{call,oneof,Gs}),
?LETSHRINK([Sm],[Smaller],{call,resize,choose(0,N),Sm}),
?LETSHRINK(L,list(constant()),{call,shuffle,L}),
?LETSHRINK(Gs,short_list(Smaller),
list_to_tuple(Gs)),
?LETSHRINK([Sm],[Smaller],{call,vector,choose(0,4),Sm})]).
%% To keep the size of generators under control, it is not enough to
%% restrict nesting depth. We also want lists of arguments to oneof,
%% frequency etc to be reasonably short.
short_list(G) ->
?SIZED(Size,
resize(Size div 3 + 1,
list(resize(Size,G)))).
positive() ->
?LET(N,nat(),N+1).
non_empty_list(G) ->
non_empty(short_list(G)).
%% When we generate {call,non_empty,G}, we need to know that G is
%% reasonably likely to produce a non-empty value... otherwise we may
%% loop when we try to use this generator!
often_empty([]) ->
true;
often_empty(<<>>) ->
true;
often_empty({call,vector,N,G}) ->
N==0;
often_empty({call,binary,N}) ->
N==0;
often_empty({call,bitstring,N}) ->
N==0;
often_empty({call,noshrink,G}) ->
often_empty(G);
often_empty({call,oneof,Gs}) ->
% conservative
lists:any(fun often_empty/1,Gs);
often_empty({call,frequency,WGs}) ->
% conservative
lists:any(fun often_empty/1, [G || {_,G} <- WGs]);
often_empty({call,default,G,V}) ->
often_empty(G) andalso often_empty(V);
often_empty({call,resize,N,G}) ->
N<4 orelse often_empty(G);
often_empty({call,shuffle,L}) ->
L==[];
often_empty(_) ->
false.
%% The values generated by a symbolic generator.
generated_by(A) when is_atom(A) ->
case erlang:function_exported(eqc_gen,A,0) of
true ->
eqc_gen:A();
false ->
A
end;
generated_by(T) when is_tuple(T), size(T)>0 ->
case tuple_to_list(T) of
[call,F|Args] ->
erlang:apply(eqc_gen,F,[generated_by(G) || G <- Args]);
Gs ->
list_to_tuple([generated_by(G) || G <- Gs])
end;
generated_by([H|T]) ->
[generated_by(H)|generated_by(T)];
generated_by(X) ->
X.
%% Check that a generated value corresponds to its generator.
is(binary,B) ->
is_binary(B);
is({call,binary,N},B) ->
is_binary(B) andalso size(B)==N;
is(bitstring,B) ->
is_bitstring(B);
is({call,bitstring,N},B) ->
is_bitstring(B) andalso size(B) == N div 8;
is(bool,B) ->
B==true orelse B==false;
is(char,C) ->
is_integer(C) andalso 0=<C andalso C=<255;
is({call,choose,Lo,Hi},N) ->
is_integer(N) andalso Lo =< N andalso N =< Hi;
is({call,default,G,D},X) ->
is(G,X) orelse X==D;
is({call,elements,L},X) ->
lists:member(X,L);
is({call,frequency,WGs},X) ->
lists:any(fun({_,G})->is(G,X) end,WGs);
is(int,N) ->
is_integer(N) andalso abs(N) =< 100;
is(largeint,N) ->
is_integer(N);
is(nat,N) ->
is_integer(N) andalso N>=0;
is(real,N) ->
is_float(N);
is({call,list,G},L) ->
is_list(L) andalso
lists:all(fun(X)->is(G,X) end,L);
is({call,orderedlist,G},L) ->
is_list(L) andalso
lists:all(fun(X)->is(G,X) end,L) andalso
lists:sort(L) == L;
is({call,non_empty,G},X) ->
is(G,X) andalso X/=[] andalso X/=<<>>;
is({call,noshrink,G},X) ->
is(G,X);
is({call,oneof,Gs},X) ->
lists:any(fun(G) -> is(G,X) end,Gs);
is({call,resize,_,G},X) ->
is(G,X);
is({call,shuffle,L},X) ->
is_list(X) andalso lists:sort(X) == lists:sort(L);
is({call,vector,N,G},V) ->
is_list(V) andalso length(V)==N andalso
lists:all(fun(X)->is(G,X) end,V);
is(GT,T) when is_tuple(GT) ->
is_tuple(T) andalso size(T)==size(GT) andalso
lists:all(fun({G,X})->is(G,X) end,
lists:zip(tuple_to_list(GT),
tuple_to_list(T)));
is(Const,X) when is_atom(Const); is_integer(Const) ->
Const == X.
%% The properties.
%% Generate symbolic generators, and report on the distribution of
%% generator functions used.
prop_generator() ->
?FORALL(G,generator(),
aggregate(generator_types(G),true)).
generator_types(G) when is_tuple(G) ->
case tuple_to_list(G) of
[call,frequency,WArgs] ->
[T || {_,G1} <- WArgs, T <- generator_types(G1)];
[call,F|Args] ->
[F|[T || A <- Args,
T <- generator_types(A)]];
L ->
[T || X <- L,
T <- generator_types(X)]
end;
generator_types(N) when is_integer(N) ->
[integer_constant];
generator_types(X) when is_float(X) ->
[float_constant];
generator_types(B) when is_binary(B) ->
[binary];
generator_types(B) when is_bitstring(B) ->
[bitstring];
generator_types(L) when is_list(L) ->
[list|[T || X <- L, T <- generator_types(X)]];
generator_types(X) ->
[X].
%% For each kind of generator, use it to generate a value, and check
%% that the value matches the generator. This tests the generators
%% (and our generator-generators!) pretty thoroughly.
prop_correct_types() ->
?FORALL(G,generator(),
?FORALL(X,generated_by(G),is(G,X))).

View file

@ -0,0 +1,40 @@
%%% File : ip_checksum.erl
%%% Author : Ulf Norell <ulf.norell@quviq.com>,
%%% Thomas Arts <thomas.arts@quviq.com>
%%% Description : Implementation of IP checksums.
%%% Created : 7 Jun 2010 by Ulf Norell
-module(ip_checksum).
-export([checksum/1, checksum/2, sum/2, pad/2, add/2, negate/1]).
checksum(Bin) ->
checksum(Bin, 16).
checksum(Bin, N) ->
negate(sum(pad(Bin, N), N)).
% Sum a binary of N bit words in ones complement representation.
sum(Bin, N) ->
lists:foldl(fun(A, B) -> add(A, B) end, <<0:N>>,
[ <<A:N>> || <<A:N>> <= Bin ]).
% Add two numbers in ones complement representation.
add(A, B) ->
N = bit_size(A),
<<X:N>> = A,
<<Y:N>> = B,
Carry = (X + Y) div (1 bsl N),
<<(X + Y + Carry):N>>.
%% invert all bits... as simple as that.
negate(BitString) ->
<< <<(1-Bit):1>> || <<Bit:1>> <= BitString >>.
pad(Binary, Bits) ->
PaddingLength =
case bit_size(Binary) rem Bits of
0 -> 0;
N -> Bits - N
end,
<<Binary/bits, 0:PaddingLength>>.

View file

@ -0,0 +1,218 @@
%%% File : ip_checksum_eqc.erl
%%% Author : Ulf Norell <ulf.norell@quviq.com>,
%%% Thomas Arts <thomas.arts@quviq.com>
%%% Description : QuickCheck properties for ip_checksum.erl
%%% Created : 7 Jun 2010 by Ulf Norell
-module(ip_checksum_eqc).
-include_lib("eqc/include/eqc.hrl").
-compile(export_all).
% == Testing IP checksum implementations ==
% In RFC 1071 efficient algorithms are discussed for computing the internet
% checksum, also known as IP checksum. Whenever you implement efficient
% algorithms, an error may sneak through.
% This article is meant to be used as test driven development specification for
% anyone that wants to implement one of the algorithms of RFC 1071 or even a new
% one to compute the IP checksum. The article can also be read as an example of
% specifying something without revealing its implementation details; a good
% example of using QuickCheck specifications.
% Whether you write your code in Erlang, C or Java, we assume that you can build
% an interface to a module called ip_checksum.erl in which Erlang functions
% either are the functions under test or call the functions under test.
% === IP Checksum ===
% The IP checksum is the 16 bit one's complement of the one's complement sum of
% all 16 bit words in the header.
% Ones complement is a way of representing negative numbers (see
% [http://en.wikipedia.org/wiki/Signed_number_representations#Ones.27_complement
% WikiPedia] for more details).
% The IP checksum uses 16 bit words. In 16 bits you can represent the numbers 0
% to 65535. The idea with ones complement is to use half the numbers in this
% interval for representing negative numbers. Thus, 0 up to 32767 are the
% positive numbers and 65535 is -0, or an alternative representation of zero.
% The number 65534 is -1 etc. Until 32768 which is -32767. Hence the interval
% -32767 up to 32767 can be represented.
% In the remainder of this article we will present properties for functions that
% you probably would like to test. The properties are always parametrized by the
% word size.
% === Utility functions ===
% First we define some functions that will come in handy.
% The maximum number that can be represented in ''N'' bits. In the ones
% complement interpretation this will be the negative zero.
max_int(N) ->
(1 bsl N) - 1.
negative_zero(N) ->
max_int(N).
% === Ones complement ===
% The first function we might want to check is the ones' complement of a word,
% which in ones' complement representation corresponds to the negation. We
% assume we have a function '''ip_checksum:negate/1''' implemented that takes a
% bit string as input and computes its ones' complement.
% Looking at the specification of ones' complement representation above we can
% see that adding the ones' complement representation of a number and the
% representation of its negation results in the representation of -0. For
% instance, the representation of -2 is 65533 and the representation of 2 is 2.
% Adding these we get 65535 which is the representation of -0. We use this
% property to test the implementation of the '''negate/2''' function.
prop_negate(N) ->
?FORALL(I, choose(0, max_int(N)),
begin
<<CI:N>> = ip_checksum:negate(<<I:N>>),
equals(negative_zero(N), I + CI)
end).
% The property above is parameterized by the word size N. We'll want to test
% our properties for a range of different word sizes, so we define a general
% function to transform a parameterized property to a property choosing random
% word sizes.
random_word_size(Prop) ->
?FORALL(N, choose(1, 64), Prop(N)).
prop_negate() ->
random_word_size(fun prop_negate/1).
% === Padding ===
% It is not clear from the specification presented above, but if you need to
% compute the checksum of a list of bytes in base 16, then there should be an
% even number of bytes. Likewise, if we would like to do ones complement in 32
% bits base, we would need to extend a sequence of bytes such that it is
% divisible by 4.
% Extending a bit string such that it is divisible by the base is called padding.
% We assume that you implemented a padding function that added the necessary
% bits, given a bit string. We assume this function to be implemented as
% '''ip_checksum:pad/1''' taking a bit string as argument and returning a new
% bit string which is an extended version with as many zero bits as needed.
prop_padding() ->
random_word_size(fun prop_padding/1).
prop_padding(N) ->
?FORALL(BitString, bitstring(),
begin
Bits = bit_size(BitString),
<<B:Bits/bits, Padded/bits>> = ip_checksum:pad(BitString,N),
Zeros = bit_size(Padded),
% If this property fails we need to know what the pad function actually
% returned in order to understand what went wrong. This is what the
% ?WHENFAIL macro is for.
?WHENFAIL(io:format("B = ~w\nPadded = ~w\nZeros = ~w\n",
[B, Padded, Zeros]),
((Bits + Zeros) rem N) == 0 andalso % the new length is divisible by N
B == BitString andalso % we don't change the bit string
<<0:Zeros>> == Padded andalso % we pad with zeros
Zeros < N % we don't pad more than we have to
)
end).
% Confident that the padding function works we can write a generator for
% correctly padded bit strings.
padded_bitstring(N) ->
?LET(Bits, bitstring(), ip_checksum:pad(Bits, N)).
% An alternative definition of this generator would not use the padding
% function, but rather first generate the length of the bit string and then
% pass that to the bit string generator (see below). The advantage of the former
% definition is that it behaves better when shrinking. The version below will
% regenerate the bit string everytime the length is getting shorter.
padded_bitstring_2(N) ->
?LET(Len, nat(), bitstring(N * Len)).
% === Ones complement sum ===
% The ones complement sum is computed by adding a number of words in ones
% complement representation. We assume this function to be implemented as
% '''ip_checksum:sum/2''' which takes a bit string as first argument and a
% word size as second argument. We assume that padding is done outside the
% sum function and only test that the function works for bit strings of
% which the length is divisible by the given word size.
% Because of our test driven development method, we have already tested the
% '''negate/1''' function and therefore trust this function in our property.
% Remember that adding the representations of a number and its negation yields
% -0. This is in fact also true if we use one's complement addition rather than
% simply adding the representations (which is not the same thing). So if we
% concatenate a bit string with the negation of its sum, the sum of the
% resulting bit string should be -0 if our implementation of '''sum/2''' is
% correct. By testing the sum function in this way we don't have to worry about
% specifying the intricacies of ones' complement arithmetic (except for the
% fact that X + (-X) = -0).
prop_sum() ->
random_word_size(fun prop_sum/1).
prop_sum(N) ->
?FORALL(Bin, padded_bitstring(N),
begin
Sum = ip_checksum:sum(Bin, N),
CSum = ip_checksum:negate(Sum),
equals(ip_checksum:sum(<<CSum/bits, Bin/bits>>, N),
<<(negative_zero(N)):N>>)
end).
% === Checksum ===
% After computing ones' complement sum, one has to take the ones' complement of
% the result to compute the checksum. Of course, we have all ingredients in
% house to do so, but in case you implement both functions as one you would
% like to test the final result '''ip_checksum:checksum/2''' with a bit string
% and word size as arguments.
% We first test that the '''checksum/2''' function takes care of padding, by
% checking that padding the bitstring before passing it to '''checksum/2'''
% doesn't change the result.
prop_checksum_pad() ->
random_word_size(fun prop_checksum_pad/1).
prop_checksum_pad(N) ->
?FORALL(Bits, bitstring(),
equals(ip_checksum:checksum(Bits, N),
ip_checksum:checksum(ip_checksum:pad(Bits, N), N))).
% We can test the '''checksum/2''' function in the same way as we tested
% '''sum/2''' above. Taking a bit string and prepending its checksum should
% result in a bit string whose checksum is zero. Here's why:
%
% % definition of checksum
% checksum(Bits) == -sum(Bits)
%
% checksum(checksum(Bits) ++ Bits) == {def. of checksum}
% -sum(-sum(Bits) ++ Bits) == {sum(Xs ++ Ys) == sum(Xs) + sum(Ys)}
% -(-sum(Bits) + sum(Bits)) == {adding -X and X}
% -(-0) ==
% 0
%
% Note that due to padding, the property doesn't hold if we append the checksum
% rather than prepending it.
prop_checksum() ->
random_word_size(fun prop_checksum/1).
prop_checksum(N) ->
?FORALL(Bin, bitstring(),
begin
Sum = ip_checksum:checksum(Bin, N),
equals(ip_checksum:checksum(<<Sum/bits, Bin/bits>>, N),
<<0:N>>)
end).

View file

@ -0,0 +1,223 @@
%%% File : lists_eqc.erl
%%% Author : Thomas Arts <thomas.arts@quviq.com>
%%% Ulf Norell <ulf.norell@quviq.com>
%%% Description : QuickCheck tests for some functions from the lists library.
%%% Created : 23 Mar 2010 by Thomas Arts <thomas.arts@quviq.com>
-module(lists_eqc).
-include_lib("eqc/include/eqc.hrl").
-compile(export_all).
% === Testing lists:delete/2 ===
% The lists:delete/2 function removes an element from a list. Here's a property
% we might want for this function: after you've removed an element from a list
% it's not there anymore. The corresponding QuickCheck property is:
prop_delete_0() ->
?FORALL({X, Xs}, {int(), list(int())},
not lists:member(X, lists:delete(X, Xs))).
% Checking this property for 100 random values and lists it might actually
% pass.
test_delete_0() ->
quickcheck(prop_delete_0()).
% However, rerunning the property a few more times will reveal a problem:
test_delete_0_more() ->
quickcheck(numtests(2000,prop_delete_0())).
% We get output looking like this:
% 74> lists_eqc:test_delete_0_more().
% ............................................................................
% ............................................................................
% ............................................................................
% ............................................................................
% ....................................................Failed! After 377 tests.
% {7,[-6,1,23,24,7,7]}
% Shrinking..(2 times)
% {7,[7,7]}
% false
% There is a problem with our specification. lists:delete/2 only removes the
% first occurrence of the element, something our specification fails to take
% into account.
% Before fixing the problem in the specification, it's worth thinking about why
% we needed so many tests to find the bug. In order to find the bug we need to
% generate a value and a list such that the value appears twice in the list.
% What's the probability of that? To answer that question we can write a new
% property:
prop_member_probability() ->
?FORALL({X, Xs}, {int(), list(int())},
collect(lists:member(X, Xs), true)).
% This property always succeeds, but for every test case it records whether the
% generated value appears (even once) in the list. Running the property a large
% number of times reveals that the probability that a random value appears in a
% random list is around 8%. No wonder it's hard to find a test case where it
% appears at least twice!
% To make it easier to find a failing case, we can change our property to only
% look at cases where the value appears at least once in the list. To do this
% we use the ?IMPLIES macro.
prop_delete_1() ->
?FORALL({X, Xs}, {int(), list(int())},
?IMPLIES(lists:member(X, Xs),
not lists:member(X, lists:delete(X, Xs)))).
% Now the output will look something like this:
% 102> eqc:quickcheck(lists_eqc:prop_delete_1()).
% xxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx.
% .xxxxxxxxxxxxxx.xx.xxxxxxx.xxxxxxxxxxx.xxxxxxxxxxxxxxxxxxx.xxx.xxxxxxxxxxxxx
% xxxxxxxxx.xxxxxxxxxxx.xxxxx.xxxxxxxxxxxxxxxxxxxxxx.xx.xxxxxxxxxxxx.xxxxxxxxx
% xxxxxxxxxxxxxxxxxxxxxxxxx..xxxxx.xxxx.xxxxxxxxx.xxxxxFailed! After 22 tests.
% {1,[31,35,35,34,-21,-13,7,1,2,1,-35,2]}
% Shrinking.....(5 times)
% {1,[1,1]}
% false
% The 'x's represent test cases that were discarded because they didn't satisfy
% the condition of the ?IMPLIES.
% Now that we have a property that fails reliably we can use it to document our
% misconception about the behaviour of lists:delete/2. The fails/1 function
% takes a property that is expected to fail and fails it doesn't.
prop_delete_2() ->
fails(
?FORALL({X, Xs}, {int(), list(int())},
?IMPLIES(lists:member(X, Xs),
not lists:member(X, lists:delete(X, Xs))))).
% Let's fix the specification. One possibility would be to count the number of
% occurrences of the value we're removing before and after calling
% lists:delete/2, but we can write a more precise specification quite elegantly:
prop_delete_3() ->
?FORALL({Xs, X, Ys}, {list(int()), int(), list(int())},
?IMPLIES(not lists:member(X, Xs),
equals(lists:delete(X, Xs ++ [X] ++ Ys),
Xs ++ Ys))).
% The equals function compares its arguments for equality and, if they're not
% equal, prints the arguments. This lets us see what lists:delete actually
% returned in the case we get a failing test case. Try removing the ?IMPLIES
% and run the property to see what it looks like.
% === Testing lists:seq/3 ===
% The lists:seq/2 function has recently changed in Erlang. The new
% specification allows lists:seq(1,0) and returns [] in that case. Copying the
% specification from the manual still reveals an error
% eqc:quickcheck(lists_eqc:prop_seq0()) due to the fact that a special case is
% overlooked. The fix is in prop_seq().
% This is the property according to the documentation. This property fails with
% a badarith exception on the test case {0, 0, 0}. The problem is that the
% specification of the length is not correct for increment 0.
prop_seq0() ->
?FORALL({From,To,Incr},{int(),int(),int()},
case catch lists:seq(From,To,Incr) of
{'EXIT',_} ->
(To < From-Incr andalso Incr > 0) orelse
(To > From-Incr andalso Incr < 0) orelse
(Incr==0 andalso From /= To);
List ->
is_list(List) andalso
length(List) == (To-From+Incr) div Incr
end).
% This is the property as it holds.
prop_seq() ->
?FORALL({From,To,Incr},{int(),int(),int()},
case catch lists:seq(From,To,Incr) of
{'EXIT',_} ->
(To < From-Incr andalso Incr > 0) orelse
(To > From-Incr andalso Incr < 0) orelse
(Incr==0 andalso From /= To);
List when Incr /= 0 ->
is_list(List) andalso
length(List) == (To-From+Incr) div Incr;
List when Incr == 0 ->
length(List) == 1
end).
% This is probably how one would like seq to behave.
prop_seq_wish(Seq) ->
?FORALL({From,To,Incr},{int(),int(),int()},
case catch Seq(From,To,Incr) of
[] -> Incr > 0 andalso From > To orelse
Incr < 0 andalso From < To;
[_] when Incr == 0 -> From == To;
List when Incr /= 0 andalso is_list(List) ->
length(List) == (To-From+Incr) div Incr;
{'EXIT', _} ->
Incr == 0 andalso From /= To;
_ ->
false
end).
prop_seq_wish() ->
prop_seq_wish(fun lists:seq/3).
% Here is a reference implementation satisfying prop_seq_wish.
seq(From, To, 0) when From /= To ->
exit("seq: increment 0");
seq(From, From, _Incr) ->
[From];
seq(From, To, Incr) when From > To andalso Incr > 0 ->
[];
seq(From, To, Incr) when From < To andalso Incr < 0 ->
[];
seq(From, To, Incr) ->
[From | seq(From + Incr, To, Incr)].
prop_seq_wish_granted() ->
prop_seq_wish(fun seq/3).
% The previous properties only specifies the length of the result of
% lists:seq/3. We also want to make sure that it contains the right elements.
% In particular, if lists:seq(From, To, Incr) returns a non-empty list, the
% first element of the list should be From, and the difference between adjacent
% elements should be Incr. We've already tested that the list has the right
% number of elements so we don't have to worry about when the list ends.
% First some helper functions:
% We're only interested in non-empty results of seq/3.
is_cons([_|_]) -> true;
is_cons(_) -> false.
% We want to look at the difference between adjacent elements.
diff_adjacent([X,Y|Xs]) ->
[Y - X|diff_adjacent([Y|Xs])];
diff_adjacent(_) ->
[].
% We use ?IMPLIES to ignore the cases when lists:seq does not return a
% non-empty list. To make sure we still get interesting test cases we collect
% the lengths of the results and the increments we've chosen.
prop_seq_elements() ->
?FORALL({From, To, Incr}, {int(), int(), int()},
begin
Seq = (catch lists:seq(From, To, Incr)),
?IMPLIES(is_cons(Seq),
begin
Adj = diff_adjacent(Seq),
?WHENFAIL(io:format("Seq = ~w\nAdj = ~w", [Seq, Adj]),
% When you have several collects in the same property you can give them
% names using collect/3 and with_title/1 to distinguish them.
% We divide the actual numbers by 5 to reduce the number of different
% values collected.
collect(with_title(lengths), length(Seq) div 5,
collect(with_title(incr), Incr div 5,
hd(Seq) == From andalso
lists:all(fun(D) -> D == Incr end, Adj)
))
)
end)
end).

View file

@ -0,0 +1,146 @@
%%% File : sets_eqc.erl
%%% Author : Thomas Arts <thomas.arts@quviq.com>
%%% Description : QuickCheck properties for sets.erl
%%% Based on "Testing Data Structures with QuickCheck"
%%% Created : 24 Mar 2010 by Thomas Arts
-module(sets_eqc).
-include_lib("eqc/include/eqc.hrl").
-compile(export_all).
%% Create a generator for the opaque type "set". The generator will generate
%% symbolic calls which when evaluated computes a set. Each symbolic call has
%% the form {call, Module, Function, Arguments} and are evaluated using the
%% function eval/1.
%% To avoid generating infinite symbolic representations we pass the size
%% parameter to the generator and use it to make sure we stop eventually.
set(G) ->
?SIZED(Size,well_defined(set(Size,G))).
set(0,G) ->
oneof([{call,sets,new,[]},
{call,sets,from_list,[list(G)]}]);
set(N,G) ->
frequency(
[{5,set(0,G)},
{3, ?LAZY(?LETSHRINK([Set],[set(N-1,G)],
{call,sets,add_element,[G, Set]}))},
{1, ?LAZY(?LETSHRINK([Set],[set(N-1,G)],
{call,sets,del_element,[G, Set]}))},
{1, ?LAZY(?LETSHRINK([Set1,Set2],[set(N div 2,G),set(N div 2,G)],
{call,sets,union,[Set1, Set2]}))},
{1, ?LAZY(?LETSHRINK(Sets,list(set(N div 3,G)),
{call,sets,union,[Sets]}))},
{1, ?LAZY(?LETSHRINK([Set1,Set2],[set(N div 2,G),set(N div 2,G)],
{call,sets,intersection,[Set1, Set2]}))},
{1, ?LAZY(?LETSHRINK(Sets,?LET(L,nat(),vector(L+1,set(N div (L+1),G))),
{call,sets,intersection,[Sets]}))},
{1, ?LAZY(?LETSHRINK([Set1,Set2],[set(N div 2,G),set(N div 2,G)],
{call,sets,subtract,[Set1, Set2]}))},
{1, ?LAZY(?LETSHRINK([Set],[set(N div 2,G)],
{call,sets,filter,[function1(bool()), Set]}))}]).
%% The next step is to define a model interpretation, i.e. a simplified,
%% obviously correct implementation of the data type. In this case we use
%% usorted lists.
model(S) ->
lists:sort(sets:to_list(S)).
%% Define the set operations on the model.
madd_element(E,S) ->
lists:usort([E|S]).
mdel_element(E,S) ->
S -- [E].
msize(S) ->
length(S).
mis_element(E,S) ->
lists:member(E,S).
munion(Ss) ->
lists:usort(lists:append(Ss)).
mintersection(Sets) ->
[ E || E <- lists:usort(lists:append(Sets)),
lists:all(fun(Set) -> lists:member(E,Set) end, Sets)].
mis_disjoint(S1,S2) ->
mintersection([S1,S2]) == [].
mfilter(Pred,S) ->
[ E || E <- S, Pred(E)].
%% Define one property for each operation. We parameterize the properties on
%% the generator for the elements. To make it easy to run the properties we
%% also define special versions that use integers.
%% Each property have the same basic form: we check that a given operation
%% on sets has the same behaviour as the corresponding model operation.
prop_create() -> prop_create(int()).
prop_create(G) ->
?FORALL(Set,set(G),
sets:is_set(eval(Set))).
prop_add_element() -> prop_add_element(int()).
prop_add_element(G) ->
?FORALL({E,Set},{G,set(G)},
model(sets:add_element(E,eval(Set))) == madd_element(E,model(eval(Set)))).
prop_del_element() -> prop_del_element(int()).
prop_del_element(G) ->
?FORALL({E,Set},{G,set(G)},
model(sets:del_element(E,eval(Set))) == mdel_element(E,model(eval(Set)))).
prop_union2() -> prop_union2(int()).
prop_union2(G) ->
?FORALL({Set1,Set2},{set(G),set(G)},
model(sets:union(eval(Set1),eval(Set2))) == munion([model(eval(Set1)),model(eval(Set2))])).
prop_union() -> prop_union(int()).
prop_union(G) ->
?FORALL(Sets,list(set(G)),
model(sets:union(eval(Sets))) == munion([model(Set) || Set<-eval(Sets)])).
prop_intersection2() -> prop_intersection2(int()).
prop_intersection2(G) ->
?FORALL({Set1,Set2},{set(G),set(G)},
model(sets:intersection(eval(Set1),eval(Set2))) ==
mintersection([model(eval(Set1)),model(eval(Set2))])).
prop_intersection() -> prop_intersection(int()).
prop_intersection(G) ->
?FORALL(Sets,eqc_gen:non_empty(list(set(G))),
model(sets:intersection(eval(Sets))) == mintersection([model(Set) || Set<-eval(Sets)])).
prop_size() -> prop_size(int()).
prop_size(G) ->
?FORALL(Set,set(G),
sets:size(eval(Set)) == msize(model(eval(Set)))).
prop_is_element() -> prop_is_element(int()).
prop_is_element(G) ->
?FORALL({E,Set},{G,set(G)},
sets:is_element(E,eval(Set)) == mis_element(E,model(eval(Set)))).
prop_is_disjoint() -> prop_is_disjoint(int()).
prop_is_disjoint(G) ->
?FORALL({Set1,Set2},{set(G),set(G)},
sets:is_disjoint(eval(Set1),eval(Set2)) == mis_disjoint(model(eval(Set1)),model(eval(Set2)))).
prop_filter() -> prop_filter(int()).
prop_filter(G) ->
?FORALL({Pred,Set},{function1(bool()),set(G)},
model(sets:filter(Pred,eval(Set))) == mfilter(Pred,model(eval(Set)))).