hegeltest 0.3.1-aarch64-linux → 0.4.0-aarch64-linux
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 +262 -0
- data/README.ja.md +408 -0
- data/README.md +209 -103
- data/Rakefile +6 -3
- data/docs/README.md +11 -0
- data/docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md +61 -0
- data/docs/adr/0026-select-the-engine-release-repository-by-version.md +47 -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 +33 -11
- 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 +455 -120
- data/lib/hegel/lib_hegel.rb +33 -8
- data/lib/hegel/libhegel/libhegel-linux-arm64.so +0 -0
- data/lib/hegel/libhegel_version.rb +1 -1
- data/lib/hegel/locate.rb +10 -10
- 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/lib/tasks/libhegel.rake +17 -5
- data/lib/tasks/platform_gems.rake +3 -3
- data/sig/hegel.rbs +296 -43
- data/skills/hegel-ruby/references/ruby/reference.md +465 -43
- metadata +15 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# 0035: Defer concurrent state machines and the engine printer
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
libhegel 0.47.2 offers two facilities that hegel-rust uses and this gem
|
|
10
|
+
does not: concurrent state machines, and a printer that builds the failure
|
|
11
|
+
report inside the engine. ADR 0015 counts a feature that this gem lacks as
|
|
12
|
+
a divergence, so each needs a recorded reason. Paths and line numbers
|
|
13
|
+
below are in hegeldev/hegel as of libhegel 0.47.2.
|
|
14
|
+
|
|
15
|
+
### Concurrent state machines
|
|
16
|
+
|
|
17
|
+
hegel-rust runs a `ConcurrentStateMachine` with `run_concurrent`
|
|
18
|
+
(`frontends/rust/src/stateful.rs:989`). `hegel_new_state_machine` draws a
|
|
19
|
+
concurrency level between `min_concurrency` and `max_concurrency`.
|
|
20
|
+
`run_concurrent_machine` (`stateful.rs:1361`) spawns one thread per level
|
|
21
|
+
inside `std::thread::scope` (`stateful.rs:1415`). Each round, it hands every
|
|
22
|
+
worker a clone of the test case, `tc.clone()`, attributed to that worker
|
|
23
|
+
with `set_worker` (`stateful.rs:1449-1450`). A `ConcurrentRule`'s `apply`
|
|
24
|
+
takes `&M` (`stateful.rs:1097-1102`): the doc comment above it
|
|
25
|
+
(`stateful.rs:1086-1096`) says the
|
|
26
|
+
machine is "shared by reference across the worker threads, so any mutable
|
|
27
|
+
model state needs interior mutability", and that the feature "is intended
|
|
28
|
+
for advanced users. We otherwise discourage using it."
|
|
29
|
+
|
|
30
|
+
The engine calls behind this are `hegel_test_case_clone`
|
|
31
|
+
(`libhegel/include/hegel.h:1553`), which gives "an independent stream of the
|
|
32
|
+
same test case" that "can be driven concurrently from different threads",
|
|
33
|
+
and `hegel_test_case_set_worker` (`hegel.h:1614`), which prefixes each line
|
|
34
|
+
the handle records with `[worker N +X.XXXms] `.
|
|
35
|
+
|
|
36
|
+
Four things stand between this gem and a port:
|
|
37
|
+
|
|
38
|
+
1. **Each worker needs its own context.** The threading section of
|
|
39
|
+
`hegel.h` (lines 101-117) says "A context must not be used concurrently
|
|
40
|
+
from multiple threads". hegel-rust keeps one context per thread in a
|
|
41
|
+
`thread_local!` (`frontends/rust/src/ffi.rs:72`). `Hegel::Runner.run`
|
|
42
|
+
opens one context for the whole run and threads it through every call,
|
|
43
|
+
so each worker would need a context of its own, and every handle
|
|
44
|
+
allocated from it freed before that context.
|
|
45
|
+
2. **Ruby threads take turns.** `Hegel::LibHegel::Real#bind` builds each
|
|
46
|
+
`FFI::Function` without the `blocking:` option, so every engine call
|
|
47
|
+
holds the GVL for its duration. Between engine calls, Ruby threads
|
|
48
|
+
running pure-Ruby code switch on the VM's timer or at blocking I/O. A
|
|
49
|
+
short rule body that does no I/O then tends to run to completion before
|
|
50
|
+
another thread gets the GVL, and the interleavings a concurrent machine
|
|
51
|
+
exists to find seldom happen. A body that waits on I/O, a database, or a
|
|
52
|
+
C extension that releases the GVL would interleave.
|
|
53
|
+
3. **Ractors cannot reach a shared machine.** Measured with ffi 1.17.4 on
|
|
54
|
+
Ruby 3.4.6: passing an unfrozen `FFI::Pointer` to `Ractor.new` raises
|
|
55
|
+
"cannot duplicate unbounded memory area", and passing an unfrozen
|
|
56
|
+
`FFI::Function` raises "cannot duplicate function instances". Both can
|
|
57
|
+
be made shareable with `Ractor.make_shareable`, and a shareable
|
|
58
|
+
`FFI::Function` is callable inside another Ractor. The handles are not
|
|
59
|
+
the obstacle. The machine is: hegel-rust's rules share one `&M`, and a
|
|
60
|
+
`Hegel::StateMachine` subclass instance with mutable state is an object
|
|
61
|
+
that only one Ractor can reach.
|
|
62
|
+
4. **`CLAUDE.md` would change.** Its standing constraints say "The engine
|
|
63
|
+
is single-threaded. A context, a run, and a test case each belong to one
|
|
64
|
+
thread at a time." A concurrent runner needs that rule restated for
|
|
65
|
+
clones and per-thread contexts.
|
|
66
|
+
|
|
67
|
+
`Hegel::LibHegel::Real#new_state_machine` passes a concurrency of exactly
|
|
68
|
+
1, and ADR 0021 records the sequential machine this gem runs.
|
|
69
|
+
|
|
70
|
+
### The engine printer
|
|
71
|
+
|
|
72
|
+
hegel-rust prints each drawn value and note into the engine's document as
|
|
73
|
+
the test body runs. `hegel_test_case_printer` (`hegel.h:3052`) gives a
|
|
74
|
+
handle onto a test-case handle's print region, the `hegel_printer_*`
|
|
75
|
+
calls (the comment above `hegel_printer_t`, `hegel.h:538-574`, describes
|
|
76
|
+
the model) lay text out in groups that
|
|
77
|
+
break to fit a width (default 79, `hegel.h:3073`), `hegel_printer_reflow`
|
|
78
|
+
(`hegel.h:2910`) re-lays a flat debug string such as Ruby's `#inspect`, and
|
|
79
|
+
`hegel_note` (`hegel.h:3079`) appends a line. A note written while a drawn
|
|
80
|
+
value is mid-print is held back and placed after the value (`hegel.h`, the
|
|
81
|
+
comment above `hegel_note`). `hegel_test_case_block` (`hegel.h:1589`)
|
|
82
|
+
indents a section, and `hegel_test_case_set_worker` prefixes a worker's
|
|
83
|
+
lines.
|
|
84
|
+
|
|
85
|
+
This gem builds its report in Ruby. `Hegel::TestCase` records draws and
|
|
86
|
+
notes on the cases the engine stamps for capture (ADR 0019),
|
|
87
|
+
`Hegel::DrawName` names each draw from the caller's source (ADR 0005 and
|
|
88
|
+
ADR 0014), and `Hegel::Report` renders the list. `Hegel::LibHegel::Real`
|
|
89
|
+
passes a NULL output callback to `hegel_run_start`.
|
|
90
|
+
|
|
91
|
+
## Decision
|
|
92
|
+
|
|
93
|
+
**Concurrent state machines are not ported.** `Hegel::Stateful.run` stays
|
|
94
|
+
sequential, at a concurrency of 1. This is a divergence under ADR 0015,
|
|
95
|
+
recorded here. It is to be revisited when a caller asks for it, together
|
|
96
|
+
with the engine printer: `hegel_test_case_set_worker` attributes
|
|
97
|
+
`hegel_note` lines and lines started through a printer from
|
|
98
|
+
`hegel_test_case_printer` (`hegel.h:1595-1599`), so a report that reads
|
|
99
|
+
across workers comes from the engine document.
|
|
100
|
+
|
|
101
|
+
**The engine printer is not adopted for draws.** `Hegel::Report` keeps
|
|
102
|
+
building the report. Moving it into the engine would change two things
|
|
103
|
+
that work today:
|
|
104
|
+
|
|
105
|
+
- `Hegel::Report.assign_names` gives a name a counter only when that name
|
|
106
|
+
appears more than once among the whole case's draws (ADR 0014 decides
|
|
107
|
+
which draws get a name at all). At draw time, the rest of the case has
|
|
108
|
+
not run, so the printed name is not yet known.
|
|
109
|
+
`hegel_printer_deferred` could hold each name open until the case ends,
|
|
110
|
+
at the price of one hole per draw and a resolve step this gem does not
|
|
111
|
+
have.
|
|
112
|
+
- The report's shape, which `test/hegel/test_report.rb` pins line by line,
|
|
113
|
+
and which a caller reads on every failure.
|
|
114
|
+
|
|
115
|
+
The gains, width wrapping, a note placed after a value whose generation
|
|
116
|
+
emitted it, indented step blocks, and worker prefixes, do not yet outweigh
|
|
117
|
+
that.
|
|
118
|
+
|
|
119
|
+
A narrower change stays open: `Hegel::Report` could lay out a long value
|
|
120
|
+
through a standalone document, `hegel_printer_new` (`hegel.h:2746`), and
|
|
121
|
+
`hegel_printer_reflow` over the value's `#inspect`, then read it back with
|
|
122
|
+
`hegel_printer_value` (`hegel.h:3011`). That brings width wrapping without
|
|
123
|
+
moving naming or the report into the engine.
|
|
124
|
+
|
|
125
|
+
## Consequences
|
|
126
|
+
|
|
127
|
+
A caller who wants to exercise concurrency can start Ruby threads inside a
|
|
128
|
+
single rule. The engine then neither chooses nor shrinks the interleaving.
|
|
129
|
+
|
|
130
|
+
A long drawn value prints on one line, as `#inspect` writes it.
|
|
131
|
+
|
|
132
|
+
A later feature that wants an indented section of the report, the effect
|
|
133
|
+
`hegel_test_case_block` gives in the engine document, builds that indent in
|
|
134
|
+
`Hegel::Report`.
|
|
135
|
+
|
|
136
|
+
A later record that ports either facility supersedes this one.
|
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
|
|
@@ -43,10 +43,13 @@ schedules it.
|
|
|
43
43
|
|
|
44
44
|
## Where meaning comes from
|
|
45
45
|
|
|
46
|
-
libhegel is
|
|
47
|
-
it from the `
|
|
48
|
-
`
|
|
49
|
-
|
|
46
|
+
libhegel is the native engine of every Hegel frontend. Since 0.47.2, the
|
|
47
|
+
hegeldev/hegel Cargo workspace builds it from the `libhegel` directory, as
|
|
48
|
+
the crate `hegeltest-c`, whose header is `libhegel/include/hegel.h`. The
|
|
49
|
+
same workspace holds the Rust frontend, hegel-rust, at `frontends/rust`.
|
|
50
|
+
Releases up to 0.47.1 came from hegeldev/hegel-rust, where the engine was at
|
|
51
|
+
`hegel-c`. The header defines the ABI this gem calls: what each function
|
|
52
|
+
does, what its error codes mean, and who owns each pointer.
|
|
50
53
|
|
|
51
54
|
The Java, Go, TypeScript, OCaml, and C++ implementations bind the same
|
|
52
55
|
engine. Each works under a constraint hegel-rust does not have: a dynamic
|
|
@@ -118,6 +121,12 @@ test-case handle, in nested `ensure`s so a failure releasing one cannot skip
|
|
|
118
121
|
the other. See
|
|
119
122
|
[ADR 0011](adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md).
|
|
120
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
|
+
|
|
121
130
|
## A rule's own control flow
|
|
122
131
|
|
|
123
132
|
Inside a stateful rule, `tc.assume(false)` means something narrower than it
|
|
@@ -132,6 +141,15 @@ exception can be raised inside an open span, which is why
|
|
|
132
141
|
than beside the run loop: two `rescue Exception` sites need it now, and both
|
|
133
142
|
have to let those four straight through before anything else.
|
|
134
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
|
+
|
|
135
153
|
## Generator validation happens at draw time
|
|
136
154
|
|
|
137
155
|
hegel-rust's own contributor documentation states the rule for its
|
|
@@ -171,12 +189,16 @@ fields.
|
|
|
171
189
|
|
|
172
190
|
## Answered
|
|
173
191
|
|
|
174
|
-
A drawn string arrives as a pointer and a length,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
A drawn string arrives as a pointer and a length, and can hold interior
|
|
193
|
+
NUL bytes, because the drawn alphabet can include U+0000. Read by length.
|
|
194
|
+
Before libhegel 0.46 the string had no NUL terminator. Since 0.46 every
|
|
195
|
+
string the engine hands back is a `hegel_string_t` with a trailing NUL that
|
|
196
|
+
its length excludes, but an interior NUL still ends the string early for a
|
|
197
|
+
reader that stops at the first NUL. Measured against 0.45.0: an alphabet
|
|
198
|
+
pinned to U+0000 produces a three-byte draw that reads as `""` when taken
|
|
199
|
+
up to the first NUL and as three NUL bytes when taken by its reported
|
|
200
|
+
length. Every string observed reports `valid_encoding?` after
|
|
201
|
+
`force_encoding(UTF-8)`.
|
|
180
202
|
|
|
181
203
|
Prism recovers a draw's assignment target through a method chain, an
|
|
182
204
|
instance-variable assignment, and an assignment spanning several lines.
|
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
|