|
1 | 1 | (** Base module for specifying STM tests *) |
2 | 2 |
|
3 | | -module STM_internal = STM_internal |
4 | | -module STM_spec = STM_spec |
5 | | -include STM_spec |
| 3 | +open QCheck |
| 4 | + |
| 5 | +type 'a ty = .. |
| 6 | + |
| 7 | +type _ ty += |
| 8 | + | Unit : unit ty |
| 9 | + | Bool : bool ty |
| 10 | + | Char : char ty |
| 11 | + | Int : int ty |
| 12 | + | Int32 : int32 ty |
| 13 | + | Int64 : int64 ty |
| 14 | + | Float : float ty |
| 15 | + | String : string ty |
| 16 | + | Bytes : bytes ty |
| 17 | + | Exn : exn ty |
| 18 | + | Option : 'a ty -> 'a option ty |
| 19 | + | Result : 'a ty * 'b ty -> ('a, 'b) result ty |
| 20 | + | List : 'a ty -> 'a list ty |
| 21 | + | Array : 'a ty -> 'a array ty |
| 22 | + | Seq : 'a ty -> 'a Seq.t ty |
| 23 | + |
| 24 | +type 'a ty_show = 'a ty * ('a -> string) |
| 25 | + |
| 26 | +let unit = (Unit, fun () -> "()") |
| 27 | +let bool = (Bool, string_of_bool) |
| 28 | +let char = (Char, fun c -> Printf.sprintf "%C" c) |
| 29 | +let int = (Int, string_of_int) |
| 30 | +let int32 = (Int32, Int32.to_string) |
| 31 | +let int64 = (Int64, Int64.to_string) |
| 32 | +let float = (Float, Float.to_string) |
| 33 | +let string = (String, QCheck.Print.string) |
| 34 | +let bytes = (Bytes, fun b -> QCheck.Print.string (Bytes.to_string b)) |
| 35 | +let option spec = |
| 36 | + let (ty,show) = spec in |
| 37 | + (Option ty, QCheck.Print.option show) |
| 38 | +let exn = (Exn, Printexc.to_string) |
| 39 | + |
| 40 | +let show_result show_ok show_err = function |
| 41 | + | Ok x -> Printf.sprintf "Ok (%s)" (show_ok x) |
| 42 | + | Error y -> Printf.sprintf "Error (%s)" (show_err y) |
| 43 | + |
| 44 | +let result spec_ok spec_err = |
| 45 | + let (ty_ok, show_ok) = spec_ok in |
| 46 | + let (ty_err, show_err) = spec_err in |
| 47 | + (Result (ty_ok, ty_err), show_result show_ok show_err) |
| 48 | +let list spec = |
| 49 | + let (ty,show) = spec in |
| 50 | + (List ty, QCheck.Print.list show) |
| 51 | +let array spec = |
| 52 | + let (ty,show) = spec in |
| 53 | + (Array ty, QCheck.Print.array show) |
| 54 | +let seq spec = |
| 55 | + let (ty,show) = spec in |
| 56 | + (Seq ty, fun s -> QCheck.Print.list show (List.of_seq s)) |
| 57 | + |
| 58 | + |
| 59 | + |
| 60 | +type res = |
| 61 | + Res : 'a ty_show * 'a -> res |
| 62 | + |
| 63 | +let show_res (Res ((_,show), v)) = show v |
| 64 | + |
| 65 | +(** The specification of a state machine. *) |
| 66 | +module type STM_spec = |
| 67 | +sig |
| 68 | + type cmd |
| 69 | + (** The type of commands *) |
| 70 | + |
| 71 | + type state |
| 72 | + (** The type of the model's state *) |
| 73 | + |
| 74 | + type sut |
| 75 | + (** The type of the system under test *) |
| 76 | + |
| 77 | + val arb_cmd : state -> cmd arbitrary |
| 78 | + (** A command generator. Accepts a state parameter to enable state-dependent [cmd] generation. *) |
| 79 | + |
| 80 | + val init_state : state |
| 81 | + (** The model's initial state. *) |
| 82 | + |
| 83 | + val show_cmd : cmd -> string |
| 84 | + (** [show_cmd c] returns a string representing the command [c]. *) |
| 85 | + |
| 86 | + val next_state : cmd -> state -> state |
| 87 | + (** Move the internal state machine to the next state. *) |
| 88 | + |
| 89 | + val init_sut : unit -> sut |
| 90 | + (** Initialize the system under test. *) |
| 91 | + |
| 92 | + val cleanup : sut -> unit |
| 93 | + (** Utility function to clean up the [sut] after each test instance, |
| 94 | + e.g., for closing sockets, files, or resetting global parameters*) |
| 95 | + |
| 96 | + val precond : cmd -> state -> bool |
| 97 | + (** [precond c s] expresses preconditions for command [c]. |
| 98 | + This is useful, e.g., to prevent the shrinker from breaking invariants when minimizing |
| 99 | + counterexamples. *) |
| 100 | + |
| 101 | + val run : cmd -> sut -> res |
| 102 | + (** [run c i] should interpret the command [c] over the system under test (typically side-effecting). *) |
| 103 | + |
| 104 | + val postcond : cmd -> state -> res -> bool |
| 105 | + (** [postcond c s res] checks whether [res] arising from interpreting the |
| 106 | + command [c] over the system under test with [run] agrees with the |
| 107 | + model's result. |
| 108 | + Note: [s] is in this case the model's state prior to command execution. *) |
| 109 | +end |
| 110 | + |
| 111 | + |
| 112 | +module STM_internal = |
| 113 | +struct |
| 114 | + module Make(Spec : STM_spec) = struct |
| 115 | + |
| 116 | + let rec gen_cmds arb s fuel = |
| 117 | + Gen.(if fuel = 0 |
| 118 | + then return [] |
| 119 | + else |
| 120 | + (arb s).gen >>= fun c -> |
| 121 | + (gen_cmds arb (Spec.next_state c s) (fuel-1)) >>= fun cs -> |
| 122 | + return (c::cs)) |
| 123 | + (** A fueled command list generator. |
| 124 | + Accepts a state parameter to enable state-dependent [cmd] generation. *) |
| 125 | + |
| 126 | + let rec cmds_ok s cs = match cs with |
| 127 | + | [] -> true |
| 128 | + | c::cs -> |
| 129 | + Spec.precond c s && |
| 130 | + let s' = Spec.next_state c s in |
| 131 | + cmds_ok s' cs |
| 132 | + |
| 133 | + let arb_cmds s = |
| 134 | + let cmds_gen = Gen.sized (gen_cmds Spec.arb_cmd s) in |
| 135 | + let shrinker = match (Spec.arb_cmd s).shrink with |
| 136 | + | None -> Shrink.list ~shrink:Shrink.nil (* no elem. shrinker provided *) |
| 137 | + | Some s -> Shrink.list ~shrink:s in |
| 138 | + let ac = QCheck.make ~shrink:(Shrink.filter (cmds_ok Spec.init_state) shrinker) cmds_gen in |
| 139 | + (match (Spec.arb_cmd s).print with |
| 140 | + | None -> ac |
| 141 | + | Some p -> set_print (Print.list p) ac) |
| 142 | + |
| 143 | + let consistency_test ~count ~name = |
| 144 | + Test.make ~name ~count (arb_cmds Spec.init_state) (cmds_ok Spec.init_state) |
| 145 | + |
| 146 | + let rec interp_agree s sut cs = match cs with |
| 147 | + | [] -> true |
| 148 | + | c::cs -> |
| 149 | + let res = Spec.run c sut in |
| 150 | + let b = Spec.postcond c s res in |
| 151 | + let s' = Spec.next_state c s in |
| 152 | + b && interp_agree s' sut cs |
| 153 | + |
| 154 | + let rec check_disagree s sut cs = match cs with |
| 155 | + | [] -> None |
| 156 | + | c::cs -> |
| 157 | + let res = Spec.run c sut in |
| 158 | + let b = Spec.postcond c s res in |
| 159 | + let s' = Spec.next_state c s in |
| 160 | + if b |
| 161 | + then |
| 162 | + match check_disagree s' sut cs with |
| 163 | + | None -> None |
| 164 | + | Some rest -> Some ((c,res)::rest) |
| 165 | + else Some [c,res] |
| 166 | + |
| 167 | + let check_and_next (c,res) s = |
| 168 | + let b = Spec.postcond c s res in |
| 169 | + let s' = Spec.next_state c s in |
| 170 | + b,s' |
| 171 | + |
| 172 | + (* checks that all interleavings of a cmd triple satisfies all preconditions *) |
| 173 | + let rec all_interleavings_ok pref cs1 cs2 s = |
| 174 | + match pref with |
| 175 | + | c::pref' -> |
| 176 | + Spec.precond c s && |
| 177 | + let s' = Spec.next_state c s in |
| 178 | + all_interleavings_ok pref' cs1 cs2 s' |
| 179 | + | [] -> |
| 180 | + match cs1,cs2 with |
| 181 | + | [],[] -> true |
| 182 | + | [],c2::cs2' -> |
| 183 | + Spec.precond c2 s && |
| 184 | + let s' = Spec.next_state c2 s in |
| 185 | + all_interleavings_ok pref cs1 cs2' s' |
| 186 | + | c1::cs1',[] -> |
| 187 | + Spec.precond c1 s && |
| 188 | + let s' = Spec.next_state c1 s in |
| 189 | + all_interleavings_ok pref cs1' cs2 s' |
| 190 | + | c1::cs1',c2::cs2' -> |
| 191 | + (Spec.precond c1 s && |
| 192 | + let s' = Spec.next_state c1 s in |
| 193 | + all_interleavings_ok pref cs1' cs2 s') |
| 194 | + && |
| 195 | + (Spec.precond c2 s && |
| 196 | + let s' = Spec.next_state c2 s in |
| 197 | + all_interleavings_ok pref cs1 cs2' s') |
| 198 | + |
| 199 | + let rec check_obs pref cs1 cs2 s = |
| 200 | + match pref with |
| 201 | + | p::pref' -> |
| 202 | + let b,s' = check_and_next p s in |
| 203 | + b && check_obs pref' cs1 cs2 s' |
| 204 | + | [] -> |
| 205 | + match cs1,cs2 with |
| 206 | + | [],[] -> true |
| 207 | + | [],p2::cs2' -> |
| 208 | + let b,s' = check_and_next p2 s in |
| 209 | + b && check_obs pref cs1 cs2' s' |
| 210 | + | p1::cs1',[] -> |
| 211 | + let b,s' = check_and_next p1 s in |
| 212 | + b && check_obs pref cs1' cs2 s' |
| 213 | + | p1::cs1',p2::cs2' -> |
| 214 | + (let b1,s' = check_and_next p1 s in |
| 215 | + b1 && check_obs pref cs1' cs2 s') |
| 216 | + || |
| 217 | + (let b2,s' = check_and_next p2 s in |
| 218 | + b2 && check_obs pref cs1 cs2' s') |
| 219 | + |
| 220 | + let gen_cmds_size gen s size_gen = Gen.sized_size size_gen (gen_cmds gen s) |
| 221 | + |
| 222 | + (* Shrinks a single cmd, starting in the given state *) |
| 223 | + let shrink_cmd arb cmd state = |
| 224 | + Option.value (arb state).shrink ~default:Shrink.nil @@ cmd |
| 225 | + |
| 226 | + (* Shrinks cmd lists, starting in the given state *) |
| 227 | + let rec shrink_cmd_list arb cs state = match cs with |
| 228 | + | [] -> Iter.empty |
| 229 | + | c::cs -> |
| 230 | + if Spec.precond c state |
| 231 | + then |
| 232 | + Iter.( |
| 233 | + map (fun c -> c::cs) (shrink_cmd arb c state) |
| 234 | + <+> |
| 235 | + map (fun cs -> c::cs) (shrink_cmd_list arb cs Spec.(next_state c state)) |
| 236 | + ) |
| 237 | + else Iter.empty |
| 238 | + |
| 239 | + (* Shrinks cmd elements in triples *) |
| 240 | + let shrink_triple_elems arb0 arb1 arb2 (seq,p1,p2) = |
| 241 | + let shrink_prefix cs state = |
| 242 | + Iter.map (fun cs -> (cs,p1,p2)) (shrink_cmd_list arb0 cs state) |
| 243 | + in |
| 244 | + let rec shrink_par_suffix cs state = match cs with |
| 245 | + | [] -> |
| 246 | + (* try only one option: p1s or p2s first - both valid interleavings *) |
| 247 | + Iter.(map (fun p1 -> (seq,p1,p2)) (shrink_cmd_list arb1 p1 state) |
| 248 | + <+> |
| 249 | + map (fun p2 -> (seq,p1,p2)) (shrink_cmd_list arb2 p2 state)) |
| 250 | + | c::cs -> |
| 251 | + (* walk seq prefix (again) to advance state *) |
| 252 | + if Spec.precond c state |
| 253 | + then shrink_par_suffix cs Spec.(next_state c state) |
| 254 | + else Iter.empty |
| 255 | + in |
| 256 | + match Spec.(arb_cmd init_state).shrink with |
| 257 | + | None -> Iter.empty (* stop early if no cmd shrinker is available *) |
| 258 | + | Some _ -> |
| 259 | + Iter.(shrink_prefix seq Spec.init_state |
| 260 | + <+> |
| 261 | + shrink_par_suffix seq Spec.init_state) |
| 262 | + |
| 263 | + (* General shrinker of cmd triples *) |
| 264 | + let shrink_triple arb0 arb1 arb2 = |
| 265 | + let open Iter in |
| 266 | + Shrink.filter |
| 267 | + (fun (seq,p1,p2) -> all_interleavings_ok seq p1 p2 Spec.init_state) |
| 268 | + (fun ((seq,p1,p2) as triple) -> |
| 269 | + (* Shrinking heuristic: |
| 270 | + First reduce the cmd list sizes as much as possible, since the interleaving |
| 271 | + is most costly over long cmd lists. *) |
| 272 | + (map (fun seq' -> (seq',p1,p2)) (Shrink.list_spine seq)) |
| 273 | + <+> |
| 274 | + (match p1 with [] -> Iter.empty | c1::c1s -> Iter.return (seq@[c1],c1s,p2)) |
| 275 | + <+> |
| 276 | + (match p2 with [] -> Iter.empty | c2::c2s -> Iter.return (seq@[c2],p1,c2s)) |
| 277 | + <+> |
| 278 | + (map (fun p1' -> (seq,p1',p2)) (Shrink.list_spine p1)) |
| 279 | + <+> |
| 280 | + (map (fun p2' -> (seq,p1,p2')) (Shrink.list_spine p2)) |
| 281 | + <+> |
| 282 | + (* Secondly reduce the cmd data of individual list elements *) |
| 283 | + (shrink_triple_elems arb0 arb1 arb2 triple)) |
| 284 | + |
| 285 | + let arb_triple seq_len par_len arb0 arb1 arb2 = |
| 286 | + let seq_pref_gen = gen_cmds_size arb0 Spec.init_state (Gen.int_bound seq_len) in |
| 287 | + let shrink_triple = shrink_triple arb0 arb1 arb2 in |
| 288 | + let gen_triple = |
| 289 | + Gen.(seq_pref_gen >>= fun seq_pref -> |
| 290 | + int_range 2 (2*par_len) >>= fun dbl_plen -> |
| 291 | + let spawn_state = List.fold_left (fun st c -> Spec.next_state c st) Spec.init_state seq_pref in |
| 292 | + let par_len1 = dbl_plen/2 in |
| 293 | + let par_gen1 = gen_cmds_size arb1 spawn_state (return par_len1) in |
| 294 | + let par_gen2 = gen_cmds_size arb2 spawn_state (return (dbl_plen - par_len1)) in |
| 295 | + triple (return seq_pref) par_gen1 par_gen2) in |
| 296 | + make ~print:(Util.print_triple_vertical Spec.show_cmd) ~shrink:shrink_triple gen_triple |
| 297 | + |
| 298 | + let arb_cmds_par seq_len par_len = arb_triple seq_len par_len Spec.arb_cmd Spec.arb_cmd Spec.arb_cmd |
| 299 | + end |
| 300 | +end |
| 301 | + |
6 | 302 | include Util |
0 commit comments