-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathlists_eqc.erl
executable file
·223 lines (189 loc) · 8.31 KB
/
lists_eqc.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
%%% File : lists_eqc.erl
%%% Author : Thomas Arts <[email protected]>
%%% Ulf Norell <[email protected]>
%%% Description : QuickCheck tests for some functions from the lists library.
%%% Created : 23 Mar 2010 by Thomas Arts <[email protected]>
-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).