branchout 0.1.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 +7 -0
  2. data/AGENTS.md +38 -0
  3. data/LICENSE +21 -0
  4. data/README.ja.md +903 -0
  5. data/README.md +586 -0
  6. data/bin/branchout +15 -0
  7. data/examples/arithmetic.rb +24 -0
  8. data/examples/hidden_bug.rb +17 -0
  9. data/examples/inputs/arithmetic.json +10 -0
  10. data/examples/inputs/arithmetic.seed.json +5 -0
  11. data/examples/inputs/hidden_bug.json +9 -0
  12. data/examples/inputs/hidden_bug.seed.json +5 -0
  13. data/examples/inputs/magic_value.json +7 -0
  14. data/examples/inputs/magic_value.seed.json +5 -0
  15. data/examples/inputs/version_parser.json +11 -0
  16. data/examples/inputs/version_parser.seed.json +5 -0
  17. data/examples/magic_value.rb +11 -0
  18. data/examples/version_parser.rb +29 -0
  19. data/lib/branchout/canon.rb +59 -0
  20. data/lib/branchout/cli.rb +302 -0
  21. data/lib/branchout/coverage.rb +9 -0
  22. data/lib/branchout/coverage_session.rb +49 -0
  23. data/lib/branchout/emitters/minitest.rb +92 -0
  24. data/lib/branchout/emitters/rspec.rb +532 -0
  25. data/lib/branchout/emitters/support.rb +1248 -0
  26. data/lib/branchout/emitters.rb +101 -0
  27. data/lib/branchout/engine.rb +303 -0
  28. data/lib/branchout/explore.rb +124 -0
  29. data/lib/branchout/input.rb +215 -0
  30. data/lib/branchout/input_file.rb +125 -0
  31. data/lib/branchout/mutator.rb +124 -0
  32. data/lib/branchout/prism_choice.rb +25 -0
  33. data/lib/branchout/report.rb +449 -0
  34. data/lib/branchout/rng.rb +31 -0
  35. data/lib/branchout/runner.rb +591 -0
  36. data/lib/branchout/sampler.rb +70 -0
  37. data/lib/branchout/target.rb +696 -0
  38. data/lib/branchout/version.rb +8 -0
  39. data/lib/branchout.rb +31 -0
  40. data/skills/branchout/SKILL.md +230 -0
  41. data/skills/branchout/inputs.example.json +10 -0
  42. metadata +114 -0
data/README.ja.md ADDED
@@ -0,0 +1,903 @@
1
+ # branchout
2
+
3
+ English: [README.md](README.md).
4
+
5
+ branchout は、Ruby のメソッドを多くの入力で実行し、どの入力も通らなかった分岐の腕を示し、メソッドの今のふるまいを固定する RSpec か minitest の例を書く。
6
+
7
+ ## Rails アプリでのクイックスタート
8
+
9
+ この手順では、RSpec とファクトリでテストしている Rails アプリに branchout を足し、モデルのメソッドを 1 つ探索する。
10
+ 探索はスイートの中で、アプリを起動し、テスト用のデータベースを使って動く。
11
+
12
+ 最初の対象には、`if`、`unless`、`case` の腕が 2 個から 5 個あり、値を返すモデルのメソッドを選ぶ。
13
+ そのモデルにはファクトリがあるとよい。
14
+ 最近のコミットが変えたメソッドから始めるのもよい。
15
+ `git log -5 --oneline -- app/models` がそのコミットを並べる。
16
+
17
+ 1. Gemfile の test グループに gem を足し、インストールする。
18
+
19
+ ```ruby
20
+ group :test do
21
+ gem "branchout", require: false
22
+ end
23
+ ```
24
+
25
+ ```sh
26
+ bundle install
27
+ ```
28
+
29
+ 2. メソッドを探索するスペックファイルを 1 つ書く。
30
+ `Loan`、`approve`、ファクトリの呼び出し、引数、2 つのファイル名を自分のものに置き換える。
31
+ レシーバと各引数は、文字列に入れた Ruby のソースである。
32
+
33
+ ```ruby
34
+ # spec/models/loan_explore_spec.rb
35
+ require "branchout/coverage" if ENV["BRANCHOUT"]
36
+ require "rails_helper"
37
+ require "branchout"
38
+
39
+ RSpec.describe Loan, type: :model do
40
+ it "explores approve", if: ENV["BRANCHOUT"] do
41
+ clerk = {ruby: "{name: 'Grace', senior: false}"}
42
+ puts Branchout.explore("Loan#approve", within: self, emit_tests: "spec/models/loan_approve_spec.rb", inputs: [
43
+ {receiver: {ruby: "create(:loan)"}, args: [clerk]}
44
+ ])
45
+ end
46
+ end
47
+ ```
48
+
49
+ 1 行目は、`rails_helper` がアプリを起動する前にカバレッジを始める。
50
+ そのため branchout はモデルのファイルの腕を数えられる。
51
+ ヘルパーが SimpleCov を始めていても、そのままでよい。
52
+ この実行では branchout がカバレッジを引き継ぐ。
53
+ `BRANCHOUT` がなければこの例は飛ばされるので、ファイルはスイートに置いたままにできる。
54
+ 各入力はロールバックされるセーブポイントの中で動くので、ほかの入力が書いた行は見えない。
55
+ 素の `create` や `build` は、スイートの設定に `FactoryBot::Syntax::Methods` が含まれていることを前提にする。
56
+ 含まれていなければ `FactoryBot.create(:loan)` と書く。
57
+ メソッドが動く前に入力自身の Ruby が例外を出すと、explore はその入力を示して止まる。
58
+ そのため入力の例外がメソッドの振る舞いとして固定されることはない。
59
+
60
+ 3. 実行する。
61
+
62
+ ```sh
63
+ BRANCHOUT=1 bundle exec rspec spec/models/loan_explore_spec.rb
64
+ ```
65
+
66
+ ### 出力の例
67
+
68
+ 次の出力は小さなサンプルアプリのものである。
69
+ メソッドは次のとおり。
70
+
71
+ ```ruby
72
+ def approve(officer)
73
+ return :already if state == "approved"
74
+ return :over_limit if borrower.approved_cents + amount_cents > borrower.limit_cents
75
+ return :needs_senior if amount_cents > 10_000 && !officer.fetch(:senior)
76
+
77
+ update!(state: "approved", approved_by: officer.fetch(:name))
78
+ :approved
79
+ end
80
+ ```
81
+
82
+ ```text
83
+ branchout explore app/models/loan.rb Loan#approve
84
+ covered: 3 of 6 arms by 1 input
85
+ raised: none
86
+ tests: spec/models/loan_approve_spec.rb
87
+
88
+ FRONTIER (add an input like the nearest above each arm, changed as the arm says)
89
+ nearest: {"args":[{"ruby":"{name: 'Grace', senior: false}"}],"receiver":{"ruby":"create(:loan)"}}
90
+ app/models/loan.rb:7:4 makes `state == "approved"` true
91
+ app/models/loan.rb:8:4 makes `borrower.approved_cents + amount_cents > borrower.limit_cents` true
92
+ app/models/loan.rb:9:4 makes `amount_cents > 10_000 && !officer.fetch(:senior)` true
93
+
94
+ RETURNED (coverage does not check a value; compare each with what the method should return)
95
+ returns :approved for inputs[0]
96
+
97
+ OPERATORS (coverage does not see which side ran; vary each left side)
98
+ app/models/loan.rb:9:50 && in `amount_cents > 10_000 && !officer.fetch(:senior)`
99
+
100
+ BOUNDARIES (each comparison in the counted code, listed whether or not an input made both sides equal; coverage does not see which did)
101
+ app/models/loan.rb:8:65 > in `borrower.approved_cents + amount_cents > borrower.limit_cents`
102
+ app/models/loan.rb:9:41 > in `amount_cents > 10_000`
103
+
104
+ Pass verbose: true for every covered arm and the run settings.
105
+ .
106
+
107
+ Finished in 0.28375 seconds (files took 2.14 seconds to load)
108
+ 1 example, 0 failures
109
+ ```
110
+
111
+ 1 件の入力が 6 個の腕のうち 3 個を通った。
112
+ `FRONTIER` の下の各行は、どの入力も通らなかった腕で、そのファイル、行、列と、腕を通るために入力がすべきことを言う。
113
+ `nearest` は最も近づいた入力なので、それを写して行の言うとおりに変える。
114
+ `OPERATORS` は、カバレッジが両辺を数えない `&&` を示す。
115
+ その左辺を真にする入力と偽にする入力を与える。
116
+ `BOUNDARIES` は `<`、`<=`、`>`、`>=` を 1 つずつ示す。
117
+ 両辺を等しくする入力がなくてもすべての腕は通れるが、`>` と `>=` を見分けるのはその値だけなので、そうした入力を与える。
118
+ この一覧はソースから作るので、入力が両辺を等しくしたあとも比較は一覧に残る。
119
+ この実行は `spec/models/loan_approve_spec.rb` も書いた。
120
+ これは各入力が返した値を固定する。
121
+
122
+ ### 次に、フロンティアを閉じる
123
+
124
+ `FRONTIER` のエントリごとに入力を 1 件足し、手順 3 をもう一度実行する。
125
+ サンプルアプリでは、借り手の限度額は 20,000 セントである。
126
+
127
+ ```ruby
128
+ # spec/models/loan_explore_spec.rb
129
+ require "branchout/coverage" if ENV["BRANCHOUT"]
130
+ require "rails_helper"
131
+ require "branchout"
132
+
133
+ RSpec.describe Loan, type: :model do
134
+ it "explores approve", if: ENV["BRANCHOUT"] do
135
+ clerk = {ruby: "{name: 'Grace', senior: false}"}
136
+ puts Branchout.explore("Loan#approve", within: self, emit_tests: "spec/models/loan_approve_spec.rb", inputs: [
137
+ {receiver: {ruby: "create(:loan)"}, args: [clerk]},
138
+ {receiver: {ruby: "create(:loan, :approved)"}, args: [clerk]},
139
+ {receiver: {ruby: "create(:loan, amount_cents: 25_000)"}, args: [clerk]},
140
+ {receiver: {ruby: "create(:loan, amount_cents: 15_000)"}, args: [clerk]}
141
+ ])
142
+ end
143
+ end
144
+ ```
145
+
146
+ ```sh
147
+ BRANCHOUT=1 bundle exec rspec spec/models/loan_explore_spec.rb
148
+ ```
149
+
150
+ ```text
151
+ branchout explore app/models/loan.rb Loan#approve
152
+ covered: 6 of 6 arms by 4 inputs
153
+ raised: none
154
+ tests: spec/models/loan_approve_spec.rb
155
+
156
+ FRONTIER
157
+ (none: every arm ran, which does not mean each value is right; check RETURNED)
158
+
159
+ RETURNED (coverage does not check a value; compare each with what the method should return)
160
+ returns :approved for inputs[0]
161
+ returns :already for inputs[1]
162
+ returns :needs_senior for inputs[3]
163
+ returns :over_limit for inputs[2]
164
+
165
+ OPERATORS (coverage does not see which side ran; vary each left side)
166
+ app/models/loan.rb:9:50 && in `amount_cents > 10_000 && !officer.fetch(:senior)`
167
+
168
+ BOUNDARIES (each comparison in the counted code, listed whether or not an input made both sides equal; coverage does not see which did)
169
+ app/models/loan.rb:8:65 > in `borrower.approved_cents + amount_cents > borrower.limit_cents`
170
+ app/models/loan.rb:9:41 > in `amount_cents > 10_000`
171
+
172
+ Pass verbose: true for every covered arm and the run settings.
173
+ .
174
+
175
+ Finished in 0.35302 seconds (files took 0.98271 seconds to load)
176
+ 1 example, 0 failures
177
+ ```
178
+
179
+ これですべての腕を通った。
180
+ これは各腕が動いたことしか言わないので、`RETURNED` の各行を、メソッドがすべきことと見比べる。
181
+ ここでは、各ローンが状態と金額に合った答えを得ている。
182
+ 書き出したスペックは、`BRANCHOUT` なしで、ほかのスペックと同じように動く。
183
+
184
+ ```sh
185
+ bundle exec rspec spec/models/loan_approve_spec.rb --format documentation
186
+ ```
187
+
188
+ ```text
189
+ Loan
190
+ #approve
191
+ with create(:loan)
192
+ returns :approved
193
+ with create(:loan, :approved)
194
+ returns :already
195
+ with create(:loan, amount_cents: 15_000)
196
+ returns :needs_senior
197
+ with create(:loan, amount_cents: 25_000)
198
+ returns :over_limit
199
+
200
+ Finished in 0.04388 seconds (files took 0.93715 seconds to load)
201
+ 4 examples, 0 failures
202
+ ```
203
+
204
+ 書き出したスペックをコミットする。
205
+ メソッドが返す値が変わると、このスペックは失敗する。
206
+ 探索のスペックは次の回のために残しても、消してもよい。
207
+ コーディングエージェントも同じループを回す。
208
+ `FRONTIER` を読み、入力を足し、再実行する。
209
+ その手順は [skills/branchout/SKILL.md](skills/branchout/SKILL.md)(英語)にある。
210
+ `Branchout.explore` のすべてのオプションは [テストスイートの中で使う](#テストスイートの中で使う) にある。
211
+
212
+ ## アプリなしで試す
213
+
214
+ CLI は、アプリなしで読み込めるメソッド(`lib/` の下のモジュールなど)を、そのファイルから探索する。
215
+
216
+ gem と、それが書くスペックを実行する RSpec を入れる。
217
+
218
+ ```sh
219
+ gem install branchout rspec
220
+ ```
221
+
222
+ 空のフォルダに、分岐がいくつかあるメソッドを書く。
223
+
224
+ ```sh
225
+ cat > shipping.rb <<'RUBY'
226
+ module Shipping
227
+ def self.fee(weight, express: false)
228
+ raise ArgumentError, "weight must be positive" unless weight.positive?
229
+
230
+ base = weight <= 2 ? 5 : 5 + (weight - 2) * 2
231
+ express ? base * 2 : base
232
+ end
233
+ end
234
+ RUBY
235
+ ```
236
+
237
+ 探索して、スペックを書き出す。
238
+
239
+ ```sh
240
+ branchout explore shipping.rb Shipping.fee --emit-tests shipping_spec.rb
241
+ ```
242
+
243
+ ```text
244
+ branchout explore shipping.rb Shipping.fee
245
+ covered: 5 of 6 arms by 13 inputs
246
+ raised: 3 inputs
247
+ tests: shipping_spec.rb
248
+
249
+ FRONTIER (add an input like the nearest above each arm, changed as the arm says)
250
+ nearest: [1]
251
+ shipping.rb:6:14 makes `express` true
252
+
253
+ RETURNED (coverage does not check a value; compare each with what the method should return)
254
+ returns 5 for [1]
255
+ returns 7 for [3]
256
+
257
+ EXCEPTIONS
258
+ ArgumentError at shipping.rb:3 raised by 3 inputs, such as [-1]: weight must be positive
259
+
260
+ BOUNDARIES (each comparison in the counted code, listed whether or not an input made both sides equal; coverage does not see which did)
261
+ shipping.rb:5:18 <= in `weight <= 2`
262
+
263
+ Pass --verbose for every covered arm and the run settings.
264
+ ```
265
+
266
+ branchout は、メソッド自身のリテラルとシード付きの標本から 13 件の入力を作って実行した。
267
+ それらは 6 本の腕のうち 5 本を通った。
268
+ `FRONTIER` は、どの入力も通らなかった腕である。
269
+ ここでは `express` が一度も真にならなかった。
270
+ `nearest` のような入力で、`express` を真にするものを足す。
271
+ `RETURNED` は、各入力が返した値を、値ごとにまとめて示す。
272
+ カバレッジは入力が通った腕を数えるだけで、値は確かめない。
273
+ それぞれの行を、メソッドが返すべき値と見比べる。
274
+ 1 行に収まらない値は、ほかの値と見分けられる Hash の葉を先に示し、収まる範囲で残りの最上位のスカラーを続ける(`returns a Hash with due_cents: 900, discount: 10, discount_cents: 100, ...`)。
275
+ 入力ファイルの入力が 1 行に収まらないときは、そのファイルでの位置で示す(`inputs[3]`)。
276
+ `--json` は、値全体を Ruby リテラル(`value`)で、入力全体もそのまま示す。
277
+ `EXCEPTIONS` は、例外とそれを出した場所ごとに 1 行で、出した入力の件数とその 1 件を示す。
278
+ `--verbose` を付けると、通った各腕の入力も表示する。
279
+
280
+ 書き出したスペックは、各戻り値と各例外を固定する。
281
+
282
+ ```sh
283
+ cat shipping_spec.rb
284
+ ```
285
+
286
+ ```ruby
287
+ # frozen_string_literal: true
288
+
289
+ # Generated by branchout 0.1.0. Characterization specs for discovered inputs.
290
+ # target: shipping.rb
291
+ # entry: Shipping.fee
292
+
293
+ require_relative "./shipping.rb"
294
+
295
+ RSpec.describe Shipping do
296
+ describe ".fee" do
297
+ it "raises ArgumentError when weight is -1" do
298
+ # covers:
299
+ # line 3: unless weight.positive? -> then
300
+ expect { described_class.fee(-1) }.to raise_error(ArgumentError, "weight must be positive")
301
+ end
302
+
303
+ it "raises ArgumentError when weight is -2" do
304
+ # covers:
305
+ # line 3: unless weight.positive? -> then
306
+ expect { described_class.fee(-2) }.to raise_error(ArgumentError, "weight must be positive")
307
+ end
308
+
309
+ it "raises ArgumentError when weight is 0" do
310
+ # covers:
311
+ # line 3: unless weight.positive? -> then
312
+ expect { described_class.fee(0) }.to raise_error(ArgumentError, "weight must be positive")
313
+ end
314
+
315
+ it "returns 5 when weight is 1" do
316
+ # covers:
317
+ # line 3: unless weight.positive? -> else
318
+ # line 5: weight <= 2 ? 5 : 5 + (weight - 2) * 2 -> then
319
+ # line 6: express ? base * 2 : base -> else
320
+ expect(described_class.fee(1)).to eq(5)
321
+ end
322
+
323
+ it "returns 7 when weight is 3" do
324
+ # covers:
325
+ # line 3: unless weight.positive? -> else
326
+ # line 5: weight <= 2 ? 5 : 5 + (weight - 2) * 2 -> else
327
+ # line 6: express ? base * 2 : base -> else
328
+ expect(described_class.fee(3)).to eq(7)
329
+ end
330
+ end
331
+ end
332
+ ```
333
+
334
+ ```sh
335
+ rspec shipping_spec.rb
336
+ ```
337
+
338
+ ```text
339
+ .....
340
+
341
+ Finished in 0.00334 seconds (files took 0.07131 seconds to load)
342
+ 5 examples, 0 failures
343
+
344
+ ```
345
+
346
+ 次に、フロンティアを閉じる。
347
+ 足りない腕を通る入力を入力ファイルに書き、同じコマンドを `--inputs` 付きで実行する。
348
+
349
+ ```sh
350
+ cat > inputs.json <<'JSON'
351
+ {"inputs": [{"args": [3], "kwargs": {"express": true}}]}
352
+ JSON
353
+ branchout explore shipping.rb Shipping.fee --inputs inputs.json --emit-tests shipping_spec.rb
354
+ ```
355
+
356
+ ```text
357
+ branchout explore shipping.rb Shipping.fee
358
+ covered: 6 of 6 arms by 14 inputs
359
+ raised: 3 inputs
360
+ tests: shipping_spec.rb
361
+
362
+ FRONTIER
363
+ (none: every arm ran, which does not mean each value is right; check RETURNED)
364
+
365
+ RETURNED (coverage does not check a value; compare each with what the method should return)
366
+ returns 5 for [1]
367
+ returns 14 for {"args":[3],"kwargs":{"express":true}}
368
+
369
+ EXCEPTIONS
370
+ ArgumentError at shipping.rb:3 raised by 3 inputs, such as [-1]: weight must be positive
371
+
372
+ BOUNDARIES (each comparison in the counted code, listed whether or not an input made both sides equal; coverage does not see which did)
373
+ shipping.rb:5:18 <= in `weight <= 2`
374
+
375
+ Pass --verbose for every covered arm and the run settings.
376
+ ```
377
+
378
+ ```sh
379
+ rspec shipping_spec.rb --format documentation
380
+ ```
381
+
382
+ ```text
383
+ Shipping
384
+ .fee
385
+ raises ArgumentError when weight is -1
386
+ raises ArgumentError when weight is -2
387
+ raises ArgumentError when weight is 0
388
+ returns 5 when weight is 1
389
+ returns 14 when weight is 3 and express is true
390
+
391
+ Finished in 0.00228 seconds (files took 0.07051 seconds to load)
392
+ 5 examples, 0 failures
393
+ ```
394
+
395
+ これですべての腕を通った。
396
+ 書き出したスペックは、入力ファイルの入力をすべて残し、それ以外は腕ごと、例外ごとに 1 件の入力を残す。
397
+ スペックはコードの横に置いておく。
398
+ メソッドのふるまいが変わると失敗する。
399
+ コーディングエージェントも同じループを回す。
400
+ `FRONTIER` を読み、入力を足し、再実行する。
401
+ その手順は [skills/branchout/SKILL.md](skills/branchout/SKILL.md)(英語)にある。
402
+
403
+ ## CLI リファレンス
404
+
405
+ テストを足しているエージェントは、腕が未達であることまではカバレッジで分かる。どの比較で外れたか、どの入力が決定のいちばん近くまで届いたかは分からない。branchout はそのフロンティアを、条件と、腕を選ぶ向きと、決定に届いた最も近い入力とともに出す。エージェントは入力を足して同じコマンドを再実行する。branchout は入力ファイルを書き換えない。探索が終わると、発見した戻り値または例外を固定する minitest か RSpec を書く。
406
+
407
+ 方法はコンコリックテスト([DART](https://doi.org/10.1145/1065010.1065036)、[CUTE](https://doi.org/10.1145/1094811.1094816))と、[ConcoLLMic](https://concollmic.github.io/) のような LLM が導くコンコリックに沿う。branchout はコンコリックテストに着想を得ているが、記号制約は解かない。次の入力は、エージェントが分岐フロンティアから提案する。
408
+
409
+ このリポジトリのチェックアウトでは、`ruby bin/branchout` が `branchout` と同じコマンドを実行する。
410
+ 種ファイルの入力は `[0]` の 1 件である。
411
+ `--verbose` はレポート全体を出す。
412
+ 動いた戦略の実行の設定と件数のあとに、`COVERED`(各腕を通った入力)、`FRONTIER`(どの入力も通らなかった各腕と、`nearest` が通った腕の `path`)、`RETURNED`(各値と、それを返した入力)、`EXCEPTIONS`(例外を出した入力ごとに 1 件)、`NOT COUNTED`、`OPERATORS`、`BOUNDARIES` が続く。
413
+ 付けないときのレポートは、件数と、入力が届いた `FRONTIER` の腕を、変えるべき `nearest` の入力の下に 1 行ずつと、どの入力も届かなかった決定ごとに 1 行と、返した値ごとに 1 行と、例外とそれを出した場所ごとに 1 行を残す。
414
+ [アプリなしで試す](#アプリなしで試す) の出力がその形である。
415
+ `--json` は、レポート全体のすべての項目を持つ。
416
+
417
+ ```sh
418
+ ruby bin/branchout explore examples/magic_value.rb MagicValue.classify \
419
+ --inputs examples/inputs/magic_value.seed.json \
420
+ --strategy inputs \
421
+ --random 0 \
422
+ --seed 1 \
423
+ --verbose
424
+ ```
425
+
426
+ ```
427
+ branchout explore
428
+ target: examples/magic_value.rb
429
+ entry: MagicValue.classify
430
+ strategies: inputs
431
+ timeout: 1
432
+ inputs_ran: 1
433
+ executions: 1
434
+ arms_covered: 1
435
+ arms_total: 2
436
+ exceptions: 0
437
+ callees_not_counted: 0
438
+ operators_not_counted: 0
439
+ boundaries_not_counted: 0
440
+
441
+ COVERED
442
+ id: if@5:4/else@8:6
443
+ loc: examples/magic_value.rb:8:6
444
+ cond: x == 114514
445
+ sense: cond-false
446
+ input: [0]
447
+
448
+ FRONTIER
449
+ id: if@5:4/then@6:6
450
+ loc: examples/magic_value.rb:6:6
451
+ cond: x == 114514
452
+ sense: cond-true
453
+ parent: reached
454
+ nearest: [0]
455
+ path: if@5:4/else@8:6
456
+
457
+ RETURNED
458
+ returns: :ordinary
459
+ input: [0]
460
+
461
+ EXCEPTIONS
462
+ (none)
463
+
464
+ NOT COUNTED
465
+ (none)
466
+
467
+ OPERATORS
468
+ (none)
469
+
470
+ BOUNDARIES
471
+ (none)
472
+ ```
473
+
474
+ `cond: x == 114514` の `sense: cond-true` が、`[0]` では通らなかった腕である。`[114514]` を足して同じコマンドを再実行する。`examples/inputs/magic_value.json` がそのファイルで、`arms_covered: 2`、`FRONTIER` は `(none)` になる。
475
+
476
+ `ENTRY` は `Const.method` か `Const#method`。
477
+ `module_function` のメソッドと、`extend self` したモジュールのメソッドは `Const.method` で指定する。
478
+ `Const#method` では、入力ごとに `Const.new` で新しいレシーバーを作る。
479
+ `initialize` が引数を取るときは、各入力の `new` に入力と同じ形で書く。
480
+ `{"args": [5], "new": [10]}` は `Const.new(10).method(5)` を呼ぶ。
481
+ キーワードだけを渡す `new` は `args` を省ける。
482
+ `{"args": [5], "new": {"kwargs": {"main": 1}}}` は `Const.new(main: 1).method(5)` を呼ぶ。
483
+ メソッドが読む状態が `new` のあとの呼び出しで決まるときは、その呼び出しを順に `setup` へ書く。
484
+ `{"args": [], "setup": [{"method": "add", "args": ["apple", 3]}]}` は、新しいレシーバーで `add("apple", 3)` を呼んでからメソッドを呼ぶ。
485
+ 各ステップは `method`、`args`、`kwargs` を取る。
486
+ カバレッジが数えるのはメソッドの呼び出しだけで、`new` と `setup` は数えない。
487
+ 対象ファイルが `initialize` を定義していれば、`mutate` と `random` はその引数から `new` も埋める。
488
+ `--entry ENTRY` は位置引数の代わりになる。
489
+
490
+ 探索中は `--strategy inputs --random 0` とし、`--seed` は固定する。新しく覆われた腕は、足した入力だけから来る。探索手順は [skills/branchout/SKILL.md](skills/branchout/SKILL.md) にある。
491
+
492
+ | フラグ | 既定 | 意味 |
493
+ | --- | --- | --- |
494
+ | `--inputs PATH` | なし | 入力の JSON ファイル |
495
+ | `--require PATH` | なし | ターゲットより先に PATH を読み込む。ライブラリの残りが要るターゲットに使う。繰り返し指定できる。PATH のディレクトリはロードパスの末尾、gem の後に入るので、`lib/name.rb` は `require "name/part"` でき、`config/environment.rb` の隣の `config/puma.rb` が puma gem を隠さない。作業ディレクトリの下にファイルがない PATH は、`ruby -r` と同じく機能名として読む。`--require shop -I lib` は `lib/shop.rb` を読み込み、どの `-I` ディレクトリにもない名前はロードパスから探す。出力するテストも PATH を読み込む |
496
+ | `-I DIR` | なし | 探索中、`ruby -I` と同じく DIR をロードパスに入れる。繰り返し指定できる。`--require-by-name` を付けると、DIR の下のファイルは DIR からの名前で読み込む。`app/models` の下の Rails のモデルなどに使う |
497
+ | `--seed N` | `0` | 乱数の種 |
498
+ | `--random N` | `16` | ランダム標本の個数 |
499
+ | `--mutate-cap N` | `256` | リテラル変異の予算 |
500
+ | `--strategy LIST` | `inputs,mutate,random` | 戦略をカンマ区切りで、下の表の順に実行する |
501
+ | `--timeout SEC` | `1` | 入力 1 件の制限時間。0 より大きいこと |
502
+ | `--verbose` | オフ | テキストのレポート全体を出す。実行の設定、通った腕すべて、フロンティアの各腕の `path` を含む。`explore` のみ。`--json` は常にそのすべてを含むので、一緒に指定すると断る |
503
+ | `--json` | オフ | テキストの代わりに JSON を標準出力へ出す |
504
+ | `--json-out PATH` | | JSON レポートを書く |
505
+ | `--md-out PATH` | | `--verbose` が出すテキストレポートを Markdown で書く |
506
+ | `--emit-tests PATH` | | 特徴づけテストを書く。`explore` のみ |
507
+ | `--format NAME` | `minitest` | `minitest` か `rspec`。`--emit-tests` が要る。省くと、パスが `_spec.rb` で終わるときだけ RSpec |
508
+ | `--pin-keys LIST` | | キーのカンマ区切り。戻り値の Hash はこのキーだけを固定する(minitest は `slice`、RSpec は `include`)。後からキーが増えてもテストは落ちない。`--emit-tests` が要る |
509
+ | `--require-by-name` | オフ | 出力するテストは `--require` のファイルと対象を、ロードパス経由で `require` する。名前は、そのファイルを含む最初の `-I` ディレクトリか `--require` ファイルのディレクトリから作る(`require "shop/cart"`)。どちらもないときは、対象より上で最も近い `lib` ディレクトリか、対象自身のディレクトリから作る。どのディレクトリにも入らないファイルはパスで読み込み、その上のコメントと標準エラーの 1 行がそう伝える。require の上の `# Load path: -I lib` というコメントが、テストに要るロードパスのディレクトリを示す。RSpec のファイルは、rspec が自分で入れる `spec` と `lib` を挙げない。`--emit-tests` が要る。`.jitcatch` ディレクトリの下に出すテストは、このフラグがなくてもこうなる。jitcatch はそのテストを各チェックアウトにコピーし、rerun の行は書いた場所から読み込むので、`require_relative` では場所ごとに別のファイルを読み込むからである |
510
+ | `--entry ENTRY` | 位置引数 | `Const.method` か `Const#method` |
511
+
512
+ | 戦略 | 実行するもの |
513
+ | --- | --- |
514
+ | `inputs` | 入力ファイルの各エントリ。canonical JSON が同じものは捨てる |
515
+ | `mutate` | ソースのリテラルを各引数スロットへ入れる。整数には `0`、`1`、`-1`、リテラル±1 も含む。浮動小数には `0.0`、`1.0`、`-1.0`、±1 も含む。文字列には `""`、`"a"`、末尾 1 文字を落としたリテラル、リテラルに `"1"` を足したものも含む。先頭 2 つの位置引数が数値のときは、そのスロットの直積も作り、`--mutate-cap` で切る。文字列リテラルは連結しない。リテラルはエントリと、それが同じファイルで呼ぶ各メソッドから集める。ファイルがリテラルを入れる定数(`HOLD = "sanctions"`)はそのリテラルとして数える。スロットの型も同じメソッドから決める。ヘルパーに渡した引数は、ヘルパーが受けた引数をどう使うかで型が決まる(`def held?(text) = text == HOLD` に `held?(reason)` と渡すと `reason` は String になる)。`doc["key"]` や `doc.dig(:key)` は `doc` を Hash にする |
516
+ | `random` | 種つきの Park-Miller。整数は -3..12。文字列は長さ 0..3、文字は `abc01.-` |
517
+
518
+ ```sh
519
+ ruby bin/branchout baselines examples/magic_value.rb MagicValue.classify \
520
+ --inputs examples/inputs/magic_value.json \
521
+ --seed 1 --random 40
522
+ ```
523
+
524
+ `baselines` は `random`、`mutate`、`agent`(入力ファイルだけ)を別々の行で実行する。random と mutate の行は入力ファイルを実行しない。この対象では、`random` が 15 件を実行して 2 腕のうち 1 腕、`mutate` が 6 件で両腕、`agent` が 2 件で両腕を覆う。
525
+
526
+ 入力ファイルは種 1 件から始める。
527
+
528
+ ```json
529
+ {
530
+ "inputs": [
531
+ {"args": [0]}
532
+ ]
533
+ }
534
+ ```
535
+
536
+ エントリは配列だけでもよい。
537
+ キーワードは `kwargs` に、コンストラクタ引数は `new` に、レシーバーの状態を作る呼び出しは `setup` に書く。
538
+ 保存済みのレコードのように `new` では作れないレシーバーは、`receiver` に Ruby で書く(`{"receiver": {"ruby": "Factories.loan(3)"}, "args": [{"ruby": "loan.amount"}]}`)。
539
+ レシーバーには、出力するテストと同じ名前(クラス名のスネークケース)が付くので、引数と `setup` からそのレシーバーを読める。
540
+ `before` の Ruby は、レシーバーと引数より前に、その入力のセーブポイントの中で動き、クラスメソッドが探す行のようにメソッドが読むものを作る(`{"before": {"ruby": "FactoryBot.create(:part, sku: \"A\")"}, "args": ["A"]}`)。
541
+ RSpec はそれを入力の context の `before` として書き、minitest は呼び出しの前の行として書く。どちらも各行を字下げする。
542
+ ただし `<<` と `<<-` のヒアドキュメントの本文と終端は元の桁のまま書くので、ヒアドキュメントは終わり、その文字列も変わらない。
543
+ 複数行の `before` は、兄弟の入力と違う行で名前を付け、呼び出しは違うキーワードで名前を付ける(`after FactoryBot.create(:membership) with enabled: false`)。
544
+ `tap` や `then` でブロックを動かす Ruby の引数は、兄弟の入力とブロックが違うとき、そのブロックで名前を付ける(`when fact.tap { Parts.rows.replace(["A"]) }`)。
545
+ そうする `before` も同じように名前を付ける(`after FactoryBot.create(:part).tap { |p| p.update!(size: 1) }`)。
546
+ 呼び出しで名前を付ける `before` の名前が 80 文字を超えるときは、兄弟のそれぞれと見分けられる最少のキーワードとブロックだけを残す(`after FactoryBot.create(:part) with code: "B"`)。
547
+ それでも 80 文字を超える文の中の 30 文字を超えるブロックは、兄弟のブロックと分かれる所で切る(`when fact.tap { ...long piece', 2]) }`)。
548
+ `new`、`receiver`、`setup`、`kwargs`、`block`、`before` のどれかがあるエントリは、位置引数のない呼び出しなら `args` を省ける(`{"receiver": {"ruby": "FactoryBot.build(:user)"}}`)。
549
+ 型推論が違うときは、`"types"` に位置引数 1 つにつき `int`、`float`、`string`、`array`、`hash`、`bool` のいずれかを置く。
550
+ 既定値が `[]`、`{}`、`true`、`false` の引数はその型になる。
551
+ `bool` の引数には、変異とランダムサンプリングが `false` と `true` の両方を試す。
552
+ `meta` のような他のトップレベルキーは無視する。
553
+ 各エントリの `note` も無視するので、その入力を選んだ理由をすぐ横に書ける。
554
+ エントリの `pin_keys`(`{"args": [3], "pin_keys": ["tax"]}`)は、その入力のテストに限って `--pin-keys` の代わりになる。
555
+ `"pin_keys": []` は、その Hash を丸ごと固定する。
556
+ レコードや例外のように JSON で書けない値は、`{"ruby": "..."}` に Ruby で書く。
557
+ `--require` したファイルのファクトリを呼ぶことが多い(`{"args": [{"ruby": "Factories.loan(3)"}]}`)。
558
+ 呼び出しごとにトップレベルで評価し直し、出力するテストは書いたとおりに写す(`Lending.approve(Factories.loan(3))`)。
559
+ メソッドが受け取るブロックは、波かっこで `block` に書く(`{"args": [5], "block": "{ |amount| amount > 2 }"}`)。
560
+ キーが `"ruby"` 1 つだけの Hash 引数も Ruby で書く(`{"ruby": "{\"ruby\" => \"3.4\"}"}`)。
561
+ 出力するテストのプロジェクト(テストより上で最も近い、`Gemfile`、gemspec、`.git` のいずれかがあるディレクトリ)の下のパスを指す String は、テスト自身のディレクトリから書く(`File.expand_path("../fixtures/files/rates.csv", __dir__)`)。
562
+ そのためテストは、jitcatch のコピーを含め、プロジェクトのどのチェックアウトでも通る。
563
+ テストの名前と説明は、プロジェクトのルートからのパス(`spec/fixtures/files/rates.csv`)で書く。
564
+ プロジェクトの外で、一時ディレクトリかホームディレクトリの下にあるパスは書いたとおりに残し、標準エラーがそのパスを示す。
565
+ 実在するパスなら、そのテストはこのマシンでしか通らないからである。
566
+ 実在しないパスでも、テストにこのマシンのディレクトリが残るからである。
567
+ そうしたパスは、たとえば `spec/fixtures/files` のようにプロジェクトの下に置く。
568
+ 戻り値やエラーメッセージがプロジェクトの下のパスを含むときも、プロジェクトのルートをテスト自身のディレクトリから書く(`"No such file or directory @ rb_sysopen - #{File.expand_path("..", __dir__)}/spec/fixtures/none.csv"`)。
569
+ プロジェクトの外にあるこのマシンのパスを含むときは書いたとおりに残し、標準エラーがそのパスを示す。
570
+ テストの名前と説明は、一時ディレクトリかホームディレクトリの下のパスをファイル名だけで書くので、どのマシンでも同じになる。
571
+ エントリは末尾へ足す。
572
+ 消したり並べ替えたりしない。
573
+
574
+ ```sh
575
+ ruby bin/branchout explore examples/hidden_bug.rb HiddenBug.scale \
576
+ --inputs examples/inputs/hidden_bug.json \
577
+ --strategy inputs --random 0 --seed 1 \
578
+ --emit-tests spec/generated/hidden_bug_spec.rb \
579
+ --format rspec
580
+ ```
581
+
582
+ この実行は 4 腕のうち 4 腕を覆い、`[24,12]` で `ZeroDivisionError`(`divided by 0`)を記録する。出力ファイルは戻り値、または例外クラスとメッセージを固定する。
583
+ 複数行のメッセージは、発生したときのまま固定する。
584
+ 各テストの先頭にある `# covers:` の一覧は、その入力が通った腕を 1 行に 1 つずつ示す。
585
+ 各行は、条件の行番号、書かれたままの条件、通った腕である(`line 20: unless rate.is_a?(Numeric) -> else`、`line 9: elsif tag.start_with?("beta") -> then`)。
586
+ 三項演算子は式全体を書く(`line 10: label ? :noted : :plain -> then`)。
587
+ `case` の腕は、その腕を示す `when`、`in`、`else` の行に置く(`line 6: when 1, 2 then :low`)。
588
+ `NameError` と `NoMethodError` は、クラスと見つからなかった名前(`error.name`)で固定する。
589
+ Ruby 3.4 でメッセージの文言が変わったためである。
590
+ バックトレースのフレーム(`lib/x.rb:3:in 'Box.run'`)を含むメッセージは正規表現で照合し、フレームのラベルだけはどちらの書式でも通す。
591
+ Ruby 3.4 はラベルを `` `run' `` から `'Box.run'` に変えたためである。
592
+ `raised in:` の行のように、フレームを含む String の戻り値も同じように照合する。
593
+ `Set` は要素で固定し(`Set[1, 2]`)、`Date` は値で固定する(`Date.new(2026, 4, 1)`)。
594
+ メンバーがすべてリテラルで書ける `Struct` と `Data` は、クラスとメンバーの値で固定する(`eq(Rule.new(action: "borrow", on: Date.new(2026, 4, 1)))`)。
595
+ `==` がメンバーを比べるためである。
596
+ 自前の `initialize` がそのメンバーから別の値を作るものは、この形では固定しない。
597
+ `Object`、`Time`、レコードのように Ruby のリテラルで書けない戻り値は、クラスだけを固定する。
598
+ RSpec では、そうした値を含む `Struct` と `Data` はクラスとメンバーで固定し、それを含む Array や Hash は要素ごとに固定する(`match([a_kind_of(Rule).and(have_attributes(action: "borrow", at: a_kind_of(Time)))])`)。
599
+ minitest も同じ部分を確かめる。
600
+ リテラルのない部分はクラスで(`assert_kind_of Time, value[0].at`)、`Struct` と `Data` はクラスとメンバーで確かめる。
601
+ Array と Hash は、それらの部分を値から読み戻したリテラルと比べる(`assert_equal([value[0]], value)`)ので、キーや要素が欠けても失敗する。
602
+ リテラルのないキーを持つ Hash は、どちらの形式でもクラスで固定する。
603
+ 期待する Hash は値そのもののキー順で書くので、失敗時の差分をキーごとに見比べられる。
604
+ private や protected の入口と private な `new` は、`explore` と同じく `send` で呼ぶ。
605
+ String の引数は素のリテラルで書き、ファイルの `frozen_string_literal` がそれを凍結する。
606
+ `explore` も凍結した String を渡す。
607
+ 凍結した String では `FrozenError` を起こし、凍結していない String では起こさない入力だけは、凍結していない方の実行を残す。
608
+ そのテストは各 String を `+"..."` と書き、その理由をコメントで示す。
609
+ この実行は 2 回目で、1 回目の実行が例外の前に変えたものを見ている。
610
+ セーブポイントは行を取り消すが、グローバル、クラス変数、ファイルはどの実行も取り消さない。
611
+ コメントもこのことを示す。
612
+ その値は、出力したテストを単独で実行して確かめる。
613
+ `private_constant` で隠したクラスやモジュールは `const_get` でたどり(`Shop.const_get(:Ledger)`)、RSpec の describe はその名前を文字列で書く。
614
+ minitest のテスト名は入力と結果から作る。
615
+ `test_24_12_raises_zero_division_error` のようになる。
616
+ 名前が同じになる入力どうしは、互いに異なる値で名付ける(`test_status_timeout_and_error_class_timeout_returns_ok` など)。
617
+ そのため、入力を足すとそれらの名前が変わることがある。
618
+ 先頭が同じ文字列どうしは、分かれたところからの単語で名付ける。
619
+ `receiver` の 2 つのファクトリー呼び出しのように先頭が同じ Ruby のソースは、分かれる直前の単語も残す(`test_limit_100_returns_ok`)。
620
+ 名前は単語の切れ目で切る。
621
+ 空白だけの文字列は `blank` になり、空の配列や Hash の引数は `empty_array` や `empty_hash` になる。
622
+ Hash の引数は値で名付ける。
623
+ Hash の引数の中のコレクションは、キーと大きさで名付ける(`groups_3`、`tags_empty`)。
624
+ コレクションの配列は大きさで名付ける(`array_of_2`)。
625
+ スカラーとコレクションが混ざる配列は、`array` を付けて、各スカラーと各 Hash のキーで名付ける(`returns [:ok, a Hash of id, name]`、`returns_array_ok_hash_id_and_name`)。
626
+ `["Done", []]` は文字列 `"Done"` と読めないよう `returns_array_done_empty_array` になる。
627
+ 16 桁以上の数は、先頭 3 桁と 10 の累乗で名付ける(`123e46`)。
628
+ 別の葉も同じキーで終わるときは、その上のキーも名前に入れる(`id` の隣の `items_id`)。
629
+ Hash の結果は、兄弟どうしを見分けられる最少の葉で名付ける。
630
+ 入れ子の葉も対象になる(`returns_due_cents_0_and_discount_100`)。
631
+ minitest と RSpec は同じ葉を使う。
632
+ 兄弟がないときはキーで名付ける(`returns_hash_lines_and_discount_cents`)。
633
+ `--pin-keys` や実行ごとに変わる id のために、リテラルのない部分を持つ Hash や配列は、リテラルのある部分で両方の形式とも同じように名付ける。
634
+ 兄弟を見分ける葉(`returns_due_cents_100`、RSpec では `returns due_cents: 100`)、なければキー(`returns_hash_name_and_start_at`、RSpec では `returns a Hash of name, start_at, end_at`)か要素数(`returns_array_of_2`、RSpec では `returns an Array of 2`)を使う。
635
+ 2 つ目からのキーの前には `and` を置き、キーは途中で切らない。
636
+ スカラーの配列は `array` を付けて名付ける(`[nil]` は `returns_array_nil`)。
637
+ `nil`、`true`、`false` と読める文字列やシンボルには `text` を付ける(`returns_text_nil`)。
638
+ そう読める Hash のキーには `key` を付ける(`{"NIL" => 0}` は `returns_key_nil_0`)。
639
+ 対象のディレクトリの外に出力したテストは、対象を絶対パスで読み込む。
640
+ `--require-by-name` を付け、`-I` か `--require` のディレクトリが対象を含むときは、ロードパス経由で読み込む。
641
+ テストが自分のプロジェクト(`Gemfile`、gemspec、`.git` のどれかがある最も近いディレクトリ)の外のファイルをパスで読み込むときは、絶対パスでも別のプロジェクトへ上る相対パスでも、このマシンのその場所でしか動かないことを標準エラーで警告し、直し方を示す。
642
+ `--require` のファイルがすでに対象を読み込むときは、対象の require を書かない。
643
+
644
+ RSpec のファイルは定数を describe し、その中で `.method` か `#method` を describe する。
645
+ クラスは `described_class` として呼ぶ。
646
+ `true` と `false` は `be(true)` と `be(false)` で、`nil` は `be_nil` で固定する。
647
+ どちらの形式でも、Hash のリテラルは波括弧の内側に空白を置く(`{ total: 2 }`)。
648
+ RuboCop と rubocop-rspec の既定に合わせるためで、出力し直したファイルに自動修正は要らない。
649
+ レシーバーは `let` に、各 setup 呼び出しは `before` に置く。
650
+ レシーバー、呼び出し、引数の値が先頭から同じ入力どうしは、1 つの `context` を共有する。
651
+ キーワードの一部だけが兄弟の入力と違う `new` のレシーバーは、そのキーワードで名前を付ける(`context "with start_at: Date.new(2026, 5, 1)"`)。
652
+ `new` にはキーワードを入力の順に渡す。
653
+ 各 example はまず結果を述べ、次にその example だけの引数の値を述べる。
654
+ そのため `rspec --format documentation` は仕様のように読める。
655
+ 引数の中の値は、そこへのパスで名付ける。
656
+ スカラーの配列は全体で名付ける(`items is [1, 2]`)。
657
+ パスは String のキーを引用符付きで、Symbol のキーを引用符なしで書く。
658
+ そのため Rails の params のような String キーの Hash が、Symbol で引けるようには読めない。
659
+ JSON の入力 `[{"sku": "B"}]` は `items[0]["sku"] is "B"` と読み、Ruby のリテラル `{"ruby": "[{sku: \"B\"}]"}` は `items[0].sku is "B"` と読む。
660
+ `{"ruby": ...}` の Ruby が単純なリテラルなら、どちらの形式でも同じようにその値で名付ける。
661
+ `FactoryBot.build(:fact, a: "X", b: "Y")` のようにキーワード引数を持つ Ruby の呼び出しは、Hash と同じようにキーワードで名付ける(`when b is "Y"`)。
662
+ そのため、長い先頭が同じファクトリ呼び出しも見分けられる。
663
+ キーワードより前の呼び出しは、違うときだけ名付ける。
664
+ Hash の結果は、各キーをリテラルと同じ形で書く(`returns "state" => "open"`、`returns due_cents: 0`)。
665
+ String のキーの下にある葉は、それを持つリテラルで書き、同じキーの下の葉は一つのリテラルにまとめる(`returns "counts" => {"discarded" => 1}`、`returns "tests" => [{"file" => "a_spec.rb", "disposition" => "flaky"}]`)。
666
+ context は、兄弟と見分けるのに要る値だけを述べる。
667
+ 文は 80 文字を超えない。
668
+ 長くなる example は、兄弟と見分けるのに要る値だけを、いちばん深いキーで述べる(`when status is "timeout"`)。
669
+ 別の葉も同じキーで終わるときは、見分けられるところまで上のキーも書く(`when result.status is "timeout"`)。
670
+ String のキーは、引数とその葉を持つリテラルで書く(`when doc holds "status" => "timeout"`、`when doc holds "result" => {"status" => "timeout"}`)。
671
+ `doc["status"]` と書くと別の葉を指すからである。
672
+ 40 文字を超える String とスカラーの配列の結果は、先頭を見せてから切る(`returns ["require_relative \"../lib/shop/cart...`)。
673
+ 長い文字列は、兄弟と分かれたところで切る。
674
+ 切れ目が単語の途中に来るときは、その単語の前で切る。
675
+ それでも 80 文字を超える example は、見分けるのに要る値を残し、先頭の結果のほうを切る(`returns ["require \"shop\"",... when target_path is "app/cart.rb"`)。
676
+ 例外を出す example は、例外のクラスを切らずに残し、そのあとの値を切る。
677
+ ダブルクォートを含む文は、エスケープが要らないようにシングルクォートで書く。
678
+ 文の中の String の値がバックスラッシュを含むときも同じである(`'returns 1 when x is "say \"hi\""'`)。
679
+
680
+ ```text
681
+ Arithmetic
682
+ .classify
683
+ returns :miss when x is 0, y is 0 and z is 0
684
+ returns :miss when x is 114514, y is 0 and z is 0
685
+ when x is 138
686
+ returns :x_only when y is 0 and z is 0
687
+ when y is 143
688
+ returns :need_z when z is 0
689
+ returns :hit when z is 7
690
+ ```
691
+
692
+ どの入力でも引数をとらないメソッドは `subject` になる。
693
+ 例外を出した入力は、setup の呼び出しを `expect` のブロックの中に残す。
694
+ そのため setup で出た例外も固定できる。
695
+ minitest のパスは `--format minitest` で、`ruby` で実行する。RSpec には `rspec` gem が要る。
696
+
697
+ ## テストスイートの中で使う
698
+
699
+ `Branchout.explore` はテストの中から動き、CLI と同じレポートを返す。
700
+ Gemfile の行と探索のスペックの例は [Rails アプリでのクイックスタート](#rails-アプリでのクイックスタート) にある。
701
+ スペックの 1 行目の `require "branchout/coverage" if ENV["BRANCHOUT"]` は、ヘルパーがアプリを起動する前に Coverage の準備をする。
702
+ そのためモデルはその後に読み込まれる。
703
+ minitest では `require "test_helper"` の前に置く。
704
+ Rails のないスイートでも同じように動く。
705
+
706
+ 探索のファイルは単独で実行する。
707
+ スイート全体の実行では、別のスペックファイルが先にモデルを読み込むことがある。
708
+ `.rspec` が `rails_helper` を require していると、スペックの 1 行目より前にアプリが起動する。
709
+ そのため、イニシャライザが読むモデルは早すぎる時点で読み込まれ、explore はそれを断る。
710
+ テスト環境が eager load する(`config.eager_load`。Rails は `ENV["CI"]` から決める)と、すべてのモデルが同じことになる。
711
+ そのときは、rspec が何かを読み込む前に Coverage を始める。
712
+ `BRANCHOUT=1 RUBYOPT="-rbundler/setup -rbranchout/coverage" bundle exec rspec spec/models/loan_explore_spec.rb` と実行する。
713
+ `bundle exec` は Bundler がロードパスを設定する前に `RUBYOPT` を読むので、`-rbundler/setup` を先に置く。
714
+ それがないと、`-rbranchout/coverage` は Gemfile が示すものではなくインストール済みの branchout を読み込む。
715
+ Gemfile が branchout を `path:` や `git:` で固定していれば `LoadError` になる。
716
+ `rspec -r branchout/coverage` では解決しない。
717
+ rspec はそれを `.rspec` が示すファイルのあとに require するからである。
718
+ 断るメッセージは、rspec が先に require したファイルと、このコマンドを示す。
719
+ `.rspec` が、モデルを読み込むヘルパーの前に読み込まないヘルパーを require しているとき(`rails_helper` の前の `spec_helper` など)、断るメッセージは `rspec --options /dev/null -r spec_helper <探索のファイル>` も示す。
720
+ これはモデルを読み込んだヘルパーと、`.rspec` のほかのオプションを外す。
721
+ Rails より前に準備すると、Coverage はすべての gem も計測する。
722
+ クイックスタートのサンプルアプリでは、`spec/rails_helper.rb` の `config/environment` を読み込む行のすぐ後に置いた場合と比べて、探索の実行は起動が約 0.3 秒、入力 1 件ごとに約 12 ms 長くなった。
723
+ 起動中にモデルを読み込むものがなければ、その置き方のほうが速い。
724
+ その時点で gem は読み込み済みで、モデルは最初に使うときに読み込まれる。
725
+
726
+ 入力は入力ファイルのエントリの並びか、そのファイルのパスである。
727
+ 入力の Ruby はそのテストの中(`within: self`)で動くので、スイートのファクトリ、フィクスチャ、ヘルパーを使える。
728
+ ActiveRecord が読み込まれていれば、各入力はセーブポイントの中で動き、最後にロールバックされる。
729
+ そのため、ある入力が書いた行を別の入力は見ず、テスト自身の行もそのまま残る。
730
+ `around:` に、入力をブロックとして受け取るラムダを渡すと、これを置き換えられる。
731
+ 凍結した String で `FrozenError` を起こした入力を凍結していない String でもう一度実行するのは、その入力が `Branchout::ROLL_BACK` の中で動いたときだけである。
732
+ 既定の場合と、`Branchout::ROLL_BACK` を呼ぶ `around:` の場合がそれにあたる。
733
+ `Branchout::RUN_ONLY` などそれ以外の `around:` では `FrozenError` を固定する。
734
+ その `around:` が 1 回目の実行の行を取り消したかどうかを branchout は判断できないためである。
735
+ `Branchout::ROLL_BACK` の中で動いた入力は、CLI の各入力と同じく、もう一度実行する。
736
+ そのため、生成された UUID の id、ファクトリのシーケンス、`SecureRandom` のトークンのように実行ごとに作られる値は固定しない。
737
+ 返された Hash は、2 回とも同じ値だったキーだけを固定する(RSpec では `include`、minitest では `slice` を使う)。
738
+ `Time` やレコードのようにリテラルのない値は、クラスで固定する。
739
+ 値全体が違った入力にはテストを書かない。
740
+ そのためにテストが 1 つもないファイルは、そのことを書いた example(`pins no input`)を skip する。
741
+ メッセージが違った例外は、クラスだけを固定する。
742
+ そうした入力はどれも標準エラーに示す。
743
+ そうした入力しか通らなかった分岐は、どの生成テストも確かめないので、レポートの `NOT PINNED` に示す。
744
+ `--emit-tests`(`emit_tests:`)を付けると、`Time` やレコードのように生成テストがクラスしか確かめない戻り値の部分を、それを返した最初の 3 つの入力とともに `PINNED BY CLASS` に示す(`value[:start_at] is a Time: ["A",{"ruby":"Time.at(0)"}]`)。
745
+ その部分で大事なことは手で確かめる。
746
+ JSON のレポート(`json: true`)は、`emit_tests:` の有無にかかわらず、同じ部分を `pinned_by_class` に持つ。
747
+ 各部分の `path` と `class`、それを返したすべての入力である。
748
+ 戦略は `strategies:` で変えない限り `inputs` だけで、`timeout:` は指定しない限り 1 秒である(`nil` で切る)。
749
+ `target:`、`emit_tests:`、`format:`、`json:`、`verbose:`、`pin_keys:`、`requires:`、`seed:`、`random:`、`mutate_cap:` は、CLI の同名のフラグと同じ意味である。
750
+ minitest では、同じ呼び出しを `ActiveSupport::TestCase` のテストに書き、`BRANCHOUT=1 bin/rails test` で動かす。
751
+
752
+ 出力するテストは、スイートが読み込んだヘルパー(`rails_helper`、なければ `test_helper`、なければ `spec_helper`)を名前で require する。
753
+ どれも読み込んでいないスイートは、`requires:` でヘルパーを指定する。
754
+ `requires:` には 1 つのパスを文字列で渡すことも、パスの配列を渡すこともできる。
755
+ ファイルを指さない `requires:` の要素はロードパスで探し、どこにもなければ `requires: "spec/helper" names no file under the working directory or on the load path` で止まる。
756
+ 指定しないと `emit_tests:` を断る。
757
+ モデルはアプリが読み込むので require しない。
758
+ minitest のテストは、探索したテストの基底クラス(`ActiveSupport::TestCase` など)を継承する。
759
+ RSpec のファイルは、探索した example group の `type`(`type: :model` など)を `describe` に付ける。
760
+ rspec はプロジェクトの `spec` と `lib` を自分でロードパスに入れるので、RSpec のファイルの `# Load path:` コメントはこの 2 つを挙げない。
761
+ 他のテストと同じく、`BRANCHOUT` なしで実行する。
762
+
763
+ Coverage が数えるのは、`branchout/coverage` の後に読み込まれたファイルだけである。
764
+ それより前に読み込まれたターゲットは、読み込み直さずに断る。
765
+ モデルを読み込み直すと、バリデーションが二重に宣言されるからである。
766
+ ヘルパーで SimpleCov を始めるスイートは、`branchout/coverage` の前でも後でも、変えなくてよい。
767
+ その実行は branchout が計測し、SimpleCov はその実行を 0% と報告する。
768
+ explore は入力ごとに数を消すからである。
769
+ 入力の Ruby は、探索する example のローカル変数を読んではいけない。
770
+ `let` や `subject` を読む入力は断る。
771
+ 出力するファイルにはどちらもないからである。
772
+ また、入力の中で初めて読まれた `let` は、その入力のロールバックされるセーブポイントの中で作られ、次の入力に持ち越される。
773
+
774
+ CLI でも、`RAILS_ENV=test` と `--require config/environment.rb` でアプリを読み込める。
775
+ ただしその場合、各入力の行はテスト用データベースに残り、後の入力はそれを見る。
776
+ テストのヘルパーも使えない。
777
+
778
+ ## フロンティアの作り方
779
+
780
+ `--require` のファイルと対象ファイルは `Coverage.setup(lines: true, branches: true)` のあとで読み、ロード時のカウントは捨てる。入力ごとに coverage を resume し、メソッドを呼び、suspend して `Coverage.result` を読む。残すのは入口メソッドと、同じファイル内で裸呼び出しまたは `self.` から届く callee だけである。
781
+ `--require` のファイルが `require` か、Rails が initializer を読むときの `Kernel#load` で読み込み済みの対象は、もう一度は読まない。
782
+ `def self.apply(doc) = new(doc).apply` のように自分のインスタンスを作るクラスメソッドは、`initialize` と、そのインスタンスで呼ぶメソッドまで追う。
783
+ `new(path).tap(&:parse)` や `then(&:parse)` で呼ぶメソッドも追う。
784
+
785
+ 別のファイルのメソッドは数えないので、その腕は合計(`covered: N of M arms` の `M`、`--verbose` と `--json` では `arms_total`)に入らない。
786
+ サブクラスのメソッドにあった検査を親クラスのヘルパーに移すと、ロジックは同じでも合計が小さくなる。
787
+ `NOT COUNTED` はそういう callee を挙げる。
788
+ 数えたコードが裸呼び出しか `self.` で呼び、Ruby が別のファイル(親クラスや mixin など)で見つけるメソッドである。
789
+ `Const.new` で作ったオブジェクトに呼ぶメソッドも挙げる。
790
+ そのオブジェクトは、ローカル変数(`journal = Ledger.new`)か、クラスのメソッドが設定するインスタンス変数(`initialize` の `@ledger = Ledger.new`)に入っているものである。
791
+ 各項目には `callee`(`Register#required_sku`)、`loc`(その `def` のファイルと行)、`arms`(その `def` の中の分岐の腕の数)がある。
792
+ そのファイルが coverage の開始より前に読み込まれたときや、`define_method` のように `def` がないときは、`arms` は `unknown` になる。
793
+ その腕を通すには、callee を入口にして explore する。
794
+ インストールした gem や Ruby 自身のライブラリのメソッドは挙げない。
795
+ アプリはそこへ検査を移せないからである。
796
+ Rails のモデルは、関連の reader や `update!` など、その種のメソッドを何十も呼ぶ。
797
+ C で書かれたメソッド、Ruby 自身の prelude のメソッド、属性の reader と writer、腕のないメソッド、クラスがコードから分からないオブジェクトへの呼び出しは挙げない。
798
+
799
+ 分岐カバレッジは `||`、`&&`、`||=`、`&&=` に腕を数えないので、右辺が実行されたかどうかは分からない。
800
+ `user&.admin? || params[:force]` の `&.` より先のように、これらだけで分岐するメソッドは、実際より少ない腕を報告する。
801
+ 腕が 1 つもなければ `covered: 0 of 0 arms` になり、すべてカバーしたように読める。
802
+ `OPERATORS` は、数えたコードにあるそうした演算子を 1 つずつ挙げ、`operators_not_counted` がその数を示す。
803
+ 各項目には `loc`(演算子の行と列)、`op`、`text`(式全体で、160 文字で切る)がある。
804
+ 左辺が真になる入力と偽になる入力を与える。
805
+ ファイルのほかの場所で代入しないインスタンス変数への `@ivar ||=` と `@ivar &&=` は挙げない。
806
+ 1 つのオブジェクトへの呼び出しをまたいでメモ化するもので、各入力はレシーバーを新しく作るため、どの入力も左辺を変えられないからである。
807
+ 入口に腕がなく、そうした演算子があるときは、stderr がそう伝える。
808
+ 分岐がそのメモ化だけのときは、stderr がそのメモ化を挙げ、右辺が呼ぶメソッドを探索し、メモ化は 1 つのオブジェクトへの 2 回の呼び出しで手で確かめるよう伝える。
809
+ 比較の `<`、`<=`、`>`、`>=` も、カバレッジにはどちらの辺も見えない。
810
+ そのため、両辺を等しくする入力がなくてもすべての腕を通れるが、`>` を `>=` に変えた変異はその入力でしか見つからない。
811
+ `BOUNDARIES` は数えたコードの比較を 1 つずつ挙げ、`boundaries_not_counted` がその数を示す。
812
+ 項目の形は `OPERATORS` と同じである。
813
+ 両辺を等しくする入力を与える。
814
+ この一覧はソースから作るので、入力がそうしても変わらない。
815
+ `&.` には自分の腕がある(`receiver-present`、`receiver-nil`)。
816
+
817
+ 腕の id は Coverage の分岐キーから作る `kind@行:列/arm@行:列` である。列は 0 始まり。`cond` は Prism の述語で、`when` または `in` の腕ではその行である。位置が一致しないときはソースの行を使う。
818
+
819
+ フロンティアはカウントが 0 の腕である。`parent: reached` は、どれかの入力がその決定を実行したことを意味する。`nearest` は、そこに届いた入力のうち、通った腕が最も少なく、次に重み(数の絶対値に、文字列とコンテナの長さを足したもの)が最も小さく、次に canonical JSON が最も小さいものである。`path` は、その入力が通った腕のソート済み集合である。Coverage は順序を記録しない。ループ回数が 1 を超えると `*回数` を付ける。
820
+
821
+ `sense` は腕を選ぶ向きである。`cond-true`(`if` の then、`unless` の else)、`cond-false`(`if` の else、`unless` の then)、`arm-matched`(その `when` または `in`)、`arm-else`、`body-entered`(`while` または `until` の本体)、`receiver-present`、`receiver-nil`(`&.`)。
822
+
823
+ 例外は腕のカバーとは別に出す。カバー済みの腕でも例外は起きる。同じ入力、同じフラグ、同じ CRuby なら標準出力は同じバイト列になる。前の入力と同じ入力は一度だけ実行し、重複した数を標準エラーに出す。レポートに時刻はない。パスは作業ディレクトリからの相対パスである。
824
+
825
+ ## インストール
826
+
827
+ Ruby 3.3 以降。探索に使うのは標準ライブラリの Coverage、Prism、json である。`gem install branchout` で `branchout` コマンドが入る。Gemfile のあるプロジェクトでは、代わりに Gemfile に `gem "branchout", group: :test, require: false` を書いて `bundle exec branchout` を実行する。このリポジトリのチェックアウトでも動く。その場合は `ruby bin/branchout` を実行するか、`bin` を `PATH` に置く。出した minitest は `ruby` で実行する。出した RSpec には `rspec` gem が要る。バンドルの外では、`bin/branchout` は入っているどの gem も受け入れる prism のうち最も新しいものを有効にする。そのため、より新しい prism が入っていても、minitest 6 (prism ~> 1.5) を require するターゲットは読み込める。
828
+
829
+ [branchout スキル](skills/branchout/SKILL.md)(英語)は [`skills` CLI](https://github.com/vercel-labs/skills)(Node.js 22.20 以降)で入れる。このリポジトリは private なので、GitHub へのアクセスが要る。
830
+
831
+ ```sh
832
+ npx skills add meganemura/branchout
833
+ ```
834
+
835
+ ```sh
836
+ npx skills add meganemura/branchout --skill branchout
837
+ ```
838
+
839
+ ```sh
840
+ npx skills add meganemura/branchout -g
841
+ ```
842
+
843
+ ```sh
844
+ npx skills add meganemura/branchout -a cursor
845
+ ```
846
+
847
+ `--skill` はこのスキルだけ、`-g` は全プロジェクト向け、`-a` は対象エージェントを指定する。
848
+
849
+ コピーまたはシンボリックリンクでも入れられる。
850
+
851
+ ```sh
852
+ mkdir -p .agents/skills
853
+ ln -s /path/to/branchout/skills/branchout .agents/skills/branchout
854
+ ```
855
+
856
+ `.claude/skills/` と `.cursor/skills/` でも同じ。
857
+
858
+ ## 限界
859
+
860
+ - 対象は実際に実行される。対象の標準出力と標準エラーは、ロード時も呼び出しごとにも捨てる。ファイル、グローバル、ネットワークはサンドボックスしない。
861
+ - `FrozenError` を起こして凍結していない String でもう一度実行した入力は、1 回目の後に同じプロセスで動く。1 回目の実行が変えたグローバル、クラス変数、ファイルは元に戻らない。
862
+ - 対象は branchout と同じプロセスに読み込まれる。
863
+ 別のチェックアウトの branchout を読み込む対象や `--require` のファイルは、ツール自身のクラスを置き換えるので拒否する。
864
+ branchout 自身のコードを調べるときは、そのチェックアウトの `bin/branchout` を使う。
865
+ 引数がツール自身のファイルを指すとき、ツールは自分を読み込む前に coverage を準備する。
866
+ そのため、そのファイルを読み込み直さずに coverage が測れる。
867
+ coverage の準備より前に読み込まれたファイル、たとえば Ruby 自身のライブラリは拒否する。
868
+ 読み込み直すとクラスが二度定義され、最初のクラスで作ったオブジェクトがそのクラスのものでなくなるからである。
869
+ - 入口は対象ファイルの中で、その定数か、同じファイルで宣言したスーパークラスの下に定義されていなければならない。
870
+ 継承した入口は、Ruby と同じくサブクラスの上書きを追う。
871
+ - 時刻、乱数、I/O に依存するメソッドは、同じ入力でもレポートが揃わない。
872
+ - 経路は腕の集合であり、順序ではない。`&&`、`||`、`||=`、`&&=` は腕にならず、`OPERATORS` に挙がる。`while` と `until` が記録するのは本体の腕で、条件は実行されたが本体に入らなかったことは行カウントで補う。
873
+ - 修飾子の `unless` は then と else が同じ位置になることがある。id の腕名で区別する。
874
+ - 追うのは同じファイルの裸呼び出し、`self.` 呼び出し、裸の `new(...)` に対する呼び出しだけである。
875
+ `Enumerable` を include したクラスの `select(&:ok?)` のように、そうして呼んだ Enumerable のメソッドは、そのファイルが定義する `each` まで追う。
876
+ `Other.new.check(x)` のような別のレシーバーへの呼び出しは追わない。
877
+ 入口がどの腕にも届かないとき、`explore` は stderr に警告を出す。
878
+ この場合は分岐するメソッドを対象にする。
879
+ それでも `--emit-tests` は入力ごとの戻り値または例外を固定する。
880
+ - `Timeout.timeout` はすべてのブロックを止められない。タイムアウトのバックトレースに対象行が無いとき、`loc` はメソッド定義行になる。
881
+ - 充足できない条件はフロンティアに残る。`examples/arithmetic.rb` の `x == 114514 && x == 0` がそれである。
882
+ - 上のランダムと変異の範囲は、意図して狭い。
883
+ - Coverage はプロセス全体で 1 系統である。CLI は別プロセスで実行する。
884
+
885
+ ## 開発
886
+
887
+ 次のコマンドはソースのチェックアウト用である。
888
+
889
+ ```sh
890
+ bundle install
891
+ ruby test/run_all.rb
892
+ bundle exec rspec spec/generated
893
+ ```
894
+
895
+ `test/run_all.rb` は `test/test_*.rb` と `test/generated/*_test.rb` を読み込む。
896
+ プロパティテストは hegeltest を使うので、Ruby 3.4 以上が要る。
897
+ Ruby 3.3 では `BUNDLE_WITHOUT=property bundle install` とし、プロパティテストはスキップ 1 件になる。
898
+ `spec/generated/` は `examples/` から出した RSpec である。Coverage はプロセス全体で 1 系統なので、CLI のテストは branchout をそれぞれ別のプロセスで動かす。
899
+ fork が使えるときは、branchout を読み込み済みのサーバーから fork する。
900
+
901
+ ## ライセンス
902
+
903
+ MIT