package hegel

  1. Overview
  2. Docs
Hegel property-based testing library for OCaml

Install

dune-project
 Dependency

Authors

Maintainers

Sources

hegel-0.24.1-opam.tar.gz
md5=d6985126c61aec88003bc03555a0d763
sha512=03e73b752dfb9ea711bc95fd87a777c068a789e7094650c5297fd37ad023292528a6fc69ea88986c02ea3f24c5c24d28353c220fd87595ce047521eecf94e0a8

doc/src/hegel/stateful.ml.html

Source file stateful.ml

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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# 1 "./stateful.ml.in"
(** Stateful property-based testing for Hegel. See [stateful.mli].
    Sequential and concurrent runners have some common code that is
    duplicated, but we have to keep them separate to prevent sequential
    stateful tests needing synchronized state for no reason to satisfy
    the OxCaml mode checker *)


# 27 "./stateful.ml.in"
module Pool_gen = Generators.Int_pool
module Concurrent_pool_gen = Generators.Int_pool_concurrent

module Pool = struct
  type 'a t = 'a Pool_gen.t

  let create ?(clone = Fun.id) tc = Pool_gen.create tc ~clone
  let add = Pool_gen.add
  let size = Pool_gen.size
  let values_reusable t = Pool_gen.pool_values t ~consume:false
  let values_consumed t = Pool_gen.pool_values t ~consume:true
end

module Concurrent_pool = struct
  
# 45 "./stateful.ml.in"
  type 'a t = 'a Concurrent_pool_gen.t

  
# 48 "./stateful.ml.in"
  let create ?(clone = Fun.id) tc = Concurrent_pool_gen.create tc ~clone
  let add = Concurrent_pool_gen.add
  let size = Concurrent_pool_gen.size
  let values_reusable t = Concurrent_pool_gen.pool_values t ~consume:false
  let values_consumed t = Concurrent_pool_gen.pool_values t ~consume:true
end

module Rule = struct
  type 'state t =
    { name : string
    ; weight : float
    ; step : Internal.test_case -> 'state -> unit
    }

  let create ~name ?(weight = 1.0) ~step () = { name; weight; step }
  let name t = t.name
  let weight t = t.weight
end

module Concurrent_rule = struct
  type ('ctx, 'state) t 
# 68 "./stateful.ml.in"
                          
# 68 "./stateful.ml.in"
                                                           =
    { name : string
    ; group : string
    ; weight : float
    ; step :
        (Internal.test_case -> 'ctx 
# 73 "./stateful.ml.in"
                                      
# 73 "./stateful.ml.in"
                                                -> 'state 
# 73 "./stateful.ml.in"
                                                            
# 73 "./stateful.ml.in"
                                                                          -> unit) 
# 73 "./stateful.ml.in"
                                                                                    
    
# 74 "./stateful.ml.in"
    }

  let create ?(group = "<anonymous>") ?(weight = 1.0) ~name ~step () =
    { name; group; weight; step }
  ;;

  let name t = t.name
  let group t = t.group
  let weight t = t.weight
end

module Invariant = struct
  type 'state t =
    { name : string
    ; inv : Internal.test_case -> 'state -> unit
    ; always_check : bool
    }

  let create ~name ~inv ?(always_check = false) () = { name; inv; always_check }
  let name invariant = invariant.name
end

let run_rules tc ~state_machine ~worker_index 
# 96 "./stateful.ml.in"
                                               ~ctx  
# 96 "./stateful.ml.in"
                                                        ~heading ~rule =
  let rec loop 
# 97 "./stateful.ml.in"
                ctx  
# 97 "./stateful.ml.in"
                          rejected =
    match Internal.state_machine_next_rule_for_worker tc ~state_machine ~worker_index with
    | None -> rejected
    | Some rule_index ->
      let name, body = rule rule_index in
      Internal.note tc (heading name);
      (match
         if Internal.should_print tc
         then Internal.with_block tc ~indent:2 (fun tc -> body tc ctx)
         else body tc ctx
       with
       | () -> loop ctx rejected
       | exception Internal.Assume_rejected ->
         Internal.state_machine_rule_rejected_for_worker tc ~state_machine ~worker_index;
         Internal.note tc "Rule stopped early due to violated assumption.";
         loop ctx true)
  in
  loop ctx false
;;

let unique_groups groups =
  List.fold_left
    (fun names group -> if List.mem group names then names else group :: names)
    []
    groups
  |> List.rev
;;

let group_indices groups names =
  List.map (fun group -> List.find_index (String.equal group) names |> Option.get) groups
;;

let dispatch_round 
# 129 "./stateful.ml.in"
                    (concurrency : _ Concurrency.t)  
# 129 "./stateful.ml.in"
                                     tc ~num_workers ~work =
  let cases =
    List.init num_workers (fun worker_index ->
      let worker_tc = Internal.clone tc in
      Internal.set_worker_index worker_tc worker_index;
      worker_tc)
  in
  concurrency.spawn_join_n ~n:num_workers ~f:(fun ctx worker_index ->
    match work ctx ~worker_index (List.nth cases worker_index) with
    | () -> None
    | exception exn -> Some (exn, Printexc.get_raw_backtrace ()))
;;

let reraise_worker_failure (outcomes : Concurrency.outcome list) =
  let find predicate =
    List.find_map
      (function
        | Some (exn, _) as failure when predicate exn -> failure
        | _ -> None)
      outcomes
  in
  let is_control_exception = function
    | Internal.Usage_error _ | Internal.Internal_error _ -> true
    | _ -> false
  in
  let is_overrun = function
    | Internal.Stop_test -> true
    | _ -> false
  in
  let is_invalid = function
    | Internal.Assume_rejected | Internal.Flaky_strategy -> true
    | _ -> false
  in
  let is_test_failure = Fun.const true in
  (* invalidated or exhausted rules can cause in other workers.
     error precedence from greatest to least: usage/internal errors, overrun,
     invalidation, actual test failure. within each category, first worker raises. *)
  let failure =
    List.find_map find [ is_control_exception; is_overrun; is_invalid; is_test_failure ]
  in
  Option.iter
    (fun (exn, backtrace) -> Printexc.raise_with_backtrace exn backtrace)
    failure
;;

let run_machine
      tc
      ~rule_names
      ~rule_groups
      ~rule_weights
      ~invariants
      ~step_count
      ~min_concurrency
      ~max_concurrency
      ~sexp_of_state
      ~init
      run_steps
  =
  let state_machine, num_workers =
    Internal.new_state_machine_with_concurrency
      tc
      ~rule_names
      ~rule_groups
      ~rule_weights
      ~invariant_names:(List.map Invariant.name invariants)
      ~invariants_always_check:
        (List.map (fun inv -> inv.Invariant.always_check) invariants)
      ~step_count
      ~min_concurrency
      ~max_concurrency
  in
  let print_state () =
    Option.iter
      (fun sexp_of -> Internal.print_line tc [ Text "state = "; Value (sexp_of init) ])
      sexp_of_state
  in
  let check_invariants ~where ~sample =
    List.iteri
      (fun i invariant ->
         if
           (not sample)
           || Internal.state_machine_should_check_invariant
                tc
                ~state_machine
                ~invariant_index:i
         then (
           match
             if Internal.should_print tc
             then
               Internal.with_block tc ~indent:2 (fun tc ->
                 invariant.Invariant.inv tc init)
             else invariant.Invariant.inv tc init
           with
           | () -> ()
           | exception e ->
             Internal.note
               tc
               (Printf.sprintf
                  "Invariant %s violated %s."
                  (Invariant.name invariant)
                  where);
             raise e))
      invariants
  in
  (* Can't use Fun.protect because its body must be a global closure *)
  let free () = Internal.state_machine_free tc ~state_machine in
  let run_test () =
    if num_workers > 1
    then Internal.note tc (Printf.sprintf "Concurrency level: %d" num_workers);
    print_state ();
    check_invariants ~where:"in the initial state" ~sample:false;
    run_steps ~state_machine ~num_workers ~print_state ~check_invariants;
    check_invariants ~where:"in the final state" ~sample:false
  in
  match run_test () with
  | () -> free ()
  | exception exn ->
    free ();
    raise exn
;;

let run_internal ~init ~rules ~invariants ?sexp_of_state ?(step_count = 50) tc =
  let run_steps ~state_machine ~num_workers:_ ~print_state ~check_invariants =
    let rec loop steps_attempted =
      Internal.start_span ~label:Generators.Private.Labels.stateful_rule tc;
      match Internal.state_machine_next_group tc ~state_machine with
      | None -> Internal.stop_span tc
      | Some _group ->
        let step = steps_attempted + 1 in
        let rejected =
          match
            run_rules
              tc
              ~state_machine
              ~worker_index:0
              ~ctx:()
              ~heading:(fun name -> Printf.sprintf "Step %d: %s" step name)
              ~rule:(fun rule_index ->
                let rule = List.nth rules rule_index in
                rule.Rule.name, fun tc () -> rule.Rule.step tc init)
          with
          | rejected -> rejected
          | exception e ->
            Internal.stop_span tc;
            raise e
        in
        Internal.stop_span ~discard:rejected tc;
        print_state ();
        check_invariants ~where:(Printf.sprintf "after step %d" step) ~sample:true;
        loop step
    in
    loop 0
  in
  run_machine
    tc
    ~rule_names:(List.map Rule.name rules)
    ~rule_groups:(List.map (fun _ -> 0) rules)
    ~rule_weights:(List.map Rule.weight rules)
    ~invariants
    ~step_count
    ~min_concurrency:1
    ~max_concurrency:1
    ~sexp_of_state
    ~init
    run_steps
;;

module type State_machine = sig
  type state

  val rules : state Rule.t list
  val invariants : state Invariant.t list
end

let run
      (type s)
      ?step_count
      ?sexp_of_state
      tc
      (module M : State_machine with type state = s)
      ~(init : s)
  =
  run_internal ~init ~rules:M.rules ~invariants:M.invariants ?sexp_of_state ?step_count tc
;;

module type Concurrent_state_machine = sig
  type ctx
  type state

  val rules : (ctx, state) Concurrent_rule.t list
  val invariants : state Invariant.t list
end

let run_concurrent_internal
      ~init
      ~rules
      ~invariants
      
# 326 "./stateful.ml.in"
       ~(concurrency : _ Concurrency.t) 
      
# 327 "./stateful.ml.in"
      ?(min_concurrency = 1)
      ?(max_concurrency = min_concurrency)
      ?sexp_of_state
      ?(step_count = 50)
      tc
  =
  let rule_groups = List.map Concurrent_rule.group rules in
  let group_names = unique_groups rule_groups in
  let run_steps ~state_machine ~num_workers ~print_state ~check_invariants =
    let work 
# 336 "./stateful.ml.in"
              ctx  
# 336 "./stateful.ml.in"
                        ~worker_index tc =
      (* returns if a rule was rejected or not, doesn't matter for concurrent *)
      let (_ : bool) =
        run_rules
          tc
          ~state_machine
          ~worker_index
          ~ctx
          ~heading:(fun name -> Printf.sprintf "Rule: %s" name)
          ~rule:(fun rule_index ->
            let rule = List.nth rules rule_index in
            rule.Concurrent_rule.name, fun tc ctx -> rule.Concurrent_rule.step tc ctx init)
      in
      ()
    in
    (* [concurrency] is passed down rather than captured, so it may be local. *)
    let rec loop concurrency round =
      match Internal.state_machine_next_group tc ~state_machine with
      | None -> ()
      | Some group ->
        Internal.note
          tc
          (Printf.sprintf
             "---------------- Round %d: group %S ----------------"
             round
             (List.nth group_names group));
        reraise_worker_failure (dispatch_round concurrency tc ~num_workers ~work);
        print_state ();
        check_invariants ~where:(Printf.sprintf "after round %d" round) ~sample:true;
        loop concurrency (round + 1)
    in
    loop concurrency 1
  in
  run_machine
    tc
    ~rule_names:(List.map Concurrent_rule.name rules)
    ~rule_groups:(group_indices rule_groups group_names)
    ~rule_weights:(List.map Concurrent_rule.weight rules)
    ~invariants
    ~step_count
    ~min_concurrency
    ~max_concurrency
    ~sexp_of_state
    ~init
    run_steps
  
# 381 "./stateful.ml.in"
   
# 382 "./stateful.ml.in"
;;

let run_concurrent
      (type c s)
      ~concurrency
      ?min_concurrency
      ?max_concurrency
      ?step_count
      ?sexp_of_state
      tc
      (module M : Concurrent_state_machine with type ctx = c and type state = s)
      ~(init : s)
  =
  run_concurrent_internal
    ~init
    ~rules:M.rules
    ~invariants:M.invariants
    ~concurrency
    ?min_concurrency
    ?max_concurrency
    ?sexp_of_state
    ?step_count
    tc
;;