Skip to content

Commit

Permalink
Merge pull request #212 from jmid/shrinking-retries
Browse files Browse the repository at this point in the history
Add parameter for retrying a property while shrinking
  • Loading branch information
jmid authored Dec 16, 2021
2 parents 6ed3441 + 3af0592 commit 1eefb22
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 25 deletions.
8 changes: 4 additions & 4 deletions src/core/QCheck.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1680,13 +1680,13 @@ module Test = struct

let make_cell ?if_assumptions_fail
?count ?long_factor ?max_gen
?max_fail ?small:_removed_in_qcheck_2 ?name arb law
?max_fail ?small:_removed_in_qcheck_2 ?retries ?name arb law
=
let {gen; shrink; print; collect; stats; _} = arb in
QCheck2.Test.make_cell_from_QCheck1 ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?name ~gen ?shrink ?print ?collect ~stats law
QCheck2.Test.make_cell_from_QCheck1 ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?retries ?name ~gen ?shrink ?print ?collect ~stats law

let make ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?small ?name arb law =
QCheck2.Test.Test (make_cell ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?small ?name arb law)
let make ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?small ?retries ?name arb law =
QCheck2.Test.Test (make_cell ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?small ?retries ?name arb law)

let fail_report = QCheck2.Test.fail_report

Expand Down
8 changes: 5 additions & 3 deletions src/core/QCheck.mli
Original file line number Diff line number Diff line change
Expand Up @@ -988,13 +988,14 @@ module Test : sig
val make_cell :
?if_assumptions_fail:([`Fatal | `Warning] * float) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int ->
?small:('a -> int) -> ?name:string -> 'a arbitrary -> ('a -> bool) ->
'a cell
?small:('a -> int) -> ?retries:int -> ?name:string ->
'a arbitrary -> ('a -> bool) -> 'a cell
(** [make_cell arb prop] builds a test that checks property [prop] on instances
of the generator [arb].
@param name the name of the test.
@param count number of test cases to run, counting only
the test cases which satisfy preconditions.
@param retries number of times to retry the tested property while shrinking.
@param long_factor the factor by which to multiply count, max_gen and
max_fail when running a long test (default: 1).
@param max_gen maximum number of times the generation function
Expand Down Expand Up @@ -1035,7 +1036,8 @@ module Test : sig
val make :
?if_assumptions_fail:([`Fatal | `Warning] * float) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int ->
?small:('a -> int) -> ?name:string -> 'a arbitrary -> ('a -> bool) -> t
?small:('a -> int) -> ?retries:int -> ?name:string -> 'a arbitrary ->
('a -> bool) -> t
(** [make arb prop] builds a test that checks property [prop] on instances
of the generator [arb].
See {!make_cell} for a description of the parameters.
Expand Down
45 changes: 36 additions & 9 deletions src/core/QCheck2.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,7 @@ module Test = struct
long_factor : int; (* multiplicative factor for long test count *)
max_gen : int; (* max number of instances to generate (>= count) *)
max_fail : int; (* max number of failures *)
retries : int; (* max number of retries during shrinking *)
law : 'a -> bool; (* the law to check *)
gen : 'a Gen.t; (* how to generate/shrink instances *)
print : 'a Print.t option; (* how to print values *)
Expand Down Expand Up @@ -1409,7 +1410,7 @@ module Test = struct

let make_cell ?(if_assumptions_fail=default_if_assumptions_fail)
?(count) ?(long_factor=1) ?max_gen
?(max_fail=1) ?(name=fresh_name()) ?print ?collect ?(stats=[]) gen law
?(max_fail=1) ?(retries=1) ?(name=fresh_name()) ?print ?collect ?(stats=[]) gen law
=
let count = global_count count in
let max_gen = match max_gen with None -> count + 200 | Some x->x in
Expand All @@ -1421,6 +1422,7 @@ module Test = struct
stats;
max_gen;
max_fail;
retries;
name;
count;
long_factor;
Expand All @@ -1430,7 +1432,7 @@ module Test = struct

let make_cell_from_QCheck1 ?(if_assumptions_fail=default_if_assumptions_fail)
?(count) ?(long_factor=1) ?max_gen
?(max_fail=1) ?(name=fresh_name()) ~gen ?shrink ?print ?collect ~stats law
?(max_fail=1) ?(retries=1) ?(name=fresh_name()) ~gen ?shrink ?print ?collect ~stats law
=
let count = global_count count in
(* Make a "fake" QCheck2 arbitrary with no shrinking *)
Expand All @@ -1444,15 +1446,16 @@ module Test = struct
stats;
max_gen;
max_fail;
retries;
name;
count;
long_factor;
if_assumptions_fail;
qcheck1_shrink = shrink;
}

let make ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?name ?print ?collect ?stats gen law =
Test (make_cell ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?name ?print ?collect ?stats gen law)
let make ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?retries ?name ?print ?collect ?stats gen law =
Test (make_cell ?if_assumptions_fail ?count ?long_factor ?max_gen ?max_fail ?retries ?name ?print ?collect ?stats gen law)

let test_get_count (Test cell) = get_count cell

Expand Down Expand Up @@ -1543,9 +1546,33 @@ module Test = struct
| Run_ok
| Run_fail of string list

let run_law law x =
(* run_law is a helper function for testing a property [law] on a
generated input [x].
When passed a ~retries number n>1, the tested property is checked
n times for each shrunk input candidate. The default value is 1,
thus causing no change in behaviour.
Retrying a property can be useful when testing non-deterministic
code with QCheck, e.g., for multicore execution. The idea is
described in
'Testing a Database for Race Conditions with QuickCheck'
Hughes and Bolinder, Erlang 2011, Sec.6:
"As we explained in section 4, we ensure that tests fail when
races are present simply by repeating each test a large number of
times, and by running on a dual core machine. We obtained the
minimal failing cases in the previous section by repeating each
test 100 times during shrinking: thus we stopped shrinking a test
case only when all of its candidate shrinkings passed 100 tests
in a row." *)
let run_law ~retries law x =
let rec loop i = match law x with
| false -> Run_fail []
| true ->
if i<=1 then Run_ok else loop (i-1) in
try
if law x then Run_ok else Run_fail []
loop retries
with User_fail msg -> Run_fail [msg]

(* QCheck1-compatibility code *)
Expand Down Expand Up @@ -1575,7 +1602,7 @@ module Test = struct
try
incr count;
st.handler st.test.name st.test (Shrinking (steps, !count, x));
begin match run_law st.test.law x with
begin match run_law ~retries:st.test.retries st.test.law x with
| Run_fail m when not is_err -> Some (Tree.pure x, Shrink_fail, m)
| _ -> None
end
Expand All @@ -1590,7 +1617,7 @@ module Test = struct
try
incr count;
st.handler st.test.name st.test (Shrinking (steps, !count, x));
begin match run_law st.test.law x with
begin match run_law ~retries:st.test.retries st.test.law x with
| Run_fail m when not is_err -> Some (x_tree, Shrink_fail, m)
| _ -> None
end
Expand Down Expand Up @@ -1668,7 +1695,7 @@ module Test = struct
let res =
try
state.handler state.test.name state.test (Testing input);
begin match run_law state.test.law input with
begin match run_law ~retries:1 state.test.law input with
| Run_ok ->
(* one test ok *)
decr_count state;
Expand Down
11 changes: 6 additions & 5 deletions src/core/QCheck2.mli
Original file line number Diff line number Diff line change
Expand Up @@ -1585,8 +1585,8 @@ module Test : sig

val make_cell :
?if_assumptions_fail:([`Fatal | `Warning] * float) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int -> ?name:string ->
?print:'a Print.t -> ?collect:('a -> string) -> ?stats:('a stat list) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int -> ?retries:int ->
?name:string -> ?print:'a Print.t -> ?collect:('a -> string) -> ?stats:('a stat list) ->
'a Gen.t -> ('a -> bool) ->
'a cell
(** [make_cell gen prop] builds a test that checks property [prop] on instances
Expand All @@ -1601,6 +1601,7 @@ module Test : sig
preconditions (should be >= count).
@param max_fail maximum number of failures before we stop generating
inputs. This is useful if shrinking takes too much time.
@param retries number of times to retry the tested property while shrinking.
@param if_assumptions_fail the minimum
fraction of tests that must satisfy the precondition for a success
to be considered valid.
Expand All @@ -1616,7 +1617,7 @@ module Test : sig
val make_cell_from_QCheck1 :
?if_assumptions_fail:([`Fatal | `Warning] * float) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int ->
?name:string -> gen:(Random.State.t -> 'a) -> ?shrink:('a -> ('a -> unit) -> unit) ->
?retries:int -> ?name:string -> gen:(Random.State.t -> 'a) -> ?shrink:('a -> ('a -> unit) -> unit) ->
?print:('a -> string) -> ?collect:('a -> string) -> stats:'a stat list -> ('a -> bool) ->
'a cell
(** ⚠️ Do not use, this is exposed for internal reasons only. ⚠️
Expand Down Expand Up @@ -1646,8 +1647,8 @@ module Test : sig

val make :
?if_assumptions_fail:([`Fatal | `Warning] * float) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int -> ?name:string ->
?print:('a Print.t) -> ?collect:('a -> string) -> ?stats:('a stat list) ->
?count:int -> ?long_factor:int -> ?max_gen:int -> ?max_fail:int -> ?retries:int ->
?name:string -> ?print:('a Print.t) -> ?collect:('a -> string) -> ?stats:('a stat list) ->
'a Gen.t -> ('a -> bool) -> t
(** [make gen prop] builds a test that checks property [prop] on instances
of the generator [gen].
Expand Down
5 changes: 5 additions & 0 deletions test/core/QCheck2_expect_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ module Overall = struct
]
(Gen.int_bound 120) (fun _ -> true)

let retries =
Test.make ~name:"with shrinking retries" ~retries:10 ~print:Print.int
Gen.small_nat (fun i -> Printf.printf "%i %!" i; i mod 3 <> 1)

let bad_assume_warn =
Test.make ~name:"WARN_unlikely_precond" ~count:2_000 ~print:Print.int
Gen.int
Expand All @@ -86,6 +90,7 @@ module Overall = struct
error;
collect;
stats;
retries;
bad_assume_warn;
bad_assume_fail;
]
Expand Down
5 changes: 5 additions & 0 deletions test/core/QCheck_expect_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ module Overall = struct
])
(fun _ -> true)

let retries =
Test.make ~name:"with shrinking retries" ~retries:10
small_nat (fun i -> Printf.printf "%i %!" i; i mod 3 <> 1)

let bad_assume_warn =
Test.make ~name:"WARN_unlikely_precond" ~count:2_000
int
Expand All @@ -88,6 +92,7 @@ module Overall = struct
error;
collect;
stats;
retries;
bad_assume_warn;
bad_assume_fail;
]
Expand Down
10 changes: 8 additions & 2 deletions test/core/qcheck2_output.txt.expected
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
random seed: 1234
2724675603984413065
50 7 0 0 0 0 0 0 0 0 0 0 3 3 3 3 3 3 3 3 3 3 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 2724675603984413065
0
1362337801992206532
0
Expand Down Expand Up @@ -221,6 +221,12 @@ stats num:
110..115: ####################################################### 9
116..121: ################## 3

--- Failure --------------------------------------------------------------------

Test with shrinking retries failed (0 shrink steps):

7

!!! Warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Warning for test WARN_unlikely_precond:
Expand Down Expand Up @@ -982,7 +988,7 @@ stats dist:
4150517416584649600.. 4611686018427387903: ################# 189
================================================================================
1 warning(s)
failure (35 tests failed, 1 tests errored, ran 83 tests)
failure (36 tests failed, 1 tests errored, ran 84 tests)
random seed: 153870556

+++ Stats for int_dist_empty_bucket ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down
10 changes: 8 additions & 2 deletions test/core/qcheck_output.txt.expected
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
random seed: 1234
2724675603984413065
50 7 4 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2724675603984413065
1362337801992206533
681168900996103267
340584450498051634
Expand Down Expand Up @@ -156,6 +156,12 @@ stats num:
110..115: ####################################################### 9
116..121: ################## 3

--- Failure --------------------------------------------------------------------

Test with shrinking retries failed (1 shrink steps):

4

!!! Warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Warning for test WARN_unlikely_precond:
Expand Down Expand Up @@ -937,7 +943,7 @@ stats dist:
4150517416584649600.. 4611686018427387903: ################# 189
================================================================================
1 warning(s)
failure (34 tests failed, 1 tests errored, ran 89 tests)
failure (35 tests failed, 1 tests errored, ran 90 tests)
random seed: 153870556

+++ Stats for int_dist_empty_bucket ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Expand Down

0 comments on commit 1eefb22

Please sign in to comment.