Added eqcmini quickcheck library to lib/
This commit is contained in:
parent
8a574db360
commit
73194d7d28
21 changed files with 2075 additions and 0 deletions
227
lib/eqc/examples/generators_eqc.erl
Normal file
227
lib/eqc/examples/generators_eqc.erl
Normal 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))).
|
||||
|
40
lib/eqc/examples/ip_checksum.erl
Normal file
40
lib/eqc/examples/ip_checksum.erl
Normal 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>>.
|
||||
|
218
lib/eqc/examples/ip_checksum_eqc.erl
Normal file
218
lib/eqc/examples/ip_checksum_eqc.erl
Normal 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).
|
||||
|
223
lib/eqc/examples/lists_eqc.erl
Normal file
223
lib/eqc/examples/lists_eqc.erl
Normal 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).
|
||||
|
146
lib/eqc/examples/sets_eqc.erl
Normal file
146
lib/eqc/examples/sets_eqc.erl
Normal 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)))).
|
||||
|
||||
|
Reference in a new issue