hegeltest 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +32 -9
  4. data/Rakefile +81 -0
  5. data/docs/README.md +9 -0
  6. data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
  7. data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
  8. data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
  9. data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
  10. data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
  11. data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
  12. data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
  13. data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
  14. data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
  15. data/docs/architecture.md +11 -6
  16. data/lib/hegel/draw_name.rb +3 -1
  17. data/lib/hegel/generator.rb +50 -9
  18. data/lib/hegel/generators.rb +134 -102
  19. data/lib/hegel/lib_hegel/real.rb +193 -94
  20. data/lib/hegel/lib_hegel.rb +30 -56
  21. data/lib/hegel/libhegel_version.rb +1 -1
  22. data/lib/hegel/report.rb +17 -8
  23. data/lib/hegel/runner.rb +153 -201
  24. data/lib/hegel/settings.rb +9 -14
  25. data/lib/hegel/state_machine.rb +15 -8
  26. data/lib/hegel/stateful/pool.rb +0 -2
  27. data/lib/hegel/stateful.rb +64 -36
  28. data/lib/hegel/syntax/methods.rb +3 -2
  29. data/lib/hegel/test_case.rb +22 -8
  30. data/lib/hegel/version.rb +1 -1
  31. data/lib/hegel.rb +11 -17
  32. data/lib/tasks/libhegel.rake +9 -2
  33. data/sig/hegel.rbs +58 -60
  34. data/skills/hegel-ruby/references/ruby/reference.md +96 -52
  35. metadata +11 -2
@@ -21,64 +21,84 @@ module Hegel
21
21
  module Stateful
22
22
  module_function
23
23
 
24
+ # The span label of one round, which at concurrency 1 runs one rule.
25
+ ROUND_LABEL = LibHegel.label_from_name("hegel-ruby.Hegel::Stateful.round")
26
+
27
+ # hegel-rust, hegel-go, and hegel-java all default a machine's step count
28
+ # to 50. The engine has no default of its own.
29
+ DEFAULT_STEP_COUNT = 50
30
+
24
31
  # +machine+ is a Hegel::StateMachine instance; +tc+ the running
25
- # Hegel::TestCase.
32
+ # Hegel::TestCase. +step_count+ is the most rounds, and so the most
33
+ # rules, one test case runs. It must be at least 1, or the engine
34
+ # rejects it.
26
35
  #
27
36
  # Raises Hegel::Error before making any libhegel call when +machine+
28
37
  # declares no rules: hegel.h documents hegel_new_state_machine's
29
38
  # rule_names as required to be non-empty, and there is nothing useful to
30
39
  # run without one.
31
- def run(machine, tc)
40
+ def run(machine, tc, step_count: DEFAULT_STEP_COUNT)
32
41
  rules = machine.class.rule_definitions
33
42
  raise Hegel::Error, "hegel: #{machine.class} has no rules; declare at least one with `rule`" if rules.empty?
34
43
 
35
44
  invariants = machine.class.invariant_definitions
36
- rule_names = rules.keys
37
- state_machine = tc.new_state_machine(rule_names, invariants.keys)
45
+ state_machine = tc.new_state_machine(
46
+ rules.keys, invariants.keys, invariants.values.map(&:always_run), step_count
47
+ )
38
48
  begin
39
49
  tc.note { "Initial invariant check." }
40
- run_invariants(machine, invariants, tc)
41
- drive(machine, rules, rule_names, invariants, state_machine, tc)
50
+ run_invariants(machine, invariants.values, tc)
51
+ drive(machine, rules, invariants.values, state_machine, tc)
52
+ tc.note { "Final invariant check." }
53
+ run_invariants(machine, invariants.values, tc)
42
54
  ensure
43
55
  tc.state_machine_free(state_machine)
44
56
  end
45
57
  end
46
58
 
47
- # Repeatedly asks +state_machine+ for the next rule to run and applies
48
- # it, until libhegel reports the step budget for this test case spent.
49
- #
50
- # Measured against libhegel 0.32.5, unseeded, on the capacity-2 stack
51
- # shrink-quality test below (test/hegel/test_stateful.rb,
52
- # test_stateful_run_shrinks_to_the_minimal_step_count_that_breaks_the_
53
- # invariant): closing the HEGEL_LABEL_STATEFUL_RULE span with
54
- # stop_span(discard: false) on the DONE branch, right before breaking
55
- # out of this loop, and leaving it open instead (matching hegel-rust's
56
- # own `run`, which never closes that last span) both shrink the same
57
- # failure to the same 3-step counterexample every time, 20 runs each.
58
- # This keeps hegel-rust's own choice -- an unclosed span at DONE --
59
- # since nothing measured favours the extra stop_span call.
60
- def drive(machine, rules, rule_names, invariants, state_machine, tc)
59
+ # Runs rounds until libhegel ends the machine. Each round asks for a
60
+ # group first, which the header requires before the first rule too, then
61
+ # pulls rules until the round ends. Between rounds, each invariant runs
62
+ # only when the engine samples it (see
63
+ # Hegel::TestCase#state_machine_should_check_invariant), as hegel-rust,
64
+ # hegel-go, and hegel-java do. The initial and final checks in #run are
65
+ # the ones that always happen.
66
+ def drive(machine, rules, invariants, state_machine, tc)
67
+ rule_names = rules.keys
61
68
  steps_attempted = 0
62
69
  loop do
63
- tc.start_span(LibHegel::HEGEL_LABEL_STATEFUL_RULE)
64
- rule_index = tc.state_machine_next_rule(state_machine)
65
- break if rule_index == LibHegel::HEGEL_STATE_MACHINE_DONE
70
+ tc.start_span(ROUND_LABEL)
71
+ if tc.state_machine_next_group(state_machine) == LibHegel::HEGEL_STATE_MACHINE_DONE
72
+ tc.stop_span(discard: false)
73
+ break
74
+ end
75
+
76
+ rejected = false
77
+ loop do
78
+ rule_index = tc.state_machine_next_rule(state_machine)
79
+ break if rule_index == LibHegel::HEGEL_STATE_MACHINE_DONE
66
80
 
67
- name = rule_names[rule_index]
68
- steps_attempted += 1
69
- tc.note { "Step #{steps_attempted}: #{name}" }
70
- apply_rule(machine, rules.fetch(name), invariants, state_machine, tc)
81
+ name = rule_names[rule_index]
82
+ steps_attempted += 1
83
+ tc.note { "Step #{steps_attempted}: #{name}" }
84
+ rejected = true unless apply_rule(machine, rules.fetch(name), state_machine, tc)
85
+ end
86
+ tc.stop_span(discard: rejected)
87
+ run_sampled_invariants(machine, invariants, state_machine, tc)
71
88
  end
72
89
  end
73
90
 
91
+ # Returns false when the rule stopped on a failed assumption, and true
92
+ # when it completed.
93
+ #
74
94
  # standard:disable Lint/RescueException -- deliberate, the same reason
75
95
  # Hegel::Runner.classify's own `rescue Exception` is: Hegel::AssumeFailed
76
96
  # and Hegel::StopTest both descend from Exception, not StandardError, so
77
97
  # only `rescue Exception` sees every path a rule can take. Every branch
78
98
  # other than AssumeFailed re-raises what it caught unchanged -- this
79
99
  # never reclassifies an exception or swallows one, it only guarantees
80
- # the span closes first, so a half-applied rule is never left mid-span
81
- # when the exception unwinds past this method.
100
+ # the round's span closes first, so a half-applied rule is never left
101
+ # mid-span when the exception unwinds past this method.
82
102
  #
83
103
  # Hegel::FATAL_EXCEPTIONS goes first and closes no span. They say the
84
104
  # process is ending, so the span has no reader left to matter to, and
@@ -89,32 +109,40 @@ module Hegel
89
109
  #
90
110
  # tc.assume(false) inside a rule is not the same event as one raised
91
111
  # directly inside a Hegel.test block: it rejects only this rule (told to
92
- # libhegel via #state_machine_rule_rejected, so the rejected attempt
112
+ # libhegel via #state_machine_rule_rejected, so the rejected round
93
113
  # does not count toward the step budget) and the loop keeps going, where
94
114
  # Hegel::Runner.classify's own AssumeFailed handling discards the whole
95
115
  # test case. Hegel::Runner.classify never sees this one: it is caught
96
116
  # and handled right here.
97
- def apply_rule(machine, block, invariants, state_machine, tc)
117
+ def apply_rule(machine, block, state_machine, tc)
98
118
  machine.instance_exec(tc, &block)
119
+ true
99
120
  rescue *Hegel::FATAL_EXCEPTIONS
100
121
  raise
101
122
  rescue Hegel::AssumeFailed
102
123
  tc.state_machine_rule_rejected(state_machine)
103
- tc.stop_span(discard: true)
104
124
  tc.note { "Rule stopped early due to violated assumption." }
125
+ false
105
126
  rescue Exception
106
127
  tc.stop_span(discard: false)
107
128
  raise
108
- else
109
- tc.stop_span(discard: false)
110
- run_invariants(machine, invariants, tc)
111
129
  end
112
130
  # standard:enable Lint/RescueException
113
131
 
114
132
  # Runs every invariant, in declaration order, via #instance_exec -- same
115
133
  # argument contract as a rule block (Hegel::StateMachine.invariant).
116
134
  def run_invariants(machine, invariants, tc)
117
- invariants.each_value { |block| machine.instance_exec(tc, &block) }
135
+ invariants.each { |invariant| machine.instance_exec(tc, &invariant.block) }
136
+ end
137
+
138
+ # Runs each invariant the engine samples at this join point, in
139
+ # declaration order.
140
+ def run_sampled_invariants(machine, invariants, state_machine, tc)
141
+ invariants.each_with_index do |invariant, index|
142
+ next unless tc.state_machine_should_check_invariant(state_machine, index)
143
+
144
+ machine.instance_exec(tc, &invariant.block)
145
+ end
118
146
  end
119
147
  end
120
148
  end
@@ -25,8 +25,9 @@ module Hegel
25
25
  end
26
26
 
27
27
  # A double in [min_value, max_value]. allow_nan and allow_infinity
28
- # both default to false.
29
- def floats(min_value: nil, max_value: nil, allow_nan: false, allow_infinity: false, exclude_min: false,
28
+ # default to what the bounds imply: NaN when the caller passed
29
+ # neither bound, an infinity when the caller left either side open.
30
+ def floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false,
30
31
  exclude_max: false)
31
32
  Generators::FloatGenerator.new(
32
33
  min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
@@ -172,6 +172,9 @@ module Hegel
172
172
  # BooleanGenerator's own primitive; #draw_boolean is this plus
173
173
  # recording.
174
174
  def generate_boolean(p = 0.5)
175
+ # has_forced is false, so the engine ignores the forced argument's value.
176
+ # .mutineer.yml ignores that mutant by id, since a line marker would also
177
+ # hide the killed mutant of has_forced.
175
178
  @impl.generate_boolean(@ctx, @handle, p, false, false)
176
179
  end
177
180
 
@@ -220,8 +223,8 @@ module Hegel
220
223
  @impl.target(@ctx, @handle, value, label)
221
224
  end
222
225
 
223
- # hegel_start_span, labelled with one of the Hegel::LibHegel::
224
- # HEGEL_LABEL_* constants. Every compound Hegel::Generator (map, filter,
226
+ # hegel_start_span, labelled with a generator's label (see
227
+ # Hegel::Generator#label). Every compound Hegel::Generator (map, filter,
225
228
  # arrays) opens one of these around its own draw; see #stop_span.
226
229
  def start_span(label)
227
230
  @impl.start_span(@ctx, @handle, label)
@@ -428,25 +431,36 @@ module Hegel
428
431
  # here rather than on that module, the same reason every other native
429
432
  # call is a method on this class: Hegel::Stateful never touches @impl or
430
433
  # @ctx directly, only this handle-scoped surface.
431
- def new_state_machine(rule_names, invariant_names)
432
- @impl.new_state_machine(@ctx, @handle, rule_names, invariant_names)
434
+ def new_state_machine(rule_names, invariant_names, always_check, step_count)
435
+ @impl.new_state_machine(@ctx, @handle, rule_names, invariant_names, always_check, step_count)
436
+ end
437
+
438
+ # hegel_state_machine_next_group: starts the next round, or returns
439
+ # LibHegel::HEGEL_STATE_MACHINE_DONE once the machine is done.
440
+ def state_machine_next_group(state_machine)
441
+ @impl.state_machine_next_group(@ctx, @handle, state_machine)
433
442
  end
434
443
 
435
444
  # hegel_state_machine_next_rule: the index (into the +rule_names+
436
- # #new_state_machine was given) of the next rule to run, or
437
- # LibHegel::HEGEL_STATE_MACHINE_DONE once this test case's step budget
438
- # is spent.
445
+ # #new_state_machine was given) of the next rule to run this round, or
446
+ # LibHegel::HEGEL_STATE_MACHINE_DONE once the round is over.
439
447
  def state_machine_next_rule(state_machine)
440
448
  @impl.state_machine_next_rule(@ctx, @handle, state_machine)
441
449
  end
442
450
 
443
451
  # hegel_state_machine_rule_rejected: tells libhegel the rule most
444
452
  # recently returned by #state_machine_next_rule stopped early on a
445
- # failed assumption, so it does not count toward the step budget.
453
+ # failed assumption, so its round does not count toward the step budget.
446
454
  def state_machine_rule_rejected(state_machine)
447
455
  @impl.state_machine_rule_rejected(@ctx, @handle, state_machine)
448
456
  end
449
457
 
458
+ # hegel_state_machine_should_check_invariant: whether to run the
459
+ # invariant at +index+ at this join point.
460
+ def state_machine_should_check_invariant(state_machine, index)
461
+ @impl.state_machine_should_check_invariant(@ctx, @handle, state_machine, index)
462
+ end
463
+
450
464
  # hegel_state_machine_free. Takes no test-case handle, unlike every
451
465
  # other #state_machine_* method here: the header documents a
452
466
  # state-machine handle as freeable through any handle of the same
data/lib/hegel/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hegel
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/hegel.rb CHANGED
@@ -31,11 +31,11 @@ module Hegel
31
31
  # +test_cases+, +seed+, +derandomize+, +verbosity+, +phases+, and
32
32
  # +suppress_health_check+ all default to nil, which means the same thing
33
33
  # for each of them: do not call the matching libhegel setter, and let the
34
- # engine's own default apply instead. See Hegel::Settings for the
35
- # keyword-to-setter mapping, the verbosity Symbols it accepts, and the
36
- # +phases+/+suppress_health_check+ Symbols each of those two accepts (as
37
- # an Array; an empty Array raises Hegel::Error rather than silently
38
- # meaning "none"). +verbosity: :quiet+ also silences the failure report
34
+ # engine's resolved settings profile apply instead (see docs/adr/0023).
35
+ # See Hegel::Settings for the keyword-to-setter mapping, the verbosity
36
+ # Symbols it accepts, and the +phases+/+suppress_health_check+ Symbols
37
+ # each of those two accepts (as an Array; an empty Array raises
38
+ # Hegel::Error rather than silently meaning "none"). +verbosity: :quiet+ also silences the failure report
39
39
  # itself, not just libhegel's own progress output.
40
40
  #
41
41
  # +database+ and +database_key+ opt a run into libhegel's example
@@ -51,14 +51,8 @@ module Hegel
51
51
  # behind it.
52
52
  #
53
53
  # +report_multiple_failures+ defaults to false, not nil, unlike every
54
- # keyword above: see Hegel::Runner.run's own comment for why departing
55
- # from libhegel's own default (true) is itself the decision here.
56
- #
57
- # +stateful_step_count+ bounds how many rules a Hegel::Stateful.run call
58
- # applies per test case; left nil (the default) leaves libhegel's own
59
- # default of 50 in place. hegel.h documents it as needing to be at least
60
- # 1; like +tc.target+'s label, that requirement is left to the engine
61
- # rather than re-checked here.
54
+ # keyword above: see Hegel::Runner.run's own comment for why it is always
55
+ # passed.
62
56
  #
63
57
  # +output+ (default $stderr) is where a failure report is written; a
64
58
  # caller passes its own IO to capture that report instead (tests do).
@@ -73,12 +67,12 @@ module Hegel
73
67
  # (#default_impl) only runs when +impl+ is not given, so a test that does
74
68
  # pass one never opens the native library at all.
75
69
  def test(test_cases: nil, seed: nil, derandomize: nil, verbosity: nil, database: nil, database_key: nil,
76
- phases: nil, suppress_health_check: nil, report_multiple_failures: false, stateful_step_count: nil,
77
- output: $stderr, reproduce_failure: nil, impl: default_impl, &block)
70
+ phases: nil, suppress_health_check: nil, report_multiple_failures: false, output: $stderr,
71
+ reproduce_failure: nil, impl: default_impl, &block)
78
72
  Runner.run(impl: impl, test_cases: test_cases, seed: seed, derandomize: derandomize, verbosity: verbosity,
79
73
  database: database, database_key: database_key, phases: phases, suppress_health_check: suppress_health_check,
80
- report_multiple_failures: report_multiple_failures, stateful_step_count: stateful_step_count, output: output,
81
- reproduce_failure: reproduce_failure, &block)
74
+ report_multiple_failures: report_multiple_failures, output: output, reproduce_failure: reproduce_failure,
75
+ &block)
82
76
  end
83
77
 
84
78
  # The Hegel::LibHegel::Real instance #test uses by default, built once and
@@ -18,6 +18,13 @@ module Hegel
18
18
 
19
19
  module_function
20
20
 
21
+ # Since libhegel 0.42.1, hegel-rust tags an engine release
22
+ # libhegel-v<version>. The plain v<version> tags belong to its hegeltest
23
+ # crate, whose version numbers differ from the engine's.
24
+ def release_base(version)
25
+ "#{RELEASE_BASE}/libhegel-v#{version}"
26
+ end
27
+
21
28
  # Fetches the host's pinned asset into <root>/<version>/<asset>,
22
29
  # skipping the download if it is already there. `downloader` is
23
30
  # injectable so the orchestration (skip-if-present, checksum parsing,
@@ -29,7 +36,7 @@ module Hegel
29
36
  dest = File.join(root, version, asset)
30
37
  return dest if File.file?(dest)
31
38
 
32
- base = "#{RELEASE_BASE}/v#{version}"
39
+ base = release_base(version)
33
40
  bytes = downloader.call("#{base}/#{asset}")
34
41
  checksum_line = downloader.call("#{base}/#{asset}.sha256")
35
42
  verify_and_install(bytes, expected_sha256(checksum_line), dest)
@@ -66,7 +73,7 @@ module Hegel
66
73
  # each published platform just to have asset_name recompute the asset
67
74
  # name it already is.
68
75
  def fetch_all_assets(version: Hegel::LIBHEGEL_VERSION, root: DEFAULT_ROOT, downloader: method(:http_get))
69
- base = "#{RELEASE_BASE}/v#{version}"
76
+ base = release_base(version)
70
77
  Hegel::Locate::ASSET_NAMES.values.map do |asset|
71
78
  dest = File.join(root, version, asset)
72
79
  next dest if File.file?(dest)
data/sig/hegel.rbs CHANGED
@@ -12,7 +12,7 @@ module Hegel
12
12
  class AssumeFailed < Exception
13
13
  end
14
14
 
15
- def self.test: (?test_cases: Integer?, ?seed: Integer?, ?derandomize: bool?, ?verbosity: Symbol?, ?database: String?, ?database_key: String?, ?phases: Array[Symbol]?, ?suppress_health_check: Array[Symbol]?, ?report_multiple_failures: bool, ?stateful_step_count: Integer?, ?output: untyped, ?reproduce_failure: String?, ?impl: untyped) { (TestCase) -> void } -> nil
15
+ def self.test: (?test_cases: Integer?, ?seed: Integer?, ?derandomize: bool?, ?verbosity: Symbol?, ?database: String?, ?database_key: String?, ?phases: Array[Symbol]?, ?suppress_health_check: Array[Symbol]?, ?report_multiple_failures: bool, ?output: untyped, ?reproduce_failure: String?, ?impl: untyped) { (TestCase) -> void } -> nil
16
16
  def self.default_impl: () -> untyped
17
17
 
18
18
  class TestCase
@@ -62,14 +62,19 @@ module Hegel
62
62
  def pool_generate: (untyped pool, bool consume) -> Integer
63
63
  def pool_free: (untyped pool) -> nil
64
64
  def free_pools: () -> void
65
- def new_state_machine: (Array[String] rule_names, Array[String] invariant_names) -> untyped
65
+ def new_state_machine: (Array[String] rule_names, Array[String] invariant_names, Array[bool] always_check, Integer step_count) -> untyped
66
+ def state_machine_next_group: (untyped state_machine) -> Integer
66
67
  def state_machine_next_rule: (untyped state_machine) -> Integer
67
68
  def state_machine_rule_rejected: (untyped state_machine) -> nil
69
+ def state_machine_should_check_invariant: (untyped state_machine, Integer index) -> bool
68
70
  def state_machine_free: (untyped state_machine) -> nil
69
71
  end
70
72
 
71
73
  class Generator
74
+ def self.label: () -> Integer
75
+ def self.label_of: (untyped part) -> Integer
72
76
  def do_draw: (untyped tc) -> untyped
77
+ def label: () -> Integer
73
78
  def map: () { (untyped) -> untyped } -> Generator
74
79
  def filter: () { (untyped) -> boolish } -> Generator
75
80
 
@@ -87,6 +92,9 @@ module Hegel
87
92
  end
88
93
 
89
94
  module Generators
95
+ def self.first_nanosecond: (Array[Integer] parts) -> Array[Integer]
96
+ def self.last_nanosecond: (Array[Integer] parts) -> Array[Integer]
97
+
90
98
  class BooleanGenerator < Generator
91
99
  def initialize: (p: Float) -> void
92
100
  def do_draw: (untyped tc) -> bool
@@ -103,7 +111,7 @@ module Hegel
103
111
  class FloatGenerator < Generator
104
112
  WIDTH: Integer
105
113
 
106
- def initialize: (min_value: Float?, max_value: Float?, allow_nan: bool, allow_infinity: bool, exclude_min: bool, exclude_max: bool) -> void
114
+ def initialize: (min_value: Float?, max_value: Float?, allow_nan: bool?, allow_infinity: bool?, exclude_min: bool, exclude_max: bool) -> void
107
115
  def do_draw: (untyped tc) -> Float
108
116
  end
109
117
 
@@ -158,6 +166,7 @@ module Hegel
158
166
  end
159
167
 
160
168
  class HashGenerator < Generator
169
+ ENTRY_LABEL: Integer
161
170
  def initialize: (Generator keys, Generator values, min_size: Integer, max_size: Integer?) -> void
162
171
  def do_draw: (untyped tc) -> Hash[untyped, untyped]
163
172
 
@@ -263,16 +272,26 @@ module Hegel
263
272
  include Syntax::Methods
264
273
 
265
274
  def self.rule: (Symbol | String name) { (TestCase) -> untyped } -> void
266
- def self.invariant: (Symbol | String name) { (TestCase) -> untyped } -> void
275
+ def self.invariant: (Symbol | String name, ?always_run: bool) { (TestCase) -> untyped } -> void
267
276
  def self.rule_definitions: () -> Hash[String, Proc]
268
- def self.invariant_definitions: () -> Hash[String, Proc]
277
+ def self.invariant_definitions: () -> Hash[String, Invariant]
278
+
279
+ class Invariant
280
+ attr_reader block: Proc
281
+ attr_reader always_run: bool
282
+ def initialize: (block: Proc, always_run: bool) -> void
283
+ end
269
284
  end
270
285
 
271
286
  module Stateful
272
- def self.run: (StateMachine machine, TestCase tc) -> void
273
- def self.drive: (StateMachine machine, Hash[String, Proc] rules, Array[String] rule_names, Hash[String, Proc] invariants, untyped state_machine, TestCase tc) -> void
274
- def self.apply_rule: (StateMachine machine, Proc block, Hash[String, Proc] invariants, untyped state_machine, TestCase tc) -> void
275
- def self.run_invariants: (StateMachine machine, Hash[String, Proc] invariants, TestCase tc) -> void
287
+ ROUND_LABEL: Integer
288
+ DEFAULT_STEP_COUNT: Integer
289
+
290
+ def self.run: (StateMachine machine, TestCase tc, ?step_count: Integer) -> void
291
+ def self.drive: (StateMachine machine, Hash[String, Proc] rules, Array[StateMachine::Invariant] invariants, untyped state_machine, TestCase tc) -> void
292
+ def self.apply_rule: (StateMachine machine, Proc block, untyped state_machine, TestCase tc) -> bool
293
+ def self.run_invariants: (StateMachine machine, Array[StateMachine::Invariant] invariants, TestCase tc) -> void
294
+ def self.run_sampled_invariants: (StateMachine machine, Array[StateMachine::Invariant] invariants, untyped state_machine, TestCase tc) -> void
276
295
 
277
296
  class Pool
278
297
  def initialize: (TestCase tc) -> void
@@ -298,7 +317,7 @@ module Hegel
298
317
  module Methods
299
318
  def booleans: (?p: Float) -> Generators::BooleanGenerator
300
319
  def integers: (?min_value: Integer?, ?max_value: Integer?) -> Generators::IntegerGenerator
301
- def floats: (?min_value: Float?, ?max_value: Float?, ?allow_nan: bool, ?allow_infinity: bool, ?exclude_min: bool, ?exclude_max: bool) -> Generators::FloatGenerator
320
+ def floats: (?min_value: Float?, ?max_value: Float?, ?allow_nan: bool?, ?allow_infinity: bool?, ?exclude_min: bool, ?exclude_max: bool) -> Generators::FloatGenerator
302
321
  def text: (?min_size: Integer, ?max_size: Integer?, ?codec: String?, ?min_codepoint: Integer?, ?max_codepoint: Integer?) -> Generators::TextGenerator
303
322
  def arrays: (Generator elements, ?min_size: Integer, ?max_size: Integer?) -> Generators::ArrayGenerator
304
323
  def just: (untyped value) -> Generators::JustGenerator
@@ -338,11 +357,12 @@ module Hegel
338
357
 
339
358
  module Report
340
359
  class Failure
341
- def initialize: (test_cases: Integer, discarded: Integer, entries: Array[[:draw, String, untyped] | [:note, untyped]], blob: String) -> void
360
+ def initialize: (test_cases: Integer, discarded: Integer, entries: Array[[:draw, String, untyped] | [:note, untyped]], blob: String?, caveat: String?) -> void
342
361
  def test_cases: () -> Integer
343
362
  def discarded: () -> Integer
344
363
  def entries: () -> Array[[:draw, String, untyped] | [:note, untyped]]
345
- def blob: () -> String
364
+ def blob: () -> String?
365
+ def caveat: () -> String?
346
366
  end
347
367
 
348
368
  def self.assign_names: (Array[[:draw, String, untyped] | [:note, untyped]] entries) -> Array[[:draw, String, untyped] | [:note, untyped]]
@@ -355,7 +375,7 @@ module Hegel
355
375
  PHASE_CODES: Hash[Symbol, Integer]
356
376
  HEALTH_CHECK_CODES: Hash[Symbol, Integer]
357
377
 
358
- def self.apply: (untyped impl, untyped ctx, untyped settings, test_cases: Integer?, seed: Integer?, derandomize: bool?, verbosity: Symbol?, database: String?, database_key: String?, phases: Array[Symbol]?, suppress_health_check: Array[Symbol]?, report_multiple_failures: bool, stateful_step_count: Integer?) -> void
378
+ def self.apply: (untyped impl, untyped ctx, untyped settings, test_cases: Integer?, seed: Integer?, derandomize: bool?, verbosity: Symbol?, database: String?, database_key: String?, phases: Array[Symbol]?, suppress_health_check: Array[Symbol]?, report_multiple_failures: bool) -> void
359
379
  def self.apply_verbosity: (untyped impl, untyped ctx, untyped settings, Symbol verbosity) -> void
360
380
  def self.apply_database: (untyped impl, untyped ctx, untyped settings, database: String?, database_key: String?) -> void
361
381
  def self.apply_phases: (untyped impl, untyped ctx, untyped settings, Array[Symbol] phases) -> void
@@ -371,25 +391,31 @@ module Hegel
371
391
  INSTALLED_GEM_DIRS: Array[String]
372
392
  STDLIB_DIR: String
373
393
 
394
+ class Capture
395
+ attr_reader exception: Exception
396
+ attr_reader entries: Array[[:draw, String, untyped] | [:note, untyped]]?
397
+ def initialize: (Exception exception, Array[[:draw, String, untyped] | [:note, untyped]]? entries) -> void
398
+ end
399
+
374
400
  class GenerationStats
375
401
  def initialize: () -> void
376
402
  def record: (Integer status, String? origin) -> void
377
403
  def for: (String? origin) -> [Integer, Integer]
378
404
  end
379
405
 
380
- def self.run: (impl: untyped, ?test_cases: Integer?, ?seed: Integer?, ?derandomize: bool?, ?verbosity: Symbol?, ?database: String?, ?database_key: String?, ?phases: Array[Symbol]?, ?suppress_health_check: Array[Symbol]?, ?report_multiple_failures: bool, ?stateful_step_count: Integer?, ?output: untyped, ?reproduce_failure: String?) { (TestCase) -> void } -> nil
381
- def self.run_and_finish: (untyped impl, untyped ctx, untyped settings, quiet: bool, output: untyped) { (TestCase) -> void } -> nil
382
- def self.drive: (untyped impl, untyped ctx, untyped run, GenerationStats stats) { (TestCase) -> void } -> void
383
- def self.with_test_case: [R] (untyped impl, untyped ctx, untyped tc, ?record: bool) { (TestCase) -> R } -> R
384
- def self.run_case: (untyped impl, untyped ctx, untyped tc, GenerationStats stats) { (TestCase) -> void } -> void
385
- def self.finish: (untyped impl, untyped ctx, untyped settings, untyped result, GenerationStats stats, quiet: bool, output: untyped) { (TestCase) -> void } -> nil
386
- def self.replay: (untyped impl, untyped ctx, untyped settings, untyped result, GenerationStats stats, quiet: bool, output: untyped) { (TestCase) -> void } -> bot
387
- def self.replay_failure: (untyped impl, untyped ctx, untyped settings, untyped failure, Integer index, GenerationStats stats) { (TestCase) -> void } -> [Exception, Report::Failure]
388
- def self.reproduce: (untyped impl, untyped ctx, untyped settings, String blob, quiet: bool, output: untyped) { (TestCase) -> void } -> bot
406
+ def self.run: (impl: untyped, ?test_cases: Integer?, ?seed: Integer?, ?derandomize: bool?, ?verbosity: Symbol?, ?database: String?, ?database_key: String?, ?phases: Array[Symbol]?, ?suppress_health_check: Array[Symbol]?, ?report_multiple_failures: bool, ?output: untyped, ?reproduce_failure: String?) { (TestCase) -> void } -> nil
407
+ def self.run_and_finish: (untyped impl, untyped ctx, untyped run, reproducing: bool, quiet: bool, output: untyped) { (TestCase) -> void } -> nil
408
+ def self.drive: (untyped impl, untyped ctx, untyped run, GenerationStats stats, Hash[String, Capture] captures) { (TestCase) -> void } -> void
409
+ def self.with_test_case: [R] (untyped impl, untyped ctx, untyped tc) { (TestCase) -> R } -> R
410
+ def self.run_case: (untyped impl, untyped ctx, untyped tc, GenerationStats stats, Hash[String, Capture] captures) { (TestCase) -> void } -> void
411
+ def self.keep_capture: (Hash[String, Capture] captures, String origin, Exception exception, Array[[:draw, String, untyped] | [:note, untyped]]? entries) -> void
412
+ def self.finish: (untyped impl, untyped ctx, untyped result, GenerationStats stats, Hash[String, Capture] captures, reproducing: bool, quiet: bool, output: untyped) -> nil
413
+ def self.report_failures: (untyped impl, untyped ctx, untyped result, GenerationStats stats, Hash[String, Capture] captures, quiet: bool, output: untyped) -> bot
414
+ def self.failure_report: (untyped impl, untyped ctx, untyped failure, GenerationStats stats, Hash[String, Capture] captures) -> [Exception, Report::Failure]
389
415
  def self.classify: (TestCase test_case) { (TestCase) -> void } -> [Integer, String?, Exception?, Array[[:draw, String, untyped] | [:note, untyped]]?]
390
416
  def self.origin_for: (Exception exception) -> String
391
417
  def self.infrastructure?: (String path) -> bool
392
- def self.flaky_message: () -> String
418
+ def self.not_reproduced_message: () -> String
393
419
  def self.multiple_failures_message: (Integer count) -> String
394
420
  end
395
421
 
@@ -433,8 +459,8 @@ module Hegel
433
459
  HEGEL_RUN_STATUS_PASSED: Integer
434
460
  HEGEL_RUN_STATUS_FAILED: Integer
435
461
  HEGEL_RUN_STATUS_ERROR: Integer
436
- HEGEL_VERBOSITY_QUIET: Integer
437
462
  HEGEL_VERBOSITY_NORMAL: Integer
463
+ HEGEL_VERBOSITY_QUIET: Integer
438
464
  HEGEL_VERBOSITY_VERBOSE: Integer
439
465
  HEGEL_VERBOSITY_DEBUG: Integer
440
466
  HEGEL_PHASE_EXPLICIT: Integer
@@ -447,39 +473,6 @@ module Hegel
447
473
  HEGEL_HC_TOO_SLOW: Integer
448
474
  HEGEL_HC_TEST_CASES_TOO_LARGE: Integer
449
475
  HEGEL_HC_LARGE_INITIAL_TEST_CASE: Integer
450
- HEGEL_LABEL_LIST: Integer
451
- HEGEL_LABEL_LIST_ELEMENT: Integer
452
- HEGEL_LABEL_SET: Integer
453
- HEGEL_LABEL_SET_ELEMENT: Integer
454
- HEGEL_LABEL_MAP: Integer
455
- HEGEL_LABEL_MAP_ENTRY: Integer
456
- HEGEL_LABEL_TUPLE: Integer
457
- HEGEL_LABEL_ONE_OF: Integer
458
- HEGEL_LABEL_OPTIONAL: Integer
459
- HEGEL_LABEL_FIXED_DICT: Integer
460
- HEGEL_LABEL_FLAT_MAP: Integer
461
- HEGEL_LABEL_FILTER: Integer
462
- HEGEL_LABEL_MAPPED: Integer
463
- HEGEL_LABEL_SAMPLED_FROM: Integer
464
- HEGEL_LABEL_ENUM_VARIANT: Integer
465
- HEGEL_LABEL_FEATURE_FLAG: Integer
466
- HEGEL_LABEL_REGEX: Integer
467
- HEGEL_LABEL_EMAIL: Integer
468
- HEGEL_LABEL_URL: Integer
469
- HEGEL_LABEL_DOMAIN: Integer
470
- HEGEL_LABEL_DATE: Integer
471
- HEGEL_LABEL_TIME: Integer
472
- HEGEL_LABEL_DATETIME: Integer
473
- HEGEL_LABEL_UUID: Integer
474
- HEGEL_LABEL_IP_ADDRESS: Integer
475
- HEGEL_LABEL_INTEGER: Integer
476
- HEGEL_LABEL_FLOAT: Integer
477
- HEGEL_LABEL_BOOLEAN: Integer
478
- HEGEL_LABEL_BYTES: Integer
479
- HEGEL_LABEL_STRING: Integer
480
- HEGEL_LABEL_STATEFUL_RULE: Integer
481
- HEGEL_LABEL_FRESH_ID: Integer
482
- HEGEL_LABEL_SET_CHOICE: Integer
483
476
  HEGEL_STATE_MACHINE_DONE: Integer
484
477
  HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED: Integer
485
478
  HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED: Float
@@ -488,6 +481,8 @@ module Hegel
488
481
  def self.with_context: [R] (untyped impl) { (untyped) -> R } -> R
489
482
  def self.check!: (untyped impl, untyped ctx, Integer code) -> void
490
483
  def self.warn_on_version_mismatch: (untyped impl, untyped ctx, ?io: untyped) -> void
484
+ def self.label_from_name: (String name) -> Integer
485
+ def self.label_combine: (Array[Integer] labels) -> Integer
491
486
  def self.encode_integer_le: (Integer n) -> String
492
487
  def self.decode_integer_le: (String bytes) -> Integer
493
488
 
@@ -504,12 +499,13 @@ module Hegel
504
499
  def settings_set_seed: (untyped ctx, untyped s, Integer seed, bool has_seed) -> nil
505
500
  def settings_set_derandomize: (untyped ctx, untyped s, bool derandomize) -> nil
506
501
  def settings_set_database: (untyped ctx, untyped s, String? database) -> nil
507
- def settings_set_stateful_step_count: (untyped ctx, untyped s, Integer n) -> nil
508
502
  def settings_set_report_multiple_failures: (untyped ctx, untyped s, bool yes) -> nil
509
503
  def settings_set_database_key: (untyped ctx, untyped s, String? key) -> nil
510
504
  def settings_set_phases: (untyped ctx, untyped s, Integer phases) -> nil
511
505
  def settings_set_suppress_health_check: (untyped ctx, untyped s, Integer checks) -> nil
512
506
  def run_start: (untyped ctx, untyped settings) -> untyped
507
+ def run_start_blob: (untyped ctx, untyped settings, String blob) -> untyped
508
+ def test_case_should_capture: (untyped ctx, untyped tc) -> bool
513
509
  def next_test_case: (untyped ctx, untyped run) -> untyped?
514
510
  def run_free: (untyped ctx, untyped run) -> nil
515
511
  def test_case_free: (untyped ctx, untyped tc) -> nil
@@ -527,7 +523,7 @@ module Hegel
527
523
  def failure_free: (untyped ctx, untyped f) -> nil
528
524
  def failure_origin: (untyped ctx, untyped f) -> String
529
525
  def failure_reproduction_blob: (untyped ctx, untyped f) -> String?
530
- def test_case_from_blob: (untyped ctx, untyped settings, String blob) -> untyped
526
+ def failure_caveat: (untyped ctx, untyped f) -> String?
531
527
  def start_span: (untyped ctx, untyped tc, Integer label) -> nil
532
528
  def stop_span: (untyped ctx, untyped tc, bool discard) -> nil
533
529
  def new_collection: (untyped ctx, untyped tc, Integer min_size, Integer max_size) -> untyped
@@ -538,9 +534,11 @@ module Hegel
538
534
  def pool_add: (untyped ctx, untyped tc, untyped pool) -> Integer
539
535
  def pool_generate: (untyped ctx, untyped tc, untyped pool, bool consume) -> Integer
540
536
  def pool_free: (untyped ctx, untyped pool) -> nil
541
- def new_state_machine: (untyped ctx, untyped tc, Array[String] rule_names, Array[String] invariant_names) -> untyped
537
+ def new_state_machine: (untyped ctx, untyped tc, Array[String] rule_names, Array[String] invariant_names, Array[bool] always_check, Integer step_count) -> untyped
538
+ def state_machine_next_group: (untyped ctx, untyped tc, untyped state_machine) -> Integer
542
539
  def state_machine_next_rule: (untyped ctx, untyped tc, untyped state_machine) -> Integer
543
540
  def state_machine_rule_rejected: (untyped ctx, untyped tc, untyped state_machine) -> nil
541
+ def state_machine_should_check_invariant: (untyped ctx, untyped tc, untyped state_machine, Integer index) -> bool
544
542
  def state_machine_free: (untyped ctx, untyped state_machine) -> nil
545
543
  def generate_float: (untyped ctx, untyped tc, Integer width, Float min_value, Float max_value, bool allow_nan, bool allow_infinity, bool exclude_min, bool exclude_max, Float smallest_nonzero_magnitude) -> Float
546
544
  def string_generator_text: (untyped ctx, min_size: Integer, max_size: Integer, ?codec: String?, ?min_codepoint: Integer, ?max_codepoint: Integer) -> untyped