quint_driver 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.
data/README.ja.md ADDED
@@ -0,0 +1,865 @@
1
+ # quint_driver
2
+
3
+ quint_driver は、Ruby のコードを [Quint](https://github.com/quint-co/quint) の仕様に照らしてテストします。
4
+ 仕様は各コマンドが何をすべきかを決め、quint_driver は同じコマンドをコードで動かして、1 手ごとに両者を比べます。
5
+
6
+ ## Rails アプリでのクイックスタート
7
+
8
+ ### gem を追加する
9
+
10
+ ```ruby
11
+ # Gemfile
12
+ group :test do
13
+ gem "quint_driver", require: false
14
+ end
15
+ ```
16
+
17
+ ```sh
18
+ bundle install
19
+ npm install --global @informalsystems/quint@0.33.0
20
+ ```
21
+
22
+ quint_driver には Ruby 3.3 以降が必要です。
23
+ この gem は `QUINT` が指す `quint` コマンドを呼ぶので、下の実行では `QUINT=quint` を指定します。
24
+ シェルのプロファイルに `export QUINT=quint` を書けば、指定を省けます。
25
+ `QUINT` の指定がなければ `npx --yes @informalsystems/quint@0.33.0` を実行します。
26
+ このときは初回に quint(43 MB)をダウンロードし、その後も毎回 node を 1 回多く起動します。
27
+ 初回の quint の実行では、quint の Rust 評価器も GitHub から `~/.quint` にダウンロードします。
28
+ ネットワークがこのダウンロードを止めるときは、[スキル](skills/quint_driver/SKILL.md#install)に手で取得する方法があります。
29
+
30
+ ### 最初のモデルを選ぶ
31
+
32
+ `state` や `status` のような状態の列と、それを変えるメソッドを持つモデルを選びます。
33
+ たとえば `Order#cancel` や `Loan#approve` です。
34
+ 最近のコミットが状態を変えたモデルから始めるとよいでしょう。
35
+ 仕様がその変更を確かめるからです。
36
+ そのようなコミットは `git log --oneline -- app/models` で一覧できます。
37
+
38
+ 下の手順では `Loan` を使います。
39
+ 新しいローンは `"pending"` で、`Loan#approve(officer)` はそれを `"approved"` にします。
40
+ `Loan`、ドライバーの `init` が作るレコード、メソッド、状態の名前を、自分のものに置き換えてください。
41
+
42
+ ### 仕様を書く
43
+
44
+ ```quint
45
+ // spec/quint/loan_state.qnt
46
+ // A loan starts pending. Approving it once makes it approved, and
47
+ // approving it again leaves it approved.
48
+ module loan_state {
49
+ var state: str
50
+
51
+ action init = state' = "pending"
52
+ action approve = all { state == "pending", state' = "approved" }
53
+ action approve_refused = all { state == "approved", state' = state }
54
+
55
+ action step = any { approve, approve_refused }
56
+ }
57
+ ```
58
+
59
+ `var` は列です。
60
+ `init` は新しいレコードが持つ値です。
61
+ ほかのアクションはそれぞれメソッドの 1 回の呼び出しで、最初の条件は、その呼び出しがアクションのとおりに動くときを決めます。
62
+ `approve` は保留中のローンを承認済みにし、`approve_refused` は承認済みのローンをそのままにします。
63
+ `step` は、トレースが選ぶアクションを並べます。
64
+ `Quint.simulate` はファイルと同じ名前のモジュールを動かすので、2 つの名前をそろえます。
65
+
66
+ ### ドライバーを書く
67
+
68
+ ```ruby
69
+ # spec/quint/loan_state_driver.rb
70
+ # Runs each action of loan_state.qnt on a Loan and reads its state back.
71
+ class LoanStateDriver
72
+ def step(action, _picks)
73
+ case action
74
+ when "init"
75
+ borrower = Borrower.create!(name: "Ada", limit_cents: 20_000)
76
+ @loan = Loan.create!(borrower: borrower, amount_cents: 500)
77
+ when "approve", "approve_refused"
78
+ @loan.approve({name: "Grace", senior: true})
79
+ end
80
+ end
81
+
82
+ def state = {"state" => @loan.reload.state}
83
+ end
84
+ ```
85
+
86
+ `step` は 1 つのアクションを本物のレコードで動かします。
87
+ `init` がレコードを作り、ほかのアクションはメソッドを呼びます。
88
+ `state` は、データベースから読み戻した仕様の変数を返します。
89
+
90
+ ### example を書く
91
+
92
+ ```ruby
93
+ # spec/quint/loan_state_spec.rb
94
+ require "rails_helper"
95
+ require "quint_driver"
96
+ require_relative "loan_state_driver"
97
+
98
+ RSpec.describe Loan, type: :model do
99
+ # The transactional fixture rolls back once, after the example. Each trace
100
+ # rolls back to a savepoint of its own, so no trace sees the rows of another.
101
+ def rolled_back
102
+ ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
103
+ yield
104
+ raise ActiveRecord::Rollback
105
+ end
106
+ end
107
+
108
+ it "moves its state the way loan_state.qnt says" do
109
+ QuintDriver::Quint.simulate("spec/quint/loan_state.qnt", traces: 20, max_steps: 5, seed: "1") do |trace|
110
+ rolled_back { QuintDriver::Trace.replay(trace, LoanStateDriver.new) }
111
+ end
112
+ end
113
+ end
114
+ ```
115
+
116
+ Quint は最大 5 手のトレースを 20 本書き、各トレースは新しいローンで動きます。
117
+
118
+ ### example を動かす
119
+
120
+ ```sh
121
+ QUINT=quint bundle exec rspec spec/quint/loan_state_spec.rb
122
+ ```
123
+
124
+ ## 実行結果
125
+
126
+ 次の出力は、`Borrower` と `Loan` のモデルを持つ小さなサンプルアプリ(貸し出し窓口)のものです。
127
+
128
+ ```text
129
+ .
130
+
131
+ Finished in 0.60298 seconds (files took 0.88104 seconds to load)
132
+ 1 example, 0 failures
133
+ ```
134
+
135
+ どのトレースも、テストのデータベースにローンを作り、承認し、もう一度承認して、1 手ごとに状態を読み戻しました。
136
+ どの手の後でも、ローンの状態は仕様と同じでした。
137
+
138
+ 次に、あるコミットが新しいローンを下書きにします。
139
+
140
+ ```diff
141
+ --- a/app/models/loan.rb
142
+ +++ b/app/models/loan.rb
143
+ def mark_pending
144
+ - self.state ||= "pending"
145
+ + self.state ||= "draft"
146
+ end
147
+ ```
148
+
149
+ ```sh
150
+ QUINT=quint bundle exec rspec spec/quint/loan_state_spec.rb
151
+ ```
152
+
153
+ ```text
154
+ F
155
+
156
+ Failures:
157
+
158
+ 1) Loan moves its state the way loan_state.qnt says
159
+ Failure/Error: rolled_back { QuintDriver::Trace.replay(trace, LoanStateDriver.new) }
160
+
161
+ QuintDriver::Mismatch:
162
+ quint: after step 0 `init` {}, the driver differs from the trace
163
+ state: trace "pending", driver "draft"
164
+ steps:
165
+ init {}
166
+ trace: trace_0.itf.json of Quint.simulate("spec/quint/loan_state.qnt", traces: 20, max_steps: 5, seed: "1", step: "step", backend: "rust")
167
+ # ./spec/quint/loan_state_spec.rb:18:in 'block (4 levels) in <top (required)>'
168
+ # ./spec/quint/loan_state_spec.rb:11:in 'block in RSpec::ExampleGroups::Loan#rolled_back'
169
+ # ./spec/quint/loan_state_spec.rb:10:in 'RSpec::ExampleGroups::Loan#rolled_back'
170
+ # ./spec/quint/loan_state_spec.rb:18:in 'block (3 levels) in <top (required)>'
171
+ # ./spec/quint/loan_state_spec.rb:17:in 'block (2 levels) in <top (required)>'
172
+
173
+ Finished in 0.61377 seconds (files took 0.90689 seconds to load)
174
+ 1 example, 1 failure
175
+
176
+ Failed examples:
177
+
178
+ rspec ./spec/quint/loan_state_spec.rb:16 # Loan moves its state the way loan_state.qnt says
179
+ ```
180
+
181
+ 最初の行は、モデルと仕様が食い違った手を示します。
182
+ `init` の直後に、仕様のローンは保留中で、モデルのローンは下書きです。
183
+ `steps:` は `init` からのすべての手を並べるので、同じコマンドを手で動かせます。
184
+ `trace:` は、同じトレースをもう一度書く呼び出しを示します。
185
+ 変更がバグなら、モデルを直します。
186
+ 変更が意図したものなら、同じコミットで仕様の `init` を変えて、仕様がアプリの動きを言い続けるようにします。
187
+
188
+ 次は、担当者や金額のような選択をアクションに持たせ、各呼び出しの結果のような 2 つ目の変数を比べます。
189
+ [もっと大きな仕様](#もっと大きな仕様拒否と選択)が両方を行います。
190
+
191
+ ## もっと大きな仕様:拒否と選択
192
+
193
+ クイックスタートの仕様は、1 つのレコードと 1 つの変数を持ちます。
194
+ 同じアプリのこの仕様は、1 人の借り手の 2 つのローンを持ち、各アクションの金額と担当者を選び、各呼び出しの結果も比べます。
195
+ ドライバーは仕様の各ローンが得た id を覚え、行を仕様の変数として読み戻します。
196
+ `Loan#approve(officer)` は、承認済みのローン、借り手の上限を超えるローン、そして担当者がシニアでないときの 10000 セントを超えるローンを断ります。
197
+
198
+ ```ruby
199
+ # spec/quint/approval_driver.rb
200
+ class ApprovalDriver
201
+ LOANS = %w[l1 l2].freeze
202
+
203
+ def step(action, picks)
204
+ @outcome = "Done"
205
+ case action.delete_suffix("_refused")
206
+ when "init"
207
+ @borrower = Borrower.create!(name: "Ada", limit_cents: 20_000)
208
+ @ids = {}
209
+ when "open"
210
+ @ids[picks.fetch("l")] = LoanDesk.open(@borrower, picks.fetch("a")).id
211
+ when "approve"
212
+ officer = {name: "Grace", senior: picks.fetch("senior")}
213
+ result = Loan.find(@ids.fetch(picks.fetch("l"))).approve(officer)
214
+ @outcome = ["Refused", result.to_s] unless result == :approved
215
+ end
216
+ end
217
+
218
+ def state
219
+ loans = LOANS.to_h { [_1, @ids[_1] && Loan.find(@ids[_1])] }
220
+ {
221
+ "amount" => loans.transform_values { _1&.amount_cents || 0 },
222
+ "approved" => loans.transform_values { _1&.state == "approved" },
223
+ "outcome" => @outcome
224
+ }
225
+ end
226
+ end
227
+ ```
228
+
229
+ トランザクションのフィクスチャは例の後に 1 回だけロールバックしますが、1 つの例は多くのトレースを再生します。
230
+ 各トレースを専用のセーブポイントで包み、どのトレースもほかのトレースの行を読まないようにします。
231
+
232
+ ```ruby
233
+ # spec/quint/approval_spec.rb
234
+ require "rails_helper"
235
+ require "quint_driver"
236
+ require_relative "approval_driver"
237
+
238
+ RSpec.describe Loan, type: :model do
239
+ def rolled_back
240
+ ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
241
+ yield
242
+ raise ActiveRecord::Rollback
243
+ end
244
+ end
245
+
246
+ it "follows every trace Quint writes for approval.qnt" do
247
+ QuintDriver::Quint.simulate("spec/quint/approval.qnt", traces: 50, max_steps: 8, seed: "1") do |path|
248
+ rolled_back { QuintDriver::Trace.replay(path, ApprovalDriver.new) }
249
+ end
250
+ end
251
+ end
252
+ ```
253
+
254
+ <details>
255
+ <summary>spec/quint/approval.qnt</summary>
256
+
257
+ ```quint
258
+ // Two loans of one borrower with a limit of 20000 cents. A clerk approves
259
+ // up to 10000 cents, and a senior officer approves any amount. approve has
260
+ // a refused twin, so a trace checks the refusals too.
261
+ module approval {
262
+ type Outcome = Done | Refused(str)
263
+
264
+ pure val LOANS = Set("l1", "l2")
265
+ pure val AMOUNTS = Set(500, 10000, 15000)
266
+ pure val LIMIT = 20000
267
+
268
+ // 0 means the loan is not open yet.
269
+ var amount: str -> int
270
+ var approved: str -> bool
271
+ var outcome: Outcome
272
+
273
+ pure def approvedCents(a: str -> int, ap: str -> bool): int =
274
+ LOANS.filter(l => ap.get(l)).fold(0, (sum, l) => sum + a.get(l))
275
+ pure def refusal(a: str -> int, ap: str -> bool, l: str, senior: bool): str =
276
+ if (ap.get(l)) "already"
277
+ else if (approvedCents(a, ap) + a.get(l) > LIMIT) "over_limit"
278
+ else if (a.get(l) > 10000 and not(senior)) "needs_senior"
279
+ else ""
280
+
281
+ action init = all {
282
+ amount' = LOANS.mapBy(_ => 0),
283
+ approved' = LOANS.mapBy(_ => false),
284
+ outcome' = Done,
285
+ }
286
+
287
+ action open(l: str, a: int): bool = all {
288
+ amount.get(l) == 0,
289
+ amount' = amount.put(l, a),
290
+ approved' = approved,
291
+ outcome' = Done,
292
+ }
293
+ action approve(l: str, senior: bool): bool = all {
294
+ amount.get(l) > 0,
295
+ refusal(amount, approved, l, senior) == "",
296
+ amount' = amount,
297
+ approved' = approved.put(l, true),
298
+ outcome' = Done,
299
+ }
300
+ action approve_refused(l: str, senior: bool): bool = all {
301
+ amount.get(l) > 0,
302
+ refusal(amount, approved, l, senior) != "",
303
+ amount' = amount,
304
+ approved' = approved,
305
+ outcome' = Refused(refusal(amount, approved, l, senior)),
306
+ }
307
+
308
+ action step = any {
309
+ nondet l = LOANS.oneOf()
310
+ nondet a = AMOUNTS.oneOf()
311
+ any { open(l, a) },
312
+ nondet l = LOANS.oneOf()
313
+ nondet senior = Set(true, false).oneOf()
314
+ any { approve(l, senior), approve_refused(l, senior) },
315
+ }
316
+
317
+ val within_limit = approvedCents(amount, approved) <= LIMIT
318
+ }
319
+ ```
320
+
321
+ </details>
322
+
323
+ ```sh
324
+ QUINT=quint bundle exec rspec spec/quint/approval_spec.rb
325
+ ```
326
+
327
+ ```text
328
+ .
329
+
330
+ Finished in 1.22 seconds (files took 0.91182 seconds to load)
331
+ 1 example, 0 failures
332
+ ```
333
+
334
+ 50 本の各トレースが、本物のモデルとテストのデータベースで動き、ロールバックしました。
335
+ `Loan#approve` がちょうど 10000 セントでシニアを求める(`> 10_000` の代わりに `amount_cents >= 10_000`)と、同じコマンドは、そのようなローンを承認する最初のトレースで失敗します。
336
+
337
+ ```text
338
+ QuintDriver::Mismatch:
339
+ quint: after step 6 `approve` {"l" => "l1", "senior" => false}, the driver differs from the trace
340
+ approved: trace {"l1" => true, "l2" => true}, driver {"l1" => false, "l2" => true}
341
+ outcome: trace "Done", driver ["Refused", "needs_senior"]
342
+ steps:
343
+ init {}
344
+ open {"a" => 500, "l" => "l2"}
345
+ approve {"l" => "l2", "senior" => false}
346
+ open {"a" => 10000, "l" => "l1"}
347
+ approve_refused {"l" => "l2", "senior" => false}
348
+ approve_refused {"l" => "l2", "senior" => true}
349
+ approve {"l" => "l1", "senior" => false}
350
+ trace: trace_1.itf.json of Quint.simulate("spec/quint/approval.qnt", traces: 50, max_steps: 8, seed: "1", step: "step", backend: "rust")
351
+ ```
352
+
353
+ セーブポイントの代わりにドライバーの `init` で行を消す方法と、セーブポイントを joinable にしてはいけない理由は、[スキル](skills/quint_driver/SKILL.md)にあります。
354
+
355
+ ## アプリなしで試す
356
+
357
+ ### インストール
358
+
359
+ ```sh
360
+ gem install quint_driver rspec
361
+ npm install --global @informalsystems/quint@0.33.0
362
+ export QUINT=quint
363
+ ```
364
+
365
+ ここでは Gemfile なしで gem をインストールします。
366
+ `QUINT` の働きは [gem を追加する](#gem-を追加する)にあります。
367
+
368
+ ### 仕様とクラスとテストを書く
369
+
370
+ 空のフォルダーで次を実行します。
371
+ 3 つのファイルを書きます。
372
+ チケットを 3 枚まで売るチケット売り場の仕様、それを実装するクラス、そして両者をつなぐドライバーを含む RSpec のファイルです。
373
+
374
+ ```sh
375
+ cat > tickets.qnt <<'QNT'
376
+ // A box office that sells at most 3 tickets.
377
+ module tickets {
378
+ var sold: int
379
+ var outcome: str
380
+
381
+ action init = all { sold' = 0, outcome' = "ok" }
382
+ action sell = all { sold < 3, sold' = sold + 1, outcome' = "ok" }
383
+ action sell_refused = all { sold >= 3, sold' = sold, outcome' = "sold_out" }
384
+ action refund = all { sold > 0, sold' = sold - 1, outcome' = "ok" }
385
+
386
+ action step = any { sell, sell_refused, refund }
387
+ }
388
+ QNT
389
+
390
+ cat > box_office.rb <<'RUBY'
391
+ class BoxOffice
392
+ LIMIT = 3
393
+
394
+ attr_reader :sold
395
+
396
+ def initialize = @sold = 0
397
+
398
+ def sell
399
+ return :sold_out if @sold >= LIMIT
400
+
401
+ @sold += 1
402
+ :ok
403
+ end
404
+
405
+ def refund
406
+ @sold -= 1
407
+ :ok
408
+ end
409
+ end
410
+ RUBY
411
+
412
+ cat > box_office_spec.rb <<'RUBY'
413
+ require "quint_driver"
414
+ require_relative "box_office"
415
+
416
+ # Runs each action of tickets.qnt on a BoxOffice and reads it back as the spec's variables.
417
+ class BoxOfficeDriver
418
+ def step(action, _picks)
419
+ @office = BoxOffice.new if action == "init"
420
+ @outcome =
421
+ case action
422
+ when "init" then "ok"
423
+ when "sell", "sell_refused" then @office.sell.to_s
424
+ when "refund" then @office.refund.to_s
425
+ end
426
+ end
427
+
428
+ def state = { "sold" => @office.sold, "outcome" => @outcome }
429
+ end
430
+
431
+ RSpec.describe BoxOffice do
432
+ it "sells tickets the way tickets.qnt says" do
433
+ QuintDriver::Quint.simulate("tickets.qnt", traces: 20, max_steps: 10, seed: "1") do |trace|
434
+ QuintDriver::Trace.replay(trace, BoxOfficeDriver.new)
435
+ end
436
+ end
437
+ end
438
+ RUBY
439
+ ```
440
+
441
+ ### 動かす
442
+
443
+ ```sh
444
+ rspec box_office_spec.rb
445
+ ```
446
+
447
+ ```text
448
+ .
449
+
450
+ Finished in 0.47699 seconds (files took 0.08795 seconds to load)
451
+ 1 example, 0 failures
452
+ ```
453
+
454
+ Quint が最大 10 手のトレースを 20 本書き、ドライバーがその各手を新しい `BoxOffice` で動かしました。
455
+ どの手の後でも、クラスの `sold` と `outcome` は仕様と同じでした。
456
+
457
+ ### 壊す
458
+
459
+ チケット売り場が 1 枚多く売るようにします。
460
+
461
+ ```sh
462
+ ruby -pi -e 'sub("@sold >= LIMIT", "@sold > LIMIT")' box_office.rb
463
+ rspec box_office_spec.rb
464
+ ```
465
+
466
+ ```text
467
+ F
468
+
469
+ Failures:
470
+
471
+ 1) BoxOffice sells tickets the way tickets.qnt says
472
+ Failure/Error: QuintDriver::Trace.replay(trace, BoxOfficeDriver.new)
473
+
474
+ QuintDriver::Mismatch:
475
+ quint: after step 8 `sell_refused` {}, the driver differs from the trace
476
+ sold: trace 3, driver 4
477
+ outcome: trace "sold_out", driver "ok"
478
+ steps:
479
+ init {}
480
+ sell {}
481
+ refund {}
482
+ sell {}
483
+ sell {}
484
+ refund {}
485
+ sell {}
486
+ sell {}
487
+ sell_refused {}
488
+ trace: trace_2.itf.json of Quint.simulate("tickets.qnt", traces: 20, max_steps: 10, seed: "1", step: "step", backend: "rust")
489
+ # ./box_office_spec.rb:22:in 'block (3 levels) in <top (required)>'
490
+ # ./box_office_spec.rb:21:in 'block (2 levels) in <top (required)>'
491
+
492
+ Finished in 0.5298 seconds (files took 0.08509 seconds to load)
493
+ 1 example, 1 failure
494
+
495
+ Failed examples:
496
+
497
+ rspec ./box_office_spec.rb:20 # BoxOffice sells tickets the way tickets.qnt says
498
+ ```
499
+
500
+ 最初の行は、クラスと仕様が食い違った手を示します。
501
+ 8 手目で、3 枚売れた状態から、仕様は販売を断り、クラスは 4 枚目を売りました。
502
+ `steps:` は `init` からのすべての手を並べるので、同じコマンドを手で動かせます。
503
+ `trace:` は、同じトレースをもう一度書く呼び出しを示します。
504
+ クラスを直して、もう一度 `rspec` を実行します。
505
+
506
+ 次は、自分のクラスの[ドライバーを書く](#ドライバーを書く)か、[Hegel](#動かす-1) に手順を選ばせて、失敗を最短の手順まで縮めます。
507
+
508
+ ## 仕組み
509
+
510
+ 手順の選び方は 2 つあり、どちらも同じドライバーを使います。
511
+
512
+ - **トレース再生。** `quint run --mbt` がトレースを書き、各トレースを 1 手ずつシステムで動かします。手順は Quint が選ぶので、深い状態にすぐ届きます。クイックスタートはこの方法を使います。
513
+ - **Hegel。** [Hegel](https://github.com/meganemura/hegel-ruby) が、仕様がいま許すアクションから 1 手ずつ選びます。システムが仕様と食い違うと、Hegel は失敗を短いコマンド列まで縮めます。
514
+
515
+ 仕様は Quint の JSON IR から読み、Ruby のプロセスの中で評価します。
516
+ そのため Hegel の 1 手で別のプロセスは起動しません。
517
+
518
+ ## インストール
519
+
520
+ ```ruby
521
+ # Gemfile
522
+ gem "quint_driver"
523
+ gem "hegeltest", "= 0.5.1" # Hegel アダプターを使うときだけ
524
+ ```
525
+
526
+ トレース再生とモデルは `require "quint_driver"` で読み込みます。
527
+ 必要なのは Ruby 3.3 以降だけで、ほかの gem は要りません。
528
+
529
+ Hegel アダプターは `require "quint_driver/hegel"` で読み込みます。
530
+ こちらには hegeltest 0.5.1 が必要で、hegeltest は Ruby 3.4 以降を必要とします。
531
+ この gem は hegeltest に依存しないので、自分の Gemfile に追加してください。
532
+
533
+ この gem は、[クイックスタート](#gem-を追加する)のとおり、`QUINT` が指す `quint` コマンドを呼びます。
534
+ `QUINT` の指定がなければ `npx --yes @informalsystems/quint@0.33.0` を実行します。
535
+ オフラインなどで npx が quint を取得できないときは、そのことを伝えて `QUINT` の指定を促す `QuintDriver::QuintFailed` を投げます。
536
+
537
+ `Spec.new` は、1 つのプロセスの中で、ファイルと quint コマンドの組ごとに 1 回だけコンパイルします。
538
+ ファイルの中身か、ファイルが import するファイルの中身が変わると、もう一度コンパイルします。
539
+ ファイルのどのモジュールもこのコンパイルを使うので、同じファイルの別の `main:` で `Spec.new` や `Quint.simulate` を呼んでも node は動きません。
540
+ 同じファイルを複数のスレッドが同時に求めても、コンパイルは 1 回で、ほかのスレッドはそれを待ちます。
541
+ そのため、並列のテストに自前のメモは要りません。
542
+
543
+ `Quint.simulate` は、Quint の Rust 評価器が見つかれば、node を通さずに直接実行します。
544
+ 探すのは、quint が `QUINT_HOME/rust-evaluator-v0.7.0/` にダウンロードしたものです(`QUINT_HOME` の指定がなければ `~/.quint`)。
545
+ 別の評価器を使うときは `QUINT_EVALUATOR` にそのパスを指定します。
546
+ 空の文字列を指定すると、代わりに `quint run` を実行します。
547
+ どちらの方法でも、同じシードからは同じトレースができます。
548
+ この gem が読めるのは、quint 0.33.0 が使う評価器 v0.7.0 の入出力の形だけです。
549
+ 別の形で答える評価器では `QuintDriver::QuintFailed` を投げます。
550
+ 評価器が見つからないとき、または `step:` がモジュールのアクションを指していないときは、`quint run` を実行します。
551
+
552
+ ### エージェント向けのスキル
553
+
554
+ [quint_driver スキル](skills/quint_driver/SKILL.md)は、仕様の形、ドライバー、トレース再生、モデルの確認、Hegel の順にコーディングエージェントを案内します。
555
+ [`skills`](https://github.com/vercel-labs/skills) CLI(Node.js 22.20 以降)でインストールします。
556
+ このリポジトリは非公開なので、次のコマンドには GitHub へのアクセスが必要です。
557
+
558
+ ```sh
559
+ npx skills add meganemura/quint_driver
560
+ ```
561
+
562
+ ```sh
563
+ npx skills add meganemura/quint_driver --skill quint_driver
564
+ ```
565
+
566
+ ```sh
567
+ npx skills add meganemura/quint_driver -g
568
+ ```
569
+
570
+ ```sh
571
+ npx skills add meganemura/quint_driver -a claude-code
572
+ ```
573
+
574
+ `--skill` はこのスキルを選び、`-g` はすべてのプロジェクトに入れ、`-a` は 1 つのエージェントを対象にします。
575
+ `-y` を付けると確認を省きます。
576
+
577
+ gem にはスキルと `examples/loans` の仕様が入っています。
578
+ `bundle install` のあとは、インストールされたコピーをコピーやシンボリックリンクで入れることもできます。
579
+ この方法では GitHub へのアクセスは要りません。
580
+
581
+ ```sh
582
+ mkdir -p .agents/skills
583
+ ln -s "$(bundle info --path quint_driver)/skills/quint_driver" .agents/skills/quint_driver
584
+ ```
585
+
586
+ `.claude/skills/` と `.cursor/skills/` でも同じです。
587
+
588
+ ## ドライバーを書く
589
+
590
+ ドライバーは、仕様のアクションをシステムで動かし、システムの状態を仕様の言葉で読み戻します。
591
+
592
+ ```ruby
593
+ class LoansDriver
594
+ # 仕様のアクションを 1 つ動かします。どの手順も最初は "init" です。
595
+ # +picks+ は nondet の値で、たとえば { "m" => "m1", "b" => "b2" } です。
596
+ def step(action, picks)
597
+ @outcome = "Done"
598
+ case action.delete_suffix("_refused")
599
+ when "init" then @loans = Loans.new
600
+ when "borrow" then @loans.borrow(picks.fetch("m"), picks.fetch("b"))
601
+ when "give_back" then @loans.give_back(picks.fetch("b"))
602
+ end
603
+ rescue Loans::Refused => refusal
604
+ @outcome = [ "Refused", refusal.reason.to_s ]
605
+ end
606
+
607
+ # 仕様の変数をすべて素の Ruby の値で返します。返さない変数があると、そのステップは失敗します。
608
+ def state
609
+ { "holder" => %w[b1 b2 b3].to_h { [ _1, @loans.holder(_1) || "" ] }, "outcome" => @outcome }
610
+ end
611
+ end
612
+ ```
613
+
614
+ 素の値の対応は次のとおりです。
615
+ Quint の集合は `Set`、マップは `Hash`、タプルとリストは `Array`、レコードは文字列のキーを持つ `Hash` です。
616
+ 直和型はタグになり、値を持つときは `[タグ, 値]` になります。
617
+ `None` と `Some` の場合を持つ直和型、つまり Option は、`None` なら `nil`、`Some` ならその値そのものです。
618
+ 再生でもマシンでも同じです。
619
+
620
+ `state` が返さない変数があると、そのステップは失敗し、メッセージがその変数を名指しします。
621
+ 例えば `count: trace 0, but the driver's state leaves count out` です。
622
+ そのため、返し忘れた変数が通ってしまうことはありません。
623
+ 仕様は、リモートのサービスが返す答えのようなコードの外の世界を、変数で表すことがあります。
624
+ ドライバーがスタブを用意するために読むだけの変数は、`Trace.replay` か `QuintDriver.machine` の `ignore:` に名前を渡すと、比べなくなります。
625
+
626
+ 仕様、システム、ドライバーをそろえた例は [examples/loans](examples/loans) にあります。
627
+
628
+ ## 動かす
629
+
630
+ ```ruby
631
+ require "quint_driver"
632
+
633
+ spec = QuintDriver::Spec.new("loans.qnt", main: "loans")
634
+
635
+ # トレース再生: Quint が手順を選びます。
636
+ QuintDriver::Quint.simulate("loans.qnt", traces: 50, max_steps: 20, seed: "1") do |path|
637
+ QuintDriver::Trace.replay(path, LoansDriver.new)
638
+ end
639
+
640
+ # Hegel が手順を選び、失敗を縮めます。
641
+ require "quint_driver/hegel"
642
+ machine = QuintDriver.machine(spec, driver: -> { LoansDriver.new }, invariants: %i[one_book_each])
643
+ Hegel.test(test_cases: 100) { |tc| Hegel::Stateful.run(machine.new, tc) }
644
+ ```
645
+
646
+ どちらも、食い違った変数と、`init` からドライバーが動かした手順を並べて `QuintDriver::Mismatch` を投げます。
647
+ この手順を順に動かせば、失敗をもう一度起こせます。
648
+ どの値も、集合の要素とハッシュのキーを並べ替えて表示します。
649
+ そのため、仕様の側とドライバーの側は、レコードを同じキーの順で表示します。
650
+ メッセージは `trace:` の行で終わります。
651
+ `Quint.simulate` から戻ったあとは、再生したファイルはもう消えています。
652
+ そのため、この行はトレースをファイル名と、同じトレースをもう一度書く呼び出しで示します。
653
+ 例えば `trace: trace_0.itf.json of Quint.simulate("loans.qnt", traces: 50, max_steps: 20, seed: "1", step: "step", backend: "rust")` です。
654
+ ブロックの中で捕まえても、メッセージは同じです。
655
+ 同じ事柄を `trace`、`seed`、`written_by` としても持っています。
656
+ ブロックの中で自分のコードが投げた `Mismatch` も、ブロックを出るときに同じ `trace:` の行を得ます。
657
+ 例えば、再生のあとに自分で確かめた結果です。
658
+ 投げるときはメッセージだけを渡します(`raise QuintDriver::Mismatch, "..."`)。
659
+ 実際に走ったトレースは gem が示すので、`Mismatch.new` はトレース、シード、呼び出しを受け取りません。
660
+ 自分で用意したトレースファイルを再生したときは、そのパスを示します。
661
+ `Mismatch` のバックトレースからは quint_driver 自身のフレームを除き、マシンでは Hegel のフレームも除きます。
662
+ そのため RSpec でも Minitest でも、再生か `Hegel.test` を呼んだ行か、`Mismatch` を投げた行が最初に出ます。
663
+ Gemfile が gem をパスから読み込むときも同じです。
664
+ その手が選ばなかった選択値は `picks` にありません。
665
+ その手が仕様の `None` を選んだ選択値は、`nil` として `picks` にあります。
666
+ なくてもよい選択値は `picks["name"]` で読み、そのときは `nil` です。
667
+ 欠かせない選択値は `picks.fetch("name")` で読みます。
668
+ `Trace.replay` は、走らせたステップを `action`、`picks`、`expected` の状態とともに返すので、トレースがどこまで届いたかを数えられます。
669
+ 再生の様子を見るときは `Trace.replay` に `log: $stdout` を渡します。
670
+ ドライバーが動かす前に、`step 1 borrow {"b" => "b1", "m" => "m2"}` のように各ステップを書き出します。
671
+ Minitest のテストでも RSpec の example でも同じコードで動きます。
672
+ RSpec で自分の example から値を引くときは `Hegel::Syntax::Methods` を include してください。
673
+ Rails アプリでは、トランザクションのフィクスチャーは example ごとに一度だけロールバックします。
674
+ そのため、1 つの example のすべてのトレースと Hegel のすべてのテストケースが、テスト用データベースを共有します。
675
+ ドライバーの `init` で行を消すか、トレースとテストケースごとに `ActiveRecord::Base.transaction(requires_new: true, joinable: false) { ...; raise ActiveRecord::Rollback }` で包んでください。
676
+ スキルには両方の書き方と、セーブポイントを joinable にしてはいけない理由があります。
677
+ machine 自体には不要です。
678
+
679
+ トレースが持つのは仕様の変数で、定義は持ちません。
680
+ そのため、承認済みのローンを合計する `def approvedSum` のように仕様が導く値は、観測したときだけ比べます。
681
+ `Trace.replay` には `spec:` と一緒に `observe: %w[approvedSum]` を、`QuintDriver.machine` には `observe:` を渡します。
682
+ ドライバーの `state` では、`"approvedSum" => borrower.approved_total` のように同じ名前でその値を返します。
683
+ ドライバーは、各状態でその定義が持つ値に照らされます。
684
+ 値を返さないドライバーは失敗します。
685
+ 定義を評価するのはモデルです。
686
+ 再生でもモデルがトレースの状態を読んで評価するので、失敗は `approvedSum: model 0, driver 15000` のようにモデルを示します。
687
+ ドライバーが返した名前がどの状態にもないときも失敗します。
688
+ 例えば `dueCents: driver 0, but dueCents is not in the trace's state, which has amount, approvedBy, outcome` です。
689
+ その名前が仕様の定義なら、その行は `observe:` に名前を渡すよう示します。
690
+ パラメーターのない `val` や `def` でない名前を渡すと、モジュールにある名前を添えて `ArgumentError` を投げます。
691
+
692
+ ステップが投げたエラーは、`Mismatch` ではなく `QuintDriver::StepRaised` になります。
693
+ エラーはドライバー自身のコードからも、その下のシステムからも来ます。
694
+ ドライバーの例は、選んだ値の名前が `m` なのに `picks.fetch("member")` と書いたときの `KeyError` です。
695
+ このとき、仕様とシステムはまだ比べられていません。
696
+ メッセージは ``quint: at step 1 `borrow` {"b" => "b2", "m" => "m2"}, the step raised KeyError: key not found: "member"`` のようにステップとエラーを示します。
697
+ 続く `raised in:` の行は、`raised in: loans_driver.rb:6:in 'Hash#fetch'` のように、投げた場所にいちばん近いプロジェクトのフレームを示します。
698
+ パスは、実行を始めたディレクトリからの相対パスです。
699
+ gem、Ruby のライブラリ、quint_driver の中のフレームは飛ばすので、gem が投げたエラーでは、その gem を呼んだ自分の行を示します。
700
+ すべてのフレームを飛ばしたときは、その行が `raised in: no frame of the project; every frame is in a gem, in Ruby's library, or in quint_driver` と示します。
701
+ そのときはバックトレースを読んでください。
702
+ Hegel のもとでは、ステップの番号は Hegel のレポートがその手に付けた番号(`Step 2: take` など)で、再生と同じく `init` はステップ 0 です。
703
+ そのあとに `init` からの手順を並べます。
704
+ 再生では、最後に同じ `trace:` の行が付きます。
705
+ バックトレースはそのエラーのものから quint_driver 自身のフレームを除いたもので、`cause` はそのエラーです。
706
+
707
+ `Quint.simulate` は、ファイル名と同じ名前のモジュールを動かします。
708
+ 同じファイルの別のモジュールを動かすときは `main:` を渡します。
709
+ `Spec.new` に渡すのと同じ名前です。
710
+ quint が失敗したときや見つからないときは、`Spec.new` と `Quint.simulate` が quint の理由を添えて `QuintDriver::QuintFailed` を投げます。
711
+ そのバックトレースは gem を呼んだあなたの行から始まり、その前に quint_driver のフレームはありません。
712
+
713
+ ある状態に至る手順を試すときは、その状態で成り立つ定義の名前を `reach:` に渡します。
714
+ 例えば `val two_on_loan = BOOKS.filter(b => onLoan(holder, b)).size() == 2` なら `reach: "two_on_loan"` です。
715
+ 各トレースは、その定義が初めて成り立つ状態で終わり、`Quint.simulate` はそこに至ったトレースだけを渡します。
716
+ Quint は `not(two_on_loan)` を不変条件として確かめ、それが破れたところでトレースを止めます。
717
+ どのトレースも至らなかったときは、`quint: no trace of 20 reached two_on_loan within 1 steps; raise max_steps or traces` のように `QuintDriver::QuintFailed` を投げます。
718
+ `reach:` が受け取るのは述語、つまりパラメーターのない真偽値の `val` か `def` です。
719
+ `step:` が受け取るのは、パラメーターのないアクションです。
720
+ ほかの種類の名前を渡すと、quint を動かす前に `ArgumentError` を投げます。
721
+ 例えば `quint: reach names borrow, an action with parameters of loans; reach takes a predicate, a boolean val or def without parameters, such as nothing_on_loan, one_book_each, two_on_loan` です。
722
+ モジュールが宣言もインポートもしていない名前を渡すと、quint が動いたあとに同じ形で `ArgumentError` を投げます。
723
+ 例えば `quint: step names borow, which loans neither declares nor imports; step takes an action without parameters, such as step` です。
724
+ トレースも、モデルも、マシンも、quint の既定と同じく `init` という名前のアクションから始まります。
725
+ `init` のないモジュールは `ArgumentError` を投げます。
726
+ 例えば ``quint: begins neither declares nor imports an action named init; quint_driver starts every trace and every model at init, so declare `action init = ...` with what starts begins`` です。
727
+ quint_driver はほかの名前を受け取りません。
728
+ トレースは最初のステップをそのアクションの名前で呼び、ドライバーとモデルはそのステップを `init` として読むからです。
729
+ 代わりに、モジュールに `action init = begin` を足してください。
730
+ quint_driver が投げる `ArgumentError` のバックトレースは、それを呼んだあなたの行から始まり、その前に quint_driver のフレームはありません。
731
+
732
+ `Quint.test(path, "lendTest", seed: "1") { |trace| ... }` は、`run lendTest = init.then(borrow("m1", "b1")).then(give_back("b1"))` のような名前付きの run のトレースを書き、そのファイルを渡します。
733
+ run がファイルの別のモジュールにあるときは `main:` を渡します。
734
+ quint test は `mbt::actionTaken` を書かないので、仕様は各ステップが取ったアクションを自分で記録する必要があります。
735
+ [Choreo](https://github.com/informalsystems/choreo) の仕様がそうしています。
736
+ 記録には、タグがアクションを名指し、レコードが選択値を持つ直和型の変数を使います。
737
+ 例えば `taken: Borrow({ m: m, b: b })` です。
738
+ その変数へのパスを `Trace.replay` の `action:` に渡します。
739
+ ドライバーが答える状態が変数の中にあるときは、そのパスを `state:` に渡します。
740
+ 例えば Choreo の仕様の 1 つの変数の `system` フィールドです。
741
+ そのとき、ドライバーの `state` はそのレコードかマップを返します。
742
+ `test/recorded.qnt` はこの形の小さな仕様で、`test/trace_test.rb` の `RecordedDriver` がそのドライバーです。
743
+
744
+ ```ruby
745
+ QuintDriver::Quint.test("test/recorded.qnt", "lendTest", seed: "1") do |trace|
746
+ QuintDriver::Trace.replay(trace, RecordedDriver.new, state: %w[s books], action: %w[s taken])
747
+ end
748
+ ```
749
+
750
+ ドライバーは `step("Init", {})`、続いて `step("Borrow", { "m" => "m1", "b" => "b1" })` のように呼ばれます。
751
+ 失敗は、トレースを `trace: test_0.itf.json of Quint.test("test/recorded.qnt", "lendTest", seed: "1")` と示します。
752
+ quint の中で run そのものが失敗したときは、quint の報告を添えて `QuintDriver::QuintFailed` を投げます。
753
+ モジュールにない run の名前なら、`quint: no run is named lend in recorded; it has lendTest, unlentTest` のように、モジュールにある run を並べます。
754
+ アクションのないトレース、例えば `action:` なしで再生した quint test のトレースは、パスを求める `QuintDriver::Unsupported` を投げます。
755
+
756
+ `QuintDriver.machine` のオプションは次のとおりです。
757
+
758
+ - `step:` は手の一覧を持つアクションで、既定は `"step"` です。各手は名前のあるアクションを呼ぶ必要があります。引数は `nondet x = S.oneOf()` から取ります。選択肢と `any` を一つの `all` で囲んだ手(`all { nondet x = S.oneOf() any { add(x), sub(x) } }` など)は、`any` のアクションで手を進めます。モジュールにないアクションを `step:` に渡すと、モジュールのアクションを並べた `ArgumentError` を投げます(`quint: no action is named steps; the module has counting, init, step`)。
759
+ - `invariants:` は、毎手のあとに成り立つべき仕様の `val` の名前です。
760
+ - `choose:` は Hegel が手を引く方法です。`:rule`(既定)はアクションを選び、仕様が許さなければ捨てます。最短の手順まで縮みます。`:rotate` は許されないアクションの番を次の許されるアクションに回します。少ないテストケースで深い状態に届きますが、縮めた手順に余分な手が残ることがあります。
761
+ Hegel の失敗の報告は、引いた値を nondet の選択値の名前で示します。
762
+ `:rotate` では `rule` と `move` です。
763
+ 一つのテストケースで同じ名前を二度以上引くと、引いた順に番号が付きます(`m_1 = "m2"`、`m_2 = "m1"`)。
764
+ 一度しか引かない名前には番号が付きません(`m = "m2"`)。
765
+
766
+ 各テストケースは、machine が始まるとき、Hegel の最初のステップより前に、ドライバーの `init` で新しいシステムから始めます。
767
+ そのため `init` で失敗するドライバーは、Hegel のレポートにステップが 1 つもない形で失敗します。
768
+ 縮める処理は同じ手順を何度も求めるので、システムが一度一致した手順は再び動かしません。
769
+ これは、同じ手順にはシステムが毎回同じ答えを返すことを前提にしています。
770
+ 食い違った手順は、毎回システムで動かし直します。
771
+
772
+ Hegel の実行には Quint のシード値がありません。
773
+ Hegel のレポートは、`reproduce_failure: "AXicXYmhDQAADMJWu/3/70BgQJCmvdEWPwRiKlCBBwzbAGE="` のような値で終わります。
774
+ それを `Hegel.test` の `reproduce_failure:` に渡すと、その失敗したケースだけを同じメッセージでもう一度動かします。
775
+
776
+ ## Ruby のモデルを確かめる
777
+
778
+ Hegel のやり方は、仕様の Ruby での評価を信じています。
779
+ モデルもドライバーと同じ受け答えをするので、Quint 自身のトレースに照らして確かめられます。
780
+ 別のモジュールやファイルから定義やアクションを import するモジュールは、quint が動かすときと同じく、それを自分のものとして読みます。
781
+ import した定義は、モジュールのどこからも届かないものもすべて残ります。
782
+ モジュールに `step` という名前のアクションがなくても読めます。
783
+ そのモジュールには、コンパイルが 1 回増えます。
784
+
785
+ ```ruby
786
+ QuintDriver::Quint.simulate("loans.qnt", traces: 200, max_steps: 30, seed: "7") do |path|
787
+ QuintDriver::Trace.replay(path, spec.model)
788
+ end
789
+ ```
790
+
791
+ モデルが評価できるのは、コマンドごとにアクションを書く仕様が使う演算子です。
792
+ 集合 (`fold` と `i.to(j)` を含む)、マップ、レコード、タプル、リスト、`match` 付きの直和型、整数、真偽値です。
793
+ `Bool` は `false` と `true` の集合として評価します。
794
+ `Int` と `Nat` は無限なので、モデルはそこから選ぶ動きの候補を並べられず、`QuintDriver::Unsupported` を投げます。
795
+ 代わりに `0.to(9)` のような有限の集合から選んでください。
796
+ マップでは `Map`、`get`、`keys`、`put`、`mapBy`、`set`、`setBy`、`setToMap` を評価します。
797
+ リストは Ruby の Array になります。
798
+ リストでは `List`、`append`、`concat`、`head`、`tail`、`length`、`nth`、`indices`、`replaceAt`、`slice`、`range`、`select`、`foldl`、`foldr` を評価します。
799
+ Quint の組み込みのドキュメントに `foldr` はありません。
800
+ ただし Quint の二つの評価器はどちらも右から畳み込み、要素を先に渡します(`(x, acc) => ...`)。
801
+ モデルも同じようにします。
802
+ 整数では `+`、`-`、`*`、単項の `-`、比較を評価します。
803
+ `/` と `%` は両辺が負でないときだけ評価します。
804
+ Quint は負の値を割ったときの丸めを決めていないからです。
805
+ Quint の二つの評価器はゼロに向けて切り捨て(`-7 % 2` は `-1`)、Ruby は小さい方へ丸めます(`-7 % 2` は `1`)。
806
+ そのため、モデルは違う答えを出さずに、負の値を断ります。
807
+ 空のリストの `head`、末尾より先の `nth`、キーが重なる組の `setToMap` のように、Quint のドキュメントが未定義とする演算では、モデルは答えを選ばずに `QuintDriver::Unsupported` を投げます。
808
+ マップに無いキーでの `get`、`set`、`setBy` も、Quint の評価器がそこで止まるので、`QuintDriver::Unsupported` を投げます。
809
+ ただし `slice(l, l.length(), l.length())` は、Quint の二つの評価器と同じく `[]` です。
810
+ `powerset` など、それ以外の演算子は `QuintDriver::Unsupported` を投げるので、足りない仕様ははっきり失敗します。
811
+ アクションを名指ししない step の枝も同じです。
812
+ そのメッセージには、その枝を持つアクションの名前が入ります。
813
+ 例えば `step` が `counting` をその場で開くときは、`quint: counting assigns n' in an arm that names no action; name every action in step` です。
814
+ メッセージには、その演算子を書いたアクションか定義の名前が入ります。
815
+ 例えば `quint: no evaluation for operator powerset in subsets` です。
816
+ nondet の定義域にある演算子では、それを選ぶアクションを示します。
817
+ `step` がその場で開くアクションのこともあります。
818
+
819
+ モデルは `Spec#model` で作ります。
820
+ モデルは quint がコンパイルした IR の形を信用するので、`Model.new` は private です。
821
+ トレースは選択値を素の値で渡し、モデルはその定義域から同じ値に読める値を取るので、直和型の選択値も `match` で使えます。
822
+
823
+ `model.moves(rule)` は、`spec.rules("step")` のルールがモデルの今の状態で許す手を、`[アクション, 選択値, 次の状態]` の形で、決まった順に並べます。
824
+ `model.enter(次の状態)` でその手を取ります。
825
+ Hegel のマシンはこの方法で手を選びます。
826
+ `model.value(node, env)` は、IR のノードを一つ、または文字列が名指す定義を、今の状態で評価します。
827
+ 評価できない演算子のメッセージは、ルールの定義域や引数なら、それを書いたアクションを示します。
828
+ 仕様のどの定義にもないノードなら、メッセージはノードが `outside any definition` だと示します。
829
+ `rule.fixed` は、どの状態でも、先の選択値がどれでも同じ集合になる定義域の名前の集合です。
830
+ Quint の effect が変数を読まないと示し、先の選択値の名前も使わない定義域がそれにあたります。
831
+ `:rule` の Hegel のマシンは、そうした定義域をマシンごとに一度だけ評価します。
832
+
833
+ ## 開発
834
+
835
+ ```sh
836
+ bundle install
837
+ bundle exec bin/test # Minitest
838
+ bundle exec rspec # RSpec
839
+ ```
840
+
841
+ Hegel のテストは `test/hegel/` と `spec/hegel/` にあります。
842
+ Ruby 3.3 では hegeltest を除き、残りを動かします。
843
+
844
+ ```sh
845
+ BUNDLE_WITHOUT=hegel bundle install
846
+ BUNDLE_WITHOUT=hegel bundle exec bin/test test/*_test.rb
847
+ BUNDLE_WITHOUT=hegel bundle exec rspec --exclude-pattern "spec/hegel/**/*_spec.rb"
848
+ ```
849
+
850
+ テストは `quint` を実行します。
851
+ `QUINT` を指定すると `npx` を使いません。
852
+
853
+ `test/readme_test.rb` は「アプリなしで試す」を空のフォルダーで動かします。
854
+ Rails のクイックスタートと「実行結果」は、`QUINT_DRIVER_README_APP` がコピー元の Rails アプリを指すときだけ動かします。
855
+ そのアプリには、サンプルアプリと同じ rspec-rails、`Borrower`、`Loan` が要り、Gemfile に quint_driver があってはいけません。
856
+ テストはこのチェックアウトから作った gem を加え、ファイルを書き、コマンドを動かし、差分を当てて、各出力を README と比べます。
857
+
858
+ `test/skill_test.rb` は、スキルの例「A choice the code leaves to the database」を、`QUINT_DRIVER_SKILL_APP` がコピー元の Rails アプリを指すときだけ動かします。
859
+ そのアプリには、rspec-rails、`Batch` に属しブロック中は割り当てられない `Item`、`/session` と `/items/:id/release` のルートが要り、Gemfile に quint_driver があってはいけません。
860
+ テストはこのチェックアウトを加え、スキルのファイルを書き、トレースを再生し、ラップを外してから失敗をスキルと比べます。
861
+ アプリのデータベースを使うので、`DATABASE_URL` を指定すると PostgreSQL で動きます。
862
+
863
+ ## ライセンス
864
+
865
+ MIT