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 +4 -4
- data/CHANGELOG.md +96 -1
- data/README.md +1 -1
- data/lib/constable/cli.rb +4 -0
- data/lib/constable/config.rb +14 -0
- data/lib/constable/jail.rb +8 -1
- data/lib/constable/runner.rb +77 -11
- data/lib/constable/version.rb +1 -1
- data/lib/constable/worker_databases.rb +104 -3
- data/lib/constable.rb +1 -1
- data/lib/generators/constable/templates/case_helper.rb.tt +1 -0
- data/lib/generators/constable/templates/config.yml.tt +55 -23
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 807163563a2d149b1f1267c2dd873ebc19e9948664b8311f4185676ac9698753
|
|
4
|
+
data.tar.gz: d7c59c367bf69908c0909b2bda248f2d5d0c320a8238bbea628ac203134105a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
[](https://badge.fury.io/rb/constable-rails)
|
|
11
11
|
[](https://github.com/Ray-Hughes/constable/actions/workflows/ci.yml)
|
|
12
12
|
[](https://www.ruby-lang.org)
|
|
13
13
|
[](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)
|
data/lib/constable/config.rb
CHANGED
|
@@ -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"])
|
data/lib/constable/jail.rb
CHANGED
|
@@ -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
|
data/lib/constable/runner.rb
CHANGED
|
@@ -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
|
-
|
|
356
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"`worker_databases: off`
|
|
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
|
data/lib/constable/version.rb
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
109
|
-
#
|
|
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
|
|
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.
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
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
|
|
92
|
-
# either way -- this only changes what you watch on the way there.
|
|
93
|
-
#
|
|
94
|
-
# concise
|
|
95
|
-
#
|
|
96
|
-
# expanded
|
|
97
|
-
#
|
|
98
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|