constable-rails 1.3.3 → 1.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb4658a48f23de1dcb97417de5bab1deda4548a334e2a198835f93b7cac656c3
4
- data.tar.gz: c844e9f7ac1c8d797d60349b7b15759bbcbbb0f1b56c914d43e2511e17f75489
3
+ metadata.gz: 807163563a2d149b1f1267c2dd873ebc19e9948664b8311f4185676ac9698753
4
+ data.tar.gz: d7c59c367bf69908c0909b2bda248f2d5d0c320a8238bbea628ac203134105a5
5
5
  SHA512:
6
- metadata.gz: c3e78d5c2d2ab5314ee501795a92a1017fbfe0eb4cab6889c7ab6883d1ff898b02a595a693c3bb774c8e8e26caed2132c4f0c033723e7389735386f7b6c43668
7
- data.tar.gz: 9f60ad3894558087d8747a5308e39558ce03d87c1518608dff21c9ca341fc776f11ceea45b7251435f052654ead1bbf348c489086b5857cea8e1b0a845781ff8
6
+ metadata.gz: 16458c866e5975ed485a03793c52c4d918b1191f8760ee45fd814d824857c943f5a848d268888e2d29334850b360d6f1df56e4cc1f619ad347f79c94a2bec0ae
7
+ data.tar.gz: 0e7fc08cf6105a1fea100ebbda9dc3d316444982fe5ed467b09e45c27f8d36577711f8f175dc020773724f1973f2748b9a0fd76b0d791d24f48e1afb15a31ed7
data/CHANGELOG.md CHANGED
@@ -5,6 +5,99 @@ All notable changes to this project are documented here. This project adheres to
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.4.1]
9
+
10
+ ### A parallel run that ran nothing is no longer a pass
11
+
12
+ Pointing a real app at `worker_databases: reuse` produced this:
13
+
14
+ ```
15
+ CONSTABLE 0 tests · 0 cases · 7.9s
16
+ ✓ 0 passed ✗ 0 failed
17
+ ```
18
+
19
+ Nineteen files scheduled. None ran. Exit 0. The third time this shape has appeared in a
20
+ week, and the most dangerous instance of it.
21
+
22
+ Two causes. A forked worker only reported `Constable::Error`, so anything else killed it
23
+ silently — the parent saw a closed pipe, no results and no reason. Workers now report
24
+ whatever they die of, including the exit status when they die below Ruby (a signal, a
25
+ segfault, an OOM kill; forking an app that already holds native database connections can
26
+ do exactly that). And the fallback to a serial run was conditional on a worker having
27
+ managed to *explain* itself; it now triggers on the fact that matters — work was
28
+ scheduled and nothing came back. If the serial fallback also produces nothing, that
29
+ raises rather than being summarised as a clean zero.
30
+
31
+ Same command now: **84 tests run**, serially, with a warning saying why.
32
+
33
+ ### Workers no longer build databases; `constable prepare` does
34
+
35
+ `reuse` had each worker build its own missing databases after forking. On Postgres the
36
+ clone must disconnect everything attached to the template first — and the template is the
37
+ shared test database *every other worker* is cloning from at the same moment. Twelve
38
+ workers terminated each other's connections and died mid-run.
39
+
40
+ Preparation happens once, in the parent, through `constable prepare`. A worker that finds
41
+ nothing to connect to says so and names the command. `constable prepare` also boots the
42
+ app first, which it was not doing: it reported "this app has no ActiveRecord test
43
+ databases to prepare" on an app with three of them.
44
+
45
+ ### Configuration is documented where you read it
46
+
47
+ Every setting in `.constable/config.yml` now carries its explanation, and the ones with
48
+ fixed choices name and describe each value inline — `worker_databases`, `output`, the
49
+ storage adapter. Two tests enforce it: a setting cannot arrive without an explanation, and
50
+ one with fixed choices has to name them.
51
+
52
+ The blotter section now says outright that it is **not** your application's database.
53
+ Two people read it the other way, which is a naming problem, not a reading problem.
54
+
55
+
56
+ ## [1.4.0]
57
+
58
+ Two problems from the same 1,277-file suite: a docket nobody asked for, and 110 minutes.
59
+
60
+ ### A flaky test is no longer jailed by itself
61
+
62
+ **This is the important one, because jailed means skipped.** A first `constable test` on a
63
+ real suite put **29 tests on a docket the user had never asked for** — each one recorded
64
+ as *"passed, then failed with no code change"* — and every one of them was silently
65
+ skipped from then on. A suite with order-dependent tests, which is most large suites and
66
+ exactly what Constable is pitched at, trips that constantly.
67
+
68
+ Automatic flake-jailing is now off by default:
69
+
70
+ ```yaml
71
+ jail_flakes: false # was: always on
72
+ ```
73
+
74
+ `constable test --jail` still jails failures, because that is a thing you asked for.
75
+ Turn the automatic route back on when you want it. Nothing stops being *reported* — a
76
+ flaky test still fails, still shows up, still gets a warrant if warrants are on. It just
77
+ does not remove itself from the suite.
78
+
79
+ ### Per-worker databases without a loadable schema
80
+
81
+ The same suite ran **serially for 110 minutes on a 12-core machine**, because its schema
82
+ cannot be loaded from `schema.rb` (Postgres custom types), so `worker_databases: off` was
83
+ the only setting that worked.
84
+
85
+ Postgres can copy a whole database in one statement, and that needs no schema at all:
86
+
87
+ ```sql
88
+ CREATE DATABASE "caseflow_test_3" TEMPLATE "caseflow_test"
89
+ ```
90
+
91
+ `worker_databases: reuse` now clones from the test database you already have, dropping a
92
+ stale copy and disconnecting the template first. It falls back to loading the schema when
93
+ there is nothing to clone, and to Postgres only — anything unexpected takes the old path
94
+ rather than failing. It is also simply faster than replaying a large schema once per
95
+ worker, so it is worth having on any Postgres app.
96
+
97
+ For a suite that could only run serially, this is the difference between one core and all
98
+ of them.
99
+
100
+
8
101
  ## [1.3.3]
9
102
 
10
103
  **1.3.2 was tagged twice.** The console fix in it was rebuilt after the gem had already
@@ -619,7 +712,9 @@ Initial release.
619
712
  - Diff-based coverage gate — only lines changed in the current diff are held to the
620
713
  threshold. `constable beat` for the full picture, `--html` for a browsable report.
621
714
 
622
- [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...HEAD
715
+ [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.1...HEAD
716
+ [1.4.1]: https://github.com/Ray-Hughes/constable/compare/v1.4.0...v1.4.1
717
+ [1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
623
718
  [1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
624
719
  [1.3.2]: https://github.com/Ray-Hughes/constable/compare/v1.3.1...v1.3.2
625
720
  [1.3.1]: https://github.com/Ray-Hughes/constable/compare/v1.3.0...v1.3.1
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **A strict Rails testing framework where fast and non-flaky are structural, not disciplinary.**
9
9
 
10
- [![Gem Version](https://img.shields.io/gem/v/constable-rails?color=1f6feb&label=constable-rails)](https://rubygems.org/gems/constable-rails)
10
+ [![Gem Version](https://badge.fury.io/rb/constable-rails.svg)](https://badge.fury.io/rb/constable-rails)
11
11
  [![CI](https://github.com/Ray-Hughes/constable/actions/workflows/ci.yml/badge.svg)](https://github.com/Ray-Hughes/constable/actions/workflows/ci.yml)
12
12
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D)](https://www.ruby-lang.org)
13
13
  [![Rails](https://img.shields.io/badge/rails-%3E%3D%207.0-D30001)](https://rubyonrails.org)
data/lib/constable/cli.rb CHANGED
@@ -428,6 +428,10 @@ module Constable
428
428
  option :workers, type: :numeric, desc: "How many to prepare (default: the configured worker count)"
429
429
  def prepare
430
430
  config = load_config
431
+ # The app has to be up before we can ask ActiveRecord anything about it.
432
+ Runner.boot!
433
+ config.apply_overrides!(Constable.configuration.overrides)
434
+
431
435
  unless WorkerDatabases.shardable?
432
436
  CLI.complain("This app has no ActiveRecord test databases to prepare.")
433
437
  exit(EXIT_USAGE)
@@ -22,6 +22,7 @@ module Constable
22
22
  "output" => "concise",
23
23
  "parallel_workers" => "auto",
24
24
  "worker_databases" => "schema",
25
+ "jail_flakes" => false,
25
26
  "tiers" => {
26
27
  "unit" => "test/cases/models/**/*",
27
28
  "integration" => "test/cases/controllers/**/*",
@@ -76,6 +77,19 @@ module Constable
76
77
 
77
78
  def cold_cases = Array(@raw["cold_cases"])
78
79
  def warrants? = truthy(@raw["warrants"])
80
+
81
+ # Should a test that passed last run and failed this one be put on the docket by
82
+ # itself? Off by default, and the reason is what jailing *does*: a jailed test is
83
+ # skipped on every later run. Turning that on automatically means a suite quietly
84
+ # stops running tests nobody chose to stop running.
85
+ #
86
+ # Observed on a real suite: a first `constable test` put 29 tests on a docket the user
87
+ # had never asked for, and every one of them was skipped from then on. A suite with
88
+ # order-dependent tests -- which is most large suites, and exactly the kind Constable
89
+ # is pitched at -- trips this constantly.
90
+ #
91
+ # `constable test --jail` still jails failures, because that is a thing you asked for.
92
+ def jail_flakes? = truthy(@raw["jail_flakes"])
79
93
  # Negative retries are a typo for "off", not an instruction to count backwards.
80
94
  def warrant_retries = [@raw["warrant_retries"].to_i, 0].max
81
95
  def auto_relink? = truthy(@raw["auto_relink"])
@@ -123,6 +123,13 @@ module Constable
123
123
 
124
124
  # Consecutive clean runs that earn an automatic release. Floors at 1 -- a period of
125
125
  # zero would mean "release on sight", which is not parole.
126
+ # Opt-in. See Config#jail_flakes? for why the default is off: jailing skips the test on
127
+ # every later run, and doing that to a test nobody nominated is how a suite quietly
128
+ # stops testing things.
129
+ def jail_flakes?
130
+ @config.respond_to?(:jail_flakes?) ? @config.jail_flakes? : false
131
+ end
132
+
126
133
  def parole_period
127
134
  period = @config.respond_to?(:parole_period) ? @config.parole_period.to_i : 0
128
135
  period.positive? ? period : 10
@@ -290,7 +297,7 @@ module Constable
290
297
 
291
298
  if jail_mode
292
299
  jail_failure(result)
293
- elsif flake_flip?(result)
300
+ elsif flake_flip?(result) && jail_flakes?
294
301
  jail_for_flake(result)
295
302
  else
296
303
  result
@@ -64,6 +64,20 @@ module Constable
64
64
  def jail_run? = @jail_run
65
65
  def coverage? = @coverage_requested
66
66
 
67
+ # Boots the app the way a run does -- test/case_helper.rb, which requires
68
+ # config/environment -- without selecting or running anything.
69
+ #
70
+ # `constable prepare` needs this: it asks ActiveRecord what databases exist, and
71
+ # before the helper has run there is no ActiveRecord to ask. It reported "this app has
72
+ # no test databases to prepare" on an app with three of them.
73
+ def self.boot!
74
+ helper = %w[test/case_helper.rb spec/case_helper.rb]
75
+ .map { |p| File.join(Constable.root, p) }
76
+ .find { |p| File.exist?(p) }
77
+ require helper if helper
78
+ helper
79
+ end
80
+
67
81
  # Loads every case file and hands back the identities the suite actually defines,
68
82
  # without running anything. `constable prune` needs this: which tests still exist is
69
83
  # only knowable once the whole suite has been loaded.
@@ -352,8 +366,18 @@ module Constable
352
366
  exit!(0)
353
367
  end
354
368
 
355
- bucket.each do |item|
356
- run_item(item).each { |result| write_message(writer, :result, result.to_h) }
369
+ # Anything at all, not just Constable::Error. An uncaught exception in a forked
370
+ # child kills it silently: the parent sees a closed pipe, no results and no
371
+ # reason, and a run that scheduled nineteen files reports zero tests and exits
372
+ # 0. A worker that dies has to say so.
373
+ begin
374
+ bucket.each do |item|
375
+ run_item(item).each { |result| write_message(writer, :result, result.to_h) }
376
+ end
377
+ rescue Exception => e # rubocop:disable Lint/RescueException
378
+ write_message(writer, :worker_error, "#{e.class}: #{e.message}")
379
+ writer.close
380
+ exit!(0)
357
381
  end
358
382
 
359
383
  # A worker owns its own cold-case session, and it dies here. Fire the engine's
@@ -374,7 +398,7 @@ module Constable
374
398
  end
375
399
 
376
400
  collected = drain(readers)
377
- pids.each { |pid| Process.waitpid(pid) rescue nil } # rubocop:disable Style/RescueModifier
401
+ record_worker_exits(pids)
378
402
 
379
403
  # No worker could build itself a database, so no test ran. Not every app can be
380
404
  # sharded: an app whose schema.rb cannot rebuild the database on its own -- Postgres
@@ -385,7 +409,13 @@ module Constable
385
409
  #
386
410
  # Nothing has run yet, so falling back to a serial run costs a restart, not
387
411
  # correctness.
388
- return run_serially_after_worker_failure(items) if collected.empty? && worker_errors.any?
412
+ #
413
+ # The condition is deliberately "nothing came back", not "a worker said why". A
414
+ # child can die without managing to report -- and then a run that scheduled
415
+ # nineteen files says "0 tests, 0 failed" and exits 0, which is the worst thing a
416
+ # test runner can do. If work was scheduled and no result arrived, something is
417
+ # wrong whether or not anyone explained it.
418
+ return run_serially_after_worker_failure(items) if collected.empty? && !items.empty?
389
419
 
390
420
  # A warning raised inside a worker only ever reached that worker's memory, so the
391
421
  # results carry them home. Nothing that bends the rules is allowed to go missing
@@ -396,22 +426,58 @@ module Constable
396
426
 
397
427
  def worker_errors = (@worker_errors ||= [])
398
428
 
429
+ # A worker can die below Ruby: a segfault, an OOM kill, a signal. No `rescue` reaches
430
+ # that, so the only evidence is the exit status, and without it the run can only say
431
+ # "the workers exited without reporting anything" -- true, and useless.
432
+ #
433
+ # Forking a process that already holds database connections is where this comes from.
434
+ # An app with a native driver -- Oracle's OCI, for instance -- can have a child die
435
+ # the moment it touches an inherited handle.
436
+ def record_worker_exits(pids)
437
+ pids.each do |pid|
438
+ _, status = Process.waitpid2(pid)
439
+ next if status.nil? || status.success?
440
+
441
+ worker_errors << if status.signaled?
442
+ "a worker was killed by SIG#{Signal.signame(status.termsig)} " \
443
+ "-- forking an app that already holds native database " \
444
+ "connections can do this"
445
+ else
446
+ "a worker exited with status #{status.exitstatus}"
447
+ end
448
+ rescue StandardError
449
+ nil
450
+ end
451
+ end
452
+
399
453
  def run_serially_after_worker_failure(items)
400
454
  reason = worker_errors.first.to_s
455
+ reason = "the workers exited without reporting anything" if reason.empty?
401
456
 
402
457
  Constable.warn!(
403
- "no parallel worker could build its own test database, so the suite ran serially " \
404
- "instead. This usually means the app's schema cannot rebuild the database by " \
405
- "itself -- Postgres custom types, functions and triggers are the common reason, " \
406
- "and `rails test` parallelization fails the same way. Set " \
407
- "`worker_databases: reuse` to keep prepared databases between runs instead, or " \
408
- "`worker_databases: off` to stop trying. The first worker said: #{reason}",
458
+ "the parallel workers produced no results, so the suite ran serially instead -- " \
459
+ "everything ran, nothing was skipped. Two things cause this: the app's schema " \
460
+ "cannot rebuild a database by itself (Postgres custom types; try " \
461
+ "`worker_databases: reuse` with `constable prepare`), or forking is unsafe in " \
462
+ "this app, which happens when a native driver's connections are inherited by a " \
463
+ "child. `worker_databases: off` stops the attempt. The workers said: #{reason}",
409
464
  kind: :parallel
410
465
  )
411
466
 
412
467
  # The blotter handle was closed before forking, and the pool was cleared. Both come
413
468
  # back on their next use, so there is nothing to reopen by hand.
414
- run_serial(items)
469
+ results = run_serial(items)
470
+
471
+ # Belt and braces. If the serial fallback also produces nothing for work that was
472
+ # scheduled, the run is broken in a way no summary can honestly describe, and
473
+ # reporting a clean zero would be a lie.
474
+ if results.empty? && !items.empty?
475
+ raise Constable::Error,
476
+ "#{items.size} test file(s) were scheduled and none of them ran. " \
477
+ "The first worker said: #{reason}"
478
+ end
479
+
480
+ results
415
481
  end
416
482
 
417
483
  # Every message on the pipe is tagged, because results are not the only thing a worker
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Constable
4
- VERSION = "1.3.3"
4
+ VERSION = "1.4.1"
5
5
  end
@@ -58,7 +58,7 @@ module Constable
58
58
  # that works when the schema cannot rebuild the database by itself.
59
59
  def after_fork!(index, mode: :schema)
60
60
  return false unless shardable?
61
- return reuse!(index) if mode.to_sym == :reuse
61
+ return connect_worker!(index) if mode.to_sym == :reuse
62
62
 
63
63
  ::ActiveRecord::TestDatabases.create_and_load_schema(index, env_name: env_name)
64
64
  true
@@ -105,8 +105,105 @@ module Constable
105
105
  .zip(names).each { |config, name| config._database = name if name }
106
106
  end
107
107
 
108
- # The :reuse half. Points every database this environment declares at its `_<index>`
109
- # sibling, and only builds the ones that are not there yet.
108
+ # Postgres can copy a whole database in one statement:
109
+ #
110
+ # CREATE DATABASE "caseflow_test_3" TEMPLATE "caseflow_test"
111
+ #
112
+ # That matters because it needs no schema.rb at all. An app whose schema cannot
113
+ # rebuild the database by itself -- custom types, functions, triggers -- can still get
114
+ # per-worker databases this way, cloned from the test database it already has. It is
115
+ # also far faster than replaying a large schema once per worker.
116
+ #
117
+ # Returns true when it cloned, false when this is not Postgres or the source is not
118
+ # there, so the caller can fall back to loading the schema.
119
+ def clone_database(db_config, index)
120
+ return false unless postgres?(db_config)
121
+
122
+ source = db_config.database.to_s.sub(/_#{index}\z/, "")
123
+ target = db_config.database.to_s
124
+ return false if source.empty? || source == target
125
+
126
+ maintenance_connection(db_config) do |connection|
127
+ return false unless database_exists?(connection, source)
128
+
129
+ # A template cannot be copied while anything is connected to it.
130
+ disconnect_everyone_from!(connection, source)
131
+ connection.execute(%(DROP DATABASE IF EXISTS "#{target}"))
132
+ connection.execute(%(CREATE DATABASE "#{target}" TEMPLATE "#{source}"))
133
+ end
134
+
135
+ true
136
+ rescue StandardError
137
+ # Cloning is the fast path, never the only one. Anything unexpected -- a permission,
138
+ # a Postgres version, a connection that will not drop -- falls back to the schema.
139
+ false
140
+ end
141
+
142
+ def postgres?(db_config)
143
+ db_config.respond_to?(:adapter) && db_config.adapter.to_s.include?("postgre")
144
+ end
145
+
146
+ # Postgres will not let you create a database while connected to the one you are
147
+ # copying, so the statements run against the cluster's own maintenance database.
148
+ def maintenance_connection(db_config)
149
+ previous = ::ActiveRecord::Base.connection_db_config
150
+ ::ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(database: "postgres"))
151
+ yield ::ActiveRecord::Base.connection
152
+ ensure
153
+ ::ActiveRecord::Base.establish_connection(previous)
154
+ end
155
+
156
+ def database_exists?(connection, name)
157
+ # Plain Ruby, not #present?: this runs inside a forked worker in somebody else's app,
158
+ # and quietly depending on ActiveSupport being loaded is how a fast path silently
159
+ # turns itself off.
160
+ value = connection.select_value("SELECT 1 FROM pg_database WHERE datname = #{connection.quote(name)}")
161
+ !value.nil?
162
+ rescue StandardError
163
+ false
164
+ end
165
+
166
+ def disconnect_everyone_from!(connection, name)
167
+ connection.execute(
168
+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity " \
169
+ "WHERE datname = #{connection.quote(name)} AND pid <> pg_backend_pid()"
170
+ )
171
+ rescue StandardError
172
+ nil
173
+ end
174
+
175
+ # The :reuse half, inside a worker: connect to `<database>_<index>` and nothing else.
176
+ #
177
+ # Building is deliberately not done here. Twelve workers forked at once would each try
178
+ # to build the same missing databases simultaneously, and on Postgres the clone has to
179
+ # disconnect everything attached to the template first -- which is the shared test
180
+ # database every other worker is also cloning from. They terminate each other's
181
+ # connections and die, silently, mid-run. Observed exactly that: nineteen files
182
+ # scheduled, zero results, workers gone without a word.
183
+ #
184
+ # So preparation happens once, in the parent, through `constable prepare`. A worker
185
+ # that finds nothing to connect to says so, and the runner falls back to serial.
186
+ def connect_worker!(index)
187
+ missing = []
188
+
189
+ each_worker_config(index) do |db_config|
190
+ missing << db_config.database unless populated?(db_config)
191
+ end
192
+
193
+ unless missing.empty?
194
+ raise Constable::Error,
195
+ "worker #{index} has no database to use (#{missing.join(", ")}). " \
196
+ "`worker_databases: reuse` expects them to exist already -- run " \
197
+ "`constable prepare` once, then run the suite."
198
+ end
199
+
200
+ ::ActiveRecord::Base.establish_connection
201
+ []
202
+ end
203
+
204
+ # The building half, run from the parent by `constable prepare`. Points every database
205
+ # this environment declares at its `_<index>` sibling, and builds the ones that are
206
+ # not there yet.
110
207
  #
111
208
  # "There" means present *and* populated: an empty database is not a prepared one, and
112
209
  # connecting to it would hand the worker a suite with no tables. Deciding that per
@@ -121,6 +218,10 @@ module Constable
121
218
  each_worker_config(index) do |db_config|
122
219
  next if populated?(db_config)
123
220
 
221
+ # Clone first: it needs no schema.rb, which is the only thing that works for an
222
+ # app whose schema cannot rebuild the database, and it is faster besides.
223
+ next built << "#{db_config.database} (cloned)" if clone_database(db_config, index)
224
+
124
225
  ::ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
125
226
  built << db_config.database
126
227
  end
data/lib/constable.rb CHANGED
@@ -149,7 +149,7 @@ module Constable
149
149
  SETTINGS = %i[
150
150
  cold_cases warrants warrant_retries auto_relink parole_period
151
151
  coverage coverage_threshold coverage_html fail_on_warnings parallel_workers
152
- worker_databases output tiers
152
+ worker_databases jail_flakes output tiers
153
153
  ].freeze
154
154
 
155
155
  # `storage` is the one setting that cannot live here, and the reason is ordering, not
@@ -159,6 +159,7 @@ end
159
159
  # c.worker_databases = :schema # or :reuse (keep them between runs) / :off
160
160
  # c.output = :concise # or :expanded -- a line per test, with timings
161
161
  # c.fail_on_warnings = false # CI: fail when the warning count is not trending down
162
+ # c.jail_flakes = false # auto-jail a pass->fail flip (jailed = skipped)
162
163
  # c.warrants = false # rerun a failure in isolation before believing it
163
164
  # c.warrant_retries = 5
164
165
  # c.parole_period = 10 # clean runs before a paroled test releases itself
@@ -43,6 +43,11 @@ cold_cases: []
43
43
 
44
44
  # The blotter: flake history, the jail docket, warrants.
45
45
  #
46
+ # NOT your application's database, and nothing to do with it. Your tests keep using
47
+ # whatever config/database.yml says -- Postgres stays Postgres. This is a separate file
48
+ # holding Constable's own bookkeeping, and it never contains a row of your data. Leaving
49
+ # it on SQLite is right even for a Postgres app.
50
+ #
46
51
  # Constable owns this store outright. It is deliberately not the app's own
47
52
  # database, for two reasons. Native cases run inside a transaction that gets
48
53
  # rolled back, so writing "this test just failed" through that connection would
@@ -54,11 +59,22 @@ cold_cases: []
54
59
  # need one queryable store shared across many CI machines -- and always at a
55
60
  # separate database from the app's own, never its test connection.
56
61
  storage:
57
- adapter: sqlite # sqlite (default) | postgres | mysql
62
+ adapter: sqlite # sqlite (default, and right for almost everyone)
63
+ # postgres / mysql: only when many CI machines
64
+ # need to share ONE blotter. Always a separate
65
+ # database from your app's own.
58
66
  path: .constable/constable.sqlite3 # sqlite only
59
67
  url: # postgres/mysql only, e.g.
60
68
  # postgres://user:pass@host/constable_metadata
61
69
 
70
+ # Put a test on the docket by itself when it passed last run and failed this one, with no
71
+ # code change in between. Off by default, and the reason is what jailing *does*: a jailed
72
+ # test is skipped on every later run. Turning that on automatically means a suite quietly
73
+ # stops running tests nobody chose to stop running -- and a suite with order-dependent
74
+ # tests, which is most large ones, trips it constantly. `constable test --jail` still
75
+ # jails failures, because that is a thing you asked for.
76
+ jail_flakes: false
77
+
62
78
  warrants: false # opt-in flaky detector -- rerun a failure in isolation before believing it
63
79
  warrant_retries: 5 # reruns before declaring a warrant (flaky) or a genuine failure
64
80
  auto_relink: false # auto-confirm high-confidence rename detection instead of suggesting it
@@ -73,29 +89,45 @@ fail_on_warnings: false # CI: fail the build when the warning count doesn't tre
73
89
 
74
90
  parallel_workers: auto # or an explicit integer
75
91
 
76
- # How a parallel worker gets a database of its own. Sharing one is not a speed/safety
77
- # trade but a correctness bug: on SQLite the run dissolves into "database is locked", and
78
- # on a client/server database tests quietly see each other's rows.
79
- #
80
- # schema rebuild <database>_<index> from schema on every run. What Rails does for
81
- # `rails test`, and correct by construction -- no drift is possible.
82
- # reuse connect to <database>_<index> when it is already there, and build it from
83
- # schema only when it is not. Faster, because a large schema is not reloaded
84
- # every run -- and the only option that works at all when the schema cannot
85
- # rebuild the database by itself, which is true of any app with Postgres custom
86
- # types (`CREATE TYPE` has no schema.rb representation, so a from-scratch load
87
- # fails). Keeping those databases current becomes your job.
88
- # off do not shard, so do not fork. An explicit serial run: no attempt, no warning.
89
- worker_databases: schema # schema | reuse | off
92
+ # How a parallel worker gets a database of its own.
93
+ #
94
+ # Sharing one is not a speed/safety trade, it is a correctness bug: on SQLite the run
95
+ # dissolves into "database is locked", and on a client/server database tests quietly see
96
+ # each other's rows, which is worse because it looks like it worked.
97
+ #
98
+ # Three options:
99
+ #
100
+ # schema Rebuild <database>_<index> from schema.rb on every run.
101
+ # What Rails itself does for `rails test`. Correct by construction -- the
102
+ # databases cannot drift, because they are thrown away and rebuilt. The right
103
+ # default, and the wrong choice only when it is slow or impossible.
104
+ #
105
+ # reuse Keep <database>_<index> between runs, building it only when it is missing.
106
+ # Two reasons to want this:
107
+ # * Speed. A large schema is not replayed once per worker per run. On
108
+ # Postgres the databases are cloned in one statement
109
+ # (CREATE DATABASE ... TEMPLATE), which is far faster than a schema load.
110
+ # * It is the ONLY option that works when your schema cannot rebuild the
111
+ # database by itself -- true of any app with Postgres custom types, since
112
+ # CREATE TYPE has no schema.rb representation and a from-scratch load
113
+ # fails on a dump that references types it never defines.
114
+ # The cost: keeping those databases current is now your job. Run
115
+ # `constable prepare` after a migration.
116
+ #
117
+ # off Do not shard, so do not fork. An explicit serial run: no attempt to build
118
+ # anything, and no warning about it. Correct, and as slow as one core.
119
+ #
120
+ worker_databases: schema # schema (rebuild each run) | reuse (keep them) | off (serial)
90
121
 
91
- # How much the live stream says while the suite is running. The summary is identical
92
- # either way -- this only changes what you watch on the way there.
93
- #
94
- # concise one glyph per test, grouped into a run per case. A thousand tests stay
95
- # inside one screen, and a wall of green is the point.
96
- # expanded a line per test: glyph, name, duration. Slower to read in bulk, but you
97
- # can see which test is hanging while it hangs, rather than after.
98
- output: concise # concise | expanded
122
+ # How much the live stream says while the suite is running. The summary at the end is
123
+ # identical either way -- this only changes what you watch on the way there.
124
+ #
125
+ # concise One glyph per test, grouped into a run per case. A thousand tests stay
126
+ # inside one screen, and a wall of green is the point.
127
+ # expanded A line per test: glyph, description, duration. Slower to read in bulk, but
128
+ # you can see which test is hanging while it hangs, rather than afterwards.
129
+ #
130
+ output: concise # concise (one glyph per test) | expanded (a line per test)
99
131
 
100
132
  # Fallback path-based tier inference, used only when a case doesn't inherit from
101
133
  # a tiered base class. The base classes in test/case_helper.rb are the primary
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: constable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.3
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Hughes
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-08 00:00:00.000000000 Z
11
+ date: 2026-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport