hegeltest 0.1.0 → 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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +72 -26
  4. data/Rakefile +81 -0
  5. data/docs/README.md +10 -0
  6. data/docs/adr/0014-name-a-drawn-value-only-when-the-draw-is-the-whole-assigned-value.md +74 -0
  7. data/docs/adr/0015-follow-the-hegeldev-interface-and-take-changes-upstream-first.md +101 -0
  8. data/docs/adr/0016-run-mutation-testing-with-mutineer.md +87 -0
  9. data/docs/adr/0017-raise-the-ruby-floor-to-3-4.md +43 -0
  10. data/docs/adr/0018-gate-mutation-testing-on-a-committed-baseline.md +74 -0
  11. data/docs/adr/0019-report-failures-from-the-cases-the-engine-stamps.md +70 -0
  12. data/docs/adr/0020-derive-span-labels-from-generator-names.md +64 -0
  13. data/docs/adr/0021-run-a-state-machine-in-rounds-with-its-own-step-count.md +59 -0
  14. data/docs/adr/0022-keep-microsecond-times-over-a-nanosecond-engine.md +45 -0
  15. data/docs/adr/0023-leave-unset-settings-to-the-engines-profile.md +50 -0
  16. data/docs/architecture.md +11 -6
  17. data/lib/hegel/draw_name.rb +24 -8
  18. data/lib/hegel/generator.rb +50 -9
  19. data/lib/hegel/generators.rb +134 -102
  20. data/lib/hegel/lib_hegel/real.rb +193 -94
  21. data/lib/hegel/lib_hegel.rb +30 -56
  22. data/lib/hegel/libhegel_version.rb +1 -1
  23. data/lib/hegel/report.rb +17 -8
  24. data/lib/hegel/runner.rb +153 -201
  25. data/lib/hegel/settings.rb +9 -14
  26. data/lib/hegel/state_machine.rb +15 -8
  27. data/lib/hegel/stateful/pool.rb +0 -2
  28. data/lib/hegel/stateful.rb +64 -36
  29. data/lib/hegel/syntax/methods.rb +3 -2
  30. data/lib/hegel/test_case.rb +29 -9
  31. data/lib/hegel/version.rb +1 -1
  32. data/lib/hegel.rb +11 -17
  33. data/lib/tasks/libhegel.rake +9 -2
  34. data/sig/hegel.rbs +60 -61
  35. data/skills/hegel-ruby/references/ruby/reference.md +147 -52
  36. metadata +12 -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,
@@ -25,6 +25,12 @@ module Hegel
25
25
  # #name_for's fallback when a draw has no better name (see below).
26
26
  DEFAULT_DRAW_NAME = "draw"
27
27
 
28
+ # The method names that reach #record_draw: #draw_integer, #draw_boolean,
29
+ # and #draw below. Hegel::DrawName.for takes this as an argument rather
30
+ # than knowing the list itself, so that class -- not DrawName -- stays
31
+ # the one place a fourth draw method needs to be added.
32
+ DRAW_METHOD_NAMES = [:draw, :draw_integer, :draw_boolean].freeze
33
+
28
34
  # Frames from #name_for's own caller_locations call up to the user's own
29
35
  # source line: #record_draw's call to #name_for (1), the public
30
36
  # draw_integer/draw_boolean/draw call to #record_draw (2), and the
@@ -166,6 +172,9 @@ module Hegel
166
172
  # BooleanGenerator's own primitive; #draw_boolean is this plus
167
173
  # recording.
168
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.
169
178
  @impl.generate_boolean(@ctx, @handle, p, false, false)
170
179
  end
171
180
 
@@ -214,8 +223,8 @@ module Hegel
214
223
  @impl.target(@ctx, @handle, value, label)
215
224
  end
216
225
 
217
- # hegel_start_span, labelled with one of the Hegel::LibHegel::
218
- # 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,
219
228
  # arrays) opens one of these around its own draw; see #stop_span.
220
229
  def start_span(label)
221
230
  @impl.start_span(@ctx, @handle, label)
@@ -422,25 +431,36 @@ module Hegel
422
431
  # here rather than on that module, the same reason every other native
423
432
  # call is a method on this class: Hegel::Stateful never touches @impl or
424
433
  # @ctx directly, only this handle-scoped surface.
425
- def new_state_machine(rule_names, invariant_names)
426
- @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)
427
442
  end
428
443
 
429
444
  # hegel_state_machine_next_rule: the index (into the +rule_names+
430
- # #new_state_machine was given) of the next rule to run, or
431
- # LibHegel::HEGEL_STATE_MACHINE_DONE once this test case's step budget
432
- # 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.
433
447
  def state_machine_next_rule(state_machine)
434
448
  @impl.state_machine_next_rule(@ctx, @handle, state_machine)
435
449
  end
436
450
 
437
451
  # hegel_state_machine_rule_rejected: tells libhegel the rule most
438
452
  # recently returned by #state_machine_next_rule stopped early on a
439
- # failed assumption, so it does not count toward the step budget.
453
+ # failed assumption, so its round does not count toward the step budget.
440
454
  def state_machine_rule_rejected(state_machine)
441
455
  @impl.state_machine_rule_rejected(@ctx, @handle, state_machine)
442
456
  end
443
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
+
444
464
  # hegel_state_machine_free. Takes no test-case handle, unlike every
445
465
  # other #state_machine_* method here: the header documents a
446
466
  # state-machine handle as freeable through any handle of the same
@@ -517,7 +537,7 @@ module Hegel
517
537
  return label if label
518
538
 
519
539
  location = caller_locations(depth, 1)&.first
520
- (location && DrawName.for(location.path, location.lineno)) || DEFAULT_DRAW_NAME
540
+ (location && DrawName.for(location.path, location.lineno, DRAW_METHOD_NAMES)) || DEFAULT_DRAW_NAME
521
541
  end
522
542
  end
523
543
  end
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.0"
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,11 +12,12 @@ 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
19
19
  DEFAULT_DRAW_NAME: String
20
+ DRAW_METHOD_NAMES: Array[Symbol]
20
21
  DRAW_CALLER_DEPTH: Integer
21
22
 
22
23
  def initialize: (untyped impl, untyped ctx, untyped handle, ?record: bool) -> void
@@ -61,14 +62,19 @@ module Hegel
61
62
  def pool_generate: (untyped pool, bool consume) -> Integer
62
63
  def pool_free: (untyped pool) -> nil
63
64
  def free_pools: () -> void
64
- 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
65
67
  def state_machine_next_rule: (untyped state_machine) -> Integer
66
68
  def state_machine_rule_rejected: (untyped state_machine) -> nil
69
+ def state_machine_should_check_invariant: (untyped state_machine, Integer index) -> bool
67
70
  def state_machine_free: (untyped state_machine) -> nil
68
71
  end
69
72
 
70
73
  class Generator
74
+ def self.label: () -> Integer
75
+ def self.label_of: (untyped part) -> Integer
71
76
  def do_draw: (untyped tc) -> untyped
77
+ def label: () -> Integer
72
78
  def map: () { (untyped) -> untyped } -> Generator
73
79
  def filter: () { (untyped) -> boolish } -> Generator
74
80
 
@@ -86,6 +92,9 @@ module Hegel
86
92
  end
87
93
 
88
94
  module Generators
95
+ def self.first_nanosecond: (Array[Integer] parts) -> Array[Integer]
96
+ def self.last_nanosecond: (Array[Integer] parts) -> Array[Integer]
97
+
89
98
  class BooleanGenerator < Generator
90
99
  def initialize: (p: Float) -> void
91
100
  def do_draw: (untyped tc) -> bool
@@ -102,7 +111,7 @@ module Hegel
102
111
  class FloatGenerator < Generator
103
112
  WIDTH: Integer
104
113
 
105
- 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
106
115
  def do_draw: (untyped tc) -> Float
107
116
  end
108
117
 
@@ -157,6 +166,7 @@ module Hegel
157
166
  end
158
167
 
159
168
  class HashGenerator < Generator
169
+ ENTRY_LABEL: Integer
160
170
  def initialize: (Generator keys, Generator values, min_size: Integer, max_size: Integer?) -> void
161
171
  def do_draw: (untyped tc) -> Hash[untyped, untyped]
162
172
 
@@ -262,16 +272,26 @@ module Hegel
262
272
  include Syntax::Methods
263
273
 
264
274
  def self.rule: (Symbol | String name) { (TestCase) -> untyped } -> void
265
- def self.invariant: (Symbol | String name) { (TestCase) -> untyped } -> void
275
+ def self.invariant: (Symbol | String name, ?always_run: bool) { (TestCase) -> untyped } -> void
266
276
  def self.rule_definitions: () -> Hash[String, Proc]
267
- 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
268
284
  end
269
285
 
270
286
  module Stateful
271
- def self.run: (StateMachine machine, TestCase tc) -> void
272
- def self.drive: (StateMachine machine, Hash[String, Proc] rules, Array[String] rule_names, Hash[String, Proc] invariants, untyped state_machine, TestCase tc) -> void
273
- def self.apply_rule: (StateMachine machine, Proc block, Hash[String, Proc] invariants, untyped state_machine, TestCase tc) -> void
274
- 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
275
295
 
276
296
  class Pool
277
297
  def initialize: (TestCase tc) -> void
@@ -297,7 +317,7 @@ module Hegel
297
317
  module Methods
298
318
  def booleans: (?p: Float) -> Generators::BooleanGenerator
299
319
  def integers: (?min_value: Integer?, ?max_value: Integer?) -> Generators::IntegerGenerator
300
- 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
301
321
  def text: (?min_size: Integer, ?max_size: Integer?, ?codec: String?, ?min_codepoint: Integer?, ?max_codepoint: Integer?) -> Generators::TextGenerator
302
322
  def arrays: (Generator elements, ?min_size: Integer, ?max_size: Integer?) -> Generators::ArrayGenerator
303
323
  def just: (untyped value) -> Generators::JustGenerator
@@ -326,7 +346,7 @@ module Hegel
326
346
  module DrawName
327
347
  ASSIGNMENT_NODE_TYPES: Array[untyped]
328
348
 
329
- def self.for: (String path, Integer lineno) -> String?
349
+ def self.for: (String path, Integer lineno, Array[Symbol] call_names) -> String?
330
350
  def self.reset_cache: () -> void
331
351
  def self.parse: (String path) -> untyped
332
352
  def self.cache: () -> Hash[String, untyped]
@@ -337,11 +357,12 @@ module Hegel
337
357
 
338
358
  module Report
339
359
  class Failure
340
- 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
341
361
  def test_cases: () -> Integer
342
362
  def discarded: () -> Integer
343
363
  def entries: () -> Array[[:draw, String, untyped] | [:note, untyped]]
344
- def blob: () -> String
364
+ def blob: () -> String?
365
+ def caveat: () -> String?
345
366
  end
346
367
 
347
368
  def self.assign_names: (Array[[:draw, String, untyped] | [:note, untyped]] entries) -> Array[[:draw, String, untyped] | [:note, untyped]]
@@ -354,7 +375,7 @@ module Hegel
354
375
  PHASE_CODES: Hash[Symbol, Integer]
355
376
  HEALTH_CHECK_CODES: Hash[Symbol, Integer]
356
377
 
357
- 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
358
379
  def self.apply_verbosity: (untyped impl, untyped ctx, untyped settings, Symbol verbosity) -> void
359
380
  def self.apply_database: (untyped impl, untyped ctx, untyped settings, database: String?, database_key: String?) -> void
360
381
  def self.apply_phases: (untyped impl, untyped ctx, untyped settings, Array[Symbol] phases) -> void
@@ -370,25 +391,31 @@ module Hegel
370
391
  INSTALLED_GEM_DIRS: Array[String]
371
392
  STDLIB_DIR: String
372
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
+
373
400
  class GenerationStats
374
401
  def initialize: () -> void
375
402
  def record: (Integer status, String? origin) -> void
376
403
  def for: (String? origin) -> [Integer, Integer]
377
404
  end
378
405
 
379
- 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
380
- def self.run_and_finish: (untyped impl, untyped ctx, untyped settings, quiet: bool, output: untyped) { (TestCase) -> void } -> nil
381
- def self.drive: (untyped impl, untyped ctx, untyped run, GenerationStats stats) { (TestCase) -> void } -> void
382
- def self.with_test_case: [R] (untyped impl, untyped ctx, untyped tc, ?record: bool) { (TestCase) -> R } -> R
383
- def self.run_case: (untyped impl, untyped ctx, untyped tc, GenerationStats stats) { (TestCase) -> void } -> void
384
- def self.finish: (untyped impl, untyped ctx, untyped settings, untyped result, GenerationStats stats, quiet: bool, output: untyped) { (TestCase) -> void } -> nil
385
- def self.replay: (untyped impl, untyped ctx, untyped settings, untyped result, GenerationStats stats, quiet: bool, output: untyped) { (TestCase) -> void } -> bot
386
- def self.replay_failure: (untyped impl, untyped ctx, untyped settings, untyped failure, Integer index, GenerationStats stats) { (TestCase) -> void } -> [Exception, Report::Failure]
387
- 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]
388
415
  def self.classify: (TestCase test_case) { (TestCase) -> void } -> [Integer, String?, Exception?, Array[[:draw, String, untyped] | [:note, untyped]]?]
389
416
  def self.origin_for: (Exception exception) -> String
390
417
  def self.infrastructure?: (String path) -> bool
391
- def self.flaky_message: () -> String
418
+ def self.not_reproduced_message: () -> String
392
419
  def self.multiple_failures_message: (Integer count) -> String
393
420
  end
394
421
 
@@ -432,8 +459,8 @@ module Hegel
432
459
  HEGEL_RUN_STATUS_PASSED: Integer
433
460
  HEGEL_RUN_STATUS_FAILED: Integer
434
461
  HEGEL_RUN_STATUS_ERROR: Integer
435
- HEGEL_VERBOSITY_QUIET: Integer
436
462
  HEGEL_VERBOSITY_NORMAL: Integer
463
+ HEGEL_VERBOSITY_QUIET: Integer
437
464
  HEGEL_VERBOSITY_VERBOSE: Integer
438
465
  HEGEL_VERBOSITY_DEBUG: Integer
439
466
  HEGEL_PHASE_EXPLICIT: Integer
@@ -446,39 +473,6 @@ module Hegel
446
473
  HEGEL_HC_TOO_SLOW: Integer
447
474
  HEGEL_HC_TEST_CASES_TOO_LARGE: Integer
448
475
  HEGEL_HC_LARGE_INITIAL_TEST_CASE: Integer
449
- HEGEL_LABEL_LIST: Integer
450
- HEGEL_LABEL_LIST_ELEMENT: Integer
451
- HEGEL_LABEL_SET: Integer
452
- HEGEL_LABEL_SET_ELEMENT: Integer
453
- HEGEL_LABEL_MAP: Integer
454
- HEGEL_LABEL_MAP_ENTRY: Integer
455
- HEGEL_LABEL_TUPLE: Integer
456
- HEGEL_LABEL_ONE_OF: Integer
457
- HEGEL_LABEL_OPTIONAL: Integer
458
- HEGEL_LABEL_FIXED_DICT: Integer
459
- HEGEL_LABEL_FLAT_MAP: Integer
460
- HEGEL_LABEL_FILTER: Integer
461
- HEGEL_LABEL_MAPPED: Integer
462
- HEGEL_LABEL_SAMPLED_FROM: Integer
463
- HEGEL_LABEL_ENUM_VARIANT: Integer
464
- HEGEL_LABEL_FEATURE_FLAG: Integer
465
- HEGEL_LABEL_REGEX: Integer
466
- HEGEL_LABEL_EMAIL: Integer
467
- HEGEL_LABEL_URL: Integer
468
- HEGEL_LABEL_DOMAIN: Integer
469
- HEGEL_LABEL_DATE: Integer
470
- HEGEL_LABEL_TIME: Integer
471
- HEGEL_LABEL_DATETIME: Integer
472
- HEGEL_LABEL_UUID: Integer
473
- HEGEL_LABEL_IP_ADDRESS: Integer
474
- HEGEL_LABEL_INTEGER: Integer
475
- HEGEL_LABEL_FLOAT: Integer
476
- HEGEL_LABEL_BOOLEAN: Integer
477
- HEGEL_LABEL_BYTES: Integer
478
- HEGEL_LABEL_STRING: Integer
479
- HEGEL_LABEL_STATEFUL_RULE: Integer
480
- HEGEL_LABEL_FRESH_ID: Integer
481
- HEGEL_LABEL_SET_CHOICE: Integer
482
476
  HEGEL_STATE_MACHINE_DONE: Integer
483
477
  HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED: Integer
484
478
  HEGEL_FLOAT64_SMALLEST_NONZERO_MAGNITUDE_UNRESTRICTED: Float
@@ -487,6 +481,8 @@ module Hegel
487
481
  def self.with_context: [R] (untyped impl) { (untyped) -> R } -> R
488
482
  def self.check!: (untyped impl, untyped ctx, Integer code) -> void
489
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
490
486
  def self.encode_integer_le: (Integer n) -> String
491
487
  def self.decode_integer_le: (String bytes) -> Integer
492
488
 
@@ -503,12 +499,13 @@ module Hegel
503
499
  def settings_set_seed: (untyped ctx, untyped s, Integer seed, bool has_seed) -> nil
504
500
  def settings_set_derandomize: (untyped ctx, untyped s, bool derandomize) -> nil
505
501
  def settings_set_database: (untyped ctx, untyped s, String? database) -> nil
506
- def settings_set_stateful_step_count: (untyped ctx, untyped s, Integer n) -> nil
507
502
  def settings_set_report_multiple_failures: (untyped ctx, untyped s, bool yes) -> nil
508
503
  def settings_set_database_key: (untyped ctx, untyped s, String? key) -> nil
509
504
  def settings_set_phases: (untyped ctx, untyped s, Integer phases) -> nil
510
505
  def settings_set_suppress_health_check: (untyped ctx, untyped s, Integer checks) -> nil
511
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
512
509
  def next_test_case: (untyped ctx, untyped run) -> untyped?
513
510
  def run_free: (untyped ctx, untyped run) -> nil
514
511
  def test_case_free: (untyped ctx, untyped tc) -> nil
@@ -526,7 +523,7 @@ module Hegel
526
523
  def failure_free: (untyped ctx, untyped f) -> nil
527
524
  def failure_origin: (untyped ctx, untyped f) -> String
528
525
  def failure_reproduction_blob: (untyped ctx, untyped f) -> String?
529
- def test_case_from_blob: (untyped ctx, untyped settings, String blob) -> untyped
526
+ def failure_caveat: (untyped ctx, untyped f) -> String?
530
527
  def start_span: (untyped ctx, untyped tc, Integer label) -> nil
531
528
  def stop_span: (untyped ctx, untyped tc, bool discard) -> nil
532
529
  def new_collection: (untyped ctx, untyped tc, Integer min_size, Integer max_size) -> untyped
@@ -537,9 +534,11 @@ module Hegel
537
534
  def pool_add: (untyped ctx, untyped tc, untyped pool) -> Integer
538
535
  def pool_generate: (untyped ctx, untyped tc, untyped pool, bool consume) -> Integer
539
536
  def pool_free: (untyped ctx, untyped pool) -> nil
540
- 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
541
539
  def state_machine_next_rule: (untyped ctx, untyped tc, untyped state_machine) -> Integer
542
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
543
542
  def state_machine_free: (untyped ctx, untyped state_machine) -> nil
544
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
545
544
  def string_generator_text: (untyped ctx, min_size: Integer, max_size: Integer, ?codec: String?, ?min_codepoint: Integer, ?max_codepoint: Integer) -> untyped