constable-rails 1.4.0 → 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 +50 -1
- data/lib/constable/cli.rb +4 -0
- data/lib/constable/runner.rb +77 -11
- data/lib/constable/version.rb +1 -1
- data/lib/constable/worker_databases.rb +33 -3
- data/lib/generators/constable/templates/config.yml.tt +47 -23
- metadata +1 -1
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,54 @@ 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
|
+
|
|
8
56
|
## [1.4.0]
|
|
9
57
|
|
|
10
58
|
Two problems from the same 1,277-file suite: a docket nobody asked for, and 110 minutes.
|
|
@@ -664,7 +712,8 @@ Initial release.
|
|
|
664
712
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
665
713
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
666
714
|
|
|
667
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.
|
|
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
|
|
668
717
|
[1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
|
|
669
718
|
[1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
|
|
670
719
|
[1.3.2]: https://github.com/Ray-Hughes/constable/compare/v1.3.1...v1.3.2
|
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/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
|
|
@@ -172,8 +172,38 @@ module Constable
|
|
|
172
172
|
nil
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
# The :reuse half
|
|
176
|
-
#
|
|
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.
|
|
177
207
|
#
|
|
178
208
|
# "There" means present *and* populated: an empty database is not a prepared one, and
|
|
179
209
|
# connecting to it would hand the worker a suite with no tables. Deciding that per
|
|
@@ -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,7 +59,10 @@ 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
|
|
@@ -81,29 +89,45 @@ fail_on_warnings: false # CI: fail the build when the warning count doesn't tre
|
|
|
81
89
|
|
|
82
90
|
parallel_workers: auto # or an explicit integer
|
|
83
91
|
|
|
84
|
-
# How a parallel worker gets a database of its own.
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
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)
|
|
98
121
|
|
|
99
|
-
# How much the live stream says while the suite is running. The summary is
|
|
100
|
-
# either way -- this only changes what you watch on the way there.
|
|
101
|
-
#
|
|
102
|
-
# concise
|
|
103
|
-
#
|
|
104
|
-
# expanded
|
|
105
|
-
#
|
|
106
|
-
|
|
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)
|
|
107
131
|
|
|
108
132
|
# Fallback path-based tier inference, used only when a case doesn't inherit from
|
|
109
133
|
# a tiered base class. The base classes in test/case_helper.rb are the primary
|