Skip to content

Commit d74e47a

Browse files
authored
Merge pull request #400 from ocaml-multicore/catch-next-state-exc
STM: Catch exceptions in next_state for nicer UX
2 parents e59e763 + 17514a4 commit d74e47a

File tree

8 files changed

+150
-41
lines changed

8 files changed

+150
-41
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Next
44

5+
- #400: Catch and delay exceptions in `STM`'s `next_state` for a nicer UX
56
- #387: Reduce needless allocations in `Lin`'s sequential consistency
67
search, as part of an `Out_channel` test cleanup
78
- #379: Extend the set of `Util.Pp` pretty-printers and teach them to

lib/STM.ml

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ struct
118118
then return []
119119
else
120120
(arb s).gen >>= fun c ->
121-
(gen_cmds arb (Spec.next_state c s) (fuel-1)) >>= fun cs ->
121+
let s' = try Spec.next_state c s with _ -> s in
122+
(gen_cmds arb s' (fuel-1)) >>= fun cs ->
122123
return (c::cs))
123124
(** A fueled command list generator.
124125
Accepts a state parameter to enable state-dependent [cmd] generation. *)
@@ -127,7 +128,7 @@ struct
127128
| [] -> true
128129
| c::cs ->
129130
Spec.precond c s &&
130-
let s' = Spec.next_state c s in
131+
let s' = try Spec.next_state c s with _ -> s in
131132
cmds_ok s' cs
132133

133134
(* This is an adaption of [QCheck.Shrink.list_spine]
@@ -180,66 +181,61 @@ struct
180181
| c::cs ->
181182
let res = Spec.run c sut in
182183
let b = Spec.postcond c s res in
183-
let s' = Spec.next_state c s in
184184
if b
185185
then
186+
let s' = Spec.next_state c s in
186187
match check_disagree s' sut cs with
187188
| None -> None
188189
| Some rest -> Some ((c,res)::rest)
189190
else Some [c,res]
190191

191-
let check_and_next (c,res) s =
192-
let b = Spec.postcond c s res in
193-
let s' = Spec.next_state c s in
194-
b,s'
195-
196192
(* checks that all interleavings of a cmd triple satisfies all preconditions *)
197193
let rec all_interleavings_ok pref cs1 cs2 s =
198194
match pref with
199195
| c::pref' ->
200196
Spec.precond c s &&
201-
let s' = Spec.next_state c s in
197+
let s' = try Spec.next_state c s with _ -> s in
202198
all_interleavings_ok pref' cs1 cs2 s'
203199
| [] ->
204200
match cs1,cs2 with
205201
| [],[] -> true
206202
| [],c2::cs2' ->
207203
Spec.precond c2 s &&
208-
let s' = Spec.next_state c2 s in
204+
let s' = try Spec.next_state c2 s with _ -> s in
209205
all_interleavings_ok pref cs1 cs2' s'
210206
| c1::cs1',[] ->
211207
Spec.precond c1 s &&
212-
let s' = Spec.next_state c1 s in
208+
let s' = try Spec.next_state c1 s with _ -> s in
213209
all_interleavings_ok pref cs1' cs2 s'
214210
| c1::cs1',c2::cs2' ->
215211
(Spec.precond c1 s &&
216-
let s' = Spec.next_state c1 s in
212+
let s' = try Spec.next_state c1 s with _ -> s in
217213
all_interleavings_ok pref cs1' cs2 s')
218214
&&
219215
(Spec.precond c2 s &&
220-
let s' = Spec.next_state c2 s in
216+
let s' = try Spec.next_state c2 s with _ -> s in
221217
all_interleavings_ok pref cs1 cs2' s')
222218

223219
let rec check_obs pref cs1 cs2 s =
224220
match pref with
225-
| p::pref' ->
226-
let b,s' = check_and_next p s in
227-
b && check_obs pref' cs1 cs2 s'
221+
| (c,res)::pref' ->
222+
let b = Spec.postcond c s res in
223+
b && check_obs pref' cs1 cs2 (Spec.next_state c s)
228224
| [] ->
229225
match cs1,cs2 with
230226
| [],[] -> true
231-
| [],p2::cs2' ->
232-
let b,s' = check_and_next p2 s in
233-
b && check_obs pref cs1 cs2' s'
234-
| p1::cs1',[] ->
235-
let b,s' = check_and_next p1 s in
236-
b && check_obs pref cs1' cs2 s'
237-
| p1::cs1',p2::cs2' ->
238-
(let b1,s' = check_and_next p1 s in
239-
b1 && check_obs pref cs1' cs2 s')
227+
| [],(c2,res2)::cs2' ->
228+
let b = Spec.postcond c2 s res2 in
229+
b && check_obs pref cs1 cs2' (Spec.next_state c2 s)
230+
| (c1,res1)::cs1',[] ->
231+
let b = Spec.postcond c1 s res1 in
232+
b && check_obs pref cs1' cs2 (Spec.next_state c1 s)
233+
| (c1,res1)::cs1',(c2,res2)::cs2' ->
234+
(let b1 = Spec.postcond c1 s res1 in
235+
b1 && check_obs pref cs1' cs2 (Spec.next_state c1 s))
240236
||
241-
(let b2,s' = check_and_next p2 s in
242-
b2 && check_obs pref cs1 cs2' s')
237+
(let b2 = Spec.postcond c2 s res2 in
238+
b2 && check_obs pref cs1 cs2' (Spec.next_state c2 s))
243239

244240
let gen_cmds_size gen s size_gen = Gen.sized_size size_gen (gen_cmds gen s)
245241

@@ -312,7 +308,7 @@ struct
312308
let gen_triple =
313309
Gen.(seq_pref_gen >>= fun seq_pref ->
314310
int_range 2 (2*par_len) >>= fun dbl_plen ->
315-
let spawn_state = List.fold_left (fun st c -> Spec.next_state c st) Spec.init_state seq_pref in
311+
let spawn_state = List.fold_left (fun st c -> try Spec.next_state c st with _ -> st) Spec.init_state seq_pref in
316312
let par_len1 = dbl_plen/2 in
317313
let par_gen1 = gen_cmds_size arb1 spawn_state (return par_len1) in
318314
let par_gen2 = gen_cmds_size arb2 spawn_state (return (dbl_plen - par_len1)) in

lib/STM.mli

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ sig
9999
val next_state : cmd -> state -> state
100100
(** [next_state c s] expresses how interpreting the command [c] moves the
101101
model's internal state machine from the state [s] to the next state.
102-
Ideally a [next_state] function is pure. *)
102+
Ideally a [next_state] function is pure, as it is run more than once. *)
103103

104104
val init_sut : unit -> sut
105105
(** Initialize the system under test. *)
@@ -139,7 +139,8 @@ sig
139139

140140
val cmds_ok : Spec.state -> Spec.cmd list -> bool
141141
(** A precondition checker (stops early, thanks to short-circuit Boolean evaluation).
142-
Accepts the initial state and the command sequence as parameters. *)
142+
Accepts the initial state and the command sequence as parameters.
143+
[cmds_ok] catches and ignores exceptions arising from {!next_state}. *)
143144

144145
val arb_cmds : Spec.state -> Spec.cmd list arbitrary
145146
(** A generator of command sequences. Accepts the initial state as parameter. *)
@@ -168,16 +169,22 @@ sig
168169
val gen_cmds_size : (Spec.state -> Spec.cmd arbitrary) -> Spec.state -> int Gen.t -> Spec.cmd list Gen.t
169170
(** [gen_cmds_size arb state gen_int] generates a program of size generated
170171
by [gen_int] using [arb] to generate [cmd]s according to the current
171-
state. [state] is the starting state. *)
172+
state. [state] is the starting state.
173+
[gen_cmds_size] catches and ignores generation-time exceptions arising
174+
from {!next_state}. *)
172175

173176
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) arbitrary
174177
(** [arb_cmds_triple seq_len par_len] generates a [cmd] triple with at most [seq_len]
175-
sequential commands and at most [par_len] parallel commands each. *)
178+
sequential commands and at most [par_len] parallel commands each.
179+
[arb_cmds_triple] catches and ignores generation-time exceptions arising
180+
from {!next_state}. *)
176181

177182
val all_interleavings_ok : Spec.cmd list -> Spec.cmd list -> Spec.cmd list -> Spec.state -> bool
178183
(** [all_interleavings_ok seq spawn0 spawn1 state] checks that
179184
preconditions of all the {!cmd}s of [seq], [spawn0], and [spawn1] are satisfied in all the
180-
possible interleavings and starting with [state] *)
185+
possible interleavings and starting with [state].
186+
[all_interleavings_ok] catches and ignores exceptions arising from
187+
{!next_state}. *)
181188

182189
val shrink_triple : (Spec.state -> Spec.cmd arbitrary) -> (Spec.state -> Spec.cmd arbitrary) -> (Spec.state -> Spec.cmd arbitrary) -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) Shrink.t
183190
(** [shrink_triple arb0 arb1 arb2] is a {!QCheck.Shrink.t} for programs (triple of list of [cmd]s) that is specialized for each part of the program. *)
@@ -186,7 +193,9 @@ sig
186193
(** [arb_triple seq_len par_len arb0 arb1 arb2] generates a [cmd] triple with at most [seq_len]
187194
sequential commands and at most [par_len] parallel commands each.
188195
The three [cmd] components are generated with [arb0], [arb1], and [arb2], respectively.
189-
Each of these take the model state as a parameter. *)
196+
Each of these take the model state as a parameter.
197+
[arb_triple] catches and ignores generation-time exceptions arising
198+
from {!next_state}. *)
190199
end
191200
[@@alert internal "This module is exposed for internal uses only, its API may change at any time"]
192201

lib/STM_domain.mli

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,32 @@ module Make : functor (Spec : STM.Spec) ->
99
val all_interleavings_ok : (Spec.cmd list * Spec.cmd list * Spec.cmd list) -> bool
1010
(** [all_interleavings_ok (seq,spawn0,spawn1)] checks that
1111
preconditions of all the {!cmd}s of [seq], [spawn0], and [spawn1] are satisfied in all the
12-
possible interleavings and starting with {!Spec.init_state}. *)
12+
possible interleavings and starting with {!Spec.init_state}.
13+
[all_interleavings_ok] catches and ignores exceptions arising from
14+
{!next_state}. *)
1315

1416
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
1517
(** [arb_cmds_triple seq_len par_len] generates a [cmd] triple with at most [seq_len]
1618
sequential commands and at most [par_len] parallel commands each.
17-
All [cmds] are generated with {!Spec.arb_cmd}. *)
19+
All [cmds] are generated with {!Spec.arb_cmd}.
20+
[arb_cmds_triple] catches and ignores generation-time exceptions arising
21+
from {!Spec.next_state}. *)
1822

1923
val arb_triple : int -> int -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
2024
(** [arb_triple seq_len par_len arb0 arb1 arb2] generates a [cmd] triple with at most [seq_len]
2125
sequential commands and at most [par_len] parallel commands each.
2226
The three {!Spec.cmd} components are generated with [arb0], [arb1], and [arb2], respectively.
23-
Each of these take the model state as a parameter. *)
27+
Each of these take the model state as a parameter.
28+
[arb_triple] catches and ignores generation-time exceptions arising
29+
from {!Spec.next_state}. *)
2430

2531
val arb_triple_asym : int -> int -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.state -> Spec.cmd QCheck.arbitrary) -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
2632
(** [arb_triple_asym seq_len par_len arb0 arb1 arb2] creates a triple [cmd]
2733
generator like {!arb_triple}. It differs in that the resulting printer
2834
is asymmetric, printing [arb1]'s result below [arb0]'s result and
29-
printing [arb2]'s result to the right of [arb1]'s result. *)
35+
printing [arb2]'s result to the right of [arb1]'s result.
36+
[arb_triple_asym] catches and ignores generation-time exceptions arising
37+
from {!Spec.next_state}. *)
3038

3139
val interp_sut_res : Spec.sut -> Spec.cmd list -> (Spec.cmd * STM.res) list
3240
(** [interp_sut_res sut cs] interprets the commands [cs] over the system {!Spec.sut}

lib/STM_sequential.mli

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ module Make : functor (Spec : STM.Spec) ->
44
sig
55
val cmds_ok : Spec.state -> Spec.cmd list -> bool
66
(** A precondition checker (stops early, thanks to short-circuit Boolean evaluation).
7-
Accepts the initial state and the command sequence as parameters. *)
7+
Accepts the initial state and the command sequence as parameters.
8+
[cmds_ok] catches and ignores exceptions arising from {!next_state}. *)
89

910
val arb_cmds : Spec.state -> Spec.cmd list QCheck.arbitrary
10-
(** A generator of {!Spec.cmd} sequences. Accepts the initial state as a parameter. *)
11+
(** A generator of {!Spec.cmd} sequences. Accepts the initial state as a parameter.
12+
[arb_cmds] catches and ignores generation-time exceptions arising from
13+
{!Spec.next_state}. *)
1114

1215
val agree_prop : Spec.cmd list -> bool
1316
(** The agreement property: the command sequence [cs] yields the same observations

lib/STM_thread.mli

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ module Make : functor (Spec : STM.Spec) ->
77
val arb_cmds_triple : int -> int -> (Spec.cmd list * Spec.cmd list * Spec.cmd list) QCheck.arbitrary
88
(** [arb_cmds_triple seq_len conc_len] generates a [cmd] triple with at most [seq_len]
99
sequential commands and at most [conc_len] concurrent commands each.
10-
All [cmds] are generated with {!Spec.arb_cmd}. *)
10+
All [cmds] are generated with {!Spec.arb_cmd}.
11+
[arb_cmds_triple] catches and ignores generation-time exceptions arising
12+
from {!Spec.next_state}. *)
1113

1214
val interp_sut_res : Spec.sut -> Spec.cmd list -> (Spec.cmd * STM.res) list
1315
(** [interp_sut_res sut cs] interprets the commands [cs] over the system [sut]

test/dune

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,10 @@
9292
(libraries qcheck-stm.sequential threads.posix)
9393
(action
9494
(with-accepted-exit-codes 1 (run ./%{test} --verbose --seed 229109553))))
95+
96+
(test
97+
(name stm_next_state_exc)
98+
(modules stm_next_state_exc)
99+
(package qcheck-stm)
100+
(libraries qcheck-stm.sequential qcheck-stm.domain)
101+
(enabled_if (>= %{ocaml_version} 5)))

test/stm_next_state_exc.ml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
open QCheck
2+
open STM
3+
4+
exception Random_next_state_failure
5+
6+
(** This is a variant of refs to test for exceptions in next_state *)
7+
8+
module RConf =
9+
struct
10+
11+
type cmd = Get | Set of int | Add of int
12+
13+
let pp_cmd par fmt x =
14+
let open Util.Pp in
15+
match x with
16+
| Get -> cst0 "Get" fmt
17+
| Set x -> cst1 pp_int "Set" par fmt x
18+
| Add x -> cst1 pp_int "Add" par fmt x
19+
20+
let show_cmd = Util.Pp.to_show pp_cmd
21+
22+
let gen_cmd =
23+
let int_gen = Gen.nat in
24+
(Gen.oneof
25+
[Gen.return Get;
26+
Gen.map (fun i -> Set i) int_gen;
27+
Gen.map (fun i -> Add i) int_gen;
28+
])
29+
let arb_cmd _ = make ~print:show_cmd gen_cmd
30+
31+
type state = int
32+
33+
let init_state = 0
34+
35+
let next_state c s = match c with
36+
| Get -> s
37+
| Set i -> i
38+
| Add i -> if i>70 then raise Random_next_state_failure; s+i
39+
40+
type sut = int ref
41+
42+
let init_sut () = ref 0
43+
44+
let cleanup _ = ()
45+
46+
let run c r = match c with
47+
| Get -> Res (int, !r)
48+
| Set i -> Res (unit, (r:=i))
49+
| Add i -> Res (unit, let old = !r in r := i + old) (* buggy: not atomic *)
50+
51+
let precond _ _ = true
52+
53+
let postcond c (s:state) res = match c,res with
54+
| Get, Res ((Int,_),r) -> r = s
55+
| Set _, Res ((Unit,_),_)
56+
| Add _, Res ((Unit,_),_) -> true
57+
| _,_ -> false
58+
end
59+
60+
module RT_int = STM.Internal.Make(RConf)[@alert "-internal"]
61+
module RT_seq = STM_sequential.Make(RConf)
62+
module RT_dom = STM_domain.Make(RConf)
63+
64+
let () = QCheck_base_runner.set_seed 301717275
65+
let _ =
66+
QCheck_base_runner.run_tests ~verbose:true
67+
[RT_int.consistency_test ~count:1000 ~name:"STM test exception during next_state consistency"]
68+
let () = (* raises Test_error not handled by neg_agree_test so handled explicitly *)
69+
let name = "STM test exception during next_state sequential" in
70+
try
71+
Test.check_exn (RT_seq.agree_test ~count:1000 ~name);
72+
Printf.printf "%s unexpectedly succeeded\n%!" name;
73+
with Test.Test_error (err_name,_,Random_next_state_failure,_) ->
74+
assert (err_name = name);
75+
Printf.printf "%s failed with Random_next_state_failure as expected\n%!" name
76+
let () = (* raises Test_error not handled by neg_agree_test so handled explicitly *)
77+
let name = "STM test exception during next_state parallel" in
78+
try
79+
Test.check_exn (RT_dom.agree_test_par ~count:1000 ~name);
80+
Printf.printf "%s unexpectedly succeeded\n%!" name;
81+
with Test.Test_error (err_name,_,Random_next_state_failure,_) ->
82+
assert (err_name = name);
83+
Printf.printf "%s failed with Random_next_state_failure as expected\n%!" name

0 commit comments

Comments
 (0)