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/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, 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.
|
|
@@ -275,10 +345,11 @@ module Hegel
|
|
|
275
345
|
end
|
|
276
346
|
|
|
277
347
|
# 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
|
-
|
|
348
|
+
# from +pattern+/+fullmatch+/+alphabet+ (see #string_generator_regex),
|
|
349
|
+
# freeing it via #with_generator_handle whether the block returns or
|
|
350
|
+
# raises. +alphabet+ stays the caller's to free.
|
|
351
|
+
def with_regex_generator(pattern, fullmatch:, alphabet: nil, &block)
|
|
352
|
+
with_generator_handle(string_generator_regex(pattern, fullmatch, alphabet), &block)
|
|
282
353
|
end
|
|
283
354
|
|
|
284
355
|
# Runs the block with an email-address string generator handle (see
|
|
@@ -304,26 +375,26 @@ module Hegel
|
|
|
304
375
|
# #with_regex_generator, #with_email_generator, #with_url_generator, or
|
|
305
376
|
# #with_domain_generator).
|
|
306
377
|
def generate_string(generator)
|
|
307
|
-
@impl.generate_string(@ctx,
|
|
378
|
+
@impl.generate_string(@ctx, live_handle, generator)
|
|
308
379
|
end
|
|
309
380
|
|
|
310
381
|
# hegel_generate_bytes. A future Hegel::Generators::BinaryGenerator's
|
|
311
382
|
# own primitive, the bytes counterpart to #generate_string.
|
|
312
383
|
def generate_bytes(min_size, max_size)
|
|
313
|
-
@impl.generate_bytes(@ctx,
|
|
384
|
+
@impl.generate_bytes(@ctx, live_handle, min_size, max_size)
|
|
314
385
|
end
|
|
315
386
|
|
|
316
387
|
# hegel_generate_ipv4, returning the address's 4 raw bytes. Converting
|
|
317
388
|
# to a caller-facing address type (IPAddr or similar) is left to the
|
|
318
389
|
# generator built on top of this call.
|
|
319
390
|
def generate_ipv4
|
|
320
|
-
@impl.generate_ipv4(@ctx,
|
|
391
|
+
@impl.generate_ipv4(@ctx, live_handle)
|
|
321
392
|
end
|
|
322
393
|
|
|
323
394
|
# hegel_generate_ipv6, returning the address's 16 raw bytes. Same
|
|
324
395
|
# division of labor as #generate_ipv4.
|
|
325
396
|
def generate_ipv6
|
|
326
|
-
@impl.generate_ipv6(@ctx,
|
|
397
|
+
@impl.generate_ipv6(@ctx, live_handle)
|
|
327
398
|
end
|
|
328
399
|
|
|
329
400
|
# hegel_generate_uuid, returning 16 raw bytes. Hegel::Generators::
|
|
@@ -331,7 +402,7 @@ module Hegel
|
|
|
331
402
|
# hex String is that generator's job, the same division of labor
|
|
332
403
|
# #generate_ipv4/#generate_ipv6 already follow.
|
|
333
404
|
def generate_uuid(version, has_version)
|
|
334
|
-
@impl.generate_uuid(@ctx,
|
|
405
|
+
@impl.generate_uuid(@ctx, live_handle, version, has_version)
|
|
335
406
|
end
|
|
336
407
|
|
|
337
408
|
# hegel_string_generator_regex. +alphabet+ is an optional string
|
|
@@ -385,7 +456,7 @@ module Hegel
|
|
|
385
456
|
# Hegel::Stateful::Pool.new cannot forget it and leak the handle --
|
|
386
457
|
# docs/adr/0011 has the ownership decision behind this.
|
|
387
458
|
def new_pool
|
|
388
|
-
pool = @impl.new_pool(@ctx,
|
|
459
|
+
pool = @impl.new_pool(@ctx, live_handle)
|
|
389
460
|
@pools << pool
|
|
390
461
|
pool
|
|
391
462
|
end
|
|
@@ -394,7 +465,7 @@ module Hegel
|
|
|
394
465
|
# associate with the value it just generated. Hegel::Stateful::Pool#add's
|
|
395
466
|
# own primitive.
|
|
396
467
|
def pool_add(pool)
|
|
397
|
-
@impl.pool_add(@ctx,
|
|
468
|
+
@impl.pool_add(@ctx, live_handle, pool)
|
|
398
469
|
end
|
|
399
470
|
|
|
400
471
|
# hegel_pool_generate: the variable id libhegel chose from +pool+ (and
|
|
@@ -405,7 +476,13 @@ module Hegel
|
|
|
405
476
|
# translates that to Hegel::AssumeFailed, the same as every other
|
|
406
477
|
# assumption failure.
|
|
407
478
|
def pool_generate(pool, consume)
|
|
408
|
-
@impl.pool_generate(@ctx,
|
|
479
|
+
@impl.pool_generate(@ctx, live_handle, pool, consume)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# hegel_pool_restore: puts back +variable_id+, a consuming
|
|
483
|
+
# #pool_generate's answer that the caller is throwing away unused.
|
|
484
|
+
def pool_restore(pool, variable_id)
|
|
485
|
+
@impl.pool_restore(@ctx, live_handle, pool, variable_id)
|
|
409
486
|
end
|
|
410
487
|
|
|
411
488
|
# hegel_pool_free. Not called directly by a caller of
|
|
@@ -431,34 +508,34 @@ module Hegel
|
|
|
431
508
|
# here rather than on that module, the same reason every other native
|
|
432
509
|
# call is a method on this class: Hegel::Stateful never touches @impl or
|
|
433
510
|
# @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,
|
|
511
|
+
def new_state_machine(rule_names, rule_weights, invariant_names, always_check, step_count)
|
|
512
|
+
@impl.new_state_machine(@ctx, live_handle, rule_names, rule_weights, invariant_names, always_check, step_count)
|
|
436
513
|
end
|
|
437
514
|
|
|
438
515
|
# hegel_state_machine_next_group: starts the next round, or returns
|
|
439
516
|
# LibHegel::HEGEL_STATE_MACHINE_DONE once the machine is done.
|
|
440
517
|
def state_machine_next_group(state_machine)
|
|
441
|
-
@impl.state_machine_next_group(@ctx,
|
|
518
|
+
@impl.state_machine_next_group(@ctx, live_handle, state_machine)
|
|
442
519
|
end
|
|
443
520
|
|
|
444
521
|
# hegel_state_machine_next_rule: the index (into the +rule_names+
|
|
445
522
|
# #new_state_machine was given) of the next rule to run this round, or
|
|
446
523
|
# LibHegel::HEGEL_STATE_MACHINE_DONE once the round is over.
|
|
447
524
|
def state_machine_next_rule(state_machine)
|
|
448
|
-
@impl.state_machine_next_rule(@ctx,
|
|
525
|
+
@impl.state_machine_next_rule(@ctx, live_handle, state_machine)
|
|
449
526
|
end
|
|
450
527
|
|
|
451
528
|
# hegel_state_machine_rule_rejected: tells libhegel the rule most
|
|
452
529
|
# recently returned by #state_machine_next_rule stopped early on a
|
|
453
530
|
# failed assumption, so its round does not count toward the step budget.
|
|
454
531
|
def state_machine_rule_rejected(state_machine)
|
|
455
|
-
@impl.state_machine_rule_rejected(@ctx,
|
|
532
|
+
@impl.state_machine_rule_rejected(@ctx, live_handle, state_machine)
|
|
456
533
|
end
|
|
457
534
|
|
|
458
535
|
# hegel_state_machine_should_check_invariant: whether to run the
|
|
459
536
|
# invariant at +index+ at this join point.
|
|
460
537
|
def state_machine_should_check_invariant(state_machine, index)
|
|
461
|
-
@impl.state_machine_should_check_invariant(@ctx,
|
|
538
|
+
@impl.state_machine_should_check_invariant(@ctx, live_handle, state_machine, index)
|
|
462
539
|
end
|
|
463
540
|
|
|
464
541
|
# hegel_state_machine_free. Takes no test-case handle, unlike every
|
|
@@ -470,11 +547,69 @@ module Hegel
|
|
|
470
547
|
@impl.state_machine_free(@ctx, state_machine)
|
|
471
548
|
end
|
|
472
549
|
|
|
550
|
+
# A hegel_new_recursion handle, and whether #recursion_free has
|
|
551
|
+
# released it.
|
|
552
|
+
Recursion = Struct.new(:handle, :freed)
|
|
553
|
+
|
|
554
|
+
# hegel_new_recursion. Hegel::Generators::RecursiveGenerator's own
|
|
555
|
+
# scope, freed by #recursion_free in that generator's `ensure`.
|
|
556
|
+
def new_recursion(max_depth, max_leaves)
|
|
557
|
+
Recursion.new(@impl.new_recursion(@ctx, live_handle, max_depth, max_leaves), false)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# hegel_recursion_branch: whether the sub-value at +depth+ is a branch.
|
|
561
|
+
def recursion_branch(recursion, depth)
|
|
562
|
+
@impl.recursion_branch(@ctx, live_handle, recursion.handle, depth)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# hegel_recursion_leaf. Raises Hegel::LeafBudgetExceeded when the
|
|
566
|
+
# attempt outgrew its leaf budget, and keeps it until #recursion_retry:
|
|
567
|
+
# every call that takes the test-case handle raises it again instead of
|
|
568
|
+
# reaching the engine, so a body that rescues it cannot draw on into an
|
|
569
|
+
# attempt the engine is about to discard.
|
|
570
|
+
def recursion_leaf(recursion)
|
|
571
|
+
return if @impl.recursion_leaf(@ctx, live_handle, recursion.handle)
|
|
572
|
+
|
|
573
|
+
@poison = LeafBudgetExceeded.new(recursion)
|
|
574
|
+
raise @poison
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# True while the attempt +recursion+ abandoned through #recursion_leaf
|
|
578
|
+
# still waits for #recursion_retry.
|
|
579
|
+
def abandoned_by?(recursion)
|
|
580
|
+
@poison.is_a?(LeafBudgetExceeded) && @poison.recursion.equal?(recursion)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# hegel_recursion_retry, which reaches the engine with the raw handle
|
|
584
|
+
# while an attempt is abandoned. HEGEL_E_ASSUME or HEGEL_E_STOP_TEST ends the
|
|
585
|
+
# test case, so its exception takes the abandoned attempt's place and
|
|
586
|
+
# later calls raise it without reaching the engine.
|
|
587
|
+
def recursion_retry(recursion)
|
|
588
|
+
@impl.recursion_retry(@ctx, @handle, recursion.handle)
|
|
589
|
+
@poison = nil
|
|
590
|
+
rescue AssumeFailed, StopTest => e
|
|
591
|
+
@poison = e
|
|
592
|
+
raise
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# hegel_recursion_finish: false when the engine discarded the finished
|
|
596
|
+
# attempt, which the caller then draws again from the root.
|
|
597
|
+
def recursion_finish(recursion)
|
|
598
|
+
@impl.recursion_finish(@ctx, live_handle, recursion.handle)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# hegel_recursion_free. Takes no test-case handle, so it runs while an
|
|
602
|
+
# attempt is abandoned.
|
|
603
|
+
def recursion_free(recursion)
|
|
604
|
+
recursion.freed = true
|
|
605
|
+
@impl.recursion_free(@ctx, recursion.handle)
|
|
606
|
+
end
|
|
607
|
+
|
|
473
608
|
# hegel_generate_date, without recording. Hegel::Generators::
|
|
474
609
|
# DatesGenerator's own primitive. +min_value+/+max_value+ are each a
|
|
475
610
|
# [year, month, day] Array; the return value is the same shape.
|
|
476
611
|
def generate_date(min_value, max_value)
|
|
477
|
-
@impl.generate_date(@ctx,
|
|
612
|
+
@impl.generate_date(@ctx, live_handle, min_value, max_value)
|
|
478
613
|
end
|
|
479
614
|
|
|
480
615
|
# hegel_generate_time, without recording. Hegel::Generators::
|
|
@@ -482,7 +617,7 @@ module Hegel
|
|
|
482
617
|
# [hour, minute, second, microsecond] Array; the return value is the
|
|
483
618
|
# same shape.
|
|
484
619
|
def generate_time(min_value, max_value)
|
|
485
|
-
@impl.generate_time(@ctx,
|
|
620
|
+
@impl.generate_time(@ctx, live_handle, min_value, max_value)
|
|
486
621
|
end
|
|
487
622
|
|
|
488
623
|
# hegel_generate_datetime, without recording. Hegel::Generators::
|
|
@@ -491,11 +626,97 @@ module Hegel
|
|
|
491
626
|
# [hour, minute, second, microsecond] Array; the return value is a
|
|
492
627
|
# [[year, month, day], [hour, minute, second, microsecond]] pair.
|
|
493
628
|
def generate_datetime(min_date, min_time, max_date, max_time)
|
|
494
|
-
@impl.generate_datetime(@ctx,
|
|
629
|
+
@impl.generate_datetime(@ctx, live_handle, min_date, min_time, max_date, max_time)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Yields the Hegel::GrammarCache::Compiled under +key+ in the run's
|
|
633
|
+
# cache, calling +build+ to compile it on the first draw. Without a
|
|
634
|
+
# cache, it compiles one for this draw alone and frees it afterwards.
|
|
635
|
+
def with_grammar(key, build)
|
|
636
|
+
return yield @grammars.fetch(key, &build) if @grammars
|
|
637
|
+
|
|
638
|
+
compiled = build.call
|
|
639
|
+
begin
|
|
640
|
+
yield compiled
|
|
641
|
+
ensure
|
|
642
|
+
grammar_free(compiled.grammar)
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def grammar_builder_new(text)
|
|
647
|
+
@impl.grammar_builder_new(@ctx, text)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def grammar_builder_file(builder, index)
|
|
651
|
+
@impl.grammar_builder_file(@ctx, builder, index)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def grammar_builder_provide(builder, index, text)
|
|
655
|
+
@impl.grammar_builder_provide(@ctx, builder, index, text)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def grammar_builder_free(builder)
|
|
659
|
+
@impl.grammar_builder_free(@ctx, builder)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def grammar_new(builder, start, max_size, max_depth, alphabet, names)
|
|
663
|
+
@impl.grammar_new(@ctx, builder, start, max_size, max_depth, alphabet, names)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def grammar_placeholder(grammar, name)
|
|
667
|
+
@impl.grammar_placeholder(@ctx, grammar, name)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def grammar_free(grammar)
|
|
671
|
+
@impl.grammar_free(@ctx, grammar)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# hegel_generate_grammar, without recording.
|
|
675
|
+
def generate_grammar(grammar, max_size)
|
|
676
|
+
@impl.generate_grammar(@ctx, live_handle, grammar, max_size)
|
|
495
677
|
end
|
|
496
678
|
|
|
497
679
|
private
|
|
498
680
|
|
|
681
|
+
def live_handle
|
|
682
|
+
raise @poison if @poison
|
|
683
|
+
|
|
684
|
+
@handle
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Runs one #repeat iteration with the indent raised by REPEAT_INDENT.
|
|
688
|
+
# hegel-rust indents an iteration's output through hegel_test_case_block,
|
|
689
|
+
# which indents only what the engine printer prints. This library
|
|
690
|
+
# renders its report itself (Hegel::Report, docs/adr/0005), so the
|
|
691
|
+
# indent is kept on this side and recorded with each entry.
|
|
692
|
+
#
|
|
693
|
+
# The rescue order follows hegel-rust's TestCase::repeat, after the
|
|
694
|
+
# fatal exceptions every `rescue Exception` here lets through first. The
|
|
695
|
+
# library's own control exceptions, a nested #repeat's Hegel::LoopDone
|
|
696
|
+
# among them, and Hegel::Error, which stands where hegel-rust's
|
|
697
|
+
# InvalidArgument and InternalError stand, re-raise unchanged. A failed
|
|
698
|
+
# assumption skips to the next iteration. Anything else is a failure:
|
|
699
|
+
# one boolean is drawn before it re-raises, as hegel-rust draws one.
|
|
700
|
+
#
|
|
701
|
+
# standard:disable Lint/RescueException -- the same reason as
|
|
702
|
+
# Hegel::Runner.classify: a failing assertion descends from Exception.
|
|
703
|
+
def repeat_iteration
|
|
704
|
+
@indent += REPEAT_INDENT
|
|
705
|
+
begin
|
|
706
|
+
yield
|
|
707
|
+
ensure
|
|
708
|
+
@indent -= REPEAT_INDENT
|
|
709
|
+
end
|
|
710
|
+
rescue *Hegel::FATAL_EXCEPTIONS, Hegel::StopTest, Hegel::LoopDone, Hegel::Error
|
|
711
|
+
raise
|
|
712
|
+
rescue Hegel::AssumeFailed
|
|
713
|
+
# Skips this iteration; #repeat goes on to the next one.
|
|
714
|
+
rescue Exception
|
|
715
|
+
generate_boolean
|
|
716
|
+
raise
|
|
717
|
+
end
|
|
718
|
+
# standard:enable Lint/RescueException
|
|
719
|
+
|
|
499
720
|
# Runs the block with +generator+, freeing it via #string_generator_free
|
|
500
721
|
# whether the block returns or raises. The one #ensure
|
|
501
722
|
# #with_regex_generator/#with_email_generator/#with_url_generator/
|
|
@@ -522,7 +743,7 @@ module Hegel
|
|
|
522
743
|
def record_draw(label, value)
|
|
523
744
|
return unless @record
|
|
524
745
|
|
|
525
|
-
@entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value]
|
|
746
|
+
@entries << [:draw, name_for(label, DRAW_CALLER_DEPTH), value, @indent]
|
|
526
747
|
end
|
|
527
748
|
|
|
528
749
|
# The single place a draw's name is decided, tried in this order: the
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runner"
|
|
4
|
+
|
|
5
|
+
module Hegel
|
|
6
|
+
# Where a Hegel.test call lives, in the four pieces
|
|
7
|
+
# hegel_settings_set_test_location takes. Under Antithesis the engine
|
|
8
|
+
# reports each run's verdict as the assertion
|
|
9
|
+
# "<class_name>::<function> passes properties" at file:begin_line, so two
|
|
10
|
+
# calls that share a class_name and a function share one assertion.
|
|
11
|
+
TestLocation = Data.define(:file, :begin_line, :class_name, :function)
|
|
12
|
+
|
|
13
|
+
class TestLocation
|
|
14
|
+
# Matches the label prefix Ruby 3.4 puts on a frame inside a block, so
|
|
15
|
+
# that the method underneath names the test.
|
|
16
|
+
BLOCK_PREFIX = /\A(?:block(?: \(\d+ levels\))? in )+/
|
|
17
|
+
|
|
18
|
+
# Matches "Class#method" and "Class.method". Greedy, so a namespaced
|
|
19
|
+
# class keeps its "::" and a "#<Class:0x...>" label splits at its last
|
|
20
|
+
# separator.
|
|
21
|
+
METHOD_LABEL = /\A(.+)[#.]([^#.]+)\z/
|
|
22
|
+
|
|
23
|
+
# The first of +locations+ that is the caller's own, by the same rule
|
|
24
|
+
# Runner.origin_for uses to name a failure (see ADR 0012), so a wrapper in
|
|
25
|
+
# this gem, a gem, or the standard library is not named as the test.
|
|
26
|
+
# +locations+ is never empty, because the frame of Hegel.test's own caller
|
|
27
|
+
# exists even at the top of a script.
|
|
28
|
+
def self.from_locations(locations)
|
|
29
|
+
location = locations.find { |candidate| !Runner.infrastructure?(candidate.path) } || locations.first
|
|
30
|
+
path = location.absolute_path || location.path
|
|
31
|
+
root = File.join(Dir.pwd, "")
|
|
32
|
+
file = path.start_with?(root) ? path.delete_prefix(root) : path
|
|
33
|
+
class_name, function = names(location.label, file, location.lineno)
|
|
34
|
+
new(file: file, begin_line: location.lineno, class_name: class_name, function: function)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# [class_name, function] for a frame +label+. A frame with no enclosing
|
|
38
|
+
# method (a script body, or an RSpec example, whose block sits at the top
|
|
39
|
+
# of its file) has no name of its own, so it takes its file and line:
|
|
40
|
+
# that keeps two examples in one file from sharing one assertion.
|
|
41
|
+
def self.names(label, file, lineno)
|
|
42
|
+
match = METHOD_LABEL.match(label.sub(BLOCK_PREFIX, ""))
|
|
43
|
+
return match.captures if match
|
|
44
|
+
|
|
45
|
+
[file.delete_suffix(".rb"), "line #{lineno}"]
|
|
46
|
+
end
|
|
47
|
+
private_class_method :names
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/hegel/version.rb
CHANGED
data/lib/hegel.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "hegel/version"
|
|
4
4
|
require_relative "hegel/errors"
|
|
5
5
|
require_relative "hegel/runner"
|
|
6
|
+
require_relative "hegel/test_location"
|
|
6
7
|
require_relative "hegel/generator"
|
|
7
8
|
require_relative "hegel/generators"
|
|
8
9
|
require_relative "hegel/syntax/methods"
|
|
@@ -28,6 +29,13 @@ module Hegel
|
|
|
28
29
|
# assertion failure rather than as a Hegel-specific one. Raises
|
|
29
30
|
# Hegel::Error for a run-level failure instead of a property failure.
|
|
30
31
|
#
|
|
32
|
+
# +profile+ names the settings profile the run starts from: one of the
|
|
33
|
+
# engine's shipped profiles, one in hegel.toml, or one registered with
|
|
34
|
+
# Hegel::Settings.register_profile. Left nil, the run starts from the
|
|
35
|
+
# default profile. A keyword passed alongside still beats the profile, and
|
|
36
|
+
# report_multiple_failures is always passed, so a profile's value for it
|
|
37
|
+
# never applies (see docs/adr/0028). An unknown name raises Hegel::Error.
|
|
38
|
+
#
|
|
31
39
|
# +test_cases+, +seed+, +derandomize+, +verbosity+, +phases+, and
|
|
32
40
|
# +suppress_health_check+ all default to nil, which means the same thing
|
|
33
41
|
# for each of them: do not call the matching libhegel setter, and let the
|
|
@@ -54,6 +62,11 @@ module Hegel
|
|
|
54
62
|
# keyword above: see Hegel::Runner.run's own comment for why it is always
|
|
55
63
|
# passed.
|
|
56
64
|
#
|
|
65
|
+
# +stop_on_first_failure+, +print_blob+, +show_statistics+,
|
|
66
|
+
# +nondeterminism_strictness+, and +backend+ default to nil, so the engine's
|
|
67
|
+
# profile decides as it does for +test_cases+; they follow hegel-rust's
|
|
68
|
+
# Settings.
|
|
69
|
+
#
|
|
57
70
|
# +output+ (default $stderr) is where a failure report is written; a
|
|
58
71
|
# caller passes its own IO to capture that report instead (tests do).
|
|
59
72
|
#
|
|
@@ -66,13 +79,16 @@ module Hegel
|
|
|
66
79
|
# real engine. Ordinary callers never pass it. Its default expression
|
|
67
80
|
# (#default_impl) only runs when +impl+ is not given, so a test that does
|
|
68
81
|
# pass one never opens the native library at all.
|
|
69
|
-
def test(test_cases: nil, seed: nil, derandomize: nil, verbosity: nil, database: nil, database_key: nil,
|
|
70
|
-
phases: nil, suppress_health_check: nil, report_multiple_failures: false,
|
|
82
|
+
def test(profile: nil, test_cases: nil, seed: nil, derandomize: nil, verbosity: nil, database: nil, database_key: nil,
|
|
83
|
+
phases: nil, suppress_health_check: nil, report_multiple_failures: false, stop_on_first_failure: nil,
|
|
84
|
+
print_blob: nil, show_statistics: nil, nondeterminism_strictness: nil, backend: nil, output: $stderr,
|
|
71
85
|
reproduce_failure: nil, impl: default_impl, &block)
|
|
72
|
-
Runner.run(impl: impl, test_cases: test_cases, seed: seed, derandomize: derandomize, verbosity: verbosity,
|
|
86
|
+
Runner.run(impl: impl, profile: profile, test_cases: test_cases, seed: seed, derandomize: derandomize, verbosity: verbosity,
|
|
73
87
|
database: database, database_key: database_key, phases: phases, suppress_health_check: suppress_health_check,
|
|
74
|
-
report_multiple_failures: report_multiple_failures,
|
|
75
|
-
|
|
88
|
+
report_multiple_failures: report_multiple_failures, stop_on_first_failure: stop_on_first_failure,
|
|
89
|
+
print_blob: print_blob, show_statistics: show_statistics, nondeterminism_strictness: nondeterminism_strictness,
|
|
90
|
+
backend: backend, output: output, reproduce_failure: reproduce_failure,
|
|
91
|
+
test_location: TestLocation.from_locations(caller_locations(1)), &block)
|
|
76
92
|
end
|
|
77
93
|
|
|
78
94
|
# The Hegel::LibHegel::Real instance #test uses by default, built once and
|