hegeltest 0.3.4-aarch64-linux → 0.4.1-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 +221 -0
- data/README.ja.md +408 -0
- data/README.md +208 -102
- data/Rakefile +6 -3
- data/docs/README.md +11 -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/adr/0036-follow-rubys-unicode-version.md +50 -0
- data/docs/adr/0037-send-a-maximum-at-the-engine-default-sentinel-as-no-bound.md +48 -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 +333 -19
- data/lib/hegel/lib_hegel.rb +63 -6
- data/lib/hegel/libhegel/libhegel-linux-arm64.so +0 -0
- 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 +259 -37
- 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 +300 -42
- data/skills/hegel-ruby/references/ruby/reference.md +463 -41
- metadata +15 -1
data/lib/hegel/syntax/methods.rb
CHANGED
|
@@ -27,26 +27,34 @@ module Hegel
|
|
|
27
27
|
# A double in [min_value, max_value]. allow_nan and allow_infinity
|
|
28
28
|
# default to what the bounds imply: NaN when the caller passed
|
|
29
29
|
# neither bound, an infinity when the caller left either side open.
|
|
30
|
+
# allow_subnormal defaults to true whenever the bounds admit a
|
|
31
|
+
# subnormal.
|
|
30
32
|
def floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false,
|
|
31
|
-
exclude_max: false)
|
|
33
|
+
exclude_max: false, allow_subnormal: nil)
|
|
32
34
|
Generators::FloatGenerator.new(
|
|
33
35
|
min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
|
|
34
|
-
exclude_min: exclude_min, exclude_max: exclude_max
|
|
36
|
+
exclude_min: exclude_min, exclude_max: exclude_max, allow_subnormal: allow_subnormal
|
|
35
37
|
)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
# A Unicode string of [min_size, max_size] characters
|
|
39
|
-
|
|
40
|
+
# A Unicode string of [min_size, max_size] characters; max_size
|
|
41
|
+
# defaults to 100, or min_size + 100 when min_size exceeds 100.
|
|
42
|
+
# +alphabet+ is a String of the permitted characters; it cannot be
|
|
43
|
+
# combined with the options after it.
|
|
44
|
+
def text(min_size: 0, max_size: nil, alphabet: nil, codec: nil, min_codepoint: nil, max_codepoint: nil,
|
|
45
|
+
categories: nil, exclude_categories: nil, include_characters: nil, exclude_characters: nil)
|
|
40
46
|
Generators::TextGenerator.new(
|
|
41
|
-
min_size: min_size, max_size: max_size, codec: codec,
|
|
42
|
-
min_codepoint: min_codepoint, max_codepoint: max_codepoint
|
|
47
|
+
min_size: min_size, max_size: max_size, alphabet: alphabet, codec: codec,
|
|
48
|
+
min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
|
|
49
|
+
exclude_categories: exclude_categories, include_characters: include_characters,
|
|
50
|
+
exclude_characters: exclude_characters
|
|
43
51
|
)
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
# An Array of values from +elements+, with [min_size, max_size]
|
|
47
|
-
# entries.
|
|
48
|
-
def arrays(elements, min_size: 0, max_size: nil)
|
|
49
|
-
Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
55
|
+
# entries. With unique: true, no two entries are equal.
|
|
56
|
+
def arrays(elements, min_size: 0, max_size: nil, unique: false)
|
|
57
|
+
Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size, unique: unique)
|
|
50
58
|
end
|
|
51
59
|
|
|
52
60
|
# Always +value+, drawing nothing.
|
|
@@ -59,6 +67,29 @@ module Hegel
|
|
|
59
67
|
Generators::SampledFromGenerator.new(collection)
|
|
60
68
|
end
|
|
61
69
|
|
|
70
|
+
# An Array holding every element of +elements+ (anything #sampled_from
|
|
71
|
+
# accepts) in an order the engine picks, shrinking towards the
|
|
72
|
+
# original one.
|
|
73
|
+
def permutations(elements)
|
|
74
|
+
Generators::PermutationsGenerator.new(elements)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An Array holding [min_size, max_size] of the elements of +elements+,
|
|
78
|
+
# in their original order. Duplicate elements count as distinct.
|
|
79
|
+
def subsequences(elements, min_size: 0, max_size: nil)
|
|
80
|
+
Generators::SubsequencesGenerator.new(elements, min_size: min_size, max_size: max_size)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An Array of [min_size, max_size] elements of +elements+. With
|
|
84
|
+
# +replacement+ (the default) each is drawn independently and the
|
|
85
|
+
# size is unbounded unless +max_size+ bounds it. Without it, each
|
|
86
|
+
# element is used at most once and the size is capped at the number
|
|
87
|
+
# of elements; hegel-rust names the two modes with_replacement and
|
|
88
|
+
# without_replacement.
|
|
89
|
+
def samples(elements, min_size: 0, max_size: nil, replacement: true)
|
|
90
|
+
Generators::SamplesGenerator.new(elements, min_size: min_size, max_size: max_size, replacement: replacement)
|
|
91
|
+
end
|
|
92
|
+
|
|
62
93
|
# A value drawn from one of +generators+, picked at random.
|
|
63
94
|
def one_of(*generators)
|
|
64
95
|
Generators::OneOfGenerator.new(generators)
|
|
@@ -88,20 +119,41 @@ module Hegel
|
|
|
88
119
|
|
|
89
120
|
# A String of exactly one character, sharing #text's own alphabet
|
|
90
121
|
# options.
|
|
91
|
-
def characters(codec: nil, min_codepoint: nil, max_codepoint: nil
|
|
92
|
-
|
|
122
|
+
def characters(codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil,
|
|
123
|
+
include_characters: nil, exclude_characters: nil)
|
|
124
|
+
Generators::CharactersGenerator.new(
|
|
125
|
+
codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint, categories: categories,
|
|
126
|
+
exclude_categories: exclude_categories, include_characters: include_characters,
|
|
127
|
+
exclude_characters: exclude_characters
|
|
128
|
+
)
|
|
93
129
|
end
|
|
94
130
|
|
|
95
|
-
# A byte String of [min_size, max_size] bytes
|
|
131
|
+
# A byte String of [min_size, max_size] bytes; max_size defaults as
|
|
132
|
+
# in #text.
|
|
96
133
|
def binary(min_size: 0, max_size: nil)
|
|
97
134
|
Generators::BinaryGenerator.new(min_size: min_size, max_size: max_size)
|
|
98
135
|
end
|
|
99
136
|
|
|
100
137
|
# A String matching +pattern+ (Python `re` syntax, not Ruby's
|
|
101
138
|
# Regexp syntax; see Generators::FromRegexGenerator). fullmatch
|
|
102
|
-
# requires the whole string to match, not just contain
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
# (the default) requires the whole string to match, not just contain
|
|
140
|
+
# a match.
|
|
141
|
+
# +alphabet+ is a #characters generator limiting every character of the
|
|
142
|
+
# string.
|
|
143
|
+
def from_regex(pattern, fullmatch: true, alphabet: nil)
|
|
144
|
+
Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch, alphabet: alphabet)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A String that +grammar+, written in the syntax of the Lark parsing
|
|
148
|
+
# library, accepts from its +start+ rule. +explicit+ maps terminal names
|
|
149
|
+
# to generators of the values drawn for them, +alphabet+ is a
|
|
150
|
+
# #characters generator limiting what terminal patterns draw, and
|
|
151
|
+
# +import_path+ is the directory %import reads files from.
|
|
152
|
+
def from_lark(grammar, start: nil, explicit: {}, alphabet: nil, max_size: nil, max_depth: nil, import_path: nil)
|
|
153
|
+
Generators::FromLarkGenerator.new(
|
|
154
|
+
grammar, start: start, explicit: explicit, alphabet: alphabet, max_size: max_size, max_depth: max_depth,
|
|
155
|
+
import_path: import_path
|
|
156
|
+
)
|
|
105
157
|
end
|
|
106
158
|
|
|
107
159
|
# An RFC 5321/5322 email address String.
|
|
@@ -152,6 +204,29 @@ module Hegel
|
|
|
152
204
|
Generators::DatetimesGenerator.new(min_value: min_value, max_value: max_value)
|
|
153
205
|
end
|
|
154
206
|
|
|
207
|
+
# A "YYYY-MM-DD" String, years 1 through 9999. Takes no options.
|
|
208
|
+
def date_strings
|
|
209
|
+
Generators::DateStringsGenerator.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# An "HH:MM:SS" String, with ".fffffffff" appended when the
|
|
213
|
+
# nanosecond is not zero. Takes no options.
|
|
214
|
+
def time_strings
|
|
215
|
+
Generators::TimeStringsGenerator.new
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A "YYYY-MM-DDTHH:MM:SS[.fffffffff]" String, years 1 through 9999.
|
|
219
|
+
# Takes no options.
|
|
220
|
+
def datetime_strings
|
|
221
|
+
Generators::DatetimeStringsGenerator.new
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A duration as Integer nanoseconds in [min_value, max_value], up to
|
|
225
|
+
# 2**64 - 1 (see Generators::DurationsGenerator and docs/adr/0030).
|
|
226
|
+
def durations(min_value: 0, max_value: (2**64) - 1)
|
|
227
|
+
Generators::DurationsGenerator.new(min_value: min_value, max_value: max_value)
|
|
228
|
+
end
|
|
229
|
+
|
|
155
230
|
# A value built from imperative code: +block+ receives a draw
|
|
156
231
|
# surface and may call #draw on it any number of times to assemble
|
|
157
232
|
# one value (see Generators::CompositeGenerator).
|
|
@@ -165,6 +240,15 @@ module Hegel
|
|
|
165
240
|
def deferred
|
|
166
241
|
Generators::DeferredGenerator.new
|
|
167
242
|
end
|
|
243
|
+
|
|
244
|
+
# A recursively defined value, such as a tree: either a value from
|
|
245
|
+
# +leaf+, or what the block's generator builds from sub-values of the
|
|
246
|
+
# same kind (see Generators::RecursiveGenerator). The block receives
|
|
247
|
+
# the generator of those sub-values and is called afresh for each
|
|
248
|
+
# branch.
|
|
249
|
+
def recursive(leaf, max_depth: 32, max_leaves: 100, &branch)
|
|
250
|
+
Generators::RecursiveGenerator.new(leaf, max_depth: max_depth, max_leaves: max_leaves, &branch)
|
|
251
|
+
end
|
|
168
252
|
end
|
|
169
253
|
end
|
|
170
254
|
|
data/lib/hegel/test_case.rb
CHANGED
|
@@ -46,6 +46,16 @@ module Hegel
|
|
|
46
46
|
# loudly.
|
|
47
47
|
DRAW_CALLER_DEPTH = 3
|
|
48
48
|
|
|
49
|
+
# The largest minimum length #repeat draws for its loop: 2**40, the
|
|
50
|
+
# bound hegel-rust's TestCase::repeat draws its own minimum under
|
|
51
|
+
# (max_safe_min_size).
|
|
52
|
+
REPEAT_MAX_MIN_SIZE = 2**40
|
|
53
|
+
|
|
54
|
+
# How many columns further #repeat indents the entries one iteration
|
|
55
|
+
# records: the indent hegel-rust's TestCase::repeat gives the block
|
|
56
|
+
# handle it runs each iteration under.
|
|
57
|
+
REPEAT_INDENT = 2
|
|
58
|
+
|
|
49
59
|
# +impl+ and +ctx+ are carried alongside +handle+ so each draw call can
|
|
50
60
|
# reach the same LibHegel implementation and context the run loop opened,
|
|
51
61
|
# without this class knowing anything about the native binding layer or
|
|
@@ -54,17 +64,26 @@ module Hegel
|
|
|
54
64
|
# +record+ defaults to false: recording is Hegel::Runner's decision, made
|
|
55
65
|
# only for the one, already-shrunk replay that produces a failure report
|
|
56
66
|
# (see #record_draw for why every other iteration skips it).
|
|
57
|
-
|
|
67
|
+
#
|
|
68
|
+
# +grammars+ is the run's Hegel::GrammarCache, which Hegel::Runner
|
|
69
|
+
# passes; a test case built without one compiles a grammar for each draw.
|
|
70
|
+
def initialize(impl, ctx, handle, record: false, grammars: nil)
|
|
58
71
|
@impl = impl
|
|
59
72
|
@ctx = ctx
|
|
60
73
|
@handle = handle
|
|
61
74
|
@record = record
|
|
62
75
|
@entries = record ? [] : nil
|
|
63
76
|
@pools = []
|
|
77
|
+
@grammars = grammars
|
|
78
|
+
# Only declares the ivar: an unset one already reads as nil.
|
|
79
|
+
@poison = nil # mutineer:disable-line statement_removal
|
|
80
|
+
@indent = 0
|
|
64
81
|
end
|
|
65
82
|
|
|
66
|
-
# The [:draw, name, value] / [:note, message] entries
|
|
67
|
-
# call order, or nil when this instance was not
|
|
83
|
+
# The [:draw, name, value, indent] / [:note, message, indent] entries
|
|
84
|
+
# recorded so far, in call order, or nil when this instance was not
|
|
85
|
+
# built to record. +indent+ is how many columns further in the report
|
|
86
|
+
# puts the entry; #repeat raises it around each iteration.
|
|
68
87
|
# Hegel::Runner reads this once, after the block that owns this test
|
|
69
88
|
# case has run to completion or raised. One tagged list rather than a
|
|
70
89
|
# draws list and a separate notes list, so a note recorded between two
|
|
@@ -151,21 +170,56 @@ module Hegel
|
|
|
151
170
|
|
|
152
171
|
return unless @record
|
|
153
172
|
|
|
154
|
-
@entries << [:note, has_message ? message : yield]
|
|
173
|
+
@entries << [:note, has_message ? message : yield, @indent]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Runs the block over and over, for as long as the engine keeps the
|
|
177
|
+
# loop going, and never returns normally: when the engine ends the loop
|
|
178
|
+
# it raises Hegel::LoopDone, which Hegel::Runner.classify reports as a
|
|
179
|
+
# passing case. A `break` in the block ends the loop early and returns
|
|
180
|
+
# from this method instead; hegel-rust's TestCase::repeat has no
|
|
181
|
+
# counterpart, since a Rust closure cannot break its caller's loop.
|
|
182
|
+
# docs/adr/0034 records both decisions.
|
|
183
|
+
#
|
|
184
|
+
# The loop is a collection whose minimum length is drawn first, from 0
|
|
185
|
+
# up to REPEAT_MAX_MIN_SIZE, with no maximum, the way hegel-rust draws
|
|
186
|
+
# it. Each iteration notes "// Repetition #N", then records what the
|
|
187
|
+
# block records REPEAT_INDENT columns further in. An iteration whose
|
|
188
|
+
# assumption failed is skipped rather than discarding the whole case;
|
|
189
|
+
# any other exception ends the loop (see #repeat_iteration).
|
|
190
|
+
#
|
|
191
|
+
# The minimum length is drawn with no span around it. hegel-rust's
|
|
192
|
+
# draw_silent opens one around every draw, and this library opens a
|
|
193
|
+
# span only around a generator that makes more than one native call
|
|
194
|
+
# (.claude/skills/new-generator), which one generate_integer is not.
|
|
195
|
+
def repeat(&block)
|
|
196
|
+
min_size = generate_integer(0, REPEAT_MAX_MIN_SIZE)
|
|
197
|
+
collection = new_collection(min_size, LibHegel::HEGEL_COLLECTION_MAX_SIZE_UNBOUNDED)
|
|
198
|
+
begin
|
|
199
|
+
iteration = 0
|
|
200
|
+
while collection_more(collection)
|
|
201
|
+
iteration += 1
|
|
202
|
+
note { "// Repetition ##{iteration}" }
|
|
203
|
+
repeat_iteration(&block)
|
|
204
|
+
end
|
|
205
|
+
ensure
|
|
206
|
+
collection_free(collection)
|
|
207
|
+
end
|
|
208
|
+
raise Hegel::LoopDone, "hegel: repeat finished its loop"
|
|
155
209
|
end
|
|
156
210
|
|
|
157
211
|
# hegel_generate_integer, without recording. Hegel::Generators::
|
|
158
212
|
# IntegerGenerator's own primitive; #draw_integer is this plus
|
|
159
213
|
# recording.
|
|
160
214
|
def generate_integer(min_value, max_value)
|
|
161
|
-
@impl.generate_integer(@ctx,
|
|
215
|
+
@impl.generate_integer(@ctx, live_handle, min_value, max_value)
|
|
162
216
|
end
|
|
163
217
|
|
|
164
218
|
# hegel_generate_integer_big, without recording. Hegel::Generators::
|
|
165
219
|
# IntegerGenerator's own primitive for bounds outside int64_t's range,
|
|
166
220
|
# used instead of #generate_integer only then; see IntegerGenerator#do_draw.
|
|
167
221
|
def generate_integer_big(min_value, max_value)
|
|
168
|
-
@impl.generate_integer_big(@ctx,
|
|
222
|
+
@impl.generate_integer_big(@ctx, live_handle, min_value, max_value)
|
|
169
223
|
end
|
|
170
224
|
|
|
171
225
|
# hegel_generate_boolean, without recording. Hegel::Generators::
|
|
@@ -175,14 +229,14 @@ module Hegel
|
|
|
175
229
|
# has_forced is false, so the engine ignores the forced argument's value.
|
|
176
230
|
# .mutineer.yml ignores that mutant by id, since a line marker would also
|
|
177
231
|
# hide the killed mutant of has_forced.
|
|
178
|
-
@impl.generate_boolean(@ctx,
|
|
232
|
+
@impl.generate_boolean(@ctx, live_handle, p, false, false)
|
|
179
233
|
end
|
|
180
234
|
|
|
181
235
|
# hegel_generate_float. Hegel::Generators::FloatGenerator's own
|
|
182
236
|
# primitive.
|
|
183
237
|
def generate_float(width, min_value, max_value, allow_nan:, allow_infinity:, exclude_min:, exclude_max:,
|
|
184
238
|
smallest_nonzero_magnitude:)
|
|
185
|
-
@impl.generate_float(@ctx,
|
|
239
|
+
@impl.generate_float(@ctx, live_handle, width, min_value, max_value, allow_nan, allow_infinity, exclude_min,
|
|
186
240
|
exclude_max, smallest_nonzero_magnitude)
|
|
187
241
|
end
|
|
188
242
|
|
|
@@ -220,33 +274,49 @@ module Hegel
|
|
|
220
274
|
# this class makes. Writing the same check on this side would only
|
|
221
275
|
# give one mistake two messages to drift apart from each other.
|
|
222
276
|
def target(value, label: "")
|
|
223
|
-
@impl.target(@ctx,
|
|
277
|
+
@impl.target(@ctx, live_handle, value, label)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def event(label)
|
|
281
|
+
@impl.event(@ctx, live_handle, label)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# No finite check here: hegel_event_value's HEGEL_E_INVALID_ARG reaches
|
|
285
|
+
# the caller through LibHegel.check!, as it does for #target.
|
|
286
|
+
def event_value(label, value)
|
|
287
|
+
@impl.event_value(@ctx, live_handle, label, value)
|
|
224
288
|
end
|
|
225
289
|
|
|
226
290
|
# hegel_start_span, labelled with a generator's label (see
|
|
227
291
|
# Hegel::Generator#label). Every compound Hegel::Generator (map, filter,
|
|
228
292
|
# arrays) opens one of these around its own draw; see #stop_span.
|
|
229
293
|
def start_span(label)
|
|
230
|
-
@impl.start_span(@ctx,
|
|
294
|
+
@impl.start_span(@ctx, live_handle, label)
|
|
231
295
|
end
|
|
232
296
|
|
|
233
297
|
# hegel_stop_span, closing the span #start_span most recently opened.
|
|
234
298
|
# +discard+ true marks it rejected (a filter predicate that did not
|
|
235
299
|
# hold), so libhegel retries from before the span opened.
|
|
300
|
+
#
|
|
301
|
+
# Makes no native call while an abandoned recursive attempt unwinds:
|
|
302
|
+
# hegel_recursion_retry discards the spans that attempt left open, and
|
|
303
|
+
# closing them here first would keep them. See docs/adr/0032.
|
|
236
304
|
def stop_span(discard: false)
|
|
305
|
+
return if @poison
|
|
306
|
+
|
|
237
307
|
@impl.stop_span(@ctx, @handle, discard)
|
|
238
308
|
end
|
|
239
309
|
|
|
240
310
|
# hegel_new_collection: Hegel::Generators::ArrayGenerator's own sizing
|
|
241
311
|
# primitive, paired with #collection_more and #collection_free.
|
|
242
312
|
def new_collection(min_size, max_size)
|
|
243
|
-
@impl.new_collection(@ctx,
|
|
313
|
+
@impl.new_collection(@ctx, live_handle, min_size, LibHegel.max_size_arg(max_size))
|
|
244
314
|
end
|
|
245
315
|
|
|
246
316
|
# hegel_collection_more: whether to draw another element into
|
|
247
317
|
# +collection+.
|
|
248
318
|
def collection_more(collection)
|
|
249
|
-
@impl.collection_more(@ctx,
|
|
319
|
+
@impl.collection_more(@ctx, live_handle, collection)
|
|
250
320
|
end
|
|
251
321
|
|
|
252
322
|
# hegel_collection_reject: tells libhegel the element most recently
|
|
@@ -255,7 +325,7 @@ module Hegel
|
|
|
255
325
|
# #collection_more call offers another attempt at the same slot instead
|
|
256
326
|
# of treating the collection as one element closer to done.
|
|
257
327
|
def collection_reject(collection, why: nil)
|
|
258
|
-
@impl.collection_reject(@ctx,
|
|
328
|
+
@impl.collection_reject(@ctx, live_handle, collection, why)
|
|
259
329
|
end
|
|
260
330
|
|
|
261
331
|
# hegel_collection_free.
|
|
@@ -270,15 +340,17 @@ module Hegel
|
|
|
270
340
|
# read the same way as #with_regex_generator/#with_email_generator/
|
|
271
341
|
# #with_url_generator/#with_domain_generator below, each named for its
|
|
272
342
|
# own hegel_string_generator_* call.
|
|
273
|
-
def with_text_generator(**kwargs, &block)
|
|
274
|
-
|
|
343
|
+
def with_text_generator(max_size:, **kwargs, &block)
|
|
344
|
+
handle = @impl.string_generator_text(@ctx, max_size: LibHegel.max_size_arg(max_size), **kwargs)
|
|
345
|
+
with_generator_handle(handle, &block)
|
|
275
346
|
end
|
|
276
347
|
|
|
277
348
|
# Runs the block with a regex-matching string generator handle built
|
|
278
|
-
# from +pattern+/+fullmatch+ (see #string_generator_regex),
|
|
279
|
-
# via #with_generator_handle whether the block returns or
|
|
280
|
-
|
|
281
|
-
|
|
349
|
+
# from +pattern+/+fullmatch+/+alphabet+ (see #string_generator_regex),
|
|
350
|
+
# freeing it via #with_generator_handle whether the block returns or
|
|
351
|
+
# raises. +alphabet+ stays the caller's to free.
|
|
352
|
+
def with_regex_generator(pattern, fullmatch:, alphabet: nil, &block)
|
|
353
|
+
with_generator_handle(string_generator_regex(pattern, fullmatch, alphabet), &block)
|
|
282
354
|
end
|
|
283
355
|
|
|
284
356
|
# Runs the block with an email-address string generator handle (see
|
|
@@ -304,26 +376,26 @@ module Hegel
|
|
|
304
376
|
# #with_regex_generator, #with_email_generator, #with_url_generator, or
|
|
305
377
|
# #with_domain_generator).
|
|
306
378
|
def generate_string(generator)
|
|
307
|
-
@impl.generate_string(@ctx,
|
|
379
|
+
@impl.generate_string(@ctx, live_handle, generator)
|
|
308
380
|
end
|
|
309
381
|
|
|
310
382
|
# hegel_generate_bytes. A future Hegel::Generators::BinaryGenerator's
|
|
311
383
|
# own primitive, the bytes counterpart to #generate_string.
|
|
312
384
|
def generate_bytes(min_size, max_size)
|
|
313
|
-
@impl.generate_bytes(@ctx,
|
|
385
|
+
@impl.generate_bytes(@ctx, live_handle, min_size, LibHegel.max_size_arg(max_size))
|
|
314
386
|
end
|
|
315
387
|
|
|
316
388
|
# hegel_generate_ipv4, returning the address's 4 raw bytes. Converting
|
|
317
389
|
# to a caller-facing address type (IPAddr or similar) is left to the
|
|
318
390
|
# generator built on top of this call.
|
|
319
391
|
def generate_ipv4
|
|
320
|
-
@impl.generate_ipv4(@ctx,
|
|
392
|
+
@impl.generate_ipv4(@ctx, live_handle)
|
|
321
393
|
end
|
|
322
394
|
|
|
323
395
|
# hegel_generate_ipv6, returning the address's 16 raw bytes. Same
|
|
324
396
|
# division of labor as #generate_ipv4.
|
|
325
397
|
def generate_ipv6
|
|
326
|
-
@impl.generate_ipv6(@ctx,
|
|
398
|
+
@impl.generate_ipv6(@ctx, live_handle)
|
|
327
399
|
end
|
|
328
400
|
|
|
329
401
|
# hegel_generate_uuid, returning 16 raw bytes. Hegel::Generators::
|
|
@@ -331,7 +403,7 @@ module Hegel
|
|
|
331
403
|
# hex String is that generator's job, the same division of labor
|
|
332
404
|
# #generate_ipv4/#generate_ipv6 already follow.
|
|
333
405
|
def generate_uuid(version, has_version)
|
|
334
|
-
@impl.generate_uuid(@ctx,
|
|
406
|
+
@impl.generate_uuid(@ctx, live_handle, version, has_version)
|
|
335
407
|
end
|
|
336
408
|
|
|
337
409
|
# hegel_string_generator_regex. +alphabet+ is an optional string
|
|
@@ -385,7 +457,7 @@ module Hegel
|
|
|
385
457
|
# Hegel::Stateful::Pool.new cannot forget it and leak the handle --
|
|
386
458
|
# docs/adr/0011 has the ownership decision behind this.
|
|
387
459
|
def new_pool
|
|
388
|
-
pool = @impl.new_pool(@ctx,
|
|
460
|
+
pool = @impl.new_pool(@ctx, live_handle)
|
|
389
461
|
@pools << pool
|
|
390
462
|
pool
|
|
391
463
|
end
|
|
@@ -394,7 +466,7 @@ module Hegel
|
|
|
394
466
|
# associate with the value it just generated. Hegel::Stateful::Pool#add's
|
|
395
467
|
# own primitive.
|
|
396
468
|
def pool_add(pool)
|
|
397
|
-
@impl.pool_add(@ctx,
|
|
469
|
+
@impl.pool_add(@ctx, live_handle, pool)
|
|
398
470
|
end
|
|
399
471
|
|
|
400
472
|
# hegel_pool_generate: the variable id libhegel chose from +pool+ (and
|
|
@@ -405,7 +477,13 @@ module Hegel
|
|
|
405
477
|
# translates that to Hegel::AssumeFailed, the same as every other
|
|
406
478
|
# assumption failure.
|
|
407
479
|
def pool_generate(pool, consume)
|
|
408
|
-
@impl.pool_generate(@ctx,
|
|
480
|
+
@impl.pool_generate(@ctx, live_handle, pool, consume)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# hegel_pool_restore: puts back +variable_id+, a consuming
|
|
484
|
+
# #pool_generate's answer that the caller is throwing away unused.
|
|
485
|
+
def pool_restore(pool, variable_id)
|
|
486
|
+
@impl.pool_restore(@ctx, live_handle, pool, variable_id)
|
|
409
487
|
end
|
|
410
488
|
|
|
411
489
|
# hegel_pool_free. Not called directly by a caller of
|
|
@@ -431,34 +509,34 @@ module Hegel
|
|
|
431
509
|
# here rather than on that module, the same reason every other native
|
|
432
510
|
# call is a method on this class: Hegel::Stateful never touches @impl or
|
|
433
511
|
# @ctx directly, only this handle-scoped surface.
|
|
434
|
-
def new_state_machine(rule_names, invariant_names, always_check, step_count)
|
|
435
|
-
@impl.new_state_machine(@ctx,
|
|
512
|
+
def new_state_machine(rule_names, rule_weights, invariant_names, always_check, step_count)
|
|
513
|
+
@impl.new_state_machine(@ctx, live_handle, rule_names, rule_weights, invariant_names, always_check, step_count)
|
|
436
514
|
end
|
|
437
515
|
|
|
438
516
|
# hegel_state_machine_next_group: starts the next round, or returns
|
|
439
517
|
# LibHegel::HEGEL_STATE_MACHINE_DONE once the machine is done.
|
|
440
518
|
def state_machine_next_group(state_machine)
|
|
441
|
-
@impl.state_machine_next_group(@ctx,
|
|
519
|
+
@impl.state_machine_next_group(@ctx, live_handle, state_machine)
|
|
442
520
|
end
|
|
443
521
|
|
|
444
522
|
# hegel_state_machine_next_rule: the index (into the +rule_names+
|
|
445
523
|
# #new_state_machine was given) of the next rule to run this round, or
|
|
446
524
|
# LibHegel::HEGEL_STATE_MACHINE_DONE once the round is over.
|
|
447
525
|
def state_machine_next_rule(state_machine)
|
|
448
|
-
@impl.state_machine_next_rule(@ctx,
|
|
526
|
+
@impl.state_machine_next_rule(@ctx, live_handle, state_machine)
|
|
449
527
|
end
|
|
450
528
|
|
|
451
529
|
# hegel_state_machine_rule_rejected: tells libhegel the rule most
|
|
452
530
|
# recently returned by #state_machine_next_rule stopped early on a
|
|
453
531
|
# failed assumption, so its round does not count toward the step budget.
|
|
454
532
|
def state_machine_rule_rejected(state_machine)
|
|
455
|
-
@impl.state_machine_rule_rejected(@ctx,
|
|
533
|
+
@impl.state_machine_rule_rejected(@ctx, live_handle, state_machine)
|
|
456
534
|
end
|
|
457
535
|
|
|
458
536
|
# hegel_state_machine_should_check_invariant: whether to run the
|
|
459
537
|
# invariant at +index+ at this join point.
|
|
460
538
|
def state_machine_should_check_invariant(state_machine, index)
|
|
461
|
-
@impl.state_machine_should_check_invariant(@ctx,
|
|
539
|
+
@impl.state_machine_should_check_invariant(@ctx, live_handle, state_machine, index)
|
|
462
540
|
end
|
|
463
541
|
|
|
464
542
|
# hegel_state_machine_free. Takes no test-case handle, unlike every
|
|
@@ -470,11 +548,69 @@ module Hegel
|
|
|
470
548
|
@impl.state_machine_free(@ctx, state_machine)
|
|
471
549
|
end
|
|
472
550
|
|
|
551
|
+
# A hegel_new_recursion handle, and whether #recursion_free has
|
|
552
|
+
# released it.
|
|
553
|
+
Recursion = Struct.new(:handle, :freed)
|
|
554
|
+
|
|
555
|
+
# hegel_new_recursion. Hegel::Generators::RecursiveGenerator's own
|
|
556
|
+
# scope, freed by #recursion_free in that generator's `ensure`.
|
|
557
|
+
def new_recursion(max_depth, max_leaves)
|
|
558
|
+
Recursion.new(@impl.new_recursion(@ctx, live_handle, max_depth, max_leaves), false)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# hegel_recursion_branch: whether the sub-value at +depth+ is a branch.
|
|
562
|
+
def recursion_branch(recursion, depth)
|
|
563
|
+
@impl.recursion_branch(@ctx, live_handle, recursion.handle, depth)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# hegel_recursion_leaf. Raises Hegel::LeafBudgetExceeded when the
|
|
567
|
+
# attempt outgrew its leaf budget, and keeps it until #recursion_retry:
|
|
568
|
+
# every call that takes the test-case handle raises it again instead of
|
|
569
|
+
# reaching the engine, so a body that rescues it cannot draw on into an
|
|
570
|
+
# attempt the engine is about to discard.
|
|
571
|
+
def recursion_leaf(recursion)
|
|
572
|
+
return if @impl.recursion_leaf(@ctx, live_handle, recursion.handle)
|
|
573
|
+
|
|
574
|
+
@poison = LeafBudgetExceeded.new(recursion)
|
|
575
|
+
raise @poison
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# True while the attempt +recursion+ abandoned through #recursion_leaf
|
|
579
|
+
# still waits for #recursion_retry.
|
|
580
|
+
def abandoned_by?(recursion)
|
|
581
|
+
@poison.is_a?(LeafBudgetExceeded) && @poison.recursion.equal?(recursion)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# hegel_recursion_retry, which reaches the engine with the raw handle
|
|
585
|
+
# while an attempt is abandoned. HEGEL_E_ASSUME or HEGEL_E_STOP_TEST ends the
|
|
586
|
+
# test case, so its exception takes the abandoned attempt's place and
|
|
587
|
+
# later calls raise it without reaching the engine.
|
|
588
|
+
def recursion_retry(recursion)
|
|
589
|
+
@impl.recursion_retry(@ctx, @handle, recursion.handle)
|
|
590
|
+
@poison = nil
|
|
591
|
+
rescue AssumeFailed, StopTest => e
|
|
592
|
+
@poison = e
|
|
593
|
+
raise
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# hegel_recursion_finish: false when the engine discarded the finished
|
|
597
|
+
# attempt, which the caller then draws again from the root.
|
|
598
|
+
def recursion_finish(recursion)
|
|
599
|
+
@impl.recursion_finish(@ctx, live_handle, recursion.handle)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# hegel_recursion_free. Takes no test-case handle, so it runs while an
|
|
603
|
+
# attempt is abandoned.
|
|
604
|
+
def recursion_free(recursion)
|
|
605
|
+
recursion.freed = true
|
|
606
|
+
@impl.recursion_free(@ctx, recursion.handle)
|
|
607
|
+
end
|
|
608
|
+
|
|
473
609
|
# hegel_generate_date, without recording. Hegel::Generators::
|
|
474
610
|
# DatesGenerator's own primitive. +min_value+/+max_value+ are each a
|
|
475
611
|
# [year, month, day] Array; the return value is the same shape.
|
|
476
612
|
def generate_date(min_value, max_value)
|
|
477
|
-
@impl.generate_date(@ctx,
|
|
613
|
+
@impl.generate_date(@ctx, live_handle, min_value, max_value)
|
|
478
614
|
end
|
|
479
615
|
|
|
480
616
|
# hegel_generate_time, without recording. Hegel::Generators::
|
|
@@ -482,7 +618,7 @@ module Hegel
|
|
|
482
618
|
# [hour, minute, second, microsecond] Array; the return value is the
|
|
483
619
|
# same shape.
|
|
484
620
|
def generate_time(min_value, max_value)
|
|
485
|
-
@impl.generate_time(@ctx,
|
|
621
|
+
@impl.generate_time(@ctx, live_handle, min_value, max_value)
|
|
486
622
|
end
|
|
487
623
|
|
|
488
624
|
# hegel_generate_datetime, without recording. Hegel::Generators::
|
|
@@ -491,11 +627,97 @@ module Hegel
|
|
|
491
627
|
# [hour, minute, second, microsecond] Array; the return value is a
|
|
492
628
|
# [[year, month, day], [hour, minute, second, microsecond]] pair.
|
|
493
629
|
def generate_datetime(min_date, min_time, max_date, max_time)
|
|
494
|
-
@impl.generate_datetime(@ctx,
|
|
630
|
+
@impl.generate_datetime(@ctx, live_handle, min_date, min_time, max_date, max_time)
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Yields the Hegel::GrammarCache::Compiled under +key+ in the run's
|
|
634
|
+
# cache, calling +build+ to compile it on the first draw. Without a
|
|
635
|
+
# cache, it compiles one for this draw alone and frees it afterwards.
|
|
636
|
+
def with_grammar(key, build)
|
|
637
|
+
return yield @grammars.fetch(key, &build) if @grammars
|
|
638
|
+
|
|
639
|
+
compiled = build.call
|
|
640
|
+
begin
|
|
641
|
+
yield compiled
|
|
642
|
+
ensure
|
|
643
|
+
grammar_free(compiled.grammar)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def grammar_builder_new(text)
|
|
648
|
+
@impl.grammar_builder_new(@ctx, text)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def grammar_builder_file(builder, index)
|
|
652
|
+
@impl.grammar_builder_file(@ctx, builder, index)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def grammar_builder_provide(builder, index, text)
|
|
656
|
+
@impl.grammar_builder_provide(@ctx, builder, index, text)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def grammar_builder_free(builder)
|
|
660
|
+
@impl.grammar_builder_free(@ctx, builder)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def grammar_new(builder, start, max_size, max_depth, alphabet, names)
|
|
664
|
+
@impl.grammar_new(@ctx, builder, start, max_size, max_depth, alphabet, names)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def grammar_placeholder(grammar, name)
|
|
668
|
+
@impl.grammar_placeholder(@ctx, grammar, name)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def grammar_free(grammar)
|
|
672
|
+
@impl.grammar_free(@ctx, grammar)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# hegel_generate_grammar, without recording.
|
|
676
|
+
def generate_grammar(grammar, max_size)
|
|
677
|
+
@impl.generate_grammar(@ctx, live_handle, grammar, max_size)
|
|
495
678
|
end
|
|
496
679
|
|
|
497
680
|
private
|
|
498
681
|
|
|
682
|
+
def live_handle
|
|
683
|
+
raise @poison if @poison
|
|
684
|
+
|
|
685
|
+
@handle
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Runs one #repeat iteration with the indent raised by REPEAT_INDENT.
|
|
689
|
+
# hegel-rust indents an iteration's output through hegel_test_case_block,
|
|
690
|
+
# which indents only what the engine printer prints. This library
|
|
691
|
+
# renders its report itself (Hegel::Report, docs/adr/0005), so the
|
|
692
|
+
# indent is kept on this side and recorded with each entry.
|
|
693
|
+
#
|
|
694
|
+
# The rescue order follows hegel-rust's TestCase::repeat, after the
|
|
695
|
+
# fatal exceptions every `rescue Exception` here lets through first. The
|
|
696
|
+
# library's own control exceptions, a nested #repeat's Hegel::LoopDone
|
|
697
|
+
# among them, and Hegel::Error, which stands where hegel-rust's
|
|
698
|
+
# InvalidArgument and InternalError stand, re-raise unchanged. A failed
|
|
699
|
+
# assumption skips to the next iteration. Anything else is a failure:
|
|
700
|
+
# one boolean is drawn before it re-raises, as hegel-rust draws one.
|
|
701
|
+
#
|
|
702
|
+
# standard:disable Lint/RescueException -- the same reason as
|
|
703
|
+
# Hegel::Runner.classify: a failing assertion descends from Exception.
|
|
704
|
+
def repeat_iteration
|
|
705
|
+
@indent += REPEAT_INDENT
|
|
706
|
+
begin
|
|
707
|
+
yield
|
|
708
|
+
ensure
|
|
709
|
+
@indent -= REPEAT_INDENT
|
|
710
|
+
end
|
|
711
|
+
rescue *Hegel::FATAL_EXCEPTIONS, Hegel::StopTest, Hegel::LoopDone, Hegel::Error
|
|
712
|
+
raise
|
|
713
|
+
rescue Hegel::AssumeFailed
|
|
714
|
+
# Skips this iteration; #repeat goes on to the next one.
|
|
715
|
+
rescue Exception
|
|
716
|
+
generate_boolean
|
|
717
|
+
raise
|
|
718
|
+
end
|
|
719
|
+
# standard:enable Lint/RescueException
|
|
720
|
+
|
|
499
721
|
# Runs the block with +generator+, freeing it via #string_generator_free
|
|
500
722
|
# whether the block returns or raises. The one #ensure
|
|
501
723
|
# #with_regex_generator/#with_email_generator/#with_url_generator/
|
|
@@ -522,7 +744,7 @@ module Hegel
|
|
|
522
744
|
def record_draw(label, value)
|
|
523
745
|
return unless @record
|
|
524
746
|
|
|
525
|
-
@entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value]
|
|
747
|
+
@entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value, @indent]
|
|
526
748
|
end
|
|
527
749
|
|
|
528
750
|
# The single place a draw's name is decided, tried in this order: the
|