hegeltest 0.3.4 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +182 -0
- data/README.ja.md +408 -0
- data/README.md +208 -102
- data/Rakefile +6 -3
- data/docs/README.md +9 -0
- data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
- data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
- data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
- data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
- data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
- data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
- data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
- data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
- data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
- data/docs/architecture.md +16 -1
- data/lib/hegel/errors.rb +24 -0
- data/lib/hegel/generator.rb +52 -25
- data/lib/hegel/generators.rb +715 -45
- data/lib/hegel/grammar_cache.rb +36 -0
- data/lib/hegel/lib_hegel/real.rb +320 -17
- data/lib/hegel/lib_hegel.rb +31 -6
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/report.rb +13 -10
- data/lib/hegel/runner.rb +61 -27
- data/lib/hegel/settings.rb +92 -16
- data/lib/hegel/state_machine.rb +17 -4
- data/lib/hegel/stateful/pool.rb +52 -0
- data/lib/hegel/stateful.rb +2 -2
- data/lib/hegel/syntax/methods.rb +99 -15
- data/lib/hegel/test_case.rb +256 -35
- data/lib/hegel/test_location.rb +49 -0
- data/lib/hegel/version.rb +1 -1
- data/lib/hegel.rb +21 -5
- data/sig/hegel.rbs +295 -42
- data/skills/hegel-ruby/references/ruby/reference.md +463 -41
- metadata +13 -1
data/docs/architecture.md
CHANGED
|
@@ -18,7 +18,7 @@ recorded, and prints a blob that `Hegel.test(reproduce_failure:)` replays.
|
|
|
18
18
|
The report comes from the cases the engine stamps for capture during the
|
|
19
19
|
run; see
|
|
20
20
|
[ADR 0019](adr/0019-report-failures-from-the-cases-the-engine-stamps.md).
|
|
21
|
-
The generator layer covers
|
|
21
|
+
The generator layer covers thirty-four generators composing through `map`
|
|
22
22
|
and `filter`, reachable bare through `Hegel::Syntax::Methods`. A test case
|
|
23
23
|
can discard itself with `tc.assume` or `tc.reject`. A run can be shaped by
|
|
24
24
|
`phases:`, `suppress_health_check:`, `report_multiple_failures:`, and the
|
|
@@ -121,6 +121,12 @@ test-case handle, in nested `ensure`s so a failure releasing one cannot skip
|
|
|
121
121
|
the other. See
|
|
122
122
|
[ADR 0011](adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md).
|
|
123
123
|
|
|
124
|
+
A compiled grammar outlives the test case that compiled it, since compiling
|
|
125
|
+
costs far more than drawing. `Hegel::Runner.run` keeps the run's grammars
|
|
126
|
+
in a `Hegel::GrammarCache` and frees them after the run and before the
|
|
127
|
+
context. See
|
|
128
|
+
[ADR 0033](adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md).
|
|
129
|
+
|
|
124
130
|
## A rule's own control flow
|
|
125
131
|
|
|
126
132
|
Inside a stateful rule, `tc.assume(false)` means something narrower than it
|
|
@@ -135,6 +141,15 @@ exception can be raised inside an open span, which is why
|
|
|
135
141
|
than beside the run loop: two `rescue Exception` sites need it now, and both
|
|
136
142
|
have to let those four straight through before anything else.
|
|
137
143
|
|
|
144
|
+
## An abandoned recursive attempt
|
|
145
|
+
|
|
146
|
+
`recursive` unwinds through open spans on purpose. When an attempt outgrows its leaf budget, `Hegel::TestCase` raises
|
|
147
|
+
`Hegel::LeafBudgetExceeded` and holds it until `hegel_recursion_retry`.
|
|
148
|
+
Meanwhile `stop_span` makes no native call, so the retry can discard the
|
|
149
|
+
spans the attempt opened, and every other call that takes the test-case
|
|
150
|
+
handle raises the held exception again instead of reaching the engine. See
|
|
151
|
+
[ADR 0032](adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md).
|
|
152
|
+
|
|
138
153
|
## Generator validation happens at draw time
|
|
139
154
|
|
|
140
155
|
hegel-rust's own contributor documentation states the rule for its
|
data/lib/hegel/errors.rb
CHANGED
|
@@ -31,6 +31,30 @@ module Hegel
|
|
|
31
31
|
# Descends from Exception for the same reason as StopTest above.
|
|
32
32
|
class AssumeFailed < Exception; end
|
|
33
33
|
|
|
34
|
+
# Raised for HEGEL_E_RETRY from hegel_recursion_leaf: the attempt at a
|
|
35
|
+
# recursive value outgrew its leaf budget and must unwind to the
|
|
36
|
+
# Generators::RecursiveGenerator that owns +recursion+, which retries it.
|
|
37
|
+
# Internal to this library. See docs/adr/0032.
|
|
38
|
+
class LeafBudgetExceeded < Exception # :nodoc:
|
|
39
|
+
attr_reader :recursion
|
|
40
|
+
|
|
41
|
+
def initialize(recursion)
|
|
42
|
+
super("hegel: a recursive draw outgrew its leaf budget; the attempt is retried")
|
|
43
|
+
@recursion = recursion
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised by Hegel::TestCase#repeat when the engine ends its loop: the
|
|
48
|
+
# repetition ran as long as the engine wanted it to and nothing failed,
|
|
49
|
+
# so Hegel::Runner.classify reports the case VALID. hegel-rust raises its
|
|
50
|
+
# own LoopDone at the same point, and its run lifecycle maps it to VALID
|
|
51
|
+
# the same way.
|
|
52
|
+
#
|
|
53
|
+
# Descends from Exception for the same reason as StopTest above: a
|
|
54
|
+
# `rescue => e` written after the loop must not turn the end of the loop
|
|
55
|
+
# into ordinary control flow. See docs/adr/0034.
|
|
56
|
+
class LoopDone < Exception; end
|
|
57
|
+
|
|
34
58
|
# standard:enable Lint/InheritException
|
|
35
59
|
|
|
36
60
|
# Say the process is ending, not that a property failed. Every
|
data/lib/hegel/generator.rb
CHANGED
|
@@ -18,6 +18,41 @@ module Hegel
|
|
|
18
18
|
raise NotImplementedError, "#{self.class} must implement #do_draw"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# The span label of one attempt at a filtered draw, shared by every
|
|
22
|
+
# generator that runs attempts.
|
|
23
|
+
FILTER_ATTEMPT_LABEL = LibHegel.label_from_name("hegel-ruby.Hegel::Generator.filter_attempt")
|
|
24
|
+
|
|
25
|
+
# Draws one value that +predicate+ accepts, the draw behind #filter, which
|
|
26
|
+
# calls this on its source generator. The default makes up to
|
|
27
|
+
# Filtered::MAX_ATTEMPTS attempts, each a #do_draw in a span of its own
|
|
28
|
+
# that closes as discarded when the predicate rejects the value, then
|
|
29
|
+
# discards the test case (Hegel::AssumeFailed).
|
|
30
|
+
#
|
|
31
|
+
# A generator whose draws have effects a rejected value must not keep
|
|
32
|
+
# overrides this and runs the attempts itself, as
|
|
33
|
+
# Hegel::Stateful::Pool::ValuesConsumed does. One that only forwards
|
|
34
|
+
# #do_draw to another generator forwards this too, so that a filter around
|
|
35
|
+
# it still reaches the other generator's attempts.
|
|
36
|
+
def do_draw_filtered(tc, predicate)
|
|
37
|
+
Filtered::MAX_ATTEMPTS.times do
|
|
38
|
+
# Each variable is set again before it is read, or stays nil, which
|
|
39
|
+
# acts like false (!nil == !false).
|
|
40
|
+
# The initializer also shows intent: discard the span when an
|
|
41
|
+
# exception skips the reassignment.
|
|
42
|
+
accepted = false # mutineer:disable-line statement_removal
|
|
43
|
+
value = nil # mutineer:disable-line statement_removal, boolean_literal
|
|
44
|
+
tc.start_span(FILTER_ATTEMPT_LABEL)
|
|
45
|
+
begin
|
|
46
|
+
value = do_draw(tc)
|
|
47
|
+
accepted = predicate.call(value)
|
|
48
|
+
ensure
|
|
49
|
+
tc.stop_span(discard: !accepted)
|
|
50
|
+
end
|
|
51
|
+
return value if accepted
|
|
52
|
+
end
|
|
53
|
+
raise Hegel::AssumeFailed
|
|
54
|
+
end
|
|
55
|
+
|
|
21
56
|
# The span label naming this generator's draws to the engine, which
|
|
22
57
|
# treats two spans with one label as draws of one generator when it
|
|
23
58
|
# shrinks. A generator built from others overrides this and combines
|
|
@@ -30,7 +65,7 @@ module Hegel
|
|
|
30
65
|
# The class name, prefixed with this binding's name as hegel.h asks, to
|
|
31
66
|
# keep clear of the engine's own hegel.<kind> labels.
|
|
32
67
|
def self.label
|
|
33
|
-
LibHegel.label_from_name("hegel-ruby.#{name}")
|
|
68
|
+
@label ||= LibHegel.label_from_name("hegel-ruby.#{name}")
|
|
34
69
|
end
|
|
35
70
|
|
|
36
71
|
# Combines this generator's class label with the labels of +parts+, the
|
|
@@ -92,13 +127,17 @@ module Hegel
|
|
|
92
127
|
end
|
|
93
128
|
end
|
|
94
129
|
|
|
95
|
-
# Generator#filter's result.
|
|
96
|
-
#
|
|
97
|
-
# the test case (Hegel::AssumeFailed) -- the same
|
|
98
|
-
# Filtered spends before calling assume(false)
|
|
99
|
-
# (src/generators/generators.rs), ported here rather than
|
|
100
|
-
# a predicate that (almost) never holds fails the same way
|
|
101
|
-
# bindings.
|
|
130
|
+
# Generator#filter's result. Hands its predicate to the source
|
|
131
|
+
# generator's #do_draw_filtered, which makes up to MAX_ATTEMPTS attempts
|
|
132
|
+
# before discarding the test case (Hegel::AssumeFailed) -- the same
|
|
133
|
+
# budget hegel-rust's Filtered spends before calling assume(false)
|
|
134
|
+
# (frontends/rust/src/generators/generators.rs), ported here rather than
|
|
135
|
+
# reinvented so a predicate that (almost) never holds fails the same way
|
|
136
|
+
# in both bindings.
|
|
137
|
+
#
|
|
138
|
+
# A filter over a filter combines the two predicates into one draw, so
|
|
139
|
+
# the pair spends MAX_ATTEMPTS attempts between them, not that many per
|
|
140
|
+
# level.
|
|
102
141
|
#
|
|
103
142
|
# A predicate that never holds does not loop forever: libhegel's own
|
|
104
143
|
# health check aborts a run that discards too many test cases in a
|
|
@@ -116,23 +155,11 @@ module Hegel
|
|
|
116
155
|
attr_reader :label
|
|
117
156
|
|
|
118
157
|
def do_draw(tc)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
accepted = false # mutineer:disable-line statement_removal
|
|
125
|
-
value = nil # mutineer:disable-line statement_removal, boolean_literal
|
|
126
|
-
tc.start_span(label)
|
|
127
|
-
begin
|
|
128
|
-
value = @source.do_draw(tc)
|
|
129
|
-
accepted = @block.call(value)
|
|
130
|
-
ensure
|
|
131
|
-
tc.stop_span(discard: !accepted)
|
|
132
|
-
end
|
|
133
|
-
return value if accepted
|
|
134
|
-
end
|
|
135
|
-
raise Hegel::AssumeFailed
|
|
158
|
+
@source.do_draw_filtered(tc, @block)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def do_draw_filtered(tc, predicate)
|
|
162
|
+
@source.do_draw_filtered(tc, ->(value) { @block.call(value) && predicate.call(value) })
|
|
136
163
|
end
|
|
137
164
|
end
|
|
138
165
|
end
|