jitcatch 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.ja.md +1052 -0
- data/README.md +529 -0
- data/bin/jitcatch +16 -0
- data/lib/jitcatch/assessor.rb +502 -0
- data/lib/jitcatch/changes.rb +1084 -0
- data/lib/jitcatch/cli.rb +201 -0
- data/lib/jitcatch/diff_parser.rb +59 -0
- data/lib/jitcatch/factories.rb +164 -0
- data/lib/jitcatch/failure.rb +86 -0
- data/lib/jitcatch/framework.rb +110 -0
- data/lib/jitcatch/git_repo.rb +180 -0
- data/lib/jitcatch/inflections.rb +142 -0
- data/lib/jitcatch/minitest_parser.rb +110 -0
- data/lib/jitcatch/minitest_report.rb +54 -0
- data/lib/jitcatch/readers.rb +907 -0
- data/lib/jitcatch/render.rb +712 -0
- data/lib/jitcatch/routes.rb +288 -0
- data/lib/jitcatch/rspec.rb +9 -0
- data/lib/jitcatch/rspec_parser.rb +122 -0
- data/lib/jitcatch/runner.rb +905 -0
- data/lib/jitcatch/source_index.rb +1382 -0
- data/lib/jitcatch/template.rb +38 -0
- data/lib/jitcatch/version.rb +5 -0
- data/lib/jitcatch.rb +96 -0
- data/skills/jitcatch/SKILL.md +244 -0
- metadata +73 -0
data/README.ja.md
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
# jitcatch
|
|
2
|
+
|
|
3
|
+
jitcatch は、変更の親で通り、その変更で落ちる Ruby のテストを残し、それぞれが届く変更されたメソッドを示す。
|
|
4
|
+
|
|
5
|
+
## Rails アプリでのクイックスタート
|
|
6
|
+
|
|
7
|
+
Ruby 3.3 以降、git、そして spec を RSpec(`rspec-rails`)で走らせるアプリが要る。
|
|
8
|
+
どの手順もアプリのルートで走らせる。
|
|
9
|
+
|
|
10
|
+
### 1. gem を足す
|
|
11
|
+
|
|
12
|
+
アプリの `Gemfile` の `:development, :test` グループに jitcatch を足し、`bundle install` を走らせる。
|
|
13
|
+
jitcatch には実行時の依存がないので、bundle にほかの gem は増えない。
|
|
14
|
+
|
|
15
|
+
<!-- rails:gemfile -->
|
|
16
|
+
```ruby
|
|
17
|
+
group :development, :test do
|
|
18
|
+
gem "jitcatch", require: false
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Rails の既定の場所にある SQLite なら、準備は要らない。
|
|
23
|
+
テスト用データベースが 1 つの名前の PostgreSQL や MySQL なら、jitcatch は base と head がそれを共有すると見つけ、プロセスを 1 つずつ動かす。
|
|
24
|
+
そのとき `warning:` が、両側を同時に動かすために `config/database.yml` のどこを変えるかを示す。
|
|
25
|
+
詳しくは [Rails アプリ](#rails-アプリ) を参照。
|
|
26
|
+
|
|
27
|
+
### 2. コミットを選ぶ
|
|
28
|
+
|
|
29
|
+
最初の対象には、分岐がいくつかあり既存の spec もあるモデルのメソッドを変えた、最近のコミットが向いている。
|
|
30
|
+
モデルを変えた最近のコミットを並べる。
|
|
31
|
+
|
|
32
|
+
<!-- rails:pick -->
|
|
33
|
+
```sh
|
|
34
|
+
git log --oneline -n 5 -- app/models
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. spec を 1 つ書いて jitcatch を走らせる
|
|
38
|
+
|
|
39
|
+
そのコミットの前にメソッドが何をしていたかを述べる spec を 1 つ書く。
|
|
40
|
+
置き場所は git が無視する `tmp/jitcatch` にし、`require "rails_helper"` で始め、アプリ自身の spec と同じようにファクトリを使う。
|
|
41
|
+
そのあと、そのコミットに対して jitcatch を走らせる。
|
|
42
|
+
`Loan`、`approve`、期待する結果、`c3ef899` は、自分のモデル、そのメソッド、コミットの前にメソッドが返していた値、選んだコミットに置き換える。
|
|
43
|
+
|
|
44
|
+
<!-- rails:script -->
|
|
45
|
+
```sh
|
|
46
|
+
mkdir -p tmp/jitcatch
|
|
47
|
+
cat > tmp/jitcatch/loan_approve_spec.rb <<'RUBY'
|
|
48
|
+
require "rails_helper"
|
|
49
|
+
|
|
50
|
+
RSpec.describe Loan, type: :model do
|
|
51
|
+
it "lets a clerk approve a loan of 10000 cents" do
|
|
52
|
+
loan = create(:loan, amount_cents: 10_000)
|
|
53
|
+
expect(loan.approve(name: "Grace", senior: false)).to eq(:approved)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
bundle exec jitcatch run c3ef899~1 c3ef899 tmp/jitcatch
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 表示される内容
|
|
61
|
+
|
|
62
|
+
次の出力は小さなサンプルアプリでのものである。
|
|
63
|
+
FactoryBot と SQLite を使う Rails 8.1 アプリで、`Loan#approve` は大きな貸付の承認をシニアの担当者に求める。
|
|
64
|
+
`git log` は 2 つのコミットを並べる。
|
|
65
|
+
|
|
66
|
+
<!-- rails:pick-output -->
|
|
67
|
+
```
|
|
68
|
+
c3ef899 Ask a senior officer from 10000 cents.
|
|
69
|
+
e994a60 Characterize Loan#approve.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
最後のコミットは、10000 セントを超えるときだけでなく、10000 セントちょうどからシニアの担当者を求めるようにした。
|
|
73
|
+
jitcatch は次を出す。
|
|
74
|
+
|
|
75
|
+
<!-- rails:output -->
|
|
76
|
+
```
|
|
77
|
+
jitcatch run
|
|
78
|
+
version 0.1.0
|
|
79
|
+
base e994a60bff5a85b3bcc92960dd251d598b15835a
|
|
80
|
+
head c3ef8997c50fc389a24ec4aa3f79caca2d6437b9
|
|
81
|
+
reruns 2
|
|
82
|
+
timeout_s 60
|
|
83
|
+
bundle base ok
|
|
84
|
+
bundle head ok
|
|
85
|
+
tests:
|
|
86
|
+
t01 reported Loan lets a clerk approve a loan of 10000 cents loan_approve_spec.rb:4 m01
|
|
87
|
+
counts generated=1 no_signal=0 fail_on_base=0 candidates=1 flaky=0 discarded=0 reported=1
|
|
88
|
+
assess:
|
|
89
|
+
c01 Loan lets a clerk approve a loan of 10000 cents base=pass head=fail m01 assertion,report,touches:changed_method
|
|
90
|
+
expected: :approved
|
|
91
|
+
got: :needs_senior
|
|
92
|
+
rerun: bundle exec rspec -I. -r jitcatch/rspec tmp/jitcatch/loan_approve_spec.rb:4
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
spec は親で通り、そのコミットで落ちるので、`reported` になる。
|
|
96
|
+
コミットが古いコードの振る舞いを変えたということである。
|
|
97
|
+
`expected:` は古い結果で、`got:` は新しい結果である。
|
|
98
|
+
`m01` は spec が届く変更されたメソッド、`Loan#approve` である。
|
|
99
|
+
次にコミットメッセージを読む。
|
|
100
|
+
このメッセージはその変更を求めているので、この行は意図どおりである。
|
|
101
|
+
その一文がなければ、回帰の疑いが強い。
|
|
102
|
+
`rerun:` の行を走らせると、その失敗をもう一度見られる。
|
|
103
|
+
`bundle exec jitcatch context c3ef899~1 c3ef899` を走らせると、変更されたメソッドごとに古いソースと新しいソースを見られる。
|
|
104
|
+
親でも落ちる spec は新しい振る舞いを書いたものなので、`fail_on_base` になり、証拠にはならない。
|
|
105
|
+
jitcatch は両側の新しいチェックアウトでアプリを起動し、そこで spec ファイルごとに最大 60 秒待つ。
|
|
106
|
+
行がタイムアウトしたら、その `hint:` の行が、何を渡すか何を上げるかを示す。
|
|
107
|
+
|
|
108
|
+
## アプリなしで試す
|
|
109
|
+
|
|
110
|
+
Ruby 3.3 以降と git が要る。
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
gem install jitcatch
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
空のフォルダで、次をシェルに貼り付ける。
|
|
117
|
+
2 つのコミットを持つ git リポジトリができる。
|
|
118
|
+
2 つ目のコミットは空の注文の送料をなくすためのものだが、ちょうど 50 の注文の送料無料もなくしてしまう。
|
|
119
|
+
そのあと候補のテストを 3 つ書き、jitcatch を走らせる。
|
|
120
|
+
|
|
121
|
+
<!-- quickstart:script -->
|
|
122
|
+
```sh
|
|
123
|
+
mkdir jitcatch-demo && cd jitcatch-demo && git init -q
|
|
124
|
+
|
|
125
|
+
mkdir lib && cat > lib/shipping.rb <<'RUBY'
|
|
126
|
+
module Shipping
|
|
127
|
+
# Orders of 50 dollars or more ship free.
|
|
128
|
+
def self.fee(total)
|
|
129
|
+
total >= 50 ? 0 : 5
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
RUBY
|
|
133
|
+
git add . && git commit -qm "Add the shipping fee" && git tag base
|
|
134
|
+
|
|
135
|
+
cat > lib/shipping.rb <<'RUBY'
|
|
136
|
+
module Shipping
|
|
137
|
+
# Orders of 50 dollars or more ship free.
|
|
138
|
+
def self.fee(total)
|
|
139
|
+
return 0 if total.zero?
|
|
140
|
+
|
|
141
|
+
total > 50 ? 0 : 5
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
RUBY
|
|
145
|
+
git commit -qam "Charge no fee on an empty order"
|
|
146
|
+
|
|
147
|
+
mkdir candidates && cat > candidates/shipping_test.rb <<'RUBY'
|
|
148
|
+
require "minitest/autorun"
|
|
149
|
+
require "shipping"
|
|
150
|
+
|
|
151
|
+
class ShippingTest < Minitest::Test
|
|
152
|
+
def test_fifty_ships_free
|
|
153
|
+
assert_equal 0, Shipping.fee(50)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_empty_order_has_no_fee
|
|
157
|
+
assert_equal 0, Shipping.fee(0)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_small_order_pays_five
|
|
161
|
+
assert_equal 5, Shipping.fee(10)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
RUBY
|
|
165
|
+
|
|
166
|
+
jitcatch run base HEAD candidates
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
jitcatch は次を出す。
|
|
170
|
+
2 つのコミット ID は手元のものになる。
|
|
171
|
+
|
|
172
|
+
<!-- quickstart:output -->
|
|
173
|
+
```
|
|
174
|
+
jitcatch run
|
|
175
|
+
version 0.1.0
|
|
176
|
+
base 81b05b22f14f2b5a1e580c048856a5e9d9b25b85
|
|
177
|
+
head 7c53dd7485d4c994d97bddd3cb7bd5481a337b61
|
|
178
|
+
reruns 2
|
|
179
|
+
timeout_s 10
|
|
180
|
+
tests:
|
|
181
|
+
t01 fail_on_base ShippingTest#test_empty_order_has_no_fee shipping_test.rb
|
|
182
|
+
base: fail Minitest::Assertion Expected: 0
|
|
183
|
+
Actual: 5
|
|
184
|
+
t02 reported ShippingTest#test_fifty_ships_free shipping_test.rb m01
|
|
185
|
+
t03 no_signal ShippingTest#test_small_order_pays_five shipping_test.rb
|
|
186
|
+
counts generated=3 no_signal=1 fail_on_base=1 candidates=1 flaky=0 discarded=0 reported=1
|
|
187
|
+
assess:
|
|
188
|
+
c01 ShippingTest#test_fifty_ships_free base=pass head=fail m01 assertion,report,touches:changed_method
|
|
189
|
+
Expected: 0
|
|
190
|
+
Actual: 5
|
|
191
|
+
rerun: ruby -Ilib -I. candidates/shipping_test.rb -n ShippingTest#test_fifty_ships_free
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`test_fifty_ships_free` は親で通り、変更で落ちるので、`reported` になる。
|
|
195
|
+
古いコードが満たしていた主張を、変更が壊したということである。
|
|
196
|
+
`test_empty_order_has_no_fee` は新しい振る舞いを書いたもので、親で落ちるので `fail_on_base` になり、証拠にはならない。
|
|
197
|
+
`test_small_order_pays_five` は両側で通る。
|
|
198
|
+
コミットメッセージが求めているのは空の注文の規則だけなので、報告された行は回帰の疑いが強い。
|
|
199
|
+
`rerun:` の行を走らせると、その失敗をもう一度見られる。
|
|
200
|
+
比較を直すか、意図のほうを変える。
|
|
201
|
+
|
|
202
|
+
`m01` は、報告されたテストが届く変更されたメソッドである。
|
|
203
|
+
`jitcatch context base HEAD` は意図を出し、変更されたメソッドごとに、古いソースと新しいソースを含むブロックを出す。
|
|
204
|
+
|
|
205
|
+
<!-- quickstart:context -->
|
|
206
|
+
```
|
|
207
|
+
methods:
|
|
208
|
+
[m01] modified Shipping.fee lib/shipping.rb:3-7 signature=same visibility=public
|
|
209
|
+
callsites: (none)
|
|
210
|
+
tests: (none)
|
|
211
|
+
old:
|
|
212
|
+
def self.fee(total)
|
|
213
|
+
total >= 50 ? 0 : 5
|
|
214
|
+
end
|
|
215
|
+
new:
|
|
216
|
+
def self.fee(total)
|
|
217
|
+
return 0 if total.zero?
|
|
218
|
+
|
|
219
|
+
total > 50 ? 0 : 5
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## 使い方
|
|
224
|
+
|
|
225
|
+
変更をレビューするエージェントは、その差分向けのテストを書く。
|
|
226
|
+
新しい振る舞いだけを書いたテストは親で落ち、回帰の証拠にならない。
|
|
227
|
+
親がすでに満たしていて変更後に落ちるテストは、古いコードが満たしていた主張を新しいコードが壊したことを示す。
|
|
228
|
+
jitcatch はそのテストを両方のリビジョンで走らせ、フレークと、ほかのいくつかの非シグナルを除いて、残ったものを出す。
|
|
229
|
+
|
|
230
|
+
方法は Meta の catching JiT tests に沿う([Harden and Catch](https://arxiv.org/abs/2504.16472)、Meta での報告は [Just-in-Time Catching Test Generation at Meta](https://arxiv.org/abs/2601.22832))。
|
|
231
|
+
|
|
232
|
+
Ruby 3.3 以降。
|
|
233
|
+
追加の gem はない。
|
|
234
|
+
`minitest` と `prism` は Ruby に付いている。
|
|
235
|
+
`jitcatch --version`(または `-v`)は `jitcatch 0.1.0` を出し、`--help` はコマンドの一覧を出す。
|
|
236
|
+
git から入れた gem のように jitcatch 自身の git のチェックアウトから動くときは、`jitcatch 0.1.0 (1fbcd18)` のようにコミットも出す。
|
|
237
|
+
RSpec の候補には、実行する場所に `rspec-core` が要る。
|
|
238
|
+
プロジェクトの bundle に入れるか、`Gemfile` が無いプロジェクトならその Ruby にインストールしておく。
|
|
239
|
+
`BASE` は親のリビジョン、`HEAD` は変更である。
|
|
240
|
+
どちらもコミットを指す。
|
|
241
|
+
未コミットのファイルは diff に入らない。
|
|
242
|
+
|
|
243
|
+
候補のファイルはスクラッチディレクトリに自分で書く。
|
|
244
|
+
Minitest のファイルは `require "minitest/autorun"` で始め、リポジトリがライブラリを読むのと同じ `require` を書く。
|
|
245
|
+
RSpec のファイルは `*_spec.rb` という名前にし、プロジェクト自身の spec と同じく `require "spec_helper"` か `require "rails_helper"` で始める。
|
|
246
|
+
実行は jitcatch が行う。
|
|
247
|
+
`reported` の各行を、意図と diff と照らして判断する。
|
|
248
|
+
ブリーフの意図は `BASE` から `HEAD` までのコミットメッセージである。
|
|
249
|
+
各メッセージの末尾で git がトレーラーとして読む行(`Co-Authored-By:` など)は含めない。
|
|
250
|
+
likely-regression は、振る舞いが変わり、意図がその変更を求めていないとき。
|
|
251
|
+
intended は、意図がその変更を求めているとき。
|
|
252
|
+
unclear は、意図だけでは決められないとき。
|
|
253
|
+
|
|
254
|
+
`examples/fixtures/shop` はセント単位のカートである。
|
|
255
|
+
小計が 10000 セントに達すると 10% のボリューム割引が付く。
|
|
256
|
+
税は 8% で、端数は half up。
|
|
257
|
+
既存テストは 10000 セントちょうどを見ていない。
|
|
258
|
+
`ruby script/setup_shop` は `examples/shop` に git の履歴を書く(このディレクトリは gitignore してある)。
|
|
259
|
+
|
|
260
|
+
| リビジョン | 内容 |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `base` | 最初のカート。 |
|
|
263
|
+
| `regression` | SAVE5 クーポンを足し、閾値の比較を `<` から `<=` に変える。メッセージはクーポンのことだけを書く。 |
|
|
264
|
+
| `intended` | 閾値を 5000 セントに下げる。メッセージはその変更が意図的だと書く。 |
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
ruby script/setup_shop
|
|
268
|
+
bin/jitcatch context --repo examples/shop base regression
|
|
269
|
+
bin/jitcatch run --repo examples/shop base regression examples/runs/regression/tests
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`context` は意図を出し、変更されたメソッドを 1 ブロックずつ出す。
|
|
273
|
+
この実行からは次のとおり。
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
intent:
|
|
277
|
+
Add SAVE5 coupon (500 cents off before tax).
|
|
278
|
+
Apply the coupon with Cart#apply_coupon. SAVE5 subtracts 500 cents
|
|
279
|
+
from the taxable amount before tax. Unknown codes raise
|
|
280
|
+
Shop::UnknownCoupon.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`m05` は閾値のメソッドである。
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
[m05] modified Shop::Cart#volume_discount_cents lib/shop/cart.rb:50-54 signature=same visibility=public
|
|
287
|
+
callsites: lib/shop/cart.rb:57 Shop::Cart#taxable_cents
|
|
288
|
+
reachable_from: Shop::Cart#tax_cents, Shop::Cart#taxable_cents, Shop::Cart#total_cents
|
|
289
|
+
tests: test/test_cart.rb grep
|
|
290
|
+
old:
|
|
291
|
+
def volume_discount_cents
|
|
292
|
+
return 0 if subtotal_cents < VOLUME_THRESHOLD_CENTS
|
|
293
|
+
|
|
294
|
+
Money.percent(subtotal_cents, VOLUME_PERCENT)
|
|
295
|
+
end
|
|
296
|
+
new:
|
|
297
|
+
def volume_discount_cents
|
|
298
|
+
return 0 if subtotal_cents <= VOLUME_THRESHOLD_CENTS
|
|
299
|
+
|
|
300
|
+
Money.percent(subtotal_cents, VOLUME_PERCENT)
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`run` はテストを 2 件残す。
|
|
305
|
+
全文は `examples/runs/regression/run.txt`。
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
counts generated=13 no_signal=8 fail_on_base=3 candidates=2 flaky=0 discarded=0 reported=2
|
|
309
|
+
assess:
|
|
310
|
+
c01 ThresholdTotalTest#test_at_threshold_total_includes_the_volume_discount base=pass head=fail m05 assertion,related:guess,report,touches:unchanged_caller
|
|
311
|
+
related guess: Shop::Cart#volume_discount_cents (m05), ranked first of 3: Shop::Cart#coupon_discount_cents (m02), Shop::Cart#taxable_cents (m04), Shop::Cart#volume_discount_cents (m05)
|
|
312
|
+
Expected: 9720
|
|
313
|
+
Actual: 10800
|
|
314
|
+
c02 VolumeBoundaryTest#test_threshold_is_inclusive base=pass head=fail m05 assertion,report,touches:changed_method
|
|
315
|
+
Expected: 1000
|
|
316
|
+
Actual: 0
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
reported の 2 件は、どちらも likely-regression である。
|
|
320
|
+
10000 セントちょうどでは割引が 1000、割引後の税込み合計が 9720 になる。
|
|
321
|
+
head は 0 と 10800 を返す。
|
|
322
|
+
10800 は割引なしの税込み合計である。
|
|
323
|
+
意図が頼んでいるのはクーポンの追加だけである。
|
|
324
|
+
SAVE5 の 3 件は base で落ちる。
|
|
325
|
+
`apply_coupon` と `Shop::UnknownCoupon` がまだ無いためで、候補にならない。
|
|
326
|
+
|
|
327
|
+
もう一方の履歴は、意図した変更である。
|
|
328
|
+
変わるのは定数だけで、メソッド本体は同じなので、`context` はそのメソッドを `affected` と出す。
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
bin/jitcatch context --repo examples/shop base intended
|
|
332
|
+
bin/jitcatch run --repo examples/shop base intended examples/runs/intended/tests
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
constants:
|
|
337
|
+
[k01] modified Shop::Cart::VOLUME_THRESHOLD_CENTS lib/shop/cart.rb:7
|
|
338
|
+
old: VOLUME_THRESHOLD_CENTS = 10_000
|
|
339
|
+
new: VOLUME_THRESHOLD_CENTS = 5_000
|
|
340
|
+
read_by: m01
|
|
341
|
+
methods:
|
|
342
|
+
[m01] affected Shop::Cart#volume_discount_cents lib/shop/cart.rb:30-34 signature=same visibility=public via=k01
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
counts generated=9 no_signal=6 fail_on_base=1 candidates=2 flaky=0 discarded=0 reported=2
|
|
347
|
+
assess:
|
|
348
|
+
c01 ThresholdTest#test_between_old_and_new_threshold_total_had_no_discount base=pass head=fail m01 assertion,report,touches:unchanged_caller
|
|
349
|
+
Expected: 8100
|
|
350
|
+
Actual: 7290
|
|
351
|
+
c02 ThresholdTest#test_five_thousand_had_no_discount base=pass head=fail m01 assertion,report,touches:changed_method
|
|
352
|
+
Expected: 0
|
|
353
|
+
Actual: 500
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
全文は `examples/runs/intended/context.txt` と `examples/runs/intended/run.txt`。
|
|
357
|
+
reported の 2 件は intended である。
|
|
358
|
+
メッセージは、5000 セント以上の注文を 10% 引きにする、と書いている。
|
|
359
|
+
新しい規則を直接書いたテストは base で落ち、候補にならない。
|
|
360
|
+
base で通り head で落ちる結果の形は、回帰の実行と同じである。
|
|
361
|
+
分けているのはコミットメッセージである。
|
|
362
|
+
|
|
363
|
+
終わったらスクラッチディレクトリは消す。
|
|
364
|
+
プロジェクトのスイートに残すのは、そのテストを採用するときだけである。
|
|
365
|
+
likely-regression のテストは、バグを直したあとに置ける。
|
|
366
|
+
intended のテストは、古い振る舞いを書いている。
|
|
367
|
+
|
|
368
|
+
branchout の特性テストも、Minitest のファイルとしても RSpec の spec(`--format rspec`)としても候補にできる。
|
|
369
|
+
base のリビジョンのチェックアウトで `--require-by-name` を付けて出力する。
|
|
370
|
+
そうすると、それぞれの側が自分のワークツリーのコードを読み込む。
|
|
371
|
+
これを付けずに出力したテストは `require_relative` か絶対パスを持つ。
|
|
372
|
+
チェックアウトの `.jitcatch/` の下に出力したテストのように、そのパスがワークツリーの中を指すなら問題ない。
|
|
373
|
+
ワークツリーの外を指すと、両側が同じファイルを読み、すべての行が `no_signal` になる。
|
|
374
|
+
|
|
375
|
+
別のリポジトリでは、そのリポジトリが作業ディレクトリでなければ `--repo` を渡す。
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
jitcatch context --repo DIR BASE HEAD
|
|
379
|
+
jitcatch run --repo DIR BASE HEAD TESTS_DIR
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### オプション
|
|
383
|
+
|
|
384
|
+
| フラグ | コマンド | 効果 |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| `--json` | `context`, `run`, `assess` | 同じ内容を JSON で出す。 |
|
|
387
|
+
| `--repo DIR` | `context`, `run` | 読む git リポジトリ。既定は作業ディレクトリ。 |
|
|
388
|
+
| `--all-readers` | `context` | `readers:` の行を、スコアの高い 10 行だけでなく全部出す。 |
|
|
389
|
+
| `--coverage FILE` | `context` | 既存テストの grep の代わりにこのファイルを使う。JSON は `"format": "jitcatch-coverage-v1"` と `tests` 配列を持つ。例は `examples/coverage/jitcatch-coverage-v1.sample.json`。 |
|
|
390
|
+
| `--reruns N` | `run` | base で通り head で落ちたあとの、head での追加実行回数。既定は 2。`0` でフレーク検査をしない。 |
|
|
391
|
+
| `--timeout SEC` | `run` | テストファイルを止めるまでの秒数。head に `config/application.rb` があれば既定は 60 で、それ以外は 10。Rails アプリは新しいチェックアウトごとに起動して `db/schema.rb` を読み込むからである。最小は 1。 |
|
|
392
|
+
| `--workers N` | `run` | 同時に実行するテストファイルの数。ワーカーごとに base と head の worktree を持つ。既定は 1。1 つのワーカーは Ruby プロセスを 2 つ同時に動かすので、ワーカーを増やして速くなるのは CPU が空いているときだけである。N によらずレポートは同じになる。 |
|
|
393
|
+
| `--number-database-url` | `run` | `--env` で渡した `DATABASE_URL` のデータベース名に各チェックアウトの `TEST_ENV_NUMBER` を足し、base と head にそれぞれのデータベースを持たせる(Rails アプリの節を参照)。`--env DATABASE_URL` が要る。 |
|
|
394
|
+
| `--setup CMD` | `run` | 候補の前に、各チェックアウトで `CMD` を `/bin/sh` で一度ずつ走らせる。その側の `TEST_ENV_NUMBER`、`DATABASE_URL`、`--env` で渡した変数を渡し、`--env` で別の値を渡さない限り `RAILS_ENV=test` も渡す。`--setup 'bin/rails db:create db:schema:load'` なら、各側のスキーマをそれぞれのテスト用データベースに読み込む。失敗すると終了コード 3 で終わる。 |
|
|
395
|
+
| `--serial` | `run` | Ruby プロセスを 1 つずつ動かす。ファイルの base 側を走らせてから head 側を走らせる。worktree の外の決まったパスに書く候補や、決まったポートで待ち受ける候補のためのものである。`--workers` に 1 より大きい値は渡せない。Rails アプリの base と head が 1 つのテスト用データベースを共有するときも、`run` は `warning:` を出してこのように動かす(Rails アプリの節を参照)。テキストのレポートは `serial` と出し、`--json` は `"serial": true` を記録する。 |
|
|
396
|
+
| `--env NAME` | `run` | この環境変数を今の環境からテストのプロセスへ渡す。複数の名前は繰り返して渡す。実行結果には値ではなく名前を記録する。設定されていない名前は `env_unset` 行に出す。これはエラーではなく注記で、その名前なしで実行したのでコード自身の既定値が使われたことを示す。名前だけを受け取る。`--env DB_PORT=55801` はシェルでの書き方を示して止まる。`export DB_PORT=<value>` のあとに `--env DB_PORT` を渡す。再実行の行は値をシェルから読むからである。 |
|
|
397
|
+
|
|
398
|
+
| コマンド | 引数 |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| `jitcatch context` | `BASE HEAD` |
|
|
401
|
+
| `jitcatch run` | `BASE HEAD TESTS_DIR` |
|
|
402
|
+
| `jitcatch assess` | `run --json` が書いた `RUN.json` |
|
|
403
|
+
|
|
404
|
+
最初のコミットを見るときは、`BASE` に `ROOT^` を渡す。
|
|
405
|
+
ルートコミットの親は空のツリーとして読む。
|
|
406
|
+
|
|
407
|
+
`BASE` が `HEAD` の祖先でないとき、diff は `BASE` にしかないコミットも打ち消す。
|
|
408
|
+
そのとき `context` と `run` は `head` の下に `warning:` 行を出す。
|
|
409
|
+
その行は、そのコミットの数と、代わりに `BASE` に渡すマージベースを示す。
|
|
410
|
+
2 つが同じコミットを指すときも、`warning:` 行がそう示す。
|
|
411
|
+
|
|
412
|
+
`assess` は run の警告、件数、候補の行を再出力する。
|
|
413
|
+
掛けるルールは `run` と同じである。
|
|
414
|
+
行には失敗の最初の行か、Minitest の差分の期待行と実際行が出る。
|
|
415
|
+
そのどちらかが 100 文字を超えると、値の違う Hash のエントリか Array の要素か、最初に違う桁のまわりと `^ column N` の印を出す。
|
|
416
|
+
この印は、追加行が削除行を置き換えるときだけ出る。
|
|
417
|
+
差分の別々の場所にある 2 行は、それぞれ 61 文字で切る。
|
|
418
|
+
失敗した `assert_raises(X)` の行には、代わりに来たエラーを示す Minitest の `Class:` 行と `Message:` 行も出る。
|
|
419
|
+
RSpec の `be` と `equal` の行には `expected` 行と `got` 行が出る。
|
|
420
|
+
rspec-mocks の引数違いの失敗では、呼び出しの行と `expected:` 行と `got:` 行が出る。
|
|
421
|
+
どちらも `assess:` の行にも `fail_on_base` の行の `base:` の下にも出る。
|
|
422
|
+
別のエラーから上げられたエラーの行には、`cause: KeyError: key not found` のように元のエラーのクラスとメッセージが出る。
|
|
423
|
+
RSpec は元のエラーを自分で出力する。
|
|
424
|
+
Minitest は出力しないので、jitcatch は Minitest の候補の各プロセスに `-r` で `lib/jitcatch/minitest_report.rb` を読み込み、Minitest のエラーの文に元のエラーを足す。
|
|
425
|
+
`rerun:` の行はこれを読み込まない。
|
|
426
|
+
行の 1 行が 300 文字を超えると、`FrozenError` に出た凍結 Array 全体のような行でも、最初と最後の 150 文字を残し、そのあいだを `... (+N characters; the whole message is in --json) ...` にする。
|
|
427
|
+
マッチャーが受け取ったエラーでは、長いパスのあとの末尾に違いがあることが多いからである。
|
|
428
|
+
その行が、下に RSpec の `Diff:` を持つメッセージの最初の行なら、行はその diff で消えた行と加わった行(最大 8 行)を足す。
|
|
429
|
+
`have_attributes` はレコードと両方の属性の Hash を 1 行に出すので、`-:state => "pending",` と `+:state => "draft",` のように違う属性がこれで見える。
|
|
430
|
+
`--json` はメッセージ全体を残す。
|
|
431
|
+
|
|
432
|
+
```sh
|
|
433
|
+
bin/jitcatch run --json --repo examples/shop --reruns 2 --timeout 10 base regression examples/runs/regression/tests > run.json
|
|
434
|
+
bin/jitcatch assess run.json
|
|
435
|
+
bin/jitcatch assess --json run.json
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| 終了コード | 意味 |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| 0 | コマンドは完了した。候補が見つかっても 0 のまま。 |
|
|
441
|
+
| 1 | 使い方の誤り。 |
|
|
442
|
+
| 2 | git の失敗。 |
|
|
443
|
+
| 3 | `run` は何もテストしなかった。ある側がセットアップの確認に失敗したか、どちらの側でも候補ファイルが 1 つも読み込めなかったか、すべての行がある側でタイムアウトしたか、データベースかテーブルが見つからなかった。 |
|
|
444
|
+
|
|
445
|
+
### 結果
|
|
446
|
+
|
|
447
|
+
| 区分 | 意味 |
|
|
448
|
+
| --- | --- |
|
|
449
|
+
| `no_signal` | base でも head でも通った。 |
|
|
450
|
+
| `fail_on_base` | 親がそのテストを満たさなかった。新しい機能だけを書いていることが多い。テキストの報告は、その行の下に base で起きたことを `base: error NoMethodError undefined method 'apply_coupon' for an instance of Shop::Cart` のように出す。 |
|
|
451
|
+
| `flaky` | base で通り head で落ちたあと、再実行が同じ落ち方をしなかった。捨てる。 |
|
|
452
|
+
| `discarded` | base で通り head で落ちたあと、ルールが除いた。`assess:` には残る。 |
|
|
453
|
+
| `reported` | base で通り head で落ち、ルールは除かなかった。この行を判断する。 |
|
|
454
|
+
|
|
455
|
+
`generated` はテストメソッドの数である。
|
|
456
|
+
`candidates` は、破棄ルールの前に、base で安定して通り head で落ちた数である。
|
|
457
|
+
`reported` はそのあと残った数である。
|
|
458
|
+
|
|
459
|
+
タグはヒントであり、判断そのものではない。
|
|
460
|
+
`touches:changed_method` は、テストが diff の変更したメソッド名を含んでいる。
|
|
461
|
+
`Owner.new` は、その `Owner` の `initialize` だけの名前として数える(`Mismatch.new` は `Spec#initialize` を指さない)。
|
|
462
|
+
変更したコンストラクタが関連メソッドになるのは、テストがほかの変更メソッドの名前を含まず、その呼び出し元も通らないときだけである。
|
|
463
|
+
`touches:changed_constant` は、テストが diff の変更した定数名を含んでいる。
|
|
464
|
+
`touches:changed_dependency` は、ロックしたバージョンが変わった gem の中からエラーが出た。
|
|
465
|
+
gem の中から始まったエラーには `touches:changed_method` を付けない。
|
|
466
|
+
`touches:unchanged_caller` は、diff が編集していない入口を通り、その入口から変更メソッドへ届いている。
|
|
467
|
+
`discard:signature_change`、`discard:load_error`、`discard:timeout`、`discard:no_database`、`discard:no_table` は、候補を除いたルールの名前である。
|
|
468
|
+
`discard:signature_change` は、削除したメソッドや新しいシグネチャが壊した呼び出しを、テスト自身がしているときだけ使う。
|
|
469
|
+
メンバーが変わった `Data` や `Struct` のクラスの `new` をテストが呼ぶときも使う。
|
|
470
|
+
たとえば `Record.new(...)` が `missing keyword: :media` で失敗するときである。
|
|
471
|
+
そうした行は発見ではなく、次の行が何が変わったかを示す。
|
|
472
|
+
たとえば `candidate is out of date with head: CatalogService::Record (k07) changed its definition; update the call at ../cand/stale_spec.rb:5 to head's code, or drop the test` となる。
|
|
473
|
+
`--json` では `out_of_date` が `id`、`name`、`change` (`removed`、`parameters`、`definition`)、`call` を持つ。
|
|
474
|
+
変更されていないコードがその呼び出しをしているときは、行を報告する。
|
|
475
|
+
たとえば `class B < A` が、head で削除した `A#helper` をまだ呼んでいるときである。
|
|
476
|
+
その行には、呼び出しの位置を示す `caller:unchanged:lib/b.rb:5` を付ける(diff が書いた行か、変更メソッドの中の行なら `caller:changed:`。`--json` の `changed_lines` は、各ファイルの hunk が書いた head の行を並べる)。
|
|
477
|
+
関連 id(`m05`、無ければ `-`)は、経路上に変更メソッドが複数あるときの、最も下流の変更メソッドである。
|
|
478
|
+
失敗のバックトレースが変更メソッドを通るときは、そのうち最も内側のものである。
|
|
479
|
+
フレームの行は head の行として読むので、base でその行にあったメソッドには当たらない。
|
|
480
|
+
外側のフレームはそれを呼んだだけなので、この選択は推測ではない。
|
|
481
|
+
`related:guess` は、id を選んだ根拠が複数の変更メソッドを指している。
|
|
482
|
+
たとえば、テストが変更されていない呼び出し元だけを含み、その呼び出し元から複数の変更メソッドへ届くときである。
|
|
483
|
+
その行の次の行は、候補をすべて名前と id で並べる。
|
|
484
|
+
たとえば `related guess: Shop::Cart#volume_discount_cents (m05), ranked first of 3: Shop::Cart#coupon_discount_cents (m02), Shop::Cart#taxable_cents (m04), Shop::Cart#volume_discount_cents (m05)` となる。
|
|
485
|
+
`--json` では `method_ids` が同じものを持つ。
|
|
486
|
+
この選択は、diff が追加したメソッドより base にあったメソッドを選ぶ。
|
|
487
|
+
ただし、追加したメソッドの呼び出しを足しただけの編集(`--json` の `forwards_to_added`)は、その追加したメソッドより下に置く。
|
|
488
|
+
`net - volume` が `net - volume - coupon(n)` になる編集がその例で、新しい値は追加したメソッドから来る。
|
|
489
|
+
テストが変更されていない呼び出し元を通してだけ届くメソッドの中では、テストが名指しする入口から最も多くの呼び出しを下ったメソッドを選ぶ。
|
|
490
|
+
たとえば、リクエストスペックが 1 回下の `LoansController#officer` と 5 回下の `Borrower#approved_cents` に届くときは `Borrower#approved_cents` を選ぶ。
|
|
491
|
+
`--json` の `caller_depths` がその回数を持つ。
|
|
492
|
+
上位の二つを id でしか並べられないときは、どれも選ばない。
|
|
493
|
+
その行は `related guess: none; the test reaches 7 changed methods equally and no frame of head's failure is in one: Book#apply_catalog_record (m02), LoanRules.for (m03), ...` となり、id の位置に `-` が出て、`method_id` は `null` になる。
|
|
494
|
+
その行は次の一手で終わる。
|
|
495
|
+
それらのメソッドか、メソッドを持たない変更されたファイル (ビューなど) のどれが head の値を作るかを、行に出る `git diff --stat BASE HEAD` から探す。
|
|
496
|
+
この行には、テストが `method_ids` のどれか一つの名前を含めば `touches:changed_method` を付ける。
|
|
497
|
+
id が最も小さいものだけを見るのではない。
|
|
498
|
+
`message_varies` は、head が再実行のたびに同じ落ち方をしてメッセージだけが動き、base が再実行のたびに通ったことを言う。
|
|
499
|
+
レコード全体ではなく、テストが決めるフィールドを比べるとよい。
|
|
500
|
+
`related:dynamic` は、関連する変更メソッドはないが、テストが `dynamic_callers:` のメソッドを呼んでいる。
|
|
501
|
+
その行の次の行が `related: none, Emit.looked_up sends render to a receiver the source does not name` のようにそれを言う。
|
|
502
|
+
|
|
503
|
+
## どう残すか
|
|
504
|
+
|
|
505
|
+
`context` は 2 つのリビジョンを diff し、ハンクを Prism でメソッドに対応づける。
|
|
506
|
+
変更メソッドは `m01`、`m02` と付く。
|
|
507
|
+
`Gemfile.lock` が変わったときは、`dependencies:` に gem ごとに `gem 旧 -> 新` を出す。
|
|
508
|
+
変わった定数は `k01` で、その定数を読むだけで本体が変わっていないメソッドは `affected`(`via=k01`)になる。
|
|
509
|
+
あるファイルから消えたメソッドを、head が別のファイル(テストファイルも含む)で同じ名前で定義していれば、それは `moved` になる。
|
|
510
|
+
新しい場所で出し、`moved_from=` が元のファイルを示す。
|
|
511
|
+
複数のファイルが定義しているときは、diff が触ったファイルを先に選ぶ。
|
|
512
|
+
定数も同じで、別のファイルへ移って値が同じなら `moved` になり、`moved_from=` が付き、その定数を読むメソッドは `affected` にならない。
|
|
513
|
+
移るときに値も変わった定数は、`moved_from=` 付きの `modified` になる。
|
|
514
|
+
`attr_reader`、`attr_writer`、`attr_accessor`、`Struct.new` や `Data.define` のメンバーが定義する読み書きのメソッドもメソッドとして数え、ソースには名前一つの形(`attr_reader :seed`)が出る。
|
|
515
|
+
そのため、その行から外した名前は `removed` になり、行に残った名前は変わらない。
|
|
516
|
+
`*FIELDS` の形で渡したリストも、同じファイルがその前に `FIELDS = %i[x y].freeze` のようにシンボルのリストを入れていれば数える。
|
|
517
|
+
このとき各メンバーは、そのリストの中の自分のシンボルの行を持つ。
|
|
518
|
+
クラス本体に書いた Active Record の三つのマクロもメソッドを定義する。
|
|
519
|
+
`scope :recent, -> { ... }`(または `scope(:recent) { ... }`)はクラスメソッド `Post.recent` になり、文全体がソースになる。
|
|
520
|
+
その本体の中の呼び出しは `Post.recent` からの呼び出しになる。
|
|
521
|
+
`delegate :name, to: :author` は `Post#name`(`prefix: true` なら `Post#author_name`)になり、名前一つの形とオプションがソースになる。
|
|
522
|
+
そのため `to:` を変えると、その行のすべての名前が変わる。
|
|
523
|
+
`enum :status, { draft: 0, live: 1 }` は値ごとに `Post#draft?`、`Post#draft!`、`Post.draft`、`Post.not_draft` を定義し、`prefix:`、`suffix:`、`scopes: false`、`instance_methods: false` も反映する。
|
|
524
|
+
ソースはその値一つの形(`enum :status, draft: 0`)なので、一つの値を変えるとその値の四つのメソッドだけが変わる。
|
|
525
|
+
Rails 8 で取り除かれた `enum status: {...}` の形は読まない。
|
|
526
|
+
Grape のエンドポイントもメソッドになる。
|
|
527
|
+
スーパークラスが `Grape::API`(またはアプリ自身の基底クラスのように `API` か `Api` で終わる名前)のクラスでは、`resource :things do` の中の `post "/refresh" do ... end` は `Things#POST /things/refresh` になる。
|
|
528
|
+
そのパスは、クラスの `prefix` と、`resource`、`resources`、`namespace`、`group`、`segment`、`route_param :id`(`:id`)の区切りをつないだものである。
|
|
529
|
+
ソースはそのブロックで、直前の `desc` と `params` の文も含む。
|
|
530
|
+
ブロックの中の呼び出しはエンドポイントからの呼び出しになるので、そこから呼ばれるメソッドの `reachable_from` にエンドポイントが出る。
|
|
531
|
+
テストが送るリクエスト(`post "/api/things/7/refresh"` など)は、動詞が同じで、パスの末尾がエンドポイントの区切りと一致するエンドポイントを名指しする。
|
|
532
|
+
`:param` は任意の一つの区切りに一致し、その前の区切りはマウント先として扱う。
|
|
533
|
+
API クラスやテストが、そのファイルで代入した文字列の定数を埋め込むパス(`namespace VERSION`、`post "#{ACTION}/now"`、`post "#{API_ROOT}/things"`)は、その文字列として読む。
|
|
534
|
+
テストでは、それ以外の埋め込みは任意の一つの区切りに一致する(`"/things/#{thing.id}/refresh"`)。
|
|
535
|
+
パスがそれで始まるとき(`"#{api_root}/things"`)は、任意のマウント先に一致する。
|
|
536
|
+
`route_param` などのパスのブロックの直前の `desc` と `params` は、その中の最初のエンドポイントに含める。
|
|
537
|
+
そのテストは `tests:` と `entry_point_tests:` に並び、それを送る候補には `touches:changed_method` が付く。
|
|
538
|
+
API クラスがそれ以外の方法でパスを計算するエンドポイントは、その部分をソースのまま波かっこに入れて名付ける(`Books#GET /books/{Settings.legacy}/list`)。
|
|
539
|
+
そのエンドポイントはどのリクエストにも一致せず、ファイルと行を示す `warning:` が出る。
|
|
540
|
+
Rails アプリでは、`context` と `run` は `config/routes.rb` が描くルート(`resources`、`resource`、`namespace`、`module:`、`as:`、`controller:` を持つ `scope`、`member`、`collection`、`on:`、`root`、`to:` か `=>` か `controller:` と `action:` を持つ動詞)を読む。
|
|
541
|
+
そのため、候補がパスヘルパー(`post things_path`、`patch thing_url(thing)`)やパスに送るリクエストは、それを受けるコントローラーのアクション(`ThingsController#create` など)を名指しする。
|
|
542
|
+
ヘルパーとコントローラーの名前は、Active Support の語形変化と、`config/initializers/inflections.rb` にあるリテラルの `irregular`、`uncountable`、`singular`、`plural`、`acronym` の呼び出しから、Rails と同じように決める。
|
|
543
|
+
たとえば `resources :people` は `person_path` を描き、`resources :news` は `news_index_path` と `news_path` を描く。
|
|
544
|
+
`inflect.acronym "API"` があれば、`resources :api_keys` は `APIKeysController` に届く。
|
|
545
|
+
そのファイルが実行時に組み立てる規則は読まない。
|
|
546
|
+
ルートが描くコントローラーのアクションが変わると、その `tests:` には、リクエストがそこに届くテストファイルと、コントローラーを名指しして `post :create` のようにアクションを呼ぶコントローラーのテストが、送るもの付きで並ぶ(`spec/requests/things_spec.rb via POST things_path`)。
|
|
547
|
+
アクション名の grep は `create(:thing)` に当たり、`post things_path` を見落としていた。
|
|
548
|
+
どのルートも描かないアクションは、これまでどおり grep を使う。
|
|
549
|
+
`draw` で読むファイル、ループ、concern などで描くヘルパーは読まない。
|
|
550
|
+
そのとき `run` は `warning: 1 candidate request names a path helper config/routes.rb does not draw (report_path), so it is tied to no controller action.` を出す。
|
|
551
|
+
クラス本体の条件分岐の中にある `def` や読み出しメソッド(`attr_reader :x if cond`)も数える。
|
|
552
|
+
分岐はすべてが順に走ったものとして読むので、分岐の中の引数のない `private` は条件分岐の後にも効く。
|
|
553
|
+
Concern の `included do`、`prepended do`、`class_methods do` ブロックの中にあるものも数える。
|
|
554
|
+
片方の側にしか無いクラスやモジュールも定数として `added` か `removed` になり、ソースには宣言の行が出る。
|
|
555
|
+
各メソッドには、旧ソースと新ソース、ライブラリ内の呼び出し、そこへ届く入口(`reachable_from`)、それに言及している既存テストが出る。
|
|
556
|
+
変更された `initialize` には `built_by:` も出る。
|
|
557
|
+
これは `new` のほかにそれを走らせる呼び出しで、Active Record のモデルの `create`、`create!`、`build` と、そのクラスを作る FactoryBot のファクトリーごとの `build(:loan)` と `create(:loan)` である。
|
|
558
|
+
その `tests:` には、そのどれかか `Owner.new` を呼ぶテストファイルが、呼んでいるもの(`spec/models/loan_spec.rb via create(:loan)`)と一緒に出る。
|
|
559
|
+
テストが `initialize` をその名前で呼ぶことはないので、コンストラクタには grep の結果を出さない。
|
|
560
|
+
ファクトリーは `spec/factories` と `test/factories` から、読み込まずにソースとして読む。
|
|
561
|
+
クラスはまず `class:`(定数、文字列、シンボル)から、次に親(`parent:` か、入れ子の外側のファクトリー)から、最後に名前を camelize したものから決まり、`aliases:` の別名も同じクラスを作る。
|
|
562
|
+
ファクトリーは、その関連が作るクラスにも数える。
|
|
563
|
+
関連とは、ファクトリーの名前と同じ素の `borrower`、`association :borrower`(`factory: :user` も)、`association` を呼ぶ属性ブロックで、そのファクトリーか継承元に書いたもののことで、関連の関連もたどる。
|
|
564
|
+
そのため `create(:loan)` は `Borrower` も保存する。
|
|
565
|
+
trait の中の関連は、その trait を名指しする呼び出しにだけ数える。
|
|
566
|
+
`:loan` の trait `:cosigned` が cosigner を作るなら、`create(:loan)` ではなく `create(:loan, :cosigned)` と `build(:loan, :cosigned)` が加わる。
|
|
567
|
+
`create(:loan, :cosigned, amount_cents: 5)` と書いたテストは、`create(:loan)` と `create(:loan, :cosigned)` の両方を挙げる。
|
|
568
|
+
ファクトリーは親と入れ子の外側のファクトリーの trait も使う。
|
|
569
|
+
ファクトリーの外で定義した trait と、別の trait を含む trait はたどらない。
|
|
570
|
+
ソースからクラスが決まらないファクトリーはたどらず、理由と一緒に `factories_skipped:` に出す。
|
|
571
|
+
実行時に計算する `class:`、そこで読んだファクトリーにない親、どのファイルも定義しないクラスがこれに当たり、最後のものはアプリが設定する活用(`ApiKey` と `APIKey`)で起こりうる。
|
|
572
|
+
アプリのクラスではなく Ruby の値を作るファクトリー(API の応答の形を作る `class: Hash` や、`Array`、`Data`、`Object`、`OpenStruct`、`Set`、`String`、`Struct`)は除き、そこにも出さない。
|
|
573
|
+
Active Record のモデルのクラス本体がコールバックかバリデーション(`after_save :count_save`、`validate :check`、またはそのどちらかの `if:` や `unless:`。`validates :amount, presence: true, if: :checked?` もこれに当たる)で名前を挙げるメソッドには `run_by:` が出る。
|
|
574
|
+
これは Rails の規則でそのメソッドを走らせる呼び出しで、後ろに名前を挙げた行が続く。
|
|
575
|
+
save のコールバックには `Loan.create`、`Loan.create!`、`Loan#save`、`Loan#save!`、`Loan#update`、`Loan#update!` と、そのクラスのファクトリーごとの `create(:loan)` と `build(:loan)` が出る(create のコールバックには update が、update のコールバックには create が出ない)。
|
|
576
|
+
バリデーションにはさらに `Loan#valid?`、`Loan#invalid?`、`Loan#validate`、`Loan#validate!` が出る。
|
|
577
|
+
バリデーションか、commit や rollback のコールバックに `on:` があるときは、その文脈の呼び出しだけを残し、行にもその `on:` を出す。
|
|
578
|
+
`before_validation :mark_pending, on: :create` には update が出ないので、`update!` を呼ぶメソッドはそこへ届かない。
|
|
579
|
+
`on: :update` には create もファクトリーも出ない。
|
|
580
|
+
アプリ独自の文脈(`on: :publish`)は文脈を受け取る呼び出しからしか走らないので、`Loan#save`、`Loan#save!` と四つのバリデーションの呼び出しが出る。
|
|
581
|
+
シンボルで書いていない `on:` は何も絞らない。
|
|
582
|
+
destroy のコールバックには `Loan#destroy` と `Loan#destroy!` が出る。
|
|
583
|
+
そのようなコールバックから届くメソッド(`reachable_from`)にも同じ行が出る。
|
|
584
|
+
テスト以外のメソッドがソースに書いた形でその呼び出しをするとき(`LoanDesk.open` の中の `Loan.create!` や、`Loan` のメソッドの中の `update!`)、そのメソッドとその呼び出し元が `reachable_from` に加わる。
|
|
585
|
+
そのため `LoanDesk.open` を呼ぶテストはそのコールバックを指す。
|
|
586
|
+
引数のように、ソースが型を決めないローカル変数やインスタンス変数への呼び出しは、Rails がその名前から決めるモデルへの呼び出しとして読む。
|
|
587
|
+
そのため `Teller#renew(loan)` の中の `loan.save!` は `Loan` の save のコールバックに届き、`record.save!` はどれにも届かない。
|
|
588
|
+
その `tests:` には、grep の結果に加えて、そのどれかを呼ぶテストファイルが出る(`spec/models/loan_spec.rb via Loan.create!`)。
|
|
589
|
+
`run` はその呼び出しをそのメソッドへの言及として数える。
|
|
590
|
+
`Loan#save` は、テストのソースからレコードのクラスが決まるとき(`loan = Loan.new`)に数える。
|
|
591
|
+
`build(:loan)` は保存しないので、コールバックが挙げるレコードへの呼び出しも同じテストがするとき(`build(:loan).save!` や、`valid?` を呼ぶ `expect(build(:loan)).to be_valid`)にだけ数える。
|
|
592
|
+
作ったレコードはソースが型を決めないローカル変数に入るので、その呼び出しのレシーバーは確かめない。
|
|
593
|
+
そのため `build(:loan)` の隣で別のレコードを保存するテストも数える。
|
|
594
|
+
作るだけのテストは `tests:` にも `run` の言及にも出ない。
|
|
595
|
+
Concern の中のコールバックの行、親クラスが宣言するもの、サブクラスへの呼び出しはたどらない。
|
|
596
|
+
言及しているテストがないときは `entry_point_tests` が出る。
|
|
597
|
+
`reachable_from` のうち public な入口について、そのクラスを書き、入口と同じ名前のメソッドを呼んでいるテストファイルである。
|
|
598
|
+
`--coverage` を渡すと、grep の代わりに JSON の行リストを使う。
|
|
599
|
+
ブリーフの最後には、変更メソッドを持つクラスやモジュールごとに `entrypoints <Const>:` の節が出る。
|
|
600
|
+
その節は、そのクラスの head での public メソッドを並べる(head に無ければ base のもの)。
|
|
601
|
+
変更メソッドが先に変更の種類つきで並び、残りは `unchanged` として続き、最大 12 件である。
|
|
602
|
+
`unchanged` のうち変更メソッドに届くものが先に並び、入らなかったメソッドの数を `(+N more)` と出す。
|
|
603
|
+
どちらかの側で `initialize` を定義するクラスは、その代わりに `Owner.new` を `Owner#initialize` の変更の種類つきで出す。
|
|
604
|
+
ただし、クラスが `new` を private にしている(`private_class_method :new`、または `class << self` の中の `private :new`)ときは出さない。
|
|
605
|
+
これはテストが呼べるメソッドであり、`run` はその名前も言及として数える。
|
|
606
|
+
|
|
607
|
+
`run` はリビジョンごとに git worktree を作り、スクラッチディレクトリをその worktree の `.jitcatch/` へコピーし、ファイルごとに 1 プロセスで実行する。
|
|
608
|
+
|
|
609
|
+
```sh
|
|
610
|
+
ruby -Ilib -I. -r<jitcatch>/lib/jitcatch/minitest_report.rb .jitcatch/<file> --seed 1
|
|
611
|
+
ruby -Ilib -I. -e 'require "rspec/core"; exit(RSpec::Core::Runner.run(ARGV))' -- \
|
|
612
|
+
--format json --out .jitcatch-report.json --seed 1 --backtrace --no-color .jitcatch/<file>_spec.rb
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
このファイルは Minitest の結果を 1 つずつ `.jitcatch-report.json` に書き、行の名前と状態は Minitest の出力ではなくそこから取る。
|
|
616
|
+
そのため、テストが何を出力しても、テストが同時に走っても(`parallelize_me!`)、1 つの行は 1 つのテストを指す。
|
|
617
|
+
|
|
618
|
+
`*_spec.rb` という名前のファイルは RSpec で実行する。
|
|
619
|
+
ただし `minitest` を require するファイル(minitest/spec のファイル)は Minitest で実行する。
|
|
620
|
+
RSpec はワークツリーの `.rspec` を読むので、そこにある `--require spec_helper` も効き、`spec/` もロードパスに入る。
|
|
621
|
+
コマンドラインで渡したフォーマッタが、`.rspec` にあるフォーマッタに代わる。
|
|
622
|
+
1 つの example が 1 行になり、行の名前はその full description(`Cart#total sums the prices`)である。
|
|
623
|
+
テキストの報告にはその行番号(`cart_spec.rb:11`)も出る。
|
|
624
|
+
期待が外れた失敗は `fail`、それ以外のエラーは `error`、`pending` は `skip` になる。
|
|
625
|
+
RSpec がファイルを読む途中のエラーは、Minitest と同じく `<file>#load` の 1 行になる。
|
|
626
|
+
|
|
627
|
+
リビジョンに `Gemfile` があるときは、`-rbundler/setup` を付け、`BUNDLE_GEMFILE` をその `Gemfile` にして実行する。
|
|
628
|
+
そのため、それぞれの側は自分のロックファイルにある gem を読む。
|
|
629
|
+
それぞれの側には、git が追跡しないチェックアウトの `.bundle/config`(`bundle config set --local` が書く `BUNDLE_WITHOUT` などの設定)も写す。
|
|
630
|
+
相対パスの `BUNDLE_PATH` は、チェックアウトのディレクトリを指したままにする。
|
|
631
|
+
`run` はテストを実行する前に、側ごとに 1 回、その側が bundle とテストフレームワーク(Minitest の候補なら `minitest`、RSpec の候補なら `rspec-core`)を読めるかを確かめる。
|
|
632
|
+
`Gemfile` が無い側では、jitcatch を動かす Ruby にフレームワークがあるかを確かめる。
|
|
633
|
+
成功した実行は、`Gemfile` がある側ごとに `bundle base ok` を出す。
|
|
634
|
+
ある側が確認に失敗すると(たとえばロックした gem がインストールされていないとき)、その側ではすべての候補が読み込みに失敗し、`fail_on_base` や回帰のように見えてしまう。
|
|
635
|
+
そのため `run` は終了コード 3 で止まり、確認を動かした Ruby(バージョンとパス)、失敗した側、失敗したコマンド、その stderr の最後の行を出す。
|
|
636
|
+
|
|
637
|
+
```text
|
|
638
|
+
jitcatch: setup failed, so no test ran; a test cannot pass or fail on a side that cannot load
|
|
639
|
+
base <sha>
|
|
640
|
+
head <sha>
|
|
641
|
+
ruby 3.3.6 /opt/rbenv/versions/3.3.6/bin/ruby
|
|
642
|
+
setup head failed: ruby -e 'require "bundler/setup"; require "minitest"'
|
|
643
|
+
Could not find gem 'jitcatch_absent_gem (= 9.9.9)' in locally installed gems.
|
|
644
|
+
not passed: BUNDLE_WITHOUT (add --env BUNDLE_WITHOUT)
|
|
645
|
+
Run that command with that Ruby in a checkout of that side, fix what it says (often `bundle install` there; a bundle installed for another Ruby reads as gems not installed), and run jitcatch again.
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
確認に通った側でも、すべての候補が読み込むものを起動できないことがある。
|
|
649
|
+
たとえば、テスト用データベースが無い Rails アプリである。
|
|
650
|
+
すべてのファイルが両方の側でエラーになった `<file>#load` の行なら、どの行も base と head を区別できないので、`run` はやはり終了コード 3 で止まる。
|
|
651
|
+
そのときの最初の行は `jitcatch: every candidate file failed to load on both sides, so no row can tell base from head` で、側ごとに最初のファイルの再実行コマンドを示す `load base failed:` の行と、その下にエラーが出る。
|
|
652
|
+
|
|
653
|
+
この文は、終了コード 3 の報告がどれもそうであるように stdout に出て、stderr には何も出ない。
|
|
654
|
+
`--json` のときは、代わりに stdout に `{"command": "run", "error": "setup_failed", "stage", "rails_app", "ruby": {"version", "path"}, "base", "head", "setup_failed": [{"side", "commit", "command", "stderr", "not_passed"}]}` を出す。
|
|
655
|
+
`stage` は `setup` か `load` である。
|
|
656
|
+
`rails_app` は head に `config/application.rb` があるときに true になる。
|
|
657
|
+
そのときだけ、`load` の報告に `config/master.key` と `RAILS_MASTER_KEY` の助言を足す。
|
|
658
|
+
`assess` はこの文書を受け付けない。
|
|
659
|
+
失敗した側は、今の環境にあって `--env` で渡していない Bundler の設定(`BUNDLE_WITHOUT`、`BUNDLE_PATH` などの `BUNDLE_` で始まる変数)を `not passed:` の行(`--json` では `not_passed`)で名指しする。
|
|
660
|
+
|
|
661
|
+
どの行も変更について何も言えないまま、行を出して終わる `run` もある。
|
|
662
|
+
すべての行が、ある側でタイムアウトしたか、ある側でデータベースが見つからなかった(base か head での `NoDatabaseError`、`ConnectionNotEstablished`、`DatabaseConnectionError`)か、ある側でテーブルが見つからなかった場合である。
|
|
663
|
+
テーブルが見つからないとは、`PG::UndefinedTable`、MySQL の `Table '...' doesn't exist`、SQLite の `no such table` か `Could not find table` による `ActiveRecord::StatementInvalid` のことで、`db:create` で作ったまま何も入れていないデータベースで起きる。
|
|
664
|
+
このような `run` は何もテストしていないので、やはり終了コード 3 で終わり、指摘の無い変更のようには読めない。
|
|
665
|
+
テキストは `jitcatch: no row ran to a result on both sides (6 of 6 timed out), so this run cannot tell base from head` の行で始まり、そのあとにいつもの報告が続くので、各行のヒントも残る。
|
|
666
|
+
括弧の中は、行に現れた原因だけを `timed out`、`found no database`、`found no table` で示し、複数あれば `or` でつなぐ。
|
|
667
|
+
行がテーブルを見つけられなかったときは、2 行目がスキーマを読み込むように言う。
|
|
668
|
+
`--number-database-url` のときは、番号付きのデータベースを名指しする。
|
|
669
|
+
たとえば `` jitcatch: load the schema into app_test_wt1_1 and app_test_wt1_2: run jitcatch again with `--setup 'bin/rails db:create db:schema:load'`, which loads each side's schema into its own database, or call `ActiveRecord::Migration.maintain_test_schema!` in the test helper `` である。
|
|
670
|
+
各側のスキーマを読み込めるのは、各チェックアウトで走るコマンドだけである。
|
|
671
|
+
`--setup` がすでにコマンドを走らせていたときは、その行はそのコマンドを名指しし、各側のスキーマを読み込むコマンドを求める。
|
|
672
|
+
`--json` はいつもの run の文書を出し、その `untested` は、どの run でもそのような行の数を数える。
|
|
673
|
+
|
|
674
|
+
そのような行と、別の理由で base で失敗した行だけの `run` も、変更をまたいで比べたテストが無い。
|
|
675
|
+
モックの引数だけを確かめる候補がその例である。
|
|
676
|
+
この `run` も終了コード 3 で終わり、テキストは `jitcatch: no row compared base with head (6 of 7 found no table, and 1 failed on base), so this run cannot tell base from head` の行で始まる。
|
|
677
|
+
隠れた行が無く、すべての行が base で失敗した `run` は終了コード 0 で終わる。
|
|
678
|
+
直すのは候補の側であり、各行の `base:` の行がその理由を示す。
|
|
679
|
+
つまり終了コード 3 は、`untested` が 0 より大きく、base が通り何も隠れていない行が無いことを意味する。
|
|
680
|
+
比べた行があるときは、見出しに `warning:` の行が出る。
|
|
681
|
+
たとえば `warning: 6 of 7 rows found no table on a side, so they checked nothing; load the schema into ...` で、同じ助言が続く。
|
|
682
|
+
base でテーブルが見つからなかった `fail_on_base` の行には、`base:` の行の下に `hint:` の行が出る。
|
|
683
|
+
それは `hint: base found no table, so this row checked nothing; load base's schema into app_test_1` で、base の番号付きのデータベースを名指しする。
|
|
684
|
+
`--number-database-url` が無ければ `its test database` となる。
|
|
685
|
+
|
|
686
|
+
base が `--timeout` の半分以内で通り、head だけがタイムアウトした行は、タグ `head_only_timeout` を付けて報告する。
|
|
687
|
+
遅い起動ではこれを説明できず、head はループし続けるか、待ち続けているかもしれない。
|
|
688
|
+
テストが止めた時計を見て待つコードがそうなる。
|
|
689
|
+
そのヒントは base にかかった時間を示し、`hint: base passed in 0.1s; head did not finish in 5s, so head may loop or wait forever` となる。
|
|
690
|
+
この行は再実行せず、`untested` にも数えない。
|
|
691
|
+
base がそれより長くかかったときは遅い起動もありうるので、その行は `discard:timeout` になり、ヒントは `hint: head timed out after 10s, and base passed in 7.4s;` で始まる。
|
|
692
|
+
`--json` では、こうした行にだけ、base の `seconds` を 0.1 秒単位で丸めて載せる。
|
|
693
|
+
時間は実行のたびに変わるからである。
|
|
694
|
+
|
|
695
|
+
テストのプロセスは決まった環境変数と `--env` で名指しした変数だけを受け取るので、シェルにある名前が無いために、届かないコマンドやネットワークを待ち続けることがある。
|
|
696
|
+
ある行がタイムアウトすると、その行の `hint:` が、`--timeout` を延ばすよう言う前に、今の環境にあり、`--env` で渡しておらず、子プロセスが必要としそうな変数を名指しする。
|
|
697
|
+
それはネットワークの設定(大文字でも小文字でも `HTTPS_PROXY`、`NO_PROXY`、`ALL_PROXY`、`HTTP_PROXY`、そして `SSL_CERT_FILE`、`SSL_CERT_DIR`、`NODE_EXTRA_CA_CERTS`)か、head の Ruby ファイルか候補がリテラルで読む名前(`ENV["QUINT"]`、`ENV.fetch("QUINT")`、`ENV.key?("QUINT")`)である。
|
|
698
|
+
ネットワークの設定を名指しするのは、head の Ruby ファイルか候補が HTTP クライアント(`net/http`、`open-uri`、`faraday`、`httparty`、`excon`、`http`、`httpx`、`httpclient`、`rest-client`、`typhoeus`)を require するか、head の `Gemfile.lock` にそのどれかがあるか、その中の文字列、バッククォート、引数の並びがネットワークから取ってくるコマンドで始まるときだけである。
|
|
699
|
+
そのコマンドは `"npx `、`"npm `、`"curl `、`"wget `、そして `bundle`、`gem`、`git` のあとに `install`、`update`、`clone`、`fetch`、`pull` が続くもの(`system("bundle", "install")` など)である。
|
|
700
|
+
スクリプトファイルが走らせるコマンドは見ない。
|
|
701
|
+
そのため、ローカルの SQLite だけを使うアプリのスペックでは名指ししない。
|
|
702
|
+
たとえば `hint: base and head timed out after 10s; jitcatch did not pass HTTPS_PROXY QUINT, so add --env HTTPS_PROXY --env QUINT if the file needs them, then raise --timeout if it needs longer` となる。
|
|
703
|
+
`--env` で渡したのに今の環境で設定されていない名前は、それらより先に出る。
|
|
704
|
+
たとえば `hint: base and head timed out after 10s; --env QUINT passed nothing, because QUINT is not set, so set it if the file needs it, then raise --timeout if it needs longer` となる。
|
|
705
|
+
そのような名前が無ければ、ヒントは `hint: base and head timed out after 10s; raise --timeout if the file needs longer` である。
|
|
706
|
+
head に `config/application.rb` があるときだけ、ヒントの最後に `, as a Rails app's first run in a new checkout can (it loads db/schema.rb)` が付く。
|
|
707
|
+
`--json` では名前が `env_not_passed` に並び、タイムアウトした行が無ければ空である。
|
|
708
|
+
`rails_app` は、head に `config/application.rb` があるとき true になる。
|
|
709
|
+
|
|
710
|
+
実行されるのは `*_test.rb`、`test_*.rb`、`*_spec.rb` である。
|
|
711
|
+
その名前が 1 つも無いときは、すべての `.rb` を実行する。
|
|
712
|
+
上限は 40 ファイル。
|
|
713
|
+
base で通り、head で失敗、エラー、またはタイムアウトになったテストが候補になる。
|
|
714
|
+
base でタイムアウトし、head で通らなかったテストも候補になる。
|
|
715
|
+
base は時間があれば通ったかもしれないので、後で述べるタイムアウトのルールがそれを除く。
|
|
716
|
+
`--reruns` は候補を head で繰り返す。
|
|
717
|
+
1 回でも結果が違えば `flaky` である。
|
|
718
|
+
比べるのは失敗メッセージの全体である。
|
|
719
|
+
比べる前に一時ディレクトリ直下の名前、オブジェクトのアドレス(`#<Class:0x00007f...>`)、日付つきの時刻(`created_at: "2026-11-04 19:02:03.212197000 +0000"`)を伏せるので、メッセージにある `Dir.mktmpdir` のパス、新しい無名クラス、レコードを保存した時刻は違いにならない。
|
|
720
|
+
日付だけなら伏せない。
|
|
721
|
+
再実行が同じ失敗かエラーで、例外のクラスも同じで、メッセージだけが違うときは、まだ食い違いとしない。
|
|
722
|
+
印字したレコードの中のランダムな ID がこれにあたる。
|
|
723
|
+
このときは base でもテストを `--reruns` 回実行し、毎回通れば行は候補のまま残り、`message_varies` タグが付く(`--json` では `message_varies: true` と `base_reruns`)。
|
|
724
|
+
base の再実行が一度でも通らなければ行は `flaky` になる。
|
|
725
|
+
再実行の行には、メッセージのうち変わった最初の行が出る。
|
|
726
|
+
メッセージだけが変わったときは、その前に `(same outcome, message differs)` が付き、base の再実行は `base rerun 1:` として続く。
|
|
727
|
+
テキストの報告は `flaky` の行の下に各再実行の結果を、行と同じく 300 文字で切って出し、JSON の行は `reruns` に持つ。
|
|
728
|
+
タイムアウトは再実行しない。
|
|
729
|
+
|
|
730
|
+
候補を `discarded` にするルールは 4 つある。
|
|
731
|
+
head での `LoadError`、どちらかの側でのタイムアウト、データベースが無いか届かないか、テーブルが無いことを示す head での Active Record のエラー(`NoDatabaseError`、`ConnectionNotEstablished`、`DatabaseConnectionError`、テーブルが無いときは `discard:no_table`)、そして diff が消したかシグネチャを書き換えた名前に対する `NameError`、`NoMethodError`、引数個数の `ArgumentError` である。
|
|
732
|
+
消えたクラスやモジュールも消えた名前に数える。
|
|
733
|
+
引数個数の `ArgumentError` とは `wrong number of arguments`、`missing keyword`、`unknown keyword`、`struct size differs`(メンバーより多くの値を渡した `Struct`)のことで、`assert_raises` や RSpec の `raise_error` が受け取って失敗にしたときも同じに扱う。
|
|
734
|
+
コードが意図して起こす `ArgumentError`(`qty must be > 0` のようなもの)は `reported` のまま残る。
|
|
735
|
+
|
|
736
|
+
同じリポジトリと同じテストなら、出力は同じバイトになる。
|
|
737
|
+
順序は固定で、テキストに時刻は無い。
|
|
738
|
+
メッセージの中の worktree の絶対パスの根は、`{worktree}/lib/cart.rb` の中でも `Dir.pwd` だけでも `{worktree}` と書き、バックトレースのフレームからは除く。
|
|
739
|
+
メッセージが `{worktree}/.jitcatch` のコピーを指すところは、`rerun:` の行と同じくテストのディレクトリを指すように書き換える。
|
|
740
|
+
リポジトリの中の `TESTS_DIR` `candidates/` なら `{worktree}/candidates/fixtures/a.json` と書き、リポジトリの外の `../catch/tests` なら `{worktree}/../catch/tests/fixtures/a.json` と書く。
|
|
741
|
+
テストの中のバックトレースのフレームも、`{worktree}/` を付けずに同じように `TESTS_DIR` を書く(`candidates/cart_test.rb` や `../catch/tests/cart_test.rb`)。
|
|
742
|
+
`run` の `--json` はそのパスを `tests_dir` に書き、`assess` はその下のフレームをテスト自身のものとして読む。
|
|
743
|
+
`__FILE__` のような相対の `.jitcatch/` はそのまま残す。
|
|
744
|
+
テストが比べる文字列と見分けられないからである。
|
|
745
|
+
ただし、読み込めなかったファイルについて Ruby が出すエラー報告の行頭の `.jitcatch/x_test.rb:2:in` は、`rerun:` の行と同じく `TESTS_DIR` の下の名前に書き換える。
|
|
746
|
+
`TESTS_DIR` を絶対パスで書くときは、`{worktree}/` を付けない。
|
|
747
|
+
Ruby 自身の prefix は `{ruby}/` と書く。
|
|
748
|
+
|
|
749
|
+
`assess:` の下の候補の行は、それぞれ `rerun:` の行で終わる。
|
|
750
|
+
`--json` のすべての行にも `rerun` がある。
|
|
751
|
+
これは、手元のチェックアウトでリポジトリの根からそのテスト 1 つだけを再実行するコマンドである。
|
|
752
|
+
候補のパスは、リポジトリのルートから届く形で書く。
|
|
753
|
+
`TESTS_DIR` がリポジトリの外にあっても、ルートからのパス(`../catch/tests/cart_test.rb`)になる。
|
|
754
|
+
そのため、出たとおりに実行でき、この行はどの手元のディレクトリも含まない。
|
|
755
|
+
ただし、ホームディレクトリの奥のチェックアウトから `/tmp` の下のディレクトリへのように、ルートからのパスが絶対パスより長くなるときは、この行と `require_relative` の警告は絶対パス(`/tmp/catch/cart_test.rb`)を出す。
|
|
756
|
+
`run` は各チェックアウトのルートに置いた `TESTS_DIR` の写しから候補を読み込む。
|
|
757
|
+
そのため、`TESTS_DIR` の外へ出る `require_relative` は、実行ではチェックアウトのファイルを指し、この行からは `TESTS_DIR` の隣のファイルを指す。
|
|
758
|
+
`TESTS_DIR` がルートの直下にない限り二つは食い違うので、そのとき `run` はその行と二つのファイルを `warning:` に出す(`a_test.rb:2 require_relative "../lib/a" loads lib/a.rb in the run but ../base/lib/a.rb from the rerun line`)。
|
|
759
|
+
メソッドやブロックの中の `require_relative` も数え、文字列リテラルの `File.join` はそれがつなぐパスとして読む。
|
|
760
|
+
`File.join(__dir__, "helper")` や式展開を含む文字列のように、ソースからパスが決まらないものは、`TESTS_DIR` がルートの直下にない限り、二つが同じファイルを読み込むかを jitcatch が確かめられないと `warning:` に出す。
|
|
761
|
+
リポジトリのコードは `require` とこの行の `-I` で読み込む。
|
|
762
|
+
この行はシェルで実行し、`--env` で渡した変数はそのシェルから来ているので、この行にも届く。
|
|
763
|
+
`DATABASE_URL` はパスワードを含むことがあり、報告はほかの人が読む場所に貼られるので、行には値を書かない。
|
|
764
|
+
`--env` で名前を渡したときは、各 `rerun:` 行の下の `rerun env:` 行がその名前と、実行のときに設定されていなかった名前を挙げる。
|
|
765
|
+
たとえば `rerun env: reads DATABASE_URL QUINT from your shell, as jitcatch run did; QUINT was unset, so the run used the code's own default` となる。
|
|
766
|
+
同じ名前が設定された場所でこの行を実行する。
|
|
767
|
+
`--number-database-url` が URL に番号を付けるのは実行の中だけなので、この行は自分のデータベースを使う。
|
|
768
|
+
たとえば `ruby -Ilib -I. -e 'require "rspec/core"; require "rspec/support/object_formatter"; RSpec::Core::RubyProject.instance_variable_set(:@project_root, Dir.pwd); RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil; exit(RSpec::Core::Runner.run(ARGV))' candidates/cart_spec.rb:11` や `ruby -Ilib -I. candidates/cart_test.rb -n CartTest#test_total` で、head に `Gemfile` があれば `bundle exec` が前に付く。
|
|
769
|
+
`<file>#load` と `<file>#timeout` の行は、ファイル全体を再実行する。
|
|
770
|
+
Rails アプリのように head の根に `spec` があり、jitcatch を実行したチェックアウトが `jitcatch/rspec` を読み込めるとき、RSpec の行は `rspec -I. -r jitcatch/rspec candidates/cart_spec.rb:11` になる。
|
|
771
|
+
チェックアウトが読み込めるのは、クイックスタートのようにその `Gemfile` が jitcatch を挙げるとき、または `Gemfile` がなく、使っている Ruby に gem が入っているときである。
|
|
772
|
+
`jitcatch/rspec` は RSpec が表示するオブジェクトを 200 文字で切る設定を外すので、`raise_error` の行は受け取ったエラーを最後まで示す。
|
|
773
|
+
それ以外のとき、この行は上の例のように、実行と同じ起動のしかたで Ruby を動かし、これも切る設定を外す。
|
|
774
|
+
その起動がただの `rspec` でないのは、`rspec` が `spec/` を持ついちばん近い親ディレクトリの `lib/` を読み込みパスの先頭に置くからである。
|
|
775
|
+
そのため Rails アプリの `tmp/` の下にあるチェックアウトでは、アプリの `lib/` を読んでしまう。
|
|
776
|
+
この起動はそのディレクトリを作業ディレクトリに固定し、実行時のワークツリーでも同じようにする。
|
|
777
|
+
head の根に `spec` があれば、RSpec はもともとその根を見つける。
|
|
778
|
+
そのときこの行は、rspec-core の内部の状態を書き換えるただ一つの部分である固定(`RSpec::Core::RubyProject.instance_variable_set(:@project_root, Dir.pwd); `)を省く。
|
|
779
|
+
|
|
780
|
+
## Rails アプリ
|
|
781
|
+
|
|
782
|
+
Rails アプリの候補は、アプリ自身の spec と同じく `require "rails_helper"` で始まる RSpec のファイルか、`require "test/test_helper"` で始まる Minitest のファイルである。
|
|
783
|
+
候補はアプリのファクトリーとテスト用データベースを使える。
|
|
784
|
+
`context` はファクトリーをそれが作るクラスまでたどるので、`create(:loan)` は `Loan#initialize` を指す。
|
|
785
|
+
|
|
786
|
+
base と head はアプリを同時に起動する。
|
|
787
|
+
相対パスの SQLite(Rails の既定の `storage/test.sqlite3`)はワークツリーごとのファイルなので、2 つの側は出会わない。
|
|
788
|
+
PostgreSQL や MySQL のようなデータベースサーバーでは、テスト用データベースの名前が両方の側で同じで、それぞれの側がもう一方の実行中に自分の `db/schema.rb` を読み込む。
|
|
789
|
+
列を消す変更では、6 つの候補のうち 1 つから 6 つが、読み込みエラー(`bin/rails aborted!`)として `reported` になった。
|
|
790
|
+
jitcatch はチェックアウトごとに `TEST_ENV_NUMBER` を設定する。
|
|
791
|
+
これは parallel_tests が設定する変数である。
|
|
792
|
+
最初のワーカーの base と head には `1` と `2`、2 番目のワーカーには `3` と `4` を渡し、以下同じように続ける。
|
|
793
|
+
両方のリビジョンの `config/database.yml` がこの変数でテスト用データベースを名付けていれば、チェックアウトごとに別のデータベースを使い、同じ実行で `reported` は 0 になった。
|
|
794
|
+
|
|
795
|
+
```yaml
|
|
796
|
+
test:
|
|
797
|
+
<<: *default
|
|
798
|
+
database: app_test<%= ENV["TEST_ENV_NUMBER"] %>
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
最初の実行の前に、ワーカー 1 つにつき 2 つのデータベースを一度だけ作る。
|
|
802
|
+
`spec/rails_helper.rb`(または `test/test_helper.rb`)が `ActiveRecord::Migration.maintain_test_schema!` を呼ぶなら、スキーマは Rails が起動するときにそれぞれのリビジョンのものを読み込む。
|
|
803
|
+
呼ばないなら、`db:create` だけで作ったデータベースにはテーブルが無く、どの行もテーブルを見つけられない。
|
|
804
|
+
そこで `run --setup 'bin/rails db:create db:schema:load'` も渡し、それぞれの側に自分のスキーマを読み込ませる(後述)。
|
|
805
|
+
|
|
806
|
+
```sh
|
|
807
|
+
for n in 1 2; do RAILS_ENV=test TEST_ENV_NUMBER=$n bin/rails db:create; done
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
一度だけ作ったデータベースは、1 つのスキーマしか持たない。
|
|
811
|
+
`db/schema.rb`、`db/structure.sql`、`db/migrate/`(または 2 つ目のデータベースの `db/<name>_schema.rb`、`db/<name>_structure.sql`、`db/<name>_migrate/`)が変わると、列の無いデータベースの側は、コードではなくデータベースで失敗する。
|
|
812
|
+
知らない属性のせいで 500 を受け取るリクエストスペックがその例である。
|
|
813
|
+
そのとき `context` と `run` は `warning: head changes the database schema (db/migrate/20261106000000_add_notify.rb, db/schema.rb), so each side's test database must hold that side's schema, or a row can fail on the database and not on the code.` を出す。
|
|
814
|
+
head に `spec/rails_helper.rb`、`spec/spec_helper.rb`、`test/test_helper.rb` のどれかがあり、どれも `maintain_test_schema!` を呼ばないときは、警告はそのファイルを名指しし、`pass --setup 'bin/rails db:create db:schema:load' to jitcatch run` と続ける。
|
|
815
|
+
`--number-database-url` のときは、各側のデータベースも名指しする(`base app_test_wt1_1, head app_test_wt1_2`)。
|
|
816
|
+
|
|
817
|
+
`run --setup CMD` は、バンドルの確認のあと、どの候補よりも前に、各チェックアウトで `CMD` を `/bin/sh` で一度ずつ、1 つずつ走らせる。
|
|
818
|
+
`bin/rails db:create db:schema:load` は、まだない番号付きのデータベースを作り、その側のスキーマを読み込む。
|
|
819
|
+
`db:schema:load` だけでは、誰も作っていないデータベースで失敗する。
|
|
820
|
+
`db:prepare` は、自分で作ったデータベースに `db/seeds.rb` も読み込む。
|
|
821
|
+
`CMD` は、その側の `TEST_ENV_NUMBER`、番号付きの `DATABASE_URL`、`--env` で渡した変数を受け取る。
|
|
822
|
+
`--env RAILS_ENV` で別の値を渡さない限り `RAILS_ENV=test` も受け取るので、スキーマの読み込みが開発用のデータベースに届くことは無い。
|
|
823
|
+
報告は `setup CMD` を出し(`--json` では `setup` で、フラグが無ければ `null`)、スキーマの警告は `` `--setup` ran `CMD` in each checkout before the candidates. `` となる。
|
|
824
|
+
コマンドが失敗するか `--timeout` を過ぎると、実行は終了コード 3 で止まる。
|
|
825
|
+
標準出力は `jitcatch: --setup failed, so no test ran` で始まり、各側のコマンドと、その標準エラーの最後の行を示す。
|
|
826
|
+
その行がデータベースに接続を拒まれたこと (`Connection refused`、`could not connect to server`、`Can't connect to MySQL server`) を示すときは、シェルが設定していて `--env` が渡さなかったデータベースの変数 (`DATABASE_URL` とその `*_DATABASE_URL` の形、`DB_*`、`PG*`、`MYSQL_*`) も各側で示す。
|
|
827
|
+
たとえば `the database refused the connection, and jitcatch did not pass DB_PORT from your shell: add --env DB_PORT` となる (`--json` では `database_not_passed`)。
|
|
828
|
+
すべてのファイルが同じ理由で読み込めなかった実行も、同じように示す。
|
|
829
|
+
1 つのテスト用データベースを共有する 2 つの側は、それぞれのスキーマを持てないので、先にデータベースを分ける。
|
|
830
|
+
|
|
831
|
+
Rails アプリで head が 500 を返した報告行には、`hint: head answered 500, and a Rails app writes the error behind it to log/test.log; run the row's rerun line and read that log` のヒントが付く。
|
|
832
|
+
チェックアウトは実行の終わりに消えるので、そのログも残らない。
|
|
833
|
+
|
|
834
|
+
head のデータベースが無いときは、その行は `discard:no_database` になり、テーブルが無いときは `discard:no_table` になる。
|
|
835
|
+
`database.yml` がこの変数を使っていないことは、`run` が候補を動かす前に見つける。
|
|
836
|
+
`run` は各側の `config/database.yml` を、その側の `TEST_ENV_NUMBER` と、`--env` で渡した `DATABASE_URL` があればそれも使って、Active Record と同じように読む。
|
|
837
|
+
base と head がサーバー上の 1 つのデータベース、または絶対パスの 1 つの SQLite ファイルを指すとき、`--workers` の値にかかわらず、`--serial` と同じく一度に 1 つのプロセスだけを動かす。
|
|
838
|
+
そのときテキストの報告は `serial` と次の 2 行を出す。
|
|
839
|
+
|
|
840
|
+
```text
|
|
841
|
+
warning: base and head use one test database, lending_test, so jitcatch ran one process at a time; that changes how long the run takes, not its results.
|
|
842
|
+
warning: To run them at once, end the test database name in config/database.yml with <%= ENV["TEST_ENV_NUMBER"] %> and create the numbered databases: for n in 1 2; do RAILS_ENV=test TEST_ENV_NUMBER=$n bin/rails db:create; done. This helps only when BASE and HEAD both have that line.
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
過去のコミットの `database.yml` は変えられないので、そのコミットどうしでは 1 つずつ動かす実行のままでよい。
|
|
846
|
+
`--env DATABASE_URL` を渡し、その URL がサーバー上のデータベースを指すときは、2 行目は代わりに `--number-database-url` を挙げる。
|
|
847
|
+
Rails はデータベース名を含む `DATABASE_URL` を `database.yml` の `database:` より優先するので、`database:` に番号を付けても分かれないからである。
|
|
848
|
+
`postgresql://localhost` のようにデータベース名を含まない URL は `database.yml` の `database:` を使うので、2 行目は上と同じになる。
|
|
849
|
+
`sqlite3:/srv/app/wt1.sqlite3` のように絶対パスの SQLite の URL は両方のチェックアウトで 1 つのファイルを指し、このフラグはファイルには番号を付けない。
|
|
850
|
+
そのため 2 行目は `give DATABASE_URL a relative path, as sqlite3:storage/test.sqlite3, so each checkout opens a file under its own root` と言う。
|
|
851
|
+
|
|
852
|
+
`--number-database-url` は、ワークツリーごとに自分の `DATABASE_URL` を渡すアプリに合う。
|
|
853
|
+
たとえば git が無視する `.env.test.local` から渡していて、チェックアウトにはそのファイルがない場合である。
|
|
854
|
+
ワークツリーの URL を `--env DATABASE_URL` で渡し、このフラグを付ける。
|
|
855
|
+
すると各チェックアウトは URL のデータベース名に自分の `TEST_ENV_NUMBER` を足すので、`postgresql://localhost/app_test_wt1` は base では `app_test_wt1_1`、head では `app_test_wt1_2` になり、ほかのワークツリーのものとも分かれ、どのコミットの `database.yml` も変わらない。
|
|
856
|
+
報告は URL の残りを除いた名前を `numbered_databases app_test_wt1_1 app_test_wt1_2` と出す(`--json` では `numbered_databases`)。
|
|
857
|
+
これらは同じサーバーに一度だけ作る。
|
|
858
|
+
たとえば `for n in 1 2; do DATABASE_URL=postgresql://localhost/app_test_wt1_$n RAILS_ENV=test bin/rails db:create; done` とする。
|
|
859
|
+
テストヘルパーが `ActiveRecord::Migration.maintain_test_schema!` を呼ばないなら、`--setup 'bin/rails db:create db:schema:load'` も渡し、それぞれの側に自分のスキーマを読み込ませる。
|
|
860
|
+
このフラグを付けて `--env DATABASE_URL` を渡さないとき、または URL のパスがサーバー上のデータベースを指さないとき、`run` は何を変えるかを言って 1 で終わる。
|
|
861
|
+
SQLite の URL なら、フラグの代わりに相対パスにするよう言う。
|
|
862
|
+
`Rails.application` を呼ぶもののように、起動したアプリの中でしか読めない `database.yml` からは答えが出ないので、実行は本来のプロセス数のまま進む。
|
|
863
|
+
PostgreSQL 16 を使うサンプルアプリで `db/schema.rb` も変える変更では、6 つのファイルが、ロードアベレージ 11 から 18 では 1 つずつで 40 秒から 50 秒、番号付きのデータベースで 18 秒、ロードアベレージ 2 ではそれぞれ 32 秒と 9 秒かかった。
|
|
864
|
+
1 つのデータベースを共有したまま `--workers 3` で動かすと、2 回のうち 1 回、変更が原因でない `loan_spec.rb#load` の行が報告された。
|
|
865
|
+
この確認が入る前は、1 つの例を持つ 6 つのファイルに `--serial` で 34 秒、番号付きのデータベースで 6 秒から 10 秒かかった。
|
|
866
|
+
|
|
867
|
+
それぞれのチェックアウトには、git が追跡するファイルしか無い。
|
|
868
|
+
認証情報を読むイニシャライザーは、`config/master.key` が無いのでそこで失敗する。
|
|
869
|
+
鍵は `RAILS_MASTER_KEY=$(cat config/master.key) jitcatch run --env RAILS_MASTER_KEY BASE HEAD TESTS_DIR` のように渡す。
|
|
870
|
+
|
|
871
|
+
ファイルごとに、それぞれの側でアプリを 1 回起動する。
|
|
872
|
+
サンプルアプリ(4 CPU)では、example が 1 つずつの 6 ファイルに 9.8 秒、同じ 6 つの example を 1 ファイルにまとめると 1.9 秒かかった。
|
|
873
|
+
bootsnap のキャッシュは、チェックアウトごとに空から始まる。
|
|
874
|
+
1 ファイルでは bootsnap ありで 3.7 秒、`--env DISABLE_BOOTSNAP` で `DISABLE_BOOTSNAP=1` を渡すと 2.5 秒だった。
|
|
875
|
+
6 ファイルでは bootsnap ありで 8.0 秒、なしで 9.8 秒だった。
|
|
876
|
+
|
|
877
|
+
### 変わった値を読むコード
|
|
878
|
+
|
|
879
|
+
変更は、名前で呼ばないコードにも効く。
|
|
880
|
+
`to_param` は、route helper がそのレコードに作る URL すべてに届く。
|
|
881
|
+
`default_scope` はそのモデルへのクエリすべてに、定数はそれを埋め込んだ SQL に、カラムのデフォルト値はそれを省いた `create` すべてに届く。
|
|
882
|
+
こうした箇所は呼び出し元ではないので、`callsites:` には出ない。
|
|
883
|
+
`context` はそれを `readers:` に、変わった値ごとに 1 ブロックで出す。
|
|
884
|
+
値はメソッド、scope、`default_scope`、定数、コールバック、バリデーション、`attribute` か `db/schema.rb` のカラムのデフォルト値、`config/` の中の `config.` の設定。
|
|
885
|
+
`HEAD` の `app/`、`config/`、`lib/`、`db/schema.rb` を git から読み、チェックアウトは読まない。
|
|
886
|
+
その中の ERB テンプレートは、行を保ったまま Ruby として読む。
|
|
887
|
+
テンプレートの中の呼び出しも呼び出し元なので、ビューが名前で呼ぶメソッドは、そのビューを `readers:` ではなく `callsites:` に出す。
|
|
888
|
+
|
|
889
|
+
4 ファイルの履歴:
|
|
890
|
+
|
|
891
|
+
```text
|
|
892
|
+
config/routes.rb resources :branches do; resources :closures, only: :index; end
|
|
893
|
+
app/controllers/closures_controller.rb before_action :set_branch ... @branch = Branch.find(params[:branch_id])
|
|
894
|
+
app/views/branches/show.html.erb <%= link_to "Closures", branch_closures_path(@branch) %>
|
|
895
|
+
app/models/branch.rb (head adds) def to_param; code; end
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
`context base HEAD` はこう出す:
|
|
899
|
+
|
|
900
|
+
```text
|
|
901
|
+
callsites: (none by name; 1 under readers:)
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
```text
|
|
905
|
+
readers: 1 for 1 changed value
|
|
906
|
+
Branch#to_param (added) method m01 aac7821:app/models/branch.rb:4 readers=1
|
|
907
|
+
100 route_param_mismatch aac7821:app/views/branches/show.html.erb:2
|
|
908
|
+
rank: the URL value and the lookup now disagree
|
|
909
|
+
why: @branch goes through Branch#to_param (now `code`) into params[:branch_id]; ClosuresController#index reads params[:branch_id] by id (Branch.find(params[:branch_id]), app/controllers/closures_controller.rb:11)
|
|
910
|
+
test: Render app/views/branches/show.html.erb:2 with a Branch whose to_param differs from its id, follow the URL branch_closures_path(@branch) builds, and check ClosuresController#index finds that same Branch.
|
|
911
|
+
not seen: receivers named unlike their model (`x = Branch.first; link_to x`); dispatch names built at run time from data outside the source (a DB column, a request param); routes drawn by `draw`, loops, or concerns; `to_param` reached through a gem's helper; serializers, jbuilder, and other template languages than ERB; callbacks and validations added by concerns or gems; config read through ENV or a YAML file at boot
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
行は、場所(`git show` に渡せる `commit:path:line`)、値の読み方(`mechanism`)、スコア、`rank:`(そのスコアの理由)、`why:`(ソースに書いてあること)、`also:`(同じ値を同じ読み方で読む別の場所)、`test:`(テストで確かめること)を持つ。
|
|
915
|
+
`note:` は、変更そのものが編集した行にある読み手に付く。
|
|
916
|
+
行はスコアの高い順に、全部で最大 10 行出る。
|
|
917
|
+
1 行目が残りの数を数え、`--all-readers` はテキストでも `--json` でも全行を出す。
|
|
918
|
+
メソッドの `callsites:` にすでにある行と、定数の `read_by:` にすでにあるメソッドの中のただの読み出しは出さない。
|
|
919
|
+
`not seen:` は、見えないものを挙げる。
|
|
920
|
+
レコードのモデルは変数名かメソッド名(`@branch`、`home_branch`、`branches`)で決めるので、`x = Branch.first; link_to x` は出ない。
|
|
921
|
+
|
|
922
|
+
## インストール
|
|
923
|
+
|
|
924
|
+
`gem install jitcatch` で `jitcatch` コマンドが入る。
|
|
925
|
+
`Gemfile` のあるアプリでは、代わりに [クイックスタート](#rails-アプリでのクイックスタート) のとおり `:development, :test` グループに gem を足し、`bundle exec jitcatch` で走らせる。
|
|
926
|
+
実行時の依存はない。
|
|
927
|
+
このリポジトリのチェックアウトからも使える。
|
|
928
|
+
その場合は `bin` ディレクトリを `PATH` に置く。
|
|
929
|
+
|
|
930
|
+
スキルは [`skills`](https://github.com/vercel-labs/skills) CLI で入れる(Node.js 22.20 以降)。
|
|
931
|
+
このリポジトリは非公開なので、Git の認証情報、GitHub CLI、または SSH で GitHub にアクセスできる必要がある。
|
|
932
|
+
|
|
933
|
+
```sh
|
|
934
|
+
npx skills add meganemura/jitcatch
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
このスキルだけ、グローバル、または特定のエージェントへ入れる。
|
|
938
|
+
|
|
939
|
+
```sh
|
|
940
|
+
npx skills add meganemura/jitcatch --skill jitcatch
|
|
941
|
+
npx skills add meganemura/jitcatch -g
|
|
942
|
+
npx skills add meganemura/jitcatch -a cursor
|
|
943
|
+
npx skills add meganemura/jitcatch -a claude-code
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
確認を飛ばすときは `-y` を足す。
|
|
947
|
+
スキルは `jitcatch` コマンドを走らせるので、gem も入れておく。
|
|
948
|
+
|
|
949
|
+
CLI を使わない場合は、`skills/jitcatch` をエージェントの skills ディレクトリへコピーまたはシンボリックリンクする(例: `.agents/skills/`、`.claude/skills/`、`.cursor/skills/`)。
|
|
950
|
+
|
|
951
|
+
## 限界
|
|
952
|
+
|
|
953
|
+
- 呼び出しを数えるのは、レシーバのクラスがソースに書かれているときだけである。
|
|
954
|
+
`self`(レシーバなしを含む)、定数(`Cart.build`)、`self.class`、それらの `new`、そしてクラスが一つの `Const.new` だけを代入するインスタンス変数がこれに当たる。
|
|
955
|
+
メソッドの中のすべての代入が同じ `Const.new` であるローカル変数(`repo = Repo.new; repo.read`)もこれに当たる。
|
|
956
|
+
ソースが型を決めない値は、Rails がその名前から決めるクラスがそのメソッドを定義していれば、そのクラスへの呼び出しになる。
|
|
957
|
+
引数とブロック引数(`Gate#check(loan)` の中の `loan.approve`)、どの代入も `Const.new` でないローカル変数、そしてどの代入も `Const.new` でないインスタンス変数(`@loan = loan`)は、どれも `Loan` として読む。
|
|
958
|
+
`belongs_to :borrower` や `has_one :borrower` が定義する読み出しは `Borrower` を返すものとして読み、`class_name:` に文字列があればそのクラスとして読む。
|
|
959
|
+
そのため `Loan#approve` の中の `borrower.approved_cents` は `Borrower#approved_cents` を呼ぶ。
|
|
960
|
+
polymorphic の関連はどのクラスとしても読まない。
|
|
961
|
+
メソッドやクラスがその変数に代入する `Const.new` と名前が食い違うときは、どのクラスとしても読まない。
|
|
962
|
+
同じクラスのメソッドが返す値への呼び出し(`emitter_for(format).render`)は、そのメソッドが定数と nil だけを返すとき(`case`、`if`、三項演算子を通して)、返しうる定数それぞれへ向かう。
|
|
963
|
+
二つの違う定数を代入したローカル変数、どのクラスも指さない名前、それ以外の戻り値への呼び出しは辺にならないので、`reachable_from` が呼び出し元を落とすことがある。
|
|
964
|
+
そうした呼び出しは、送る名前が変更メソッドの名前か、その `reachable_from` にあるメソッドの名前であれば、その変更メソッドの `dynamic_callers:` に `Emit.looked_up (render)` のように出る。
|
|
965
|
+
ただし名前が `Controller` で終わるクラスのインスタンスメソッドの名前は除く。
|
|
966
|
+
コントローラーのアクションはルーターが呼び、アプリのコードがその名前を送ることはないので、`ThingsController#update` が変更に届いても `cipher.update` は出ない。
|
|
967
|
+
クラスが定義していないメソッドを `self` に呼ぶと、その名前を持つ索引上ただ一つのメソッドへ向かう。
|
|
968
|
+
モジュールやスーパークラスのメソッドのためである。
|
|
969
|
+
インスタンスメソッドからはインスタンスメソッドだけが対象になる。
|
|
970
|
+
`Const.new` と、クラスメソッドの中でレシーバなしに呼ぶ `new` は、そのクラスの `initialize` へも向かう。
|
|
971
|
+
クラスが定義していなければ、スーパークラスをたどって最も近い `initialize` へ向かう。
|
|
972
|
+
そのため `Runner.new` の呼び出し元は `Runner#initialize` とそこから呼ぶメソッドに届く。
|
|
973
|
+
スーパークラスは Ruby と同じく、そのクラスの外から探す。
|
|
974
|
+
そのため `module Shop` の中の `class Error < Error` は外側の `Error` を指す。
|
|
975
|
+
`reachable_from` は 4 回の呼び出しまで呼び出し元をたどり、`new` から `initialize` へ入る一歩はその 4 回に数えない。
|
|
976
|
+
残すのは近い順に 8 個までである。
|
|
977
|
+
`--json` の `reached_by` は、深さも個数も限らず、すべての呼び出し元を持つ。
|
|
978
|
+
`caller_depths` は、その呼び出し元それぞれから最も少ない呼び出しの回数を持つ。
|
|
979
|
+
`run` は言及と関連 id にこちらを読むので、上から入るテストも、5 回以上の呼び出しの先にある変更メソッドを名指しできる。
|
|
980
|
+
たとえばコントローラとサービスを通るリクエストスペックや、`CLI.start` を呼ぶ CLI のテストである。
|
|
981
|
+
そうしたテストはたいていの変更メソッドに届くので、その行はふつう `related:guess` になり、そのメソッドが一覧に入る。
|
|
982
|
+
`create`、`create!`、`build` も、リポジトリ内のスーパークラスの連なりが `ActiveRecord::Base` に届くクラス(`class Post < ApplicationRecord`)では同じく `initialize` へ向かう。
|
|
983
|
+
Active Record はこれらを `new` を呼ぶメソッドとして定義しているからである。
|
|
984
|
+
ほかのクラスでは、定義しているコードが索引にないので辺にならない。
|
|
985
|
+
連なりの上に同じ名前のクラスメソッドがあれば、呼び出しはそちらへ向かう。
|
|
986
|
+
クラスが `attr_reader`、`attr_writer`、`attr_accessor` で宣言した名前は、その読み書きのメソッドへの辺になる。
|
|
987
|
+
- `define_method` と `class_eval` は索引しない。
|
|
988
|
+
`def self.foo` 以外のレシーバ付き定義は粗い。
|
|
989
|
+
- リネームは削除と追加に見える(`git diff --no-renames`)。
|
|
990
|
+
- カバレッジは集めない。
|
|
991
|
+
`--coverage` は渡された JSON を読むだけである。
|
|
992
|
+
- 破棄ルールは、intended と regression を分けない。
|
|
993
|
+
- 候補テストが古い振る舞いを書いていなければ、その穴にある回帰は `reported` が 0 のままである。
|
|
994
|
+
- 新機能のテストは base で落ち、候補にならない。
|
|
995
|
+
- 関連 id は、失敗の原因の行から一段ずれていることがある。
|
|
996
|
+
- 言及は、そのテストメソッドと、それが直接または間接に呼ぶ同じファイルのメソッドの中にある、メソッド呼び出し、定数、シンボルである。
|
|
997
|
+
`Owner.new` は、名前が `Owner` で終わるクラスの `initialize` への言及になり、`mentions` にはテストが書いたとおりに出る。
|
|
998
|
+
`A::Owner.new` は、名前が `A::Owner` で終わるクラスだけを指すので、`B::Owner#initialize` を指さない。
|
|
999
|
+
変更されたコンストラクタの `built_by:` にある呼び出しも同じく言及になる。
|
|
1000
|
+
Active Record のモデルへの `Loan.create!` や、FactoryBot の呼び出し(`create(:loan)`、`build(:loan)`、`create_list(:loan, 2)`、`build_stubbed(:loan)`)がこれに当たり、後者は `mentions` に、保存する呼び出し(`create`、`create_list`、`create_pair`)なら `create(:loan)`、保存しないものなら `build(:loan)` として出る。
|
|
1001
|
+
その名前のメソッドを自分で定義するファイルはそのメソッドを呼ぶので FactoryBot の呼び出しにならず、`attributes_for` はオブジェクトを作らない。
|
|
1002
|
+
テストが名前を書いたクラスか、`.new` で作ったそのインスタンスに送る呼び出しは、`Index.parse` や `Cart#total` のようにそのクラスと一緒にも出る。
|
|
1003
|
+
同じ名前の変更メソッドが複数あるとき、関連 id はそのクラス(または名前がそれで終わるクラス)のものになる。
|
|
1004
|
+
スーパークラスが定義するメソッドのように、そのクラスに該当するものがなければ、全部を残す。
|
|
1005
|
+
テストがリポジトリの定義しない定数にだけ送るメソッド名は、`File.write(path, text)` や `Dir.mktmpdir` のように、言及にならない。
|
|
1006
|
+
そのため `File.write` は、`Emitter.write` から届く変更メソッドを指さない。
|
|
1007
|
+
`emitter.send(:write)` は `emitter` に `write` を送るので、両方を書くテストは `write` に言及する。
|
|
1008
|
+
名前をリテラルで渡す `send`、`__send__`、`public_send` はコードの中でもその名前の呼び出しとして読むので、メソッドの中の `send(:helper)` は `helper` に届く。
|
|
1009
|
+
文字列とコメントの中の語は言及にならない。
|
|
1010
|
+
ただし、`send`、`public_send`、`method`、`respond_to?` に普通の文字列で渡したメソッド名は、`send("total")` のように言及になる。
|
|
1011
|
+
`snap[:total]`、`snap.fetch(:total)`、`{ total: 1 }` のようにハッシュのキーとして使ったシンボルは言及にならない。
|
|
1012
|
+
`message`、`full_message`、`detailed_message` の呼び出しは、`to_s` への言及にもなる。
|
|
1013
|
+
エラークラスは、メッセージを作るために `to_s` を上書きするからである。
|
|
1014
|
+
`"exe/tool"` や `"../bin/x"` のような相対パスの形の文字列が head のファイルを指すときは、テストが別のプロセスで起動するスクリプトとみなし、そのファイルの中の名前も言及になる。
|
|
1015
|
+
パスは、リポジトリの根から、候補のディレクトリから、または候補の隣で `File.expand_path` が読むとおりに読む。
|
|
1016
|
+
別のファイルのヘルパーは読まない。
|
|
1017
|
+
- `--workers` を渡さなければ、ファイルは 1 つずつ実行する。
|
|
1018
|
+
1 つのファイルの base 側と head 側は、それぞれの worktree で同時に走る。
|
|
1019
|
+
そのため、worktree の外の決まったパスに書くテストや、決まったポートで待ち受けるテストは、もう一方の側とぶつかることがある。
|
|
1020
|
+
`--workers` を渡すと、ほかのファイルともぶつかることがある。
|
|
1021
|
+
そのようなファイルには `--serial` を渡す。
|
|
1022
|
+
リポジトリの既存スイートは走らない。
|
|
1023
|
+
worktree を追加できる git リポジトリが要る。
|
|
1024
|
+
- アサーションのバックトレースはテストの中で止まりがちなので、ライブラリの行はテストが言及した名前から探す。
|
|
1025
|
+
- RSpec の example の言及は、その example のブロック、それを囲むグループの `let`、`subject`、フックのブロック、それらのグループの引数、そしてそれらが呼ぶ同じファイルのヘルパーから取る。
|
|
1026
|
+
`"#total"` や `".build"` と記述したグループは、そのメソッドを指す。
|
|
1027
|
+
`described_class.new` は `Owner.new` を指す。
|
|
1028
|
+
`described_class.parse` は `Owner.parse` として出る。
|
|
1029
|
+
`let(:result)` や `subject(:result)` の名前は spec 自身のヘルパーなので、その名前も、レシーバのない `result` の呼び出しも、変更メソッド `result` への言及にならない。
|
|
1030
|
+
そのブロックが一つのクラスを作るときは、`let(:cart) { Cart.new }` のように、`cart.total` は `Cart#total` として出る。
|
|
1031
|
+
暗黙の `subject`、共有 example グループ、`spec/support` のヘルパーは読まない。
|
|
1032
|
+
別のファイルで定義した共有グループの example は、候補ファイル全体を読む。
|
|
1033
|
+
- データベースサーバーを使う Rails アプリの base と head は、両方のリビジョンの `config/database.yml` が `TEST_ENV_NUMBER` でテスト用データベースを名付けていない限り、1 つのテスト用データベースを共有する。
|
|
1034
|
+
そのとき `run` はプロセスを 1 つずつ動かす(Rails アプリの節を参照)。
|
|
1035
|
+
起動したアプリの中でしか読めない `database.yml` では、共有しているかを判断できない。
|
|
1036
|
+
MySQL では動かしておらず、共有する名前は Active Record による `database.yml` の読み取りだけで試した。
|
|
1037
|
+
テストスイートが試すのは Rails プロジェクトの形だけである。
|
|
1038
|
+
Rails の手順は、PostgreSQL 16 と SQLite を使う Rails 8.1 のアプリで測った。
|
|
1039
|
+
- RSpec のすべての example の外で起きたエラー(`after(:suite)` フックの中など)は、example が 1 つも走らなかったときだけ報告する。
|
|
1040
|
+
|
|
1041
|
+
## 開発
|
|
1042
|
+
|
|
1043
|
+
```sh
|
|
1044
|
+
ruby -Ilib test/run.rb
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
エンジンテストは一時 git リポジトリを作り、context、実行、フレーク、タイムアウト、シグネチャ変更の破棄を見る。
|
|
1048
|
+
`test/jitcatch/test_shop_demo.rb` はショップのフィクスチャを作り直し、`context` と `run` を `examples/runs/*/context.txt` および `run.txt` とバイト単位で比べる。
|
|
1049
|
+
|
|
1050
|
+
## ライセンス
|
|
1051
|
+
|
|
1052
|
+
MIT
|