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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -0
  3. data/README.ja.md +408 -0
  4. data/README.md +208 -102
  5. data/Rakefile +6 -3
  6. data/docs/README.md +9 -0
  7. data/docs/adr/0027-report-each-runs-test-location-from-the-callers-frame.md +63 -0
  8. data/docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md +91 -0
  9. data/docs/adr/0029-keep-notes-local-and-defer-run-error-kinds.md +72 -0
  10. data/docs/adr/0030-represent-a-duration-as-integer-nanoseconds.md +48 -0
  11. data/docs/adr/0031-take-the-alphabet-options-from-hegel-rust.md +52 -0
  12. data/docs/adr/0032-suppress-span-closes-while-an-abandoned-recursion-attempt-unwinds.md +147 -0
  13. data/docs/adr/0033-cache-compiled-grammars-for-the-length-of-a-run.md +80 -0
  14. data/docs/adr/0034-repeat-a-test-body-with-a-ruby-side-indent.md +120 -0
  15. data/docs/adr/0035-defer-concurrent-state-machines-and-the-engine-printer.md +136 -0
  16. data/docs/architecture.md +16 -1
  17. data/lib/hegel/errors.rb +24 -0
  18. data/lib/hegel/generator.rb +52 -25
  19. data/lib/hegel/generators.rb +715 -45
  20. data/lib/hegel/grammar_cache.rb +36 -0
  21. data/lib/hegel/lib_hegel/real.rb +320 -17
  22. data/lib/hegel/lib_hegel.rb +31 -6
  23. data/lib/hegel/libhegel_version.rb +1 -1
  24. data/lib/hegel/report.rb +13 -10
  25. data/lib/hegel/runner.rb +61 -27
  26. data/lib/hegel/settings.rb +92 -16
  27. data/lib/hegel/state_machine.rb +17 -4
  28. data/lib/hegel/stateful/pool.rb +52 -0
  29. data/lib/hegel/stateful.rb +2 -2
  30. data/lib/hegel/syntax/methods.rb +99 -15
  31. data/lib/hegel/test_case.rb +256 -35
  32. data/lib/hegel/test_location.rb +49 -0
  33. data/lib/hegel/version.rb +1 -1
  34. data/lib/hegel.rb +21 -5
  35. data/sig/hegel.rbs +295 -42
  36. data/skills/hegel-ruby/references/ruby/reference.md +463 -41
  37. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecb67897e8ff8580eee45713f3ef1bf6f4f17878f5206830f40a47761ed1d193
4
- data.tar.gz: 68143dade773e79ac88b16197a3213717808645b5e47f6799560867cac1c1009
3
+ metadata.gz: 19a8534cce74c0577ca61891ead16d02d336efd87f3178a8672d6482bed4c6bb
4
+ data.tar.gz: 65a01d9c2400e3890cab677cb3ada046341305caa514f9d18ddd069f8e537fb3
5
5
  SHA512:
6
- metadata.gz: e91326c8d22876cddef53a04b3f62a6ff5b91929c70588e5117f1dbdb3089de014b4721c6058d0016a6b687585db6bfb72d90d6826aa04dc4e849c6a37090216
7
- data.tar.gz: cd65227abec3bc433db3b1acc900050cfd403f8c76046309cff7c703c3025d99b665194b2c20b4b9627ca3b4d64ef5221e58779dfe373d1364f27b88dc579593
6
+ metadata.gz: 9a2c433f9d32ba001b0c7667ce248ffebf8dab48c48a0655050cf80e8c0113305a0c957436ad510dca730b5b9b583ed99f3388cd959831765a10327310a4d7fb
7
+ data.tar.gz: 715fc4b8c0a810e09382593cd2d0eba009da5b78bd6747bb23fdd599bd6d89c8262ce6ab0d7e70b24c0067b6387c1b57ce3420e4b2c1863ce9392eb463561110
data/CHANGELOG.md CHANGED
@@ -2,6 +2,187 @@
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
+
5
186
  ## [0.3.4] - 2026-10-20
6
187
 
7
188
  ### Engine: libhegel 0.47.2 (was 0.47.1)
@@ -273,6 +454,7 @@ The engine is called through the `ffi` gem. Each platform gem carries the
273
454
  matching `libhegel` 0.32.5 build. The platform-independent gem carries none,
274
455
  so a run on it needs `HEGEL_LIBHEGEL_PATH` pointing at a local build.
275
456
 
457
+ [0.4.0]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.4.0
276
458
  [0.3.4]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.4
277
459
  [0.3.3]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.3
278
460
  [0.3.2]: https://github.com/meganemura/hegel-ruby/releases/tag/v0.3.2
data/README.ja.md ADDED
@@ -0,0 +1,408 @@
1
+ # Hegel for Ruby
2
+
3
+ [English README](README.md)
4
+
5
+ > [!IMPORTANT]
6
+ > **これは非公式のサードパーティ実装です。** Hegel プロジェクトと `libhegel`
7
+ > エンジンは Antithesis, LLC のものです。このリポジトリは Antithesis とも
8
+ > `hegeldev` organization とも関係がなく、先方のレビューや承認も受けていません。
9
+ >
10
+ > 公式実装は
11
+ > [Rust](https://github.com/hegeldev/hegel/tree/main/frontends/rust)、
12
+ > [Go](https://github.com/hegeldev/hegel-go)、
13
+ > [C++](https://github.com/hegeldev/hegel-cpp)、
14
+ > [TypeScript](https://github.com/hegeldev/hegel-typescript)、
15
+ > [Java](https://github.com/hegeldev/hegel-java)、
16
+ > [OCaml](https://github.com/hegeldev/hegel-ocaml) です。この gem の問題は
17
+ > 公式実装ではなく、このリポジトリに報告してください。
18
+
19
+ > [!NOTE]
20
+ > Hegel 自体はベータ版で、今後も互換性のない変更が入る予定です。
21
+ > <https://hegel.dev/compatibility> を参照してください。
22
+
23
+ Hegel は [Hypothesis](https://github.com/hypothesisworks/hypothesis) を土台にした
24
+ プロパティベーステストのフレームワークです。手で選んだ入力でテストを書く代わりに、
25
+ どの入力でも成り立つべき性質を書きます。Hegel は入力を生成して性質を破ろうとし、
26
+ 失敗を見つけたら最小の反例まで縮めます。
27
+
28
+ この gem は、公式実装も使っているネイティブエンジン
29
+ [libhegel](https://hegel.dev/reference/libhegel) を
30
+ [`ffi`](https://github.com/ffi/ffi) gem 経由で同じプロセス内で動かします。
31
+ サーバーも Python も要りません。
32
+
33
+ ## インストール
34
+
35
+ gem 名は [`hegeltest`](https://rubygems.org/gems/hegeltest) です。
36
+
37
+ ```ruby
38
+ # Gemfile
39
+ gem "hegeltest", group: :test
40
+ ```
41
+
42
+ require パスは `hegel`、名前空間は `Hegel` です。Bundler の自動 require のために
43
+ `require "hegeltest"` も使えます。
44
+
45
+ Ruby 3.4 以上が必要です。各プラットフォーム向け gem には対応する `libhegel` の
46
+ ビルドが同梱されているので、インストール時にコンパイルは不要で、エンジンを
47
+ 別に用意する必要もありません。
48
+
49
+ | プラットフォーム | gem のプラットフォーム |
50
+ | --- | --- |
51
+ | Linux x64 | `x86_64-linux` |
52
+ | Linux arm64 | `aarch64-linux` |
53
+ | macOS arm64 | `arm64-darwin` |
54
+ | Windows x64 | `x64-mingw-ucrt` |
55
+ | Windows arm64 | `aarch64-mingw-ucrt` |
56
+
57
+ Hegel のメンテナーは、サードパーティによるリリースバイナリの再配布を認めています
58
+ ([hegeldev/hegel-rust#411])。
59
+
60
+ すでに `Gemfile.lock` があるプロジェクトでは、もう一手間要ります。Bundler は
61
+ `bundle install` を実行したマシンのプラットフォームしか lock に足さないので、
62
+ Mac で作った lock には Linux が載りません。そのままだと Linux の CI で gem が
63
+ 見つかりません。CI で使うプラットフォームを追加してください。
64
+
65
+ ```bash
66
+ bundle lock --add-platform x86_64-linux
67
+ ```
68
+
69
+ Intel Mac のように `libhegel` のビルドが公開されていないプラットフォームでは、
70
+ エンジンを自分でビルドし、`HEGEL_LIBHEGEL_PATH` にそのライブラリファイルか、
71
+ それを含むディレクトリを指定してください。同じ変数で、どのプラットフォームでも
72
+ 同梱のエンジンの代わりにローカルビルドを使えます。
73
+
74
+ [hegeldev/hegel-rust#411]: https://github.com/hegeldev/hegel-rust/issues/411
75
+
76
+ ### どの libhegel が入っているか
77
+
78
+ gem の各リリースは libhegel のリリースを 1 つ固定しています。インストール済みの
79
+ gem が固定しているバージョンは次のコマンドで確認できます。
80
+
81
+ ```bash
82
+ ruby -rhegel -e 'puts Hegel::LIBHEGEL_VERSION'
83
+ ```
84
+
85
+ [CHANGELOG](CHANGELOG.md) の各リリースの節は、エンジンの更新があればその内容から
86
+ 始まり、そのエンジンのリリースノートにリンクしています。
87
+ [GitHub Releases](https://github.com/meganemura/hegel-ruby/releases) にも
88
+ 同じ内容が載ります。
89
+
90
+ libhegel の新しいリリースが出ると、それに追従する pull request が作られます。
91
+ gem のバージョンとエンジンのバージョンは独立していて、gem のリリースには
92
+ エンジンの更新、Ruby コードの変更、またはその両方が含まれます。
93
+
94
+ ## クイックスタート
95
+
96
+ ```ruby
97
+ # spec/spec_helper.rb
98
+ require "hegel"
99
+
100
+ RSpec.configure do |config|
101
+ config.include Hegel::Syntax::Methods
102
+ end
103
+ ```
104
+
105
+ ```ruby
106
+ # spec/my_sort_spec.rb
107
+ def my_sort(ls) = ls.sort.uniq # oops: uniq drops duplicates
108
+
109
+ RSpec.describe "my_sort" do
110
+ it "matches the builtin sort" do
111
+ Hegel.test do |tc|
112
+ xs = tc.draw(arrays(integers))
113
+ expect(my_sort(xs)).to eq(xs.sort)
114
+ end
115
+ end
116
+ end
117
+ ```
118
+
119
+ このテストは失敗し、Hegel はバグを再現する最小の入力まで縮めます。
120
+ レポートは次のようになります。
121
+
122
+ ```
123
+ Falsified after 11 test cases (0 discarded):
124
+
125
+ xs = [0, 0]
126
+
127
+ To reproduce this failure, pass the blob below to Hegel.test:
128
+ reproduce_failure: "AXicY2VgYGBkZOBiZEBhMAAAAd8AIQ=="
129
+ ```
130
+
131
+ テストケースの数と blob は実行ごと、エンジンのリリースごとに変わります。
132
+ 見るべきなのは縮めた値で、重複が 2 つあれば十分だとわかります。Hegel は
133
+ draw を書いた行を読んで値に `xs` という名前を付け、RSpec 自身の expectation
134
+ 失敗を投げ直すので、RSpec は普段どおりの失敗として報告します。
135
+
136
+ `Hegel::Syntax::Methods` を include すると、FactoryBot の `create` のように
137
+ ジェネレーターを接頭辞なしで使えます。include しなくても
138
+ `Hegel::Generators.arrays(...)` として使えます。
139
+
140
+ 独自のヘルパーを持つ example group にジェネレーター名を持ち込みたくない場合は、
141
+ タグを付けた group だけに include します。
142
+
143
+ ```ruby
144
+ config.include Hegel::Syntax::Methods, :hegel
145
+ ```
146
+
147
+ Hegel は値を命令的に draw するので、普通の Ruby メソッドで複数の draw を
148
+ プロパティ間で共有できます。メソッドの引数には、RuboCop の既定の命名規則に
149
+ 合う説明的な名前を付けてください。
150
+
151
+ ```ruby
152
+ def draw_sorted_pair(test_case)
153
+ low = test_case.draw(integers(min_value: 0, max_value: 100))
154
+ high = test_case.draw(integers(min_value: low, max_value: 200))
155
+ [low, high]
156
+ end
157
+
158
+ Hegel.test do |tc|
159
+ low, high = draw_sorted_pair(tc)
160
+ expect(low).to be <= high
161
+ end
162
+ ```
163
+
164
+ ### Minitest
165
+
166
+ Hegel はテストフレームワークに何も要求しません。ジェネレーターのメソッドを
167
+ 一度 include し、普通のテストの中にプロパティを書きます。
168
+
169
+ ```ruby
170
+ # test/test_helper.rb
171
+ require "hegel"
172
+
173
+ class Minitest::Test
174
+ include Hegel::Syntax::Methods
175
+ end
176
+ ```
177
+
178
+ ```ruby
179
+ # test/my_sort_test.rb
180
+ class MySortTest < Minitest::Test
181
+ def test_matches_the_builtin_sort
182
+ Hegel.test do |tc|
183
+ xs = tc.draw(arrays(integers))
184
+ assert_equal xs.sort, my_sort(xs)
185
+ end
186
+ end
187
+ end
188
+ ```
189
+
190
+ Minitest は縮めたケースを、テストを書いた行での通常の assertion 失敗として
191
+ 報告します。
192
+
193
+ ```
194
+ 1) Failure:
195
+ MySortTest#test_matches_the_builtin_sort [test/my_sort_test.rb:5]:
196
+ Expected: [0, 0]
197
+ Actual: [0]
198
+ ```
199
+
200
+ ## ジェネレーター
201
+
202
+ | 種類 | ジェネレーター |
203
+ | --- | --- |
204
+ | 数値と真偽値 | `integers`, `floats`, `booleans` |
205
+ | 文字列とバイト列 | `text`, `characters`, `binary`, `from_regex`, `from_lark` |
206
+ | コレクション | `arrays`, `sets`, `hashes`, `tuples` |
207
+ | 選択 | `just`, `sampled_from`, `permutations`, `subsequences`, `samples`, `one_of`, `optional` |
208
+ | 書式 | `emails`, `urls`, `domains`, `ip_addresses`, `uuids` |
209
+ | 日時 | `dates`, `times`, `datetimes`, `date_strings`, `time_strings`, `datetime_strings`, `durations` |
210
+ | 自作 | `composite`, `deferred`, `recursive` |
211
+
212
+ どれも `map` と `filter` で組み合わせられます。オプションはキーワード引数で、
213
+ `integers(min_value: 0, max_value: 9)`、`arrays(text, max_size: 5)`、`arrays(integers, unique: true)` のように、
214
+ 他の Hegel 実装と同じ名前を使います。オプションの組み合わせが不正な場合は、
215
+ ジェネレーターを作ったときではなく draw したときに例外になります。
216
+
217
+ ## ステートフルテスト
218
+
219
+ 入力 1 つではなく、操作の列で初めて出るバグもあります。操作を
220
+ `Hegel::StateMachine` のルールとして宣言すると、Hegel が次に実行するルールを選び、
221
+ 途中で不変条件を確かめ、失敗をそれが再現する最短の操作列まで縮めます。
222
+
223
+ ```ruby
224
+ class StackMachine < Hegel::StateMachine
225
+ def initialize
226
+ @stack = BoundedStack.new(3)
227
+ @model = []
228
+ end
229
+
230
+ rule :push do |tc|
231
+ x = tc.draw(integers(min_value: 0, max_value: 9))
232
+ @stack.push(x)
233
+ @model.push(x) if @model.size < 3
234
+ end
235
+
236
+ rule :pop do |tc|
237
+ tc.assume(!@model.empty?)
238
+ raise "pop disagreed" unless @stack.pop == @model.pop
239
+ end
240
+
241
+ invariant :size_agrees do
242
+ raise "size disagreed" unless @stack.size == @model.size
243
+ end
244
+ end
245
+
246
+ Hegel.test { |tc| Hegel::Stateful.run(StackMachine.new, tc) }
247
+ ```
248
+
249
+ ルールの中の `tc.assume` は、テストケース全体を捨てるのではなく、そのルールだけを
250
+ 却下して別のルールを選ばせます。`alloc` が返したハンドルを解放するなど、前のルールが
251
+ 作った値を使うルールでは、値を `Hegel::Stateful::Pool` に入れて、そこから draw します。
252
+
253
+ すべての不変条件は最初のルールの前と最後のルールの後に実行されます。ルールの間では
254
+ エンジンが各不変条件をサンプリングします。毎回のルールの後に確かめたい不変条件は
255
+ `always_run: true` 付きで宣言してください。
256
+ `Hegel::Stateful.run(machine, tc, step_count: 50)` で 1 テストケースあたりの最大
257
+ ルール数を指定できます。既定は 50 です。`rule :name, weight: 2.5` と書くと、そのルールが
258
+ 他より選ばれやすくなります。既定の重みは 1.0 です。
259
+
260
+ ## 実行の設定
261
+
262
+ `Hegel.test` は次のキーワードを受け取ります。
263
+
264
+ | キーワード | 効果 |
265
+ | --- | --- |
266
+ | `profile` | 実行の出発点にする設定プロファイル。[プロファイル](#プロファイル)を参照 |
267
+ | `test_cases` | 生成するテストケースの数 |
268
+ | `seed` | ランダム生成のシード |
269
+ | `derandomize` | 生成を決定的にする |
270
+ | `verbosity` | エンジンの出力量。`:quiet` は失敗レポートも隠す |
271
+ | `phases` | 生成や縮小など、実行するエンジンのフェーズ |
272
+ | `suppress_health_check` | 無効にするヘルスチェック |
273
+ | `report_multiple_failures` | 異なる失敗をそれぞれ報告する。既定は `false` |
274
+ | `stop_on_first_failure` | `false` にすると `test_cases` を使い切ってから報告する |
275
+ | `print_blob` | `false` にすると失敗レポートから再現用の行を外す |
276
+ | `show_statistics` | `tc.event` と `tc.event_value` で記録した内容を実行の最後に表示する |
277
+ | `nondeterminism_strictness` | 再実行で振る舞いが変わるテストへの反応。`:quiet`、`:warn`、`:error` |
278
+ | `backend` | `:default`、または値を引くたびに外部のエントロピーを使う `:urandom`(Antithesis 向け) |
279
+ | `database_key` | このプロパティで example database を有効にする |
280
+ | `database` | example database のディレクトリ |
281
+ | `reproduce_failure` | レポートの blob から 1 つの失敗を再生する |
282
+
283
+ 指定しなかったキーワードは、エンジンの設定プロファイルの値になります。これは
284
+ `hegel.toml` や `HEGEL_*` 環境変数で変えない限り libhegel 自身の既定値です。
285
+ CI サーバー上ではエンジンが `ci` プロファイルを使い、実行を決定的にして
286
+ example database を無効にします。
287
+
288
+ example database は失敗したケースを保存し、次回最初に再生します。
289
+ `database_key` で有効になり、`database` でディレクトリを選びます。
290
+
291
+ ```ruby
292
+ Hegel.test(database_key: "my_sort matches the builtin sort") { |tc| ... }
293
+ ```
294
+
295
+ プロパティごとに別のキーを付けてください。ディレクトリではなくキーで有効になる
296
+ 理由は [ADR 0009](docs/adr/0009-turn-the-example-database-on-with-a-key.md) に
297
+ あります。
298
+
299
+ テストの中では、`tc.note` で失敗レポートに出すメッセージを記録でき、
300
+ `tc.target` で Hegel が探索を寄せる方向を指定でき、`tc.repeat` で
301
+ Hegel が選んだ回数だけブロックを繰り返し実行できます。
302
+
303
+ `tc.event` と `tc.event_value` は、テストが狙ったケースに届いているかを確かめる
304
+ ためのものです。`show_statistics: true` にすると、各イベントが起きた割合と、
305
+ 各値の分布の要約をエンジンが表示します。数値は実行ごとに変わります。
306
+
307
+ ```ruby
308
+ Hegel.test(show_statistics: true) do |tc|
309
+ xs = tc.draw(arrays(integers))
310
+ tc.event("empty") if xs.empty?
311
+ tc.event_value("size", xs.size)
312
+ end
313
+ ```
314
+
315
+ ```
316
+ Statistics (over 100 test cases):
317
+ * empty: 6.0% of test cases
318
+ * size: count 100, min 0, median 6, mean 7.99, p90 19, max 20
319
+ ```
320
+
321
+ ## プロファイル
322
+
323
+ プロファイルは名前の付いた設定の組です。エンジンは `development`、`ci`、
324
+ `workload` を同梱しており、`hegel.toml` でさらに定義できます。`profile:` を
325
+ 渡すと 1 回の実行をそのプロファイルから始められ、独自のプロファイルは
326
+ `Hegel::Settings.register_profile` で登録できます。
327
+
328
+ ```ruby
329
+ Hegel::Settings.register_profile("nightly", profile: "ci", test_cases: 10_000)
330
+
331
+ Hegel.test(profile: "nightly") { |tc| ... }
332
+ ```
333
+
334
+ `Hegel.test` に渡したキーワードはプロファイルより優先されます。名前を指定した
335
+ プロファイルは既定のプロファイルを継承するので、設定していない項目は既定の
336
+ プロファイルから来ます。`Hegel::Settings.set_default_profile("nightly")` は
337
+ そのプロセスのすべての実行の既定にし、`set_default_profile("")` で解除します。
338
+ 登録と既定はプロセス全体に効き、すでに始まった実行は変わりません。
339
+ `register_profile` は `database_key` を除く `Hegel.test` のキーワードを受け取ります。
340
+ プロファイルは `database_key` を持たないためです。また `Hegel.test` は常に
341
+ `report_multiple_failures` を渡すので、プロファイル側の値は使われません。
342
+ 未知の名前は `Hegel::Error` になります。
343
+ [ADR 0028](docs/adr/0028-expose-settings-profiles-as-a-keyword-and-module-functions.md)
344
+ を参照してください。
345
+
346
+ ## エージェント向けスキル
347
+
348
+ 2 つのスキルを組み合わせて使います。Hegel のメンテナーによる公式の
349
+ [hegel-skill](https://github.com/hegeldev/hegel-skill) は、どの性質を探すか、
350
+ エージェントが書いたテストをどう確かめるかといった手法を教えるもので、
351
+ どの Hegel 実装でも使えます。[`skills/hegel-ruby`](skills/hegel-ruby/SKILL.md) は
352
+ Ruby の部分、つまりこの gem の API と RSpec・Minitest での使い方を足します。
353
+ 両方をインストールしてください。
354
+
355
+ Claude Code では、それぞれのリポジトリをマーケットプレイスとして追加し、
356
+ プラグインをインストールします。
357
+
358
+ ```
359
+ /plugin marketplace add hegeldev/hegel-skill
360
+ /plugin install hegel-skill@hegeldev
361
+ /plugin marketplace add meganemura/hegel-ruby
362
+ /plugin install hegel-ruby@meganemura
363
+ ```
364
+
365
+ Codex などほかのエージェントでのインストール方法は、
366
+ [hegel-skill の README](https://github.com/hegeldev/hegel-skill#installation) を
367
+ 参照してください。この gem のスキルは、同じ手順で
368
+ <https://github.com/meganemura/hegel-ruby/tree/main/skills/hegel-ruby> を指定すればインストールできます。
369
+
370
+ ## 開発
371
+
372
+ ```bash
373
+ bin/setup # 依存関係をインストール
374
+ bundle exec rake libhegel:fetch # 固定したエンジンを SHA-256 で検証してダウンロード
375
+ bundle exec rake # カバレッジ付きのテストとリンター
376
+ bundle exec rake mutation # ミューテーションテスト
377
+ bin/console # 対話プロンプト
378
+ ```
379
+
380
+ `just` のレシピは同じ Rake タスクを呼ぶので、他の Hegel 実装で `just` を使っている
381
+ 人は `just check`、`just test`、`just lint` をそのまま使えます。
382
+
383
+ CI は pull request と main への push ごとに、上の表のすべてのプラットフォームで
384
+ Ruby 3.4 と 4.0 のテストを実行します。
385
+
386
+ 設計の記録は [docs/](docs/README.md) にあります。
387
+
388
+ ### リリース
389
+
390
+ main へのマージで `lib/hegel/version.rb` が変わると、リリースが始まります。
391
+ Release ワークフローは `release` 環境の承認を待ち、承認されるとテストを実行し、
392
+ gem を RubyGems に公開し、タグ `v<version>` と GitHub Release を作ります。
393
+ リリースノートは [CHANGELOG.md](CHANGELOG.md) のそのバージョンの節から作られます。
394
+ [ADR 0024](docs/adr/0024-release-when-a-merge-changes-the-gem-version.md) を
395
+ 参照してください。
396
+
397
+ ## コントリビュート
398
+
399
+ バグ報告と pull request は <https://github.com/meganemura/hegel-ruby> へ。
400
+ コントリビューターは[行動規範](CODE_OF_CONDUCT.md)に従ってください。
401
+
402
+ ## ライセンス
403
+
404
+ この gem は [MIT License](LICENSE.txt) で利用できます。
405
+
406
+ リリースされた gem には `libhegel` 自身のライセンス(MIT、copyright Antithesis, LLC)
407
+ も含まれます。各プラットフォーム向け gem はエンジンの隣に `NOTICE-libhegel.txt` を
408
+ 同梱しており、その全文が入っています。