constable-rails 1.4.0 → 1.4.2
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 +84 -1
- data/lib/constable/cli.rb +4 -0
- data/lib/constable/cold_case/rspec.rb +76 -4
- data/lib/constable/runner.rb +77 -11
- data/lib/constable/version.rb +1 -1
- data/lib/constable/worker_databases.rb +53 -4
- data/lib/generators/constable/templates/case_helper.rb.tt +66 -0
- 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: 04f058e6367c5b8a7739a80141601c3db8fc4f088ff1a040353cad17c435e4f8
|
|
4
|
+
data.tar.gz: 18ab049c33174d1028c94352c20612fb79c39ed6aee70bce6a462321a4916d32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f1e99fc5bbf329fc0bff1bfd9b35afb396ae1ac2f23a1db8f75be493507dce586ff604bf016749975e384f764637c79dfa53e44092f417f582fd63d6009e38f
|
|
7
|
+
data.tar.gz: 18ff4465d65894ee18dbe1e5d116ae1a4128798ed759d41dd894135dd0b30554ab95329301c2559079e35066ce8cfc489ec88fe40760ef6eeedcbfa7f547972c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,87 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.4.2]
|
|
9
|
+
|
|
10
|
+
### Cold cases run inside a parallel worker
|
|
11
|
+
|
|
12
|
+
The defect recorded as known in 1.4.1, now understood and fixed. Two bugs, stacked, and
|
|
13
|
+
the second one hid the first.
|
|
14
|
+
|
|
15
|
+
**RSpec swallows errors in required files.** `Configuration#requires=` routes through
|
|
16
|
+
`load_file_handling_errors`, which rescues anything raised while loading, reports it
|
|
17
|
+
through a formatter, and sets `world.wants_to_quit = true`. Every later
|
|
18
|
+
`ExampleGroup.run` then returns immediately. Constable points RSpec's streams at a
|
|
19
|
+
throwaway `StringIO` — stdout belongs to the reporter — so that report went nowhere: the
|
|
20
|
+
file loaded, the examples registered, and none of them ran. A forked worker produced no
|
|
21
|
+
results, no error and no exception, and exited 0. Requires are now loaded directly, where
|
|
22
|
+
an exception carries its own message and backtrace, and the quit flag is checked and
|
|
23
|
+
surfaced if anything sets it anyway.
|
|
24
|
+
|
|
25
|
+
**Not every database can be sharded.** With the error visible, the real cause was one
|
|
26
|
+
line: worker setup renamed *every* database config to `<database>_<index>`, including
|
|
27
|
+
connections that are not per-worker test data at all. Caseflow talks to a legacy Oracle
|
|
28
|
+
system (VACOLS) alongside its own Postgres databases, and appending `_3` to an Oracle TNS
|
|
29
|
+
service name produces:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
OCIError: ORA-12162: TNS:net service name is incorrectly specified
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
raised inside a `before(:suite)` hook — which is exactly what RSpec was swallowing. Only
|
|
36
|
+
adapters that per-worker copies make sense for are renamed now (postgresql, postgis,
|
|
37
|
+
mysql2, trilogy, sqlite3). An external system stays shared by every worker, which is what
|
|
38
|
+
it is for.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## [1.4.1]
|
|
42
|
+
|
|
43
|
+
### A parallel run that ran nothing is no longer a pass
|
|
44
|
+
|
|
45
|
+
Pointing a real app at `worker_databases: reuse` produced this:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
CONSTABLE 0 tests · 0 cases · 7.9s
|
|
49
|
+
✓ 0 passed ✗ 0 failed
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Nineteen files scheduled. None ran. Exit 0. The third time this shape has appeared in a
|
|
53
|
+
week, and the most dangerous instance of it.
|
|
54
|
+
|
|
55
|
+
Two causes. A forked worker only reported `Constable::Error`, so anything else killed it
|
|
56
|
+
silently — the parent saw a closed pipe, no results and no reason. Workers now report
|
|
57
|
+
whatever they die of, including the exit status when they die below Ruby (a signal, a
|
|
58
|
+
segfault, an OOM kill; forking an app that already holds native database connections can
|
|
59
|
+
do exactly that). And the fallback to a serial run was conditional on a worker having
|
|
60
|
+
managed to *explain* itself; it now triggers on the fact that matters — work was
|
|
61
|
+
scheduled and nothing came back. If the serial fallback also produces nothing, that
|
|
62
|
+
raises rather than being summarised as a clean zero.
|
|
63
|
+
|
|
64
|
+
Same command now: **84 tests run**, serially, with a warning saying why.
|
|
65
|
+
|
|
66
|
+
### Workers no longer build databases; `constable prepare` does
|
|
67
|
+
|
|
68
|
+
`reuse` had each worker build its own missing databases after forking. On Postgres the
|
|
69
|
+
clone must disconnect everything attached to the template first — and the template is the
|
|
70
|
+
shared test database *every other worker* is cloning from at the same moment. Twelve
|
|
71
|
+
workers terminated each other's connections and died mid-run.
|
|
72
|
+
|
|
73
|
+
Preparation happens once, in the parent, through `constable prepare`. A worker that finds
|
|
74
|
+
nothing to connect to says so and names the command. `constable prepare` also boots the
|
|
75
|
+
app first, which it was not doing: it reported "this app has no ActiveRecord test
|
|
76
|
+
databases to prepare" on an app with three of them.
|
|
77
|
+
|
|
78
|
+
### Configuration is documented where you read it
|
|
79
|
+
|
|
80
|
+
Every setting in `.constable/config.yml` now carries its explanation, and the ones with
|
|
81
|
+
fixed choices name and describe each value inline — `worker_databases`, `output`, the
|
|
82
|
+
storage adapter. Two tests enforce it: a setting cannot arrive without an explanation, and
|
|
83
|
+
one with fixed choices has to name them.
|
|
84
|
+
|
|
85
|
+
The blotter section now says outright that it is **not** your application's database.
|
|
86
|
+
Two people read it the other way, which is a naming problem, not a reading problem.
|
|
87
|
+
|
|
88
|
+
|
|
8
89
|
## [1.4.0]
|
|
9
90
|
|
|
10
91
|
Two problems from the same 1,277-file suite: a docket nobody asked for, and 110 minutes.
|
|
@@ -664,7 +745,9 @@ Initial release.
|
|
|
664
745
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
665
746
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
666
747
|
|
|
667
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.
|
|
748
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.2...HEAD
|
|
749
|
+
[1.4.2]: https://github.com/Ray-Hughes/constable/compare/v1.4.1...v1.4.2
|
|
750
|
+
[1.4.1]: https://github.com/Ray-Hughes/constable/compare/v1.4.0...v1.4.1
|
|
668
751
|
[1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
|
|
669
752
|
[1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
|
|
670
753
|
[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)
|
|
@@ -120,6 +120,7 @@ module Constable
|
|
|
120
120
|
with_engine do
|
|
121
121
|
collector = Collector.new
|
|
122
122
|
load_error = capture_load(path)
|
|
123
|
+
load_error ||= quit_flag_error(path)
|
|
123
124
|
unless load_error
|
|
124
125
|
# After the file has loaded -- that load is what registers them -- and
|
|
125
126
|
# before its examples run. See #run_pending_before_suite_hooks.
|
|
@@ -306,10 +307,22 @@ module Constable
|
|
|
306
307
|
requires = rspec_option_requires
|
|
307
308
|
return if requires.empty?
|
|
308
309
|
|
|
309
|
-
# `requires
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
|
|
310
|
+
# Deliberately NOT `configuration.requires =`, which is RSpec's own accessor.
|
|
311
|
+
# That routes through Configuration#load_file_handling_errors, which rescues
|
|
312
|
+
# anything raised while loading, reports it through a formatter, and sets
|
|
313
|
+
# `world.wants_to_quit`. Since stdout belongs to Constable's reporter, RSpec's
|
|
314
|
+
# streams are a throwaway StringIO, so that report goes nowhere -- and every
|
|
315
|
+
# later ExampleGroup.run returns immediately. The file loads, the examples
|
|
316
|
+
# register, and none of them run. A forked worker produced no results, no error
|
|
317
|
+
# and no exception, and exited 0.
|
|
318
|
+
#
|
|
319
|
+
# So the load path is set up the same way and the requires are done here, where
|
|
320
|
+
# an exception is an exception and carries its own message and backtrace.
|
|
321
|
+
add_spec_load_paths!(configuration)
|
|
322
|
+
requires.each { |path| require path }
|
|
323
|
+
configuration.instance_variable_set(:@requires, requires)
|
|
324
|
+
rescue Constable::Error
|
|
325
|
+
raise
|
|
313
326
|
rescue StandardError => e
|
|
314
327
|
# A helper that will not load is the suite's problem to fix, and it will say so
|
|
315
328
|
# loudly on the first file. Constable's job here is not to disappear.
|
|
@@ -317,6 +330,53 @@ module Constable
|
|
|
317
330
|
"Cold cases will run without it.", kind: :cold_case)
|
|
318
331
|
end
|
|
319
332
|
|
|
333
|
+
# What `configuration.requires=` does before requiring anything: puts `lib` and the
|
|
334
|
+
# default path (`spec`) on the load path, which is what makes a bare
|
|
335
|
+
# `require "rails_helper"` resolve at all.
|
|
336
|
+
def add_spec_load_paths!(configuration)
|
|
337
|
+
default_path = configuration.default_path if configuration.respond_to?(:default_path)
|
|
338
|
+
["lib", default_path].compact.uniq.each do |dir|
|
|
339
|
+
absolute = File.expand_path(dir, Constable.root)
|
|
340
|
+
$LOAD_PATH.unshift(absolute) if File.directory?(absolute) && !$LOAD_PATH.include?(absolute)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# RSpec does not let an error in a required file reach you.
|
|
345
|
+
# Configuration#load_file_handling_errors rescues anything raised while loading,
|
|
346
|
+
# reports it through `notify_non_example_exception`, and sets
|
|
347
|
+
# `world.wants_to_quit = true`. Every later ExampleGroup.run then returns
|
|
348
|
+
# immediately, so the file loads, the examples register, and none of them run.
|
|
349
|
+
#
|
|
350
|
+
# We point RSpec's output at a throwaway StringIO -- stdout belongs to the
|
|
351
|
+
# reporter -- so that report goes nowhere. The result was a forked worker that
|
|
352
|
+
# produced no results, no error and no exception, and exited 0. It took a probe
|
|
353
|
+
# inside the child to find that `wants_to_quit` was the difference.
|
|
354
|
+
#
|
|
355
|
+
# So: ask, and put the swallowed message back in front of the user.
|
|
356
|
+
def raise_if_loading_failed!(configuration, requires)
|
|
357
|
+
return unless ::RSpec.world.wants_to_quit
|
|
358
|
+
|
|
359
|
+
# Clear it, or every later file in this session inherits the flag and runs
|
|
360
|
+
# nothing either.
|
|
361
|
+
::RSpec.world.wants_to_quit = false
|
|
362
|
+
|
|
363
|
+
raise Constable::Error,
|
|
364
|
+
"RSpec could not load #{requires.join(", ")} (from .rspec). " \
|
|
365
|
+
"#{swallowed_output(configuration)}".strip
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Whatever RSpec wrote about it before we could ask. Its streams are ours, so this
|
|
369
|
+
# is the only copy in existence.
|
|
370
|
+
def swallowed_output(configuration)
|
|
371
|
+
[configuration.error_stream, configuration.output_stream, configuration.deprecation_stream]
|
|
372
|
+
.uniq
|
|
373
|
+
.filter_map { |io| io.string.to_s.strip if io.respond_to?(:string) }
|
|
374
|
+
.reject(&:empty?)
|
|
375
|
+
.first.to_s
|
|
376
|
+
rescue StandardError
|
|
377
|
+
""
|
|
378
|
+
end
|
|
379
|
+
|
|
320
380
|
# Parsed by RSpec itself, so `.rspec`, `~/.rspec`, `.rspec-local` and SPEC_OPTS are
|
|
321
381
|
# all read with its precedence rather than a guess at the format.
|
|
322
382
|
def rspec_option_requires
|
|
@@ -372,6 +432,18 @@ module Constable
|
|
|
372
432
|
configuration.start_time = ::RSpec::Core::Time.now if configuration.respond_to?(:start_time=)
|
|
373
433
|
end
|
|
374
434
|
|
|
435
|
+
# Same swallowing, one level down: a `require` inside the spec file that fails is
|
|
436
|
+
# rescued by RSpec, flagged, and never surfaced. Without this the file reports as
|
|
437
|
+
# a clean run of zero tests.
|
|
438
|
+
def quit_flag_error(path)
|
|
439
|
+
return nil unless ::RSpec.world.wants_to_quit
|
|
440
|
+
|
|
441
|
+
::RSpec.world.wants_to_quit = false
|
|
442
|
+
Constable::Error.new(
|
|
443
|
+
"RSpec stopped while loading #{path}. #{swallowed_output(::RSpec.configuration)}".strip
|
|
444
|
+
)
|
|
445
|
+
end
|
|
446
|
+
|
|
375
447
|
# A file that won't even parse is news, not a crash. Report it as one errored
|
|
376
448
|
# result so the run keeps going and the summary names the file.
|
|
377
449
|
def capture_load(path)
|
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
|
|
@@ -206,13 +236,32 @@ module Constable
|
|
|
206
236
|
# Every database config for this environment, renamed to its per-worker sibling.
|
|
207
237
|
# `_database=` is exactly how Rails' own TestDatabases does the renaming, and this
|
|
208
238
|
# runs in a forked child, so the mutation dies with the worker.
|
|
239
|
+
# Adapters where "give each worker its own copy" is a thing that can be done at all.
|
|
240
|
+
#
|
|
241
|
+
# An app can hold connections Constable has no business renaming. Caseflow talks to a
|
|
242
|
+
# legacy Oracle system (VACOLS) alongside its own Postgres databases; appending `_3` to
|
|
243
|
+
# an Oracle TNS service name produces `ORA-12162: TNS:net service name is incorrectly
|
|
244
|
+
# specified`, and since that surfaces inside a `before(:suite)` hook, RSpec swallowed
|
|
245
|
+
# it and the worker ran nothing at all.
|
|
246
|
+
#
|
|
247
|
+
# An external system like that is shared by every worker on purpose: it is not
|
|
248
|
+
# per-worker test data, and there is no per-worker copy of it to make.
|
|
249
|
+
SHARDABLE_ADAPTERS = %w[postgresql postgis mysql2 trilogy sqlite3].freeze
|
|
250
|
+
|
|
251
|
+
def shardable_adapter?(db_config)
|
|
252
|
+
return false unless db_config.respond_to?(:adapter)
|
|
253
|
+
|
|
254
|
+
SHARDABLE_ADAPTERS.include?(db_config.adapter.to_s)
|
|
255
|
+
end
|
|
256
|
+
|
|
209
257
|
def each_worker_config(index)
|
|
210
258
|
configs = ::ActiveRecord::Base.configurations.configs_for(env_name: env_name,
|
|
211
259
|
include_hidden: true)
|
|
212
260
|
configs.each do |db_config|
|
|
213
|
-
db_config._database = "#{db_config.database}_#{index}"
|
|
214
261
|
next unless db_config.database_tasks?
|
|
262
|
+
next unless shardable_adapter?(db_config)
|
|
215
263
|
|
|
264
|
+
db_config._database = "#{db_config.database}_#{index}"
|
|
216
265
|
yield db_config
|
|
217
266
|
end
|
|
218
267
|
end
|
|
@@ -180,6 +180,72 @@ end
|
|
|
180
180
|
# -----------------------------------------------------------------------------
|
|
181
181
|
|
|
182
182
|
Constable.configure do |c|
|
|
183
|
+
# --- Settings. Uncomment what you want; each is shown at its default. --------
|
|
184
|
+
|
|
185
|
+
# Workers default to `auto` -- processor count minus a little headroom, so the
|
|
186
|
+
# machine stays usable while the suite runs. Pin it when a CI container lies
|
|
187
|
+
# about its core count, or to rule parallelism out while debugging.
|
|
188
|
+
#
|
|
189
|
+
# c.parallel_workers = 4
|
|
190
|
+
|
|
191
|
+
# How a parallel worker gets a database of its own. Sharing one is not a
|
|
192
|
+
# speed/safety trade, it is a correctness bug: on SQLite the run dissolves into
|
|
193
|
+
# "database is locked", and on a client/server database tests quietly see each
|
|
194
|
+
# other's rows, which is worse because it looks like it worked.
|
|
195
|
+
#
|
|
196
|
+
# :schema rebuild <database>_<index> from schema.rb every run. What Rails
|
|
197
|
+
# does for `rails test`, and correct by construction -- the
|
|
198
|
+
# databases cannot drift, because they are thrown away each time.
|
|
199
|
+
# The default, and the wrong choice only when it is slow or
|
|
200
|
+
# impossible.
|
|
201
|
+
# :reuse keep them between runs. Faster (a big schema is not replayed per
|
|
202
|
+
# worker), and the ONLY option that works when your schema cannot
|
|
203
|
+
# rebuild the database by itself -- true of any app with Postgres
|
|
204
|
+
# custom types, since CREATE TYPE has no schema.rb representation.
|
|
205
|
+
# Run `constable prepare` once to build them, and again after a
|
|
206
|
+
# migration; keeping them current is your job.
|
|
207
|
+
# :off do not shard, so do not fork. An explicit serial run: no attempt,
|
|
208
|
+
# no warning. Correct, and as slow as one core.
|
|
209
|
+
#
|
|
210
|
+
# c.worker_databases = :reuse
|
|
211
|
+
|
|
212
|
+
# The live stream while the suite runs. The summary is identical either way.
|
|
213
|
+
# :concise one glyph per test, grouped per case (default)
|
|
214
|
+
# :expanded a line per test: glyph, description, duration -- so you can see
|
|
215
|
+
# which test is hanging while it hangs, not afterwards
|
|
216
|
+
#
|
|
217
|
+
# c.output = :expanded
|
|
218
|
+
|
|
219
|
+
# Put a test on the docket by itself when it passed last run and failed this
|
|
220
|
+
# one. Off by default, and the reason is what jailing does: a jailed test is
|
|
221
|
+
# SKIPPED on every later run. On automatically, a suite with order-dependent
|
|
222
|
+
# tests quietly stops running things nobody chose to stop running.
|
|
223
|
+
#
|
|
224
|
+
# c.jail_flakes = true
|
|
225
|
+
|
|
226
|
+
# Rerun a failure in isolation before believing it. A test that then passes is
|
|
227
|
+
# flaky rather than broken: it gets a warrant instead of failing the build, and
|
|
228
|
+
# stays loudly visible until someone deals with it.
|
|
229
|
+
#
|
|
230
|
+
# c.warrants = true
|
|
231
|
+
|
|
232
|
+
# CI: fail the build when the warning count is not trending down. Warnings are
|
|
233
|
+
# cold-case files and `unsafe` blocks -- allowed, but visible.
|
|
234
|
+
#
|
|
235
|
+
# c.fail_on_warnings = true
|
|
236
|
+
|
|
237
|
+
# Diff-based coverage: only lines changed in the current diff are held to it.
|
|
238
|
+
#
|
|
239
|
+
# c.coverage = true
|
|
240
|
+
# c.coverage_threshold = 90
|
|
241
|
+
|
|
242
|
+
# Your existing RSpec/Minitest files, run verbatim as cold cases through their
|
|
243
|
+
# own engine. `constable import --from=rspec` writes this for you.
|
|
244
|
+
#
|
|
245
|
+
# c.cold_cases = ["spec/**/*_spec.rb"]
|
|
246
|
+
|
|
247
|
+
# --- Code. This can only be written here. -----------------------------------
|
|
248
|
+
|
|
183
249
|
# One-time global setup, run once per process before the whole suite. For
|
|
184
250
|
# configuring the world -- drivers, adapters, formats -- and deliberately not
|
|
185
251
|
# for creating records that tests then share. See the note at the bottom of
|
|
@@ -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
|