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
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
- [Setup](#setup)
|
|
6
6
|
- [Test Structure](#test-structure): `Hegel.test`, its block, return value, failure behavior
|
|
7
|
-
- [Settings](#settings): `test_cases:` `seed:` `derandomize:` `verbosity:` `database:` `database_key:` `phases:` `suppress_health_check:` `report_multiple_failures:` `output:` `reproduce_failure:`
|
|
8
|
-
- [TestCase Methods](#testcase-methods): `draw`, `draw_integer`, `draw_boolean`, `assume`, `reject`, `note`, `target`
|
|
9
|
-
- [Generator Reference](#generator-reference): `booleans`, `integers`, `floats`, `text`, `arrays`, `just`, `sampled_from`, `one_of`, `optional`, `tuples`, `sets`, `hashes`, `characters`, `binary`, `from_regex`, `emails`, `urls`, `domains`, `ip_addresses`, `uuids`, `dates`, `times`, `datetimes`, `composite`, `deferred`
|
|
7
|
+
- [Settings](#settings): `profile:` `test_cases:` `seed:` `derandomize:` `verbosity:` `database:` `database_key:` `phases:` `suppress_health_check:` `report_multiple_failures:` `stop_on_first_failure:` `print_blob:` `show_statistics:` `nondeterminism_strictness:` `backend:` `output:` `reproduce_failure:`
|
|
8
|
+
- [TestCase Methods](#testcase-methods): `draw`, `draw_integer`, `draw_boolean`, `assume`, `reject`, `note`, `repeat`, `target`, `event`, `event_value`
|
|
9
|
+
- [Generator Reference](#generator-reference): `booleans`, `integers`, `floats`, `text`, `arrays`, `just`, `sampled_from`, `permutations`, `subsequences`, `samples`, `one_of`, `optional`, `tuples`, `sets`, `hashes`, `characters`, `binary`, `from_regex`, `from_lark`, `emails`, `urls`, `domains`, `ip_addresses`, `uuids`, `dates`, `times`, `datetimes`, `date_strings`, `time_strings`, `datetime_strings`, `durations`, `composite`, `deferred`, `recursive`
|
|
10
10
|
- [Stateful Testing](#stateful-testing): `Hegel::StateMachine`, `rule`, `invariant`, `Hegel::Stateful.run`, `Hegel::Stateful::Pool`
|
|
11
11
|
- [Combinator Methods](#combinator-methods): `.map`, `.filter`
|
|
12
12
|
- [Gotchas](#gotchas)
|
|
@@ -34,7 +34,7 @@ include Hegel::Syntax::Methods
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`Hegel::Syntax::Methods` makes every generator method (`booleans` through
|
|
37
|
-
`
|
|
37
|
+
`recursive`; see [Generator Reference](#generator-reference)) callable bare,
|
|
38
38
|
the way FactoryBot's own `Syntax::Methods` makes `create` callable bare.
|
|
39
39
|
With Minitest, include it once on the base test class:
|
|
40
40
|
|
|
@@ -142,6 +142,7 @@ the reproduction line.
|
|
|
142
142
|
|
|
143
143
|
| Keyword | Type | Default | Purpose |
|
|
144
144
|
|---|---|---|---|
|
|
145
|
+
| `profile` | `String` or `nil` | `nil` (the default profile) | Settings profile the run starts from: `"development"`, `"ci"`, `"workload"`, one in `hegel.toml`, or one from `Hegel::Settings.register_profile`; an unknown name raises `Hegel::Error`; a keyword passed alongside still wins |
|
|
145
146
|
| `test_cases` | `Integer` or `nil` | `nil` (libhegel's own default: 100) | Number of test cases to run |
|
|
146
147
|
| `seed` | `Integer` or `nil` | `nil` (libhegel picks its own) | Fixed RNG seed; pair with `derandomize: true` for a reproducible sequence |
|
|
147
148
|
| `derandomize` | `true`/`false` or `nil` | `nil` (libhegel's own default) | Derive the seed deterministically from the test itself, instead of drawing a random one |
|
|
@@ -151,12 +152,19 @@ the reproduction line.
|
|
|
151
152
|
| `phases` | `Array` of `Symbol` or `nil` | `nil` (libhegel's own default: every phase) | Which run phases to enable: `:explicit`, `:reuse`, `:generate`, `:target`, `:shrink` |
|
|
152
153
|
| `suppress_health_check` | `Array` of `Symbol` or `nil` | `nil` (no suppression) | Which health checks to turn off: `:filter_too_much`, `:too_slow`, `:test_cases_too_large`, `:large_initial_test_case` |
|
|
153
154
|
| `report_multiple_failures` | `true`/`false` | `false` | `true` summarizes every distinct failure into one `Hegel::Error` instead of re-raising a single failure's own exception |
|
|
155
|
+
| `stop_on_first_failure` | `true`/`false` or `nil` | `nil` (libhegel's own default: `true`) | `false` keeps generating after a failure and spends the whole `test_cases` budget before reporting |
|
|
156
|
+
| `print_blob` | `true`/`false` or `nil` | `nil` (libhegel's own default: `true`) | `false` leaves the `reproduce_failure:` line out of the failure report |
|
|
157
|
+
| `show_statistics` | `true`/`false` or `nil` | `nil` (libhegel's own default: `false`) | `true` prints the statistics `event` and `event_value` recorded, on stderr, at the end of the run |
|
|
158
|
+
| `nondeterminism_strictness` | `Symbol` or `nil` | `nil` (libhegel's own default: `:quiet`) | How a run reacts to a test that behaves differently on a replay: `:quiet`, `:warn` (print a notice), or `:error` (raise `Hegel::Error`) |
|
|
159
|
+
| `backend` | `Symbol` or `nil` | `nil` (libhegel's own default: `:default`) | `:urandom` takes fresh entropy on every draw, for Antithesis; `:default` is the seeded generator |
|
|
154
160
|
| `output` | `IO` | `$stderr` | Where a failure report is written |
|
|
155
161
|
| `reproduce_failure` | `String` or `nil` | `nil` | Replays the blob as a run until a replay fails, instead of running a full property |
|
|
156
162
|
|
|
157
163
|
`nil` means the same thing for `test_cases`, `seed`, `derandomize`,
|
|
158
|
-
`verbosity`, `phases`,
|
|
159
|
-
|
|
164
|
+
`verbosity`, `phases`, `suppress_health_check`, `stop_on_first_failure`,
|
|
165
|
+
`print_blob`, `show_statistics`, `nondeterminism_strictness`, and `backend`:
|
|
166
|
+
do not call the matching libhegel setter, and let the engine's settings
|
|
167
|
+
profile decide. That is
|
|
160
168
|
libhegel's own default, unless a `hegel.toml` or a `HEGEL_*` environment
|
|
161
169
|
variable (such as `HEGEL_TEST_CASES`) sets the value. On a CI server the
|
|
162
170
|
engine picks its `ci` profile, which derandomizes the run, turns the example
|
|
@@ -194,6 +202,21 @@ Hegel.test(phases: []) { |tc| tc.draw(integers) }
|
|
|
194
202
|
# :reuse, :generate, :target, :shrink], got an empty Array"
|
|
195
203
|
```
|
|
196
204
|
|
|
205
|
+
`Hegel::Settings.register_profile(name, profile: nil, **keywords)` registers
|
|
206
|
+
the settings built from `profile` (default profile when `nil`) and the
|
|
207
|
+
keywords as a profile for the whole process. It takes the keywords above
|
|
208
|
+
except `database_key` (a profile does not carry it), and passes `database:`
|
|
209
|
+
straight to the engine (`""` disables the database). `report_multiple_failures:`
|
|
210
|
+
is applied only when given, and `Hegel.test` overrides it anyway.
|
|
211
|
+
`Hegel::Settings.set_default_profile(name)` makes a profile the default for
|
|
212
|
+
every run in the process, and `""` clears it. Neither changes a run that has
|
|
213
|
+
already started.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Hegel::Settings.register_profile("nightly", profile: "ci", test_cases: 10_000)
|
|
217
|
+
Hegel.test(profile: "nightly") { |tc| ... }
|
|
218
|
+
```
|
|
219
|
+
|
|
197
220
|
`report_multiple_failures:` defaults to `false`, and is always passed, so a
|
|
198
221
|
profile cannot turn it on by accident. With the default `false`, a run
|
|
199
222
|
stops at its first failing example and re-raises that failure's own
|
|
@@ -252,7 +275,10 @@ written to it.
|
|
|
252
275
|
| `assume` | `assume(condition)` | Discard this test case (see `reject`) unless `condition` holds, read as Ruby truthiness |
|
|
253
276
|
| `reject` | `reject` | Discard this test case unconditionally |
|
|
254
277
|
| `note` | `note(message = nil, &block)` | Record `message` (or the block's return value) for the failure report, interleaved with draws in call order |
|
|
278
|
+
| `repeat` | `repeat { ... }` | Run the block again and again, for as many iterations as the engine chooses; never returns unless the block calls `break` |
|
|
255
279
|
| `target` | `target(value, label: "")` | Feed `value` to libhegel's own hill-climbing search between generation rounds |
|
|
280
|
+
| `event` | `event(label)` | Record `label` for the statistics that `show_statistics: true` prints |
|
|
281
|
+
| `event_value` | `event_value(label, value)` | Record a numeric `value` under `label` for the same statistics |
|
|
256
282
|
|
|
257
283
|
```ruby
|
|
258
284
|
result = Hegel.test(test_cases: 10, verbosity: :quiet) do |tc|
|
|
@@ -356,6 +382,48 @@ To reproduce this failure, pass the blob below to Hegel.test:
|
|
|
356
382
|
reproduce_failure: "AAEAAAAACgIAAAD1AQ=="
|
|
357
383
|
```
|
|
358
384
|
|
|
385
|
+
`repeat` runs its block again and again, for as many iterations as the
|
|
386
|
+
engine chooses, and shrinks a failure toward fewer iterations. State kept
|
|
387
|
+
outside the block carries from one iteration to the next. The report shows
|
|
388
|
+
each iteration as a `// Repetition #N` line, with what the iteration drew or
|
|
389
|
+
noted indented under it:
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
output = StringIO.new
|
|
393
|
+
begin
|
|
394
|
+
Hegel.test(output: output, seed: 1, derandomize: true) do |tc|
|
|
395
|
+
stack = []
|
|
396
|
+
tc.repeat do
|
|
397
|
+
n = tc.draw(integers(min_value: 0, max_value: 10))
|
|
398
|
+
stack.push(n)
|
|
399
|
+
raise "stack sum too big: #{stack.sum}" if stack.sum > 10
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
rescue => e
|
|
403
|
+
e.message # => "stack sum too big: 11"
|
|
404
|
+
end
|
|
405
|
+
output.string
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
Falsified after 2 test cases (0 discarded):
|
|
410
|
+
|
|
411
|
+
// Repetition #1
|
|
412
|
+
n_1 = 1
|
|
413
|
+
// Repetition #2
|
|
414
|
+
n_2 = 10
|
|
415
|
+
|
|
416
|
+
To reproduce this failure, pass the blob below to Hegel.test:
|
|
417
|
+
reproduce_failure: "AXicY2MAAi5GEMnICGEwwhhAAgADbwA4"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
A `tc.assume` or `tc.reject` inside the block skips that iteration, not the
|
|
421
|
+
whole test case. When the engine ends the loop, `repeat` raises
|
|
422
|
+
`Hegel::LoopDone` instead of returning, and `Hegel.test` counts the test
|
|
423
|
+
case as passing; code after `tc.repeat` runs only when the block calls
|
|
424
|
+
`break`. `Hegel::LoopDone` descends from `Exception`, so `rescue => e` does
|
|
425
|
+
not catch it.
|
|
426
|
+
|
|
359
427
|
`target` feeds `value` to libhegel's own hill-climbing search between
|
|
360
428
|
generation rounds, under `label` (default `""`); it does not appear in the
|
|
361
429
|
failure report. The same `label` used twice on one test case raises
|
|
@@ -377,6 +445,24 @@ Hegel.test(test_cases: 5, verbosity: :quiet) { |tc| tc.target(1, label: "score")
|
|
|
377
445
|
# observed at most once per test case"
|
|
378
446
|
```
|
|
379
447
|
|
|
448
|
+
`event` and `event_value` record what a test reached, for the statistics
|
|
449
|
+
that `show_statistics: true` prints on stderr at the end of the run. `event`
|
|
450
|
+
counts the share of generated test cases that recorded a label. `event_value`
|
|
451
|
+
summarizes the values recorded under a label, which can be recorded any
|
|
452
|
+
number of times per test case. Neither steers generation, and neither
|
|
453
|
+
appears in the failure report. A non-finite `value` raises `Hegel::Error`:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
Hegel.test(show_statistics: true) do |tc|
|
|
457
|
+
xs = tc.draw(arrays(integers))
|
|
458
|
+
tc.event("empty") if xs.empty?
|
|
459
|
+
tc.event_value("size", xs.size)
|
|
460
|
+
end
|
|
461
|
+
# Statistics (over 100 test cases):
|
|
462
|
+
# * empty: 6.0% of test cases
|
|
463
|
+
# * size: count 100, min 0, median 6, mean 7.99, p90 19, max 20
|
|
464
|
+
```
|
|
465
|
+
|
|
380
466
|
## Generator Reference
|
|
381
467
|
|
|
382
468
|
Every generator method lives in `Hegel::Syntax::Methods` (bare, once
|
|
@@ -435,7 +521,7 @@ Hegel.test(verbosity: :quiet) { |tc| tc.draw(integers(min_value: 10**30, max_val
|
|
|
435
521
|
# => nil
|
|
436
522
|
```
|
|
437
523
|
|
|
438
|
-
### `floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false, exclude_max: false)`
|
|
524
|
+
### `floats(min_value: nil, max_value: nil, allow_nan: nil, allow_infinity: nil, exclude_min: false, exclude_max: false, allow_subnormal: nil)`
|
|
439
525
|
|
|
440
526
|
A double in `[min_value, max_value]`, unbounded by default.
|
|
441
527
|
|
|
@@ -445,6 +531,12 @@ side open, `false` when both bounds are set. hegel-rust and
|
|
|
445
531
|
hegel-typescript's `floats()` default the same way. Pass `true` or `false` to
|
|
446
532
|
override either (see [Gotchas](#gotchas)).
|
|
447
533
|
|
|
534
|
+
`allow_subnormal` defaults to `true` whenever the bounds admit a subnormal
|
|
535
|
+
(a double below `Float::MIN` in magnitude, other than zero), and to `false`
|
|
536
|
+
when every value the bounds allow is at least `Float::MIN` in magnitude.
|
|
537
|
+
Pass `false` for code that may run with flush-to-zero floating point, where
|
|
538
|
+
a subnormal input silently becomes zero.
|
|
539
|
+
|
|
448
540
|
```ruby
|
|
449
541
|
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
450
542
|
f = tc.draw(floats(min_value: 0.0, max_value: 1.0, exclude_min: true, exclude_max: true))
|
|
@@ -465,10 +557,24 @@ Three combinations raise `Hegel::Error` at draw time:
|
|
|
465
557
|
- `allow_infinity: true` with both bounds:
|
|
466
558
|
`"floats: cannot have allow_infinity=true with both min_value and max_value"`
|
|
467
559
|
|
|
468
|
-
|
|
560
|
+
Three more concern `allow_subnormal`, each prefixed `"floats: InvalidArgument:
|
|
561
|
+
"`:
|
|
562
|
+
|
|
563
|
+
- `allow_subnormal: true` with a `min_value` of `Float::MIN` or more:
|
|
564
|
+
`"allow_subnormal=true, but min_value excludes all values below the
|
|
565
|
+
smallest positive normal 2.2250738585072014e-308"`
|
|
566
|
+
- `allow_subnormal: true` with a `max_value` of `-Float::MIN` or less:
|
|
567
|
+
`"allow_subnormal=true, but max_value excludes all values above the
|
|
568
|
+
smallest negative normal -2.2250738585072014e-308"`
|
|
569
|
+
- `allow_subnormal: false` with both bounds, no zero between them, and only
|
|
570
|
+
subnormals inside: `"allow_subnormal=false leaves no 64-bit floating-point
|
|
571
|
+
values in [5.0e-324, 1.0e-310]"`
|
|
572
|
+
|
|
573
|
+
### `text(min_size: 0, max_size: nil, alphabet: nil, codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil, include_characters: nil, exclude_characters: nil)`
|
|
469
574
|
|
|
470
|
-
A Unicode string of `[min_size, max_size]` characters
|
|
471
|
-
|
|
575
|
+
A Unicode string of `[min_size, max_size]` characters. `max_size` defaults
|
|
576
|
+
to 100, as in hegel-rust, or to `min_size + 100` when `min_size` is above
|
|
577
|
+
100.
|
|
472
578
|
|
|
473
579
|
```ruby
|
|
474
580
|
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
@@ -487,28 +593,71 @@ Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
|
487
593
|
end
|
|
488
594
|
```
|
|
489
595
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
596
|
+
The alphabet options select which characters a string can hold:
|
|
597
|
+
|
|
598
|
+
```ruby
|
|
599
|
+
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
600
|
+
s = tc.draw(text(alphabet: "ßaİ", max_size: 4))
|
|
601
|
+
raise "left the alphabet" unless s.match?(/\A[ßaİ]*\z/)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
605
|
+
s = tc.draw(text(categories: ["Lu"], min_size: 1, max_size: 5))
|
|
606
|
+
raise "not uppercase" unless s.match?(/\A\p{Lu}+\z/)
|
|
607
|
+
end
|
|
495
608
|
|
|
496
|
-
|
|
497
|
-
|
|
609
|
+
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
610
|
+
s = tc.draw(text(exclude_characters: ",\t\n", max_size: 6))
|
|
611
|
+
raise "drew an excluded character" if s.match?(/[,\t\n]/)
|
|
612
|
+
end
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
- `alphabet:` is a `String` of the permitted characters. It cannot be combined
|
|
616
|
+
with any of the options below.
|
|
617
|
+
- `codec:` is `"ascii"`, `"latin-1"` (or `"iso-8859-1"`), or `"utf-8"`. It
|
|
618
|
+
sets the starting range, which `min_codepoint:` and `max_codepoint:`
|
|
619
|
+
narrow.
|
|
620
|
+
- `categories:` keeps the union of the named Unicode general categories
|
|
621
|
+
(`"Lu"`, `"Nd"`, and so on); `[]` is an empty alphabet. `exclude_categories:`
|
|
622
|
+
removes categories. When both are given, `categories:` wins and
|
|
623
|
+
`exclude_categories:` is ignored, as in hegel-rust.
|
|
624
|
+
- `include_characters:` adds characters to the alphabet whatever the other
|
|
625
|
+
options say; `exclude_characters:` removes them. A character in both raises
|
|
626
|
+
`Hegel::Error`.
|
|
627
|
+
|
|
628
|
+
Strings never hold surrogates, and `categories:` rejects `"Cs"` and `"C"`.
|
|
629
|
+
The engine's Unicode tables can be newer than Ruby's, so a check with
|
|
630
|
+
`\p{...}` in a test can disagree with the engine on a recently assigned
|
|
631
|
+
codepoint.
|
|
632
|
+
|
|
633
|
+
These cases raise `Hegel::Error` at draw time:
|
|
634
|
+
|
|
635
|
+
- `max_size < min_size`: `"text: max_size < min_size"`.
|
|
636
|
+
- An `alphabet:` beside another character option: `"text: Cannot combine
|
|
637
|
+
.alphabet() with character methods."`.
|
|
638
|
+
- An `alphabet:` that is not a `String`: `"text: alphabet must be a String,
|
|
639
|
+
not a Integer"`.
|
|
640
|
+
- An unknown codec: `"text: invalid codec: utf-16"`.
|
|
641
|
+
- A surrogate category in `categories:`: `"text: Category \"Cs\" includes
|
|
642
|
+
surrogate codepoints (Cs), which a valid UTF-8 String cannot hold."`.
|
|
643
|
+
- An unknown category, an empty range (`min_codepoint:` above
|
|
644
|
+
`max_codepoint:`, or every character excluded), and an overlap between
|
|
645
|
+
`include_characters:` and `exclude_characters:`: the engine writes the
|
|
646
|
+
message, as `"HEGEL_E_INVALID_ARG (-5): \"foo\" is not a valid Unicode
|
|
647
|
+
category"`.
|
|
648
|
+
|
|
649
|
+
A regular expression still suits a pattern that is more than an alphabet.
|
|
650
|
+
These results were measured with libhegel 0.47.1:
|
|
498
651
|
|
|
499
652
|
| Job | Generator | Measured output |
|
|
500
653
|
|---|---|---|
|
|
501
654
|
| Characters to include | `from_regex("[ぁ-んA-Z0-9]{1,5}", fullmatch: true)` | `"0"`, `"D"`, `"Yもで"`; 30 draws all matched the pattern |
|
|
502
655
|
| Characters to exclude | `from_regex("[^,\\t\\n]{0,6}", fullmatch: true)` | `""`, `"RÈbÁ¡"`; 30 draws contained no comma, tab, or newline |
|
|
503
|
-
| A contiguous block | `text(min_codepoint: 0x300, max_codepoint: 0x36F)` | Every character of 100 draws was in U+0300 through U+036F |
|
|
504
|
-
| An arbitrary alphabet | `arrays(sampled_from(%w[ß a İ]), max_size: 4).map(&:join)` | 30 draws, each built only from `ß`, `a`, and `İ` |
|
|
505
656
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
The
|
|
510
|
-
filtering `text` to `\p{Han}` produced no valid inputs. The engine filtered 50
|
|
511
|
-
inputs and reported this health check:
|
|
657
|
+
`.filter` is not a substitute for an alphabet option. In a measured run of 50
|
|
658
|
+
test cases, filtering `text` to `\p{Han}` produced no valid inputs. A script
|
|
659
|
+
such as Han is not a general category, so `categories:` cannot select it
|
|
660
|
+
either. The engine filtered 50 inputs and reported this health check:
|
|
512
661
|
|
|
513
662
|
```
|
|
514
663
|
FailedHealthCheck: FilterTooMuch — it looks like this test is filtering out
|
|
@@ -525,10 +674,11 @@ Unconfigured `text` draws from a much wider Unicode range. In 200 draws of
|
|
|
525
674
|
characters, and control characters. The highest codepoint was U+10A78F. A
|
|
526
675
|
property that accepts any string needs no character-set options.
|
|
527
676
|
|
|
528
|
-
### `arrays(elements, min_size: 0, max_size: nil)`
|
|
677
|
+
### `arrays(elements, min_size: 0, max_size: nil, unique: false)`
|
|
529
678
|
|
|
530
679
|
An `Array` of values from the `elements` generator, with `[min_size,
|
|
531
|
-
max_size]` entries, unbounded above by default.
|
|
680
|
+
max_size]` entries, unbounded above by default. With `unique: true`, no two
|
|
681
|
+
entries are equal.
|
|
532
682
|
|
|
533
683
|
```ruby
|
|
534
684
|
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
@@ -539,9 +689,23 @@ end
|
|
|
539
689
|
|
|
540
690
|
`max_size < min_size` raises `Hegel::Error` at draw time: `"arrays:
|
|
541
691
|
max_size < min_size"`. A negative `min_size` also raises at draw time:
|
|
542
|
-
`"arrays: min_size must not be negative"`.
|
|
543
|
-
|
|
544
|
-
|
|
692
|
+
`"arrays: min_size must not be negative"`.
|
|
693
|
+
|
|
694
|
+
With `unique: true`, an element equal (`==`) to one already drawn is
|
|
695
|
+
rejected and drawn again, so `min_size` still counts distinct entries.
|
|
696
|
+
Three slots over three values hold each exactly once:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
xs = nil
|
|
700
|
+
Hegel.test(test_cases: 1, verbosity: :quiet) do |tc|
|
|
701
|
+
xs = tc.draw(arrays(sampled_from([0, 1, 2]), min_size: 3, max_size: 3, unique: true))
|
|
702
|
+
end
|
|
703
|
+
xs.sort # => [0, 1, 2]
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
A `min_size` the elements cannot satisfy, such as `arrays(just(1), min_size:
|
|
707
|
+
5, unique: true)`, ends the run with a `Hegel::Error` from the FilterTooMuch
|
|
708
|
+
health check.
|
|
545
709
|
|
|
546
710
|
### `just(value)`
|
|
547
711
|
|
|
@@ -570,6 +734,62 @@ result # => nil
|
|
|
570
734
|
An empty `collection` raises `Hegel::Error` at draw time:
|
|
571
735
|
`"sampled_from: collection must not be empty"`.
|
|
572
736
|
|
|
737
|
+
### `permutations(elements)`
|
|
738
|
+
|
|
739
|
+
An `Array` holding every element of `elements` in an order Hegel picks,
|
|
740
|
+
shrinking towards the original order. `elements` is anything `sampled_from`
|
|
741
|
+
accepts.
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
seen = []
|
|
745
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
746
|
+
seen << tc.draw(permutations([1, 2, 3]))
|
|
747
|
+
end
|
|
748
|
+
seen # => [[1, 2, 3], [2, 3, 1], [3, 2, 1]]
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### `subsequences(elements, min_size: 0, max_size: nil)`
|
|
752
|
+
|
|
753
|
+
An `Array` of `[min_size, max_size]` of the `elements`, in their original
|
|
754
|
+
order. Duplicate elements count as distinct.
|
|
755
|
+
|
|
756
|
+
```ruby
|
|
757
|
+
seen = []
|
|
758
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
759
|
+
seen << tc.draw(subsequences([1, 2, 3, 4], min_size: 1))
|
|
760
|
+
end
|
|
761
|
+
seen # => [[1], [3], [1, 2, 3, 4]]
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
`max_size < min_size` raises `Hegel::Error` at draw time: `"subsequences:
|
|
765
|
+
Cannot have max_size < min_size"`. So does a `min_size` above the number of
|
|
766
|
+
elements: `"subsequences: Cannot generate a subsequence: min_size 4 is larger
|
|
767
|
+
than the 3 elements in the sequence"`.
|
|
768
|
+
|
|
769
|
+
### `samples(elements, min_size: 0, max_size: nil, replacement: true)`
|
|
770
|
+
|
|
771
|
+
An `Array` of `[min_size, max_size]` of the `elements`, in the order drawn.
|
|
772
|
+
With `replacement: true` each is drawn independently from all of `elements`,
|
|
773
|
+
so a value can repeat, and the size is unbounded unless `max_size` bounds
|
|
774
|
+
it. With `replacement: false` each element is used at most once, duplicates
|
|
775
|
+
count as distinct, and the size is capped at the number of elements.
|
|
776
|
+
|
|
777
|
+
```ruby
|
|
778
|
+
seen = []
|
|
779
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
780
|
+
seen << tc.draw(samples([1, 2, 3], max_size: 4))
|
|
781
|
+
end
|
|
782
|
+
seen # => [[], [1, 3], []]
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Three errors are raised at draw time, each beginning `"samples: "`:
|
|
786
|
+
`max_size < min_size` gives `"Cannot have max_size < min_size"`; a
|
|
787
|
+
non-empty sample from an empty `elements` gives `"Cannot generate a
|
|
788
|
+
non-empty sample from an empty sequence"`; and with `replacement: false`, a
|
|
789
|
+
`min_size` above the number of elements gives `"Cannot generate a sample
|
|
790
|
+
without replacement: min_size 4 is larger than the 3 elements in the
|
|
791
|
+
sequence"`.
|
|
792
|
+
|
|
573
793
|
### `one_of(*generators)`
|
|
574
794
|
|
|
575
795
|
A value drawn from one of `generators`, picked at random.
|
|
@@ -677,9 +897,10 @@ seen.to_a # => [[5, false], [3, false]]
|
|
|
677
897
|
max_size < min_size"`. A negative `min_size` also raises at draw time:
|
|
678
898
|
`"hashes: min_size must not be negative"`.
|
|
679
899
|
|
|
680
|
-
### `characters(codec: nil, min_codepoint: nil, max_codepoint: nil)`
|
|
900
|
+
### `characters(codec: nil, min_codepoint: nil, max_codepoint: nil, categories: nil, exclude_categories: nil, include_characters: nil, exclude_characters: nil)`
|
|
681
901
|
|
|
682
902
|
A `String` of exactly one character, sharing `text`'s own alphabet options.
|
|
903
|
+
Pass it as `from_regex(alphabet:)` to limit the characters of a match.
|
|
683
904
|
|
|
684
905
|
```ruby
|
|
685
906
|
result = Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
@@ -695,10 +916,13 @@ end
|
|
|
695
916
|
seen # => ["A", "Z", "E", "T", "G"]
|
|
696
917
|
```
|
|
697
918
|
|
|
919
|
+
The options and their draw-time errors are the ones listed under `text`,
|
|
920
|
+
with `characters:` in place of `text:` in each message.
|
|
921
|
+
|
|
698
922
|
### `binary(min_size: 0, max_size: nil)`
|
|
699
923
|
|
|
700
|
-
A byte `String` of `[min_size, max_size]` bytes
|
|
701
|
-
|
|
924
|
+
A byte `String` of `[min_size, max_size]` bytes. `max_size` defaults to 100,
|
|
925
|
+
as in hegel-rust, or to `min_size + 100` when `min_size` is above 100.
|
|
702
926
|
|
|
703
927
|
```ruby
|
|
704
928
|
result = Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
@@ -717,22 +941,24 @@ seen # => ["\x00\x00", "\xCA\x95", "\xD5N"]
|
|
|
717
941
|
`max_size < min_size` raises `Hegel::Error` at draw time: `"binary:
|
|
718
942
|
max_size < min_size"`.
|
|
719
943
|
|
|
720
|
-
### `from_regex(pattern, fullmatch:
|
|
944
|
+
### `from_regex(pattern, fullmatch: true, alphabet: nil)`
|
|
721
945
|
|
|
722
946
|
A `String` matching `pattern` (Python `re` syntax, not Ruby's `Regexp`
|
|
723
|
-
syntax). `fullmatch
|
|
724
|
-
a match.
|
|
947
|
+
syntax). `fullmatch`, true by default as in hegel-rust, requires the whole
|
|
948
|
+
string to match; `fullmatch: false` lets the string merely contain a match.
|
|
949
|
+
`alphabet` is a `characters` generator that limits every character of the
|
|
950
|
+
string, padding around a partial match included.
|
|
725
951
|
|
|
726
952
|
```ruby
|
|
727
953
|
result = Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
728
|
-
v = tc.draw(from_regex("[a-z]{3}"))
|
|
954
|
+
v = tc.draw(from_regex("[a-z]{3}", fullmatch: false))
|
|
729
955
|
raise "no match" unless v.match?(/[a-z]{3}/)
|
|
730
956
|
end
|
|
731
957
|
result # => nil
|
|
732
958
|
|
|
733
959
|
seen = []
|
|
734
960
|
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
735
|
-
seen << tc.draw(from_regex("[a-z]{3}"
|
|
961
|
+
seen << tc.draw(from_regex("[a-z]{3}"))
|
|
736
962
|
end
|
|
737
963
|
seen # => ["aaa", "pgq", "iqy"]
|
|
738
964
|
```
|
|
@@ -744,6 +970,18 @@ translated): `from_regex(/[a-z]{3}/)` raises `Hegel::Error` at draw time,
|
|
|
744
970
|
Regexp"`. Pass `my_regexp.source` explicitly for a pattern confirmed to
|
|
745
971
|
need no flags and to use only syntax the two grammars share.
|
|
746
972
|
|
|
973
|
+
```ruby
|
|
974
|
+
Hegel.test(test_cases: 30, verbosity: :quiet) do |tc|
|
|
975
|
+
v = tc.draw(from_regex("[a-z]+", fullmatch: true, alphabet: characters(codec: "ascii")))
|
|
976
|
+
raise "no match" unless v.match?(/\A[a-z]+\z/)
|
|
977
|
+
end
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
An `alphabet` that is not a `characters` generator raises `Hegel::Error` at
|
|
981
|
+
draw time: `"from_regex: alphabet must be a characters generator, not a
|
|
982
|
+
String"`. A pattern that cannot match any string over the alphabet raises
|
|
983
|
+
with the engine's message, `"... matches no string over the alphabet ..."`.
|
|
984
|
+
|
|
747
985
|
A pattern that no string matches, such as `"(?!)"`, also raises
|
|
748
986
|
`Hegel::Error` at draw time. The engine writes the message, and it names
|
|
749
987
|
the pattern: `from_regex("(?!)")` raises
|
|
@@ -751,6 +989,68 @@ the pattern: `from_regex("(?!)")` raises
|
|
|
751
989
|
generator can produce"`. Since libhegel 0.45.7 the engine makes this
|
|
752
990
|
check.
|
|
753
991
|
|
|
992
|
+
### `from_lark(grammar, start: nil, explicit: {}, alphabet: nil, max_size: nil, max_depth: nil, import_path: nil)`
|
|
993
|
+
|
|
994
|
+
A `String` that `grammar`, written in the syntax of the
|
|
995
|
+
[Lark](https://lark-parser.readthedocs.io/en/stable/grammar.html) parsing
|
|
996
|
+
library, accepts from its `start` rule (`"start"` when `start` is nil).
|
|
997
|
+
`%import` of Lark's bundled `common`, `python`, `unicode`, and `lark`
|
|
998
|
+
grammars works without `import_path`; `import_path` (a `String` or a
|
|
999
|
+
`Pathname`) is the one directory other imports are read from, `%import
|
|
1000
|
+
a.b.X` reading `a/b.lark` there. Strings run up to 8192 characters by
|
|
1001
|
+
default; `max_size` caps the characters (at most 1048576) and `max_depth`
|
|
1002
|
+
how deeply rules nest (default 200, at most 500). `alphabet` is a
|
|
1003
|
+
`characters` generator limiting what a terminal's pattern draws.
|
|
1004
|
+
|
|
1005
|
+
```ruby
|
|
1006
|
+
INT_LIST = "start: INT (\",\" INT)*\n%import common.INT\n"
|
|
1007
|
+
|
|
1008
|
+
result = Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
1009
|
+
v = tc.draw(from_lark(INT_LIST))
|
|
1010
|
+
raise "not an int list" unless v.match?(/\A\d+(,\d+)*\z/)
|
|
1011
|
+
end
|
|
1012
|
+
result # => nil
|
|
1013
|
+
|
|
1014
|
+
seen = []
|
|
1015
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1016
|
+
seen << tc.draw(from_lark(INT_LIST, max_size: 12))
|
|
1017
|
+
end
|
|
1018
|
+
seen # => ["0", "7", "62010192143"]
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
`explicit` maps a terminal's name, a `String` or a `Symbol`, to a generator
|
|
1022
|
+
of `String`s drawn in its place. Every terminal the grammar `%declare`s
|
|
1023
|
+
needs one; any other terminal may be given one to override its pattern.
|
|
1024
|
+
The values count toward `max_size`: a string they push over it is drawn
|
|
1025
|
+
again with less room for the grammar's own text, and after three tries the
|
|
1026
|
+
test case is rejected, as a failed `assume` is.
|
|
1027
|
+
|
|
1028
|
+
```ruby
|
|
1029
|
+
seen = []
|
|
1030
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1031
|
+
seen << tc.draw(from_lark("start: WORD \"=\" NUM\n%declare NUM\nWORD: /[a-z]+/\n",
|
|
1032
|
+
explicit: {NUM: integers(min_value: 0, max_value: 99).map(&:to_s)}, max_size: 10))
|
|
1033
|
+
end
|
|
1034
|
+
seen # => ["a=0", "t=99", "tokndf=53"]
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
The grammar is compiled on the first draw of a run and kept until the run
|
|
1038
|
+
ends, so a `from_lark` built inside the test body compiles once per run too.
|
|
1039
|
+
|
|
1040
|
+
Each of these raises `Hegel::Error` at draw time, with a message starting
|
|
1041
|
+
`"from_lark: "`: a `grammar` or `start` that is not a `String`;
|
|
1042
|
+
`"max_size must be at least 1"` and `"max_depth must be at least 1"`; an
|
|
1043
|
+
`explicit` that is not a `Hash`, a name that is not a `String` or a
|
|
1044
|
+
`Symbol`, or a value that is not a generator; `"explicit terminal 'NUM' is
|
|
1045
|
+
given more than one generator"`, for `{:NUM => g, "NUM" => h}`; an `alphabet`
|
|
1046
|
+
that is not a `characters` generator; an `import_path` that is not a
|
|
1047
|
+
`String` or a `Pathname`; an explicit generator that draws something other
|
|
1048
|
+
than a `String`; and an import that exists but cannot be read, or is not
|
|
1049
|
+
UTF-8 (`"Could not read grammar"`, naming the file). A grammar the engine rejects
|
|
1050
|
+
raises with the engine's message: `from_lark("start: NUM\n%declare NUM\n")`
|
|
1051
|
+
raises `"HEGEL_E_INVALID_ARG (-5): Terminal 'NUM' is declared but has no
|
|
1052
|
+
definition: supply a generator for it with explicit"`.
|
|
1053
|
+
|
|
754
1054
|
### `emails`
|
|
755
1055
|
|
|
756
1056
|
An RFC 5321/5322 email address `String`.
|
|
@@ -930,6 +1230,72 @@ seen # => ["2020-01-01 00:00:00 UTC", "2020-07-23 00:00:00 UTC", "2020-04-28 00:
|
|
|
930
1230
|
`max_value < min_value` raises `Hegel::Error` at draw time: `"datetimes:
|
|
931
1231
|
max_value < min_value"`.
|
|
932
1232
|
|
|
1233
|
+
### `date_strings`
|
|
1234
|
+
|
|
1235
|
+
A `"YYYY-MM-DD"` String over year 1 through year 9999. It takes no options.
|
|
1236
|
+
|
|
1237
|
+
```ruby
|
|
1238
|
+
seen = []
|
|
1239
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1240
|
+
seen << tc.draw(date_strings)
|
|
1241
|
+
end
|
|
1242
|
+
seen # => ["2000-01-01", "2000-08-26", "2028-11-11"]
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### `time_strings`
|
|
1246
|
+
|
|
1247
|
+
An `"HH:MM:SS"` String over the whole day, with `".fffffffff"` (nine digits)
|
|
1248
|
+
appended when the nanosecond is not zero. It takes no options. Unlike
|
|
1249
|
+
`times`, it keeps the engine's nanoseconds.
|
|
1250
|
+
|
|
1251
|
+
```ruby
|
|
1252
|
+
seen = []
|
|
1253
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1254
|
+
seen << tc.draw(time_strings)
|
|
1255
|
+
end
|
|
1256
|
+
seen # => ["00:00:00", "00:00:00.000080023", "00:00:00.030484623"]
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
### `datetime_strings`
|
|
1260
|
+
|
|
1261
|
+
A `"YYYY-MM-DDTHH:MM:SS"` String over year 1 through year 9999, formatted as
|
|
1262
|
+
`date_strings` and `time_strings` are, with no timezone. It takes no options.
|
|
1263
|
+
|
|
1264
|
+
```ruby
|
|
1265
|
+
seen = []
|
|
1266
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1267
|
+
seen << tc.draw(datetime_strings)
|
|
1268
|
+
end
|
|
1269
|
+
seen # => ["2000-01-01T00:00:00", "2000-08-26T00:00:00.000002047", "2028-11-11T00:00:00.000383651"]
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
### `durations(min_value: 0, max_value: 2**64 - 1)`
|
|
1273
|
+
|
|
1274
|
+
A duration as an `Integer` count of nanoseconds in `[min_value,
|
|
1275
|
+
max_value]`. The largest duration is `2**64 - 1` nanoseconds, about 584
|
|
1276
|
+
years. `Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)` returns
|
|
1277
|
+
the same unit.
|
|
1278
|
+
|
|
1279
|
+
```ruby
|
|
1280
|
+
Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
1281
|
+
v = tc.draw(durations(min_value: 1_000_000_000, max_value: 60_000_000_000))
|
|
1282
|
+
raise "out of range" unless v.between?(1_000_000_000, 60_000_000_000)
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
seen = []
|
|
1286
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1287
|
+
seen << tc.draw(durations(min_value: 1_000_000_000, max_value: 60_000_000_000))
|
|
1288
|
+
end
|
|
1289
|
+
seen # => [1000000000, 7679018401, 21643530102]
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
A `max_value` above `2**64 - 1` is treated as `2**64 - 1`. A `min_value`
|
|
1293
|
+
above it raises `Hegel::Error` at draw time: `"durations: min_value exceeds
|
|
1294
|
+
the largest generatable Duration (u64::MAX nanoseconds, about 584 years)"`.
|
|
1295
|
+
`max_value < min_value` raises `"durations: Cannot have max_value <
|
|
1296
|
+
min_value"`, and a negative `min_value` raises `"durations: min_value must
|
|
1297
|
+
not be negative"`.
|
|
1298
|
+
|
|
933
1299
|
### `composite(&block)`
|
|
934
1300
|
|
|
935
1301
|
A value built from imperative code: `block` receives a draw surface and
|
|
@@ -989,6 +1355,52 @@ Drawing from it before `#set` raises `Hegel::Error` at draw time:
|
|
|
989
1355
|
`"deferred: draw called before set"`. Calling `#set` a second time raises
|
|
990
1356
|
immediately (not at draw time): `"deferred: set called more than once"`.
|
|
991
1357
|
|
|
1358
|
+
### `recursive(leaf, max_depth: 32, max_leaves: 100, &branch)`
|
|
1359
|
+
|
|
1360
|
+
A recursively defined value, such as a tree: either a value from `leaf`, or
|
|
1361
|
+
what the block's generator builds from sub-values of the same kind. The
|
|
1362
|
+
block receives the generator of those sub-values and returns a generator
|
|
1363
|
+
that combines some number of them. It is called afresh for each branch.
|
|
1364
|
+
|
|
1365
|
+
```ruby
|
|
1366
|
+
tree = recursive(integers(min_value: 0, max_value: 10), max_leaves: 10) do |subtrees|
|
|
1367
|
+
arrays(subtrees, max_size: 3)
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
result = Hegel.test(test_cases: 20, verbosity: :quiet) do |tc|
|
|
1371
|
+
v = tc.draw(tree)
|
|
1372
|
+
raise "wrong type" unless v.is_a?(Integer) || v.is_a?(Array)
|
|
1373
|
+
end
|
|
1374
|
+
result # => nil
|
|
1375
|
+
|
|
1376
|
+
seen = []
|
|
1377
|
+
Hegel.test(test_cases: 3, seed: 1, derandomize: true, verbosity: :quiet) do |tc|
|
|
1378
|
+
seen << tc.draw(tree)
|
|
1379
|
+
end
|
|
1380
|
+
seen # => [0, [[]], []]
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
The engine decides each leaf-or-branch choice. `max_depth` caps how deeply
|
|
1384
|
+
branches nest; at that depth every sub-value is a leaf, so `max_depth: 0`
|
|
1385
|
+
draws only leaves. An attempt that draws more than `max_leaves` leaves is
|
|
1386
|
+
discarded and drawn again, and the test case is discarded, as by `assume`,
|
|
1387
|
+
when several attempts in a row do not fit. A tree shrinks toward one of its
|
|
1388
|
+
own subtrees.
|
|
1389
|
+
|
|
1390
|
+
Draw the sub-values only through the generator the block returns. Drawing
|
|
1391
|
+
the block's argument after the recursive draw has ended raises
|
|
1392
|
+
`Hegel::Error`: `"recursive: a subtree generator was drawn outside the draw
|
|
1393
|
+
that made it"`. These raise at draw time:
|
|
1394
|
+
|
|
1395
|
+
- no block: `"recursive: block is required"`
|
|
1396
|
+
- a `leaf` that is not a generator: `"recursive: leaf must be a generator,
|
|
1397
|
+
got 5"`
|
|
1398
|
+
- a bound that is not an Integer from 0 to 2\*\*64 - 1: `"recursive:
|
|
1399
|
+
max_depth must be an Integer between 0 and 2**64 - 1, got -1"`, and the
|
|
1400
|
+
same for `max_leaves`
|
|
1401
|
+
- a block that returns something other than a generator: `"recursive: the
|
|
1402
|
+
block must return a generator, got 5"`
|
|
1403
|
+
|
|
992
1404
|
## Stateful Testing
|
|
993
1405
|
|
|
994
1406
|
A stateful test compares a real object against a simplified model of it,
|
|
@@ -1000,7 +1412,7 @@ block: `Hegel.test { |tc| Hegel::Stateful.run(machine, tc) }` (see
|
|
|
1000
1412
|
|
|
1001
1413
|
### Declaring a machine
|
|
1002
1414
|
|
|
1003
|
-
`rule(name, &block)` and `invariant(name, &block)` are class-level
|
|
1415
|
+
`rule(name, weight: 1.0, &block)` and `invariant(name, &block)` are class-level
|
|
1004
1416
|
declarations. `block` runs via `instance_exec` against the machine instance
|
|
1005
1417
|
under test, and is handed the running `Hegel::TestCase` as its one argument
|
|
1006
1418
|
(ignored if the block takes none). So it reads and writes the machine's own
|
|
@@ -1074,6 +1486,13 @@ A test case with more than one declared rule enables only a random subset of
|
|
|
1074
1486
|
them, and picks each step from that subset. So a machine with two rules can
|
|
1075
1487
|
run a case where one of them never appears at all.
|
|
1076
1488
|
|
|
1489
|
+
`rule(:name, weight: 2.5)` makes the engine pick a rule more often than the
|
|
1490
|
+
others in the cases that enable it. The weight must be a finite number above
|
|
1491
|
+
zero. A weight that is not a number raises `Hegel::Error` at declaration, and
|
|
1492
|
+
one that is zero, negative, NaN, or infinite raises it when the machine runs.
|
|
1493
|
+
It is a hint, not a distribution: which rules a case enables also decides how
|
|
1494
|
+
often each runs.
|
|
1495
|
+
|
|
1077
1496
|
Inside a rule, `tc.assume(false)` means something narrower than it does
|
|
1078
1497
|
everywhere else in this library: it discards only that one rule application
|
|
1079
1498
|
(the loop moves on and tries another rule), not the whole test case, unlike
|
|
@@ -1152,7 +1571,9 @@ pool = Hegel::Stateful::Pool.new(tc)
|
|
|
1152
1571
|
- `#values_reusable` is a `Hegel::Generator` over the pool's values that
|
|
1153
1572
|
leaves the chosen value in place, so it can be drawn again.
|
|
1154
1573
|
- `#values_consumed` is a `Hegel::Generator` over the pool's values that
|
|
1155
|
-
removes the chosen value, so it is never drawn again.
|
|
1574
|
+
removes the chosen value, so it is never drawn again. A `.filter` on it
|
|
1575
|
+
consumes only the value the predicate accepts: the values it rejected go
|
|
1576
|
+
back into the pool.
|
|
1156
1577
|
- `#size` and `#empty?` read the pool's own count directly.
|
|
1157
1578
|
|
|
1158
1579
|
```ruby
|
|
@@ -1206,7 +1627,8 @@ end
|
|
|
1206
1627
|
```
|
|
1207
1628
|
|
|
1208
1629
|
`.filter` retries its source generator up to 3 times per draw, then
|
|
1209
|
-
discards the test case. A
|
|
1630
|
+
discards the test case. A filter over a filter spends those 3 attempts
|
|
1631
|
+
between the two predicates, not 3 per filter. A predicate that (almost) never holds does not loop
|
|
1210
1632
|
forever: libhegel's own health check aborts a run that discards too many
|
|
1211
1633
|
test cases in a row, surfacing as `Hegel::Error`:
|
|
1212
1634
|
|