hegeltest 0.3.1 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +262 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +209 -103
  5. data/Rakefile +6 -3
  6. data/docs/README.md +11 -0
  7. data/docs/adr/0025-copy-each-string-input-into-a-hegel-string-t.md +61 -0
  8. data/docs/adr/0026-select-the-engine-release-repository-by-version.md +47 -0
  9. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  10. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  11. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  12. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  13. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  14. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  15. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  16. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  17. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  18. data/docs/architecture.md +33 -11
  19. data/lib/hegel/errors.rb +24 -0
  20. data/lib/hegel/generator.rb +52 -25
  21. data/lib/hegel/generators.rb +715 -45
  22. data/lib/hegel/grammar_cache.rb +36 -0
  23. data/lib/hegel/lib_hegel/real.rb +455 -120
  24. data/lib/hegel/lib_hegel.rb +33 -8
  25. data/lib/hegel/libhegel_version.rb +1 -1
  26. data/lib/hegel/locate.rb +10 -10
  27. data/lib/hegel/report.rb +13 -10
  28. data/lib/hegel/runner.rb +61 -27
  29. data/lib/hegel/settings.rb +92 -16
  30. data/lib/hegel/state_machine.rb +17 -4
  31. data/lib/hegel/stateful/pool.rb +52 -0
  32. data/lib/hegel/stateful.rb +2 -2
  33. data/lib/hegel/syntax/methods.rb +99 -15
  34. data/lib/hegel/test_case.rb +256 -35
  35. data/lib/hegel/test_location.rb +49 -0
  36. data/lib/hegel/version.rb +1 -1
  37. data/lib/hegel.rb +21 -5
  38. data/lib/tasks/libhegel.rake +17 -5
  39. data/lib/tasks/platform_gems.rake +3 -3
  40. data/sig/hegel.rbs +296 -43
  41. data/skills/hegel-ruby/references/ruby/reference.md +465 -43
  42. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd84332deb01b6efaed63023ba0f08d7515227f05eb5dc8e38e090b8a3ae59ad
4
- data.tar.gz: 7a8139bea766f95a11188c57b71cd4f8006c448dd7fd4668aee6e269fe5ecb79
3
+ metadata.gz: 19a8534cce74c0577ca61891ead16d02d336efd87f3178a8672d6482bed4c6bb
4
+ data.tar.gz: 65a01d9c2400e3890cab677cb3ada046341305caa514f9d18ddd069f8e537fb3
5
5
  SHA512:
6
- metadata.gz: 3689c4ec01d1999502205014194e864b02a968097041a56950534928f97361df2cf5ceb5550a78ada28138645bc4bb7b50973c3863bb28ba261b6cc61ac3cf70
7
- data.tar.gz: 82a57b5fd774e5f75c6ccaff351caca2c21f9998bc1b01969c48a1f5984c965603e842d57ef42e375fd44edcd50e5b4ae03dfa34a1a6a37fe53247fbade5d465
6
+ metadata.gz: 9a2c433f9d32ba001b0c7667ce248ffebf8dab48c48a0655050cf80e8c0113305a0c957436ad510dca730b5b9b583ed99f3388cd959831765a10327310a4d7fb
7
+ data.tar.gz: 715fc4b8c0a810e09382593cd2d0eba009da5b78bd6747bb23fdd599bd6d89c8262ce6ab0d7e70b24c0067b6387c1b57ce3420e4b2c1863ce9392eb463561110
data/CHANGELOG.md CHANGED
@@ -2,6 +2,264 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.0] - 2026-10-21
6
+
7
+ ### Engine: libhegel 0.48.0 (was 0.47.2)
8
+
9
+ The [libhegel 0.48.0 release notes](https://github.com/hegeldev/hegel/releases/tag/libhegel/0.48.0)
10
+ describe 0.48.0.
11
+
12
+ Each platform gem carries `libhegel` 0.48.0. The engine adds one function to
13
+ its C interface, `hegel_set_unicode_version`, and changes no other function.
14
+ These bindings do not call it yet, so the string generators continue to use
15
+ the newest Unicode tables of the engine, as before. A caller has nothing to
16
+ change in code. The engine move changes these results for a caller:
17
+
18
+ - With the default `report_multiple_failures: false`, a run can find a
19
+ different, smaller failure while it shrinks the first one. The run now
20
+ shrinks that failure too before it reports it. In 0.47.2, the run reported
21
+ that failure without shrinking it.
22
+ - `from_regex` accepts the end-of-string anchor `\z`. In 0.47.2, a pattern
23
+ with `\z` raised `Hegel::Error` with `HEGEL_E_INVALID_ARG`.
24
+ - `floats(min_value: Float::INFINITY, allow_infinity: false)` and
25
+ `floats(max_value: -Float::INFINITY, allow_infinity: false)` leave no value
26
+ to draw. The draw now raises `Hegel::Error` with `HEGEL_E_INVALID_ARG` and
27
+ a message that contains "leaves no value". In 0.47.2, the draw raised
28
+ `Hegel::Error` with `HEGEL_E_INTERNAL`.
29
+
30
+ ### Profiles
31
+
32
+ `Hegel.test` takes `profile:`, the name of a settings profile to start the
33
+ run from: a shipped one, one in `hegel.toml`, or one you register. Left
34
+ `nil`, the run starts from the default profile as before, and a keyword
35
+ passed alongside still beats the profile.
36
+
37
+ `Hegel::Settings.register_profile(name, profile: nil, **keywords)`
38
+ registers a profile for the whole process, and
39
+ `Hegel::Settings.set_default_profile(name)` makes a profile the default
40
+ for every run; `""` clears it. They follow hegel-rust's
41
+ `Settings::register_profile` and `set_default_profile`. An unknown or
42
+ reserved name raises `Hegel::Error` with the engine's message. See
43
+ [ADR 0028](docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md).
44
+
45
+ ### Settings
46
+
47
+ `Hegel.test` takes five more keywords, with the names and meanings of
48
+ hegel-rust's `Settings` methods. Each one left `nil` takes its value from
49
+ the engine's settings profile, like `test_cases:` and `seed:`.
50
+
51
+ - `stop_on_first_failure: false` keeps generating after a failure and
52
+ spends the whole `test_cases` budget before reporting.
53
+ - `print_blob: false` leaves the `reproduce_failure:` line out of the
54
+ failure report. Left `nil`, a profile or `HEGEL_PRINT_BLOB` decides.
55
+ - `show_statistics: true` makes the engine print, at the end of the run,
56
+ what `tc.event` and `tc.event_value` recorded.
57
+ - `nondeterminism_strictness:` takes `:quiet`, `:warn`, or `:error`.
58
+ `:error` ends a run whose test behaves differently on a replay with
59
+ `Hegel::Error`.
60
+ - `backend:` takes `:default` or `:urandom`.
61
+
62
+ ### Test location
63
+
64
+ Every `Hegel.test` run now reports where its test lives to libhegel, with
65
+ `hegel_settings_set_test_location`. Under Antithesis the engine writes one
66
+ `always` assertion, "<class>::<function> passes properties", for each
67
+ location; elsewhere it uses nothing, and no keyword changes. The file is
68
+ relative to the working directory when it lies under it, and the class and
69
+ function come from the frame that called `Hegel.test`. A call inside a
70
+ block takes the name of the method holding the block, and a call outside
71
+ any method, such as an RSpec example, takes the file without `.rb` as the
72
+ class and `line <n>` as the function, so each example in a file has its
73
+ own name.
74
+
75
+ ### Test cases
76
+
77
+ `tc.event(label)` and `tc.event_value(label, value)` record what a test
78
+ reached, for the statistics that `show_statistics: true` prints. They follow
79
+ hegel-rust's `TestCase#event` and `TestCase#event_value`.
80
+
81
+ ### Generators
82
+
83
+ - `date_strings`, `time_strings`, and `datetime_strings` generate ISO 8601
84
+ date, time, and datetime Strings, with no options, as hegel-rust does.
85
+ A time keeps nanoseconds: nine fractional digits appear when the
86
+ nanosecond is not zero.
87
+ - `permutations`, `subsequences`, and `samples` draw reorderings, ordered
88
+ subsets, and samples of a given sequence, as hegel-rust does. Its
89
+ `with_replacement` and `without_replacement` setters are one keyword here,
90
+ `samples(replacement: true)`, which is the default.
91
+ - `durations(min_value: 0, max_value: 2**64 - 1)` draws a duration as an
92
+ `Integer` count of nanoseconds, as hegel-rust's `durations` does for a
93
+ `Duration`. It follows [ADR 0030](docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md).
94
+ - `arrays(elements, unique: true)` draws an array with no two equal entries,
95
+ as hegel-rust's `vecs(...).unique(true)` does. An element equal to one
96
+ already drawn is rejected and drawn again, so `min_size` still counts
97
+ distinct entries.
98
+ - `floats(allow_subnormal: nil)` allows or rules out subnormal values, as
99
+ hegel-rust's `allow_subnormal` does. Left `nil`, it allows them whenever
100
+ the bounds admit one. A `floats` call whose bounds admit none, such as
101
+ `floats(min_value: 1.0, max_value: 2.0)`, now tells the engine so.
102
+ - `recursive(leaf, max_depth: 32, max_leaves: 100) { |subtrees| ... }`
103
+ draws a recursively defined value, such as a tree, through the engine's
104
+ recursion calls, as hegel-rust's `recursive` does. The engine decides each
105
+ leaf-or-branch choice and owns both budgets. An attempt that outgrows
106
+ `max_leaves` is drawn again from the root, and the spans it opened are
107
+ discarded, not kept. See
108
+ [ADR 0032](docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md).
109
+
110
+ ### Text generators
111
+
112
+ `text` and `characters` take `categories:`, `exclude_categories:`,
113
+ `include_characters:`, and `exclude_characters:`, with the names hegel-rust
114
+ gives them. `text` also takes `alphabet:`, a `String` of the permitted
115
+ characters, and raises `Hegel::Error` at draw
116
+ time when it is combined with another character option. `from_regex` takes
117
+ `alphabet:`, a `characters` generator that limits every character of the
118
+ string, padding included. An unknown `codec:` now raises `"text: invalid
119
+ codec: ..."` (or `characters:`) before the engine is called, and a
120
+ surrogate category in `categories:` raises too.
121
+
122
+ ### Grammar strings
123
+
124
+ `from_lark(grammar)` draws strings that a grammar written in the syntax of
125
+ the [Lark](https://lark-parser.readthedocs.io/en/stable/grammar.html)
126
+ parsing library accepts, as hegel-rust's `from_lark` does. It takes
127
+ `start:`, `explicit:` (a `Hash` from terminal name to a generator of the
128
+ values drawn for it), `alphabet:` (a `characters` generator),
129
+ `max_size:`, `max_depth:`, and `import_path:`. An invalid option raises
130
+ `Hegel::Error` at draw time, with hegel-rust's message where it has one.
131
+ A grammar is compiled on its first draw in a run and freed when the run
132
+ ends.
133
+
134
+ ### Added
135
+
136
+ - `rule(name, weight: 1.0)` sets how often the engine picks a rule relative to
137
+ the machine's other rules, as `#[rule(weight = w)]` does in hegel-rust. A
138
+ weight that is not finite and positive raises `Hegel::Error` when the machine
139
+ runs. `Hegel::StateMachine.rule_definitions` now maps each name to a
140
+ `Hegel::StateMachine::Rule` with `block` and `weight`, where it mapped to the
141
+ block alone.
142
+
143
+ ### Fixed
144
+
145
+ - A filter on `Pool#values_consumed` no longer loses the values it rejects. They
146
+ go back into the pool through `hegel_pool_restore` once the draw is over, and
147
+ only the accepted value is consumed, as in hegel-rust.
148
+
149
+ ### Changed
150
+
151
+ Three defaults now match hegel-rust, which defines the interface. The results
152
+ change for a caller who relied on the old default.
153
+
154
+ - `from_regex` defaults to `fullmatch: true`, where it was `false`. The whole
155
+ drawn string matches the pattern, instead of merely containing a match. To
156
+ keep the old behaviour, pass `fullmatch: false`.
157
+ - `text` defaults `max_size` to 100, where it was unbounded. When `min_size`
158
+ is above 100, the default is `min_size + 100`. Strings drawn without a
159
+ `max_size` are shorter than before. To draw longer strings, pass the
160
+ larger `max_size` you need.
161
+ - `binary` defaults `max_size` the same way, to 100 or `min_size + 100`.
162
+
163
+ - A filter over a filter now spends 3 attempts between the two predicates, as
164
+ hegel-rust does, where each level used to spend 3 of its own.
165
+ - `Hegel::Generator#do_draw_filtered(tc, predicate)` is the draw behind
166
+ `#filter`. A generator whose draws have effects that a rejected value must
167
+ not keep overrides it.
168
+
169
+ ### Repeat a test body
170
+
171
+ `tc.repeat { ... }` runs its block again and again, for as long as the
172
+ engine keeps the loop going, the way hegel-rust's `TestCase::repeat` does.
173
+ The engine chooses how many times it runs, and shrinks a failure toward
174
+ fewer iterations. Each iteration shows in the failure report as a
175
+ `// Repetition #N` line, with what the iteration recorded indented under
176
+ it. A `tc.assume` or `tc.reject` inside the block skips that iteration
177
+ instead of discarding the whole test case.
178
+
179
+ The loop does not return when it ends. It raises `Hegel::LoopDone`, which
180
+ `Hegel.test` counts as a passing test case. It descends from `Exception`,
181
+ like the other control exceptions, so a `rescue => e` in the body does not
182
+ catch it. A `break` in the block ends the loop and returns from
183
+ `tc.repeat`. See
184
+ [ADR 0034](docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md).
185
+
186
+ ## [0.3.4] - 2026-10-20
187
+
188
+ ### Engine: libhegel 0.47.2 (was 0.47.1)
189
+
190
+ The [libhegel 0.47.2 release notes](https://github.com/hegeldev/hegel/releases/tag/libhegel/0.47.2)
191
+ describe 0.47.2. It is the first engine release from the hegeldev/hegel
192
+ repository.
193
+
194
+ Each platform gem carries `libhegel` 0.47.2. The engine does not change its
195
+ C interface. A caller has nothing to change in code, and gets the same
196
+ results. The engine changes one text: the message of an internal engine
197
+ error now gives <https://github.com/hegeldev/hegel/issues> as the place to
198
+ report the problem, not the hegel-rust issue tracker.
199
+
200
+ ### Development
201
+
202
+ - `rake libhegel:fetch` and `rake libhegel:fetch_all` download libhegel
203
+ 0.47.2 and later from the hegeldev/hegel releases, and earlier versions
204
+ from the hegeldev/hegel-rust releases.
205
+
206
+ ## [0.3.3] - 2026-10-17
207
+
208
+ ### Engine: libhegel 0.47.1 (was 0.45.13)
209
+
210
+ The [libhegel 0.47.1 release notes](https://github.com/hegeldev/hegel-rust/releases/tag/libhegel-v0.47.1)
211
+ describe 0.47.1 alone. The
212
+ [engine changelog](https://github.com/hegeldev/hegel-rust/blob/libhegel-v0.47.1/hegel-c/CHANGELOG.md)
213
+ describes each engine release after 0.45.13, up to 0.47.1.
214
+
215
+ Each platform gem carries `libhegel` 0.47.1. The engine changes its C
216
+ interface: every string is now a `hegel_string_t`, a pointer and a length.
217
+ These bindings follow that change. A caller has nothing to change in code.
218
+ The engine move changes these results for a caller:
219
+
220
+ - A run with the default `report_multiple_failures: false` that finds more
221
+ than one distinct failure shrinks only the smallest of them and reports
222
+ it. Before, the engine shrank each failure and then reported one. The
223
+ example database keeps every distinct failure, as found. Such a run calls
224
+ the test body fewer times: for a body with two distinct failures and
225
+ `test_cases: 100`, 73 to 131 calls over 20 seeded runs, where 0.45.13
226
+ made 157 to 202. Both versions reported the same failure in each run.
227
+ - A string that holds a NUL character, such as a `target` label or a
228
+ `database:` path, reaches the engine whole. Before, it raised
229
+ `ArgumentError` ("string contains null byte") from the `ffi` gem.
230
+
231
+ ## [0.3.2] - 2026-10-16
232
+
233
+ ### Engine: libhegel 0.45.13 (was 0.45.7)
234
+
235
+ The [libhegel 0.45.13 release notes](https://github.com/hegeldev/hegel-rust/releases/tag/libhegel-v0.45.13)
236
+ describe 0.45.13 alone. The
237
+ [engine changelog](https://github.com/hegeldev/hegel-rust/blob/libhegel-v0.45.13/hegel-c/CHANGELOG.md)
238
+ describes each engine release after 0.45.7, up to 0.45.13.
239
+
240
+ Each platform gem carries `libhegel` 0.45.13. The engine only adds to its C
241
+ interface, and these bindings do not use the addition. A caller has nothing
242
+ to change in code. The engine changes these results for a caller:
243
+
244
+ - A run with a `seed:` can draw different values than it did before. The
245
+ engine changed how it copies values between spans that have the same
246
+ label, and how it draws an integer from a range wider than 128 bits.
247
+ - A failure that depends on an integer that is a multiple of a number, such
248
+ as 6, shrinks to the smallest such value more often. Before, the shrinker
249
+ could stop at a larger multiple, such as 78.
250
+ - A run of 1,000 test cases or more starts to use `target` scores soon
251
+ after it starts. Before, it started after half of the test cases.
252
+ - The `note:` line with the engine's nondeterminism caveat shows only for a
253
+ failure that the engine saw pass at least one time. Before, every failure
254
+ of a run that the engine treated as nondeterministic showed it.
255
+ - `reproduce_failure:` stops a replay when the test body draws differently
256
+ from the recorded choices. If no replay reproduces the failure, the run
257
+ raises `Hegel::Error` with the message that says that the blob did not
258
+ reproduce a failure, and the engine writes one line to standard error
259
+ that names the position where the draws left the recorded choices.
260
+ Before, the engine continued the replay with new draws and could report a
261
+ different failure as the failure of the blob.
262
+
5
263
  ## [0.3.1] - 2026-10-15
6
264
 
7
265
  ### Engine: libhegel 0.45.7 (was 0.45.4)
@@ -196,6 +454,10 @@ The engine is called through the `ffi` gem. Each platform gem carries the
196
454
  matching `libhegel` 0.32.5 build. The platform-independent gem carries none,
197
455
  so a run on it needs `HEGEL_LIBHEGEL_PATH` pointing at a local build.
198
456
 
457
+ [0.4.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.4.0
458
+ [0.3.4]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.4
459
+ [0.3.3]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.3
460
+ [0.3.2]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.2
199
461
  [0.3.1]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.1
200
462
  [0.3.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.0
201
463
  [0.2.1]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.2.1