constable-rails 1.2.0 → 1.3.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 +123 -1
- data/README.md +6 -1
- data/lib/constable/cli.rb +35 -1
- data/lib/constable/cold_case/rspec.rb +44 -0
- data/lib/constable/cold_case.rb +5 -1
- data/lib/constable/config.rb +19 -0
- data/lib/constable/importer/reopener.rb +29 -4
- data/lib/constable/reporter.rb +31 -1
- data/lib/constable/runner.rb +7 -3
- data/lib/constable/version.rb +1 -1
- data/lib/constable/worker_databases.rb +99 -8
- data/lib/constable.rb +3 -3
- data/lib/generators/constable/templates/case_helper.rb.tt +1 -0
- data/lib/generators/constable/templates/config.yml.tt +15 -0
- 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: e3b589fed13c38522dab7443ab1bb967bedeaec7abb34beaecd5c4f2c9a4298a
|
|
4
|
+
data.tar.gz: b646c7160830f81bc2c6c5af9fe60728a8c7e1f40236e594fb64a51eed04080f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee0579e80cb81345601ce2c40090db63b87b1498698a77fbc8c34d0df478d36f3210295b3909bd8290835a1f245f2522952f491caca6776d7ac28981dcae7b74
|
|
7
|
+
data.tar.gz: aea8a0a67d5bdc8868706db9820af271ded921871da905c9e09fe83ac74a359c8c5e9ef8649d737367075f8faa9fabc32fff13156a8f2a9f6e9f69516464441a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,126 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.3.1]
|
|
9
|
+
|
|
10
|
+
### Cold cases now read `.rspec`
|
|
11
|
+
|
|
12
|
+
The bug that mattered. `.rspec` is where an RSpec suite says what to load before any spec
|
|
13
|
+
file:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
--require spec_helper
|
|
17
|
+
--require rails_helper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is what `rspec --init` generates, and it is why a real spec file usually has no
|
|
21
|
+
`require` line of its own — there is nothing for it to repeat. RSpec's own runner reads
|
|
22
|
+
those files. Constable's cold-case driver did not.
|
|
23
|
+
|
|
24
|
+
So a cold case ran with no `rails_helper` at all: no FactoryBot, no shoulda-matchers, no
|
|
25
|
+
`spec/support/**`. On a 1,277-file suite that is **entirely green under
|
|
26
|
+
`bundle exec rspec`**, `constable test spec/models --full` reported:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
✓ 774 passed ✗ 3589 failed
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2,637 of them `undefined method 'create'`, the rest `belong_to`,
|
|
33
|
+
`validate_presence_of`, missing support constants. Every one of them Constable's fault,
|
|
34
|
+
and every one of them looking like the user's.
|
|
35
|
+
|
|
36
|
+
Cold cases now apply the `--require` directives from `.rspec`, `~/.rspec`, `.rspec-local`
|
|
37
|
+
and `SPEC_OPTS`, parsed by RSpec itself so the precedence is its own rather than a guess.
|
|
38
|
+
Only `--require` is taken: formatters, colour and output streams belong to Constable's
|
|
39
|
+
reporter, ordering is Constable's job, and a `--tag` filter meant for a different run
|
|
40
|
+
should not silently drop tests from this one.
|
|
41
|
+
|
|
42
|
+
The same file is now 14 passed, 0 failed — matching `bundle exec rspec` exactly.
|
|
43
|
+
|
|
44
|
+
This never showed up before because the app it was developed against wrote
|
|
45
|
+
`require "rails_helper"` at the top of every spec, which is the one arrangement that
|
|
46
|
+
hides it.
|
|
47
|
+
|
|
48
|
+
### `constable import` says what it actually did
|
|
49
|
+
|
|
50
|
+
It reported `did reopen 1277 file(s)`. "Reopen" is this code's internal word and means
|
|
51
|
+
nothing to a reader, and "import" on its own suggests the files were copied somewhere —
|
|
52
|
+
which is the opposite of what happened. A real user read it exactly that way and asked
|
|
53
|
+
why their specs had not moved into `test/`.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Adopted 1277 rspec files as cold cases.
|
|
57
|
+
|
|
58
|
+
Your rspec files stay exactly where they are and are not changed.
|
|
59
|
+
One line was added to .constable/config.yml:
|
|
60
|
+
|
|
61
|
+
cold_cases:
|
|
62
|
+
- spec/**/*_spec.rb
|
|
63
|
+
|
|
64
|
+
Constable runs them from there, through real RSpec, and folds
|
|
65
|
+
the results into its own reporting, flake history and CI gate.
|
|
66
|
+
|
|
67
|
+
Next: constable test --full run everything, cold and native
|
|
68
|
+
constable test --unsafe run only these
|
|
69
|
+
constable modernize PATH see what one file would look like
|
|
70
|
+
as a native case (writes nothing)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [1.3.0]
|
|
75
|
+
|
|
76
|
+
Adoption at scale. Everything here came from installing 1.2.0 into a 1,277-spec-file
|
|
77
|
+
Postgres app.
|
|
78
|
+
|
|
79
|
+
### `worker_databases: reuse`
|
|
80
|
+
|
|
81
|
+
Per-worker databases have been rebuilt from schema on every run since 1.0.0, which is
|
|
82
|
+
what Rails does for `rails test`. It is correct by construction — no drift is possible —
|
|
83
|
+
and it is useless for an app whose schema **cannot** rebuild the database by itself. Any
|
|
84
|
+
app with Postgres custom types is in that position: `CREATE TYPE` has no `schema.rb`
|
|
85
|
+
representation, so a from-scratch load fails on a schema that references a type it never
|
|
86
|
+
defines.
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
worker_databases: schema # schema (default) | reuse | off
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`reuse` connects to `<database>_<index>` when it is already there and builds it from
|
|
93
|
+
schema only when it is not — checking each database separately, which matters for a
|
|
94
|
+
multi-database app where one may be prepared and another not. "Already there" means
|
|
95
|
+
present *and* holding tables: an empty database is not a prepared one, and running a
|
|
96
|
+
suite against no tables is the worst available outcome.
|
|
97
|
+
|
|
98
|
+
It is also just faster for everyone. A 3,000-line schema is no longer reloaded once per
|
|
99
|
+
worker per run.
|
|
100
|
+
|
|
101
|
+
`off` skips sharding entirely — an explicit serial run, no attempt and no warning.
|
|
102
|
+
|
|
103
|
+
New command, for the one-time setup `reuse` needs:
|
|
104
|
+
|
|
105
|
+
```console
|
|
106
|
+
$ constable prepare # build <database>_0 .. _<N-1>, once
|
|
107
|
+
$ constable prepare --workers 8
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Hundreds of cold cases no longer bury the summary
|
|
111
|
+
|
|
112
|
+
Every cold-case file warns, once per run, so that "12 tests not yet under native rules"
|
|
113
|
+
is never something the suite quietly forgets to mention. At 1,277 files that is four
|
|
114
|
+
thousand lines of the same sentence, and the warnings that actually need a decision — an
|
|
115
|
+
`unsafe` block, a jailed test — are lost inside it.
|
|
116
|
+
|
|
117
|
+
Past ten files they collapse into one line that keeps the numbers, which are the part
|
|
118
|
+
that is supposed to shrink:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
⚠ 1277 files running as cold cases, 18432 tests — not yet under native rules.
|
|
122
|
+
`constable test --unsafe` runs just these.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Fewer than ten are still listed individually, and nothing else is ever collapsed.
|
|
126
|
+
|
|
127
|
+
|
|
8
128
|
## [1.2.0]
|
|
9
129
|
|
|
10
130
|
Two bugs found installing 1.1.0 into a large real Postgres app (~3,000-line schema,
|
|
@@ -408,7 +528,9 @@ Initial release.
|
|
|
408
528
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
409
529
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
410
530
|
|
|
411
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.
|
|
531
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.3.1...HEAD
|
|
532
|
+
[1.3.1]: https://github.com/Ray-Hughes/constable/compare/v1.3.0...v1.3.1
|
|
533
|
+
[1.3.0]: https://github.com/Ray-Hughes/constable/compare/v1.2.0...v1.3.0
|
|
412
534
|
[1.2.0]: https://github.com/Ray-Hughes/constable/compare/v1.1.0...v1.2.0
|
|
413
535
|
[1.1.0]: https://github.com/Ray-Hughes/constable/compare/v1.0.0...v1.1.0
|
|
414
536
|
[1.0.0]: https://github.com/Ray-Hughes/constable/compare/v0.1.0...v1.0.0
|
data/README.md
CHANGED
|
@@ -399,6 +399,7 @@ worse than one that resets.
|
|
|
399
399
|
| `constable status` | How the suite is doing over time |
|
|
400
400
|
| `constable beat [--html]` | Coverage: overall %, per-file, the unpatrolled list |
|
|
401
401
|
| `constable history relink OLD NEW` | Carry history across a real body change |
|
|
402
|
+
| `constable prepare [--workers N]` | Build the per-worker test databases `worker_databases: reuse` needs |
|
|
402
403
|
| `constable prune [--dry-run]` | Forget docket rows and warrants for tests that no longer exist |
|
|
403
404
|
| `constable import --from=rspec` | Adopt an existing suite as cold cases |
|
|
404
405
|
| `constable modernize PATH` | Opt-in AST rewrite into the native DSL |
|
|
@@ -409,7 +410,11 @@ Order is randomized every run for native cases, with the seed printed and replay
|
|
|
409
410
|
`--seed`. Cold cases keep their own engine's order. Workers run in parallel by default,
|
|
410
411
|
load-balanced by a cached per-test duration index.
|
|
411
412
|
|
|
412
|
-
Each worker gets **its own database**, built from schema the way `rails test` does it
|
|
413
|
+
Each worker gets **its own database**, built from schema the way `rails test` does it —
|
|
414
|
+
or kept between runs, with `worker_databases: reuse`, which is both faster and the only
|
|
415
|
+
thing that works for an app whose schema cannot rebuild the database by itself (any app
|
|
416
|
+
with Postgres custom types: `CREATE TYPE` has no `schema.rb` representation). Prepare
|
|
417
|
+
those once with `constable prepare`.
|
|
413
418
|
Sharing one would not be a speed/safety trade but a correctness bug: on SQLite the run
|
|
414
419
|
dissolves into `database is locked`, and on a client/server database tests quietly see
|
|
415
420
|
each other's rows. If your app has ActiveRecord but cannot shard, Constable runs serially
|
data/lib/constable/cli.rb
CHANGED
|
@@ -168,8 +168,8 @@ module Constable
|
|
|
168
168
|
strategy: options[:strategy].to_sym
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
+
# The summary says this in full now, in the reader's own terms.
|
|
171
172
|
say result.summary
|
|
172
|
-
say "\nNothing was rewritten -- cold cases run through their own engine, unchanged." if result.any_changes?
|
|
173
173
|
exit(EXIT_CLEAN)
|
|
174
174
|
end
|
|
175
175
|
|
|
@@ -399,6 +399,40 @@ module Constable
|
|
|
399
399
|
end
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
+
desc "prepare", "Build the per-worker test databases parallel runs need"
|
|
403
|
+
long_desc <<~DESC
|
|
404
|
+
For `worker_databases: reuse`. Creates `<database>_0` .. `<database>_<N-1>` and
|
|
405
|
+
loads the schema into each, once, so later runs can connect straight to them.
|
|
406
|
+
|
|
407
|
+
Only useful for an app that has opted into reuse -- the default `schema` mode
|
|
408
|
+
rebuilds them on every run and needs no preparation. It is the answer for an app
|
|
409
|
+
whose schema cannot rebuild the database by itself: prepare these once, by whatever
|
|
410
|
+
means already works for you, and Constable will use them from then on.
|
|
411
|
+
DESC
|
|
412
|
+
option :workers, type: :numeric, desc: "How many to prepare (default: the configured worker count)"
|
|
413
|
+
def prepare
|
|
414
|
+
config = load_config
|
|
415
|
+
unless WorkerDatabases.shardable?
|
|
416
|
+
warn "This app has no ActiveRecord test databases to prepare."
|
|
417
|
+
exit(EXIT_USAGE)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
count = (options[:workers] || config.parallel_workers).to_i.clamp(1, 64)
|
|
421
|
+
say "Preparing #{count} worker #{count == 1 ? "database" : "databases"}..."
|
|
422
|
+
|
|
423
|
+
count.times do |index|
|
|
424
|
+
built = WorkerDatabases.prepare!(index)
|
|
425
|
+
say " worker #{index}: #{built.empty? ? "already prepared" : "built #{built.join(", ")}"}"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
say "\nDone. Set `worker_databases: reuse` so runs connect to these instead of " \
|
|
429
|
+
"rebuilding them."
|
|
430
|
+
0
|
|
431
|
+
rescue Constable::Error => e
|
|
432
|
+
warn e.message
|
|
433
|
+
exit(EXIT_FAILED)
|
|
434
|
+
end
|
|
435
|
+
|
|
402
436
|
desc "prune", "Forget docket rows and warrants for tests that no longer exist"
|
|
403
437
|
long_desc <<~DESC
|
|
404
438
|
A test's key is a content hash of its body, so editing a jailed test gives it a new
|
|
@@ -278,9 +278,53 @@ module Constable
|
|
|
278
278
|
# rspec-core's own default; restated because rspec-rails turns it off and a
|
|
279
279
|
# verbatim legacy file may well open with a bare `describe`.
|
|
280
280
|
configuration.expose_dsl_globally = true if configuration.respond_to?(:expose_dsl_globally=)
|
|
281
|
+
apply_rspec_options(configuration)
|
|
281
282
|
@session_prepared = true
|
|
282
283
|
end
|
|
283
284
|
|
|
285
|
+
# `.rspec` is where a suite says what to load before any spec file.
|
|
286
|
+
#
|
|
287
|
+
# --require spec_helper
|
|
288
|
+
# --require rails_helper
|
|
289
|
+
#
|
|
290
|
+
# That is what `rspec --init` generates, and it is why a real spec file usually
|
|
291
|
+
# has no `require` line of its own -- there is nothing for it to repeat. RSpec's
|
|
292
|
+
# own runner reads those files; driving example groups directly does not, so
|
|
293
|
+
# without this a cold case runs with no rails_helper at all: no FactoryBot, no
|
|
294
|
+
# shoulda-matchers, no spec/support. In one real suite that was 2,637 tests
|
|
295
|
+
# failing on `undefined method 'create'` -- a suite that is entirely green under
|
|
296
|
+
# `bundle exec rspec`.
|
|
297
|
+
#
|
|
298
|
+
# Only `--require` is taken. Formatters, colour and output streams belong to
|
|
299
|
+
# Constable's reporter, ordering is Constable's job, and a `--tag` filter from a
|
|
300
|
+
# file RSpec would apply to its own run should not silently drop tests from this
|
|
301
|
+
# one. Requires are the part that decides whether the suite can run at all.
|
|
302
|
+
def apply_rspec_options(configuration)
|
|
303
|
+
requires = rspec_option_requires
|
|
304
|
+
return if requires.empty?
|
|
305
|
+
|
|
306
|
+
# `requires=` rather than plain Kernel#require: it is RSpec's own accessor, and
|
|
307
|
+
# it puts `lib` and the default path (`spec`) on the load path first, which is
|
|
308
|
+
# what makes a bare `require "rails_helper"` resolve.
|
|
309
|
+
configuration.requires = requires
|
|
310
|
+
rescue StandardError => e
|
|
311
|
+
# A helper that will not load is the suite's problem to fix, and it will say so
|
|
312
|
+
# loudly on the first file. Constable's job here is not to disappear.
|
|
313
|
+
Constable.warn!("could not load what .rspec requires (#{e.class}: #{e.message}). " \
|
|
314
|
+
"Cold cases will run without it.", kind: :cold_case)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Parsed by RSpec itself, so `.rspec`, `~/.rspec`, `.rspec-local` and SPEC_OPTS are
|
|
318
|
+
# all read with its precedence rather than a guess at the format.
|
|
319
|
+
def rspec_option_requires
|
|
320
|
+
return [] unless defined?(::RSpec::Core::ConfigurationOptions)
|
|
321
|
+
|
|
322
|
+
options = ::RSpec::Core::ConfigurationOptions.new([]).options
|
|
323
|
+
Array(options[:requires])
|
|
324
|
+
rescue StandardError
|
|
325
|
+
[]
|
|
326
|
+
end
|
|
327
|
+
|
|
284
328
|
def clear_examples
|
|
285
329
|
world = ::RSpec.instance_variable_get(:@world)
|
|
286
330
|
world.reset if world.respond_to?(:reset)
|
data/lib/constable/cold_case.rb
CHANGED
|
@@ -195,7 +195,11 @@ module Constable
|
|
|
195
195
|
"running as a cold case (#{base_class_name}) — " \
|
|
196
196
|
"#{count} #{count == 1 ? "test" : "tests"} not yet under native rules",
|
|
197
197
|
location: location,
|
|
198
|
-
kind: :cold_case
|
|
198
|
+
kind: :cold_case,
|
|
199
|
+
# Carried so the reporter can total them when there are too many to list. A
|
|
200
|
+
# suite mid-adoption has hundreds of these, and printing every one buries the
|
|
201
|
+
# things that actually need a decision.
|
|
202
|
+
tests: count
|
|
199
203
|
)
|
|
200
204
|
end
|
|
201
205
|
|
data/lib/constable/config.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Constable
|
|
|
21
21
|
"fail_on_warnings" => false,
|
|
22
22
|
"output" => "concise",
|
|
23
23
|
"parallel_workers" => "auto",
|
|
24
|
+
"worker_databases" => "schema",
|
|
24
25
|
"tiers" => {
|
|
25
26
|
"unit" => "test/cases/models/**/*",
|
|
26
27
|
"integration" => "test/cases/controllers/**/*",
|
|
@@ -103,6 +104,24 @@ module Constable
|
|
|
103
104
|
# but you can see which test is hanging without waiting for the summary.
|
|
104
105
|
#
|
|
105
106
|
# The summary itself is identical either way. This only affects the live stream.
|
|
107
|
+
# How a parallel worker gets a database of its own.
|
|
108
|
+
#
|
|
109
|
+
# schema rebuild `<database>_<index>` from schema on every run. What Rails does for
|
|
110
|
+
# `rails test`, and correct by construction: no drift is possible.
|
|
111
|
+
# reuse connect to `<database>_<index>` when it already exists, and build it from
|
|
112
|
+
# schema only when it does not. Faster -- a large schema is not reloaded on
|
|
113
|
+
# every run -- and the only option that works for an app whose schema cannot
|
|
114
|
+
# rebuild the database by itself, which is any app with Postgres custom
|
|
115
|
+
# types. The cost is that keeping those databases current is now yours.
|
|
116
|
+
# off do not shard, so do not fork. An explicit serial run, with no attempt and
|
|
117
|
+
# no warning.
|
|
118
|
+
WORKER_DATABASE_MODES = %i[schema reuse off].freeze
|
|
119
|
+
|
|
120
|
+
def worker_databases
|
|
121
|
+
mode = @raw["worker_databases"].to_s.strip.downcase.to_sym
|
|
122
|
+
WORKER_DATABASE_MODES.include?(mode) ? mode : :schema
|
|
123
|
+
end
|
|
124
|
+
|
|
106
125
|
OUTPUT_MODES = %i[concise expanded].freeze
|
|
107
126
|
|
|
108
127
|
def output_mode
|
|
@@ -114,12 +114,26 @@ module Constable
|
|
|
114
114
|
}
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
# Written for somebody who has just typed `constable import` and does not yet know
|
|
118
|
+
# what a cold case is. "reopen" is the word this code uses internally and it means
|
|
119
|
+
# nothing to a reader; "import" itself suggests files were copied somewhere, which
|
|
120
|
+
# is the opposite of what happened. So: say where the files are, say what changed,
|
|
121
|
+
# and say what to do next.
|
|
117
122
|
def summary
|
|
118
|
-
verb = dry_run? ? "
|
|
119
|
-
|
|
123
|
+
verb = dry_run? ? "Would adopt" : "Adopted"
|
|
124
|
+
noun = imported_count == 1 ? "file" : "files"
|
|
125
|
+
lines = ["#{verb} #{imported_count} #{@from} #{noun} as cold cases."]
|
|
126
|
+
|
|
120
127
|
unless @globs_added.empty?
|
|
121
|
-
lines << "
|
|
122
|
-
|
|
128
|
+
lines << ""
|
|
129
|
+
lines << " Your #{@from} files stay exactly where they are and are not changed."
|
|
130
|
+
lines << " #{dry_run? ? "One line would be added to" : "One line was added to"} " \
|
|
131
|
+
"#{@config_path}:"
|
|
132
|
+
lines << ""
|
|
133
|
+
@globs_added.each { |glob| lines << " cold_cases:\n - #{glob}" }
|
|
134
|
+
lines << ""
|
|
135
|
+
lines << " Constable runs them from there, through real #{engine_label}, and folds"
|
|
136
|
+
lines << " the results into its own reporting, flake history and CI gate."
|
|
123
137
|
unless comments_preserved?
|
|
124
138
|
lines << " note: config.yml was rewritten from parsed YAML; comments were not preserved"
|
|
125
139
|
end
|
|
@@ -133,9 +147,20 @@ module Constable
|
|
|
133
147
|
@skipped.each { |s| lines << " - #{s[:path]} (#{s[:reason]})" }
|
|
134
148
|
end
|
|
135
149
|
@errors.each { |e| lines << " error: #{e[:path]} -- #{e[:message]}" }
|
|
150
|
+
|
|
151
|
+
unless dry_run? || imported_count.zero?
|
|
152
|
+
lines << ""
|
|
153
|
+
lines << " Next: constable test --full run everything, cold and native"
|
|
154
|
+
lines << " constable test --unsafe run only these"
|
|
155
|
+
lines << " constable modernize PATH see what one file would look like"
|
|
156
|
+
lines << " as a native case (writes nothing)"
|
|
157
|
+
end
|
|
158
|
+
|
|
136
159
|
lines.join("\n")
|
|
137
160
|
end
|
|
138
161
|
|
|
162
|
+
def engine_label = @from.to_s == "rspec" ? "RSpec" : "Minitest"
|
|
163
|
+
|
|
139
164
|
def superclass_name = ENGINES.fetch(@from)[:superclass]
|
|
140
165
|
|
|
141
166
|
def covered_by(glob)
|
data/lib/constable/reporter.rb
CHANGED
|
@@ -44,6 +44,12 @@ module Constable
|
|
|
44
44
|
# without a limit a quiet case could hold the buffer forever.
|
|
45
45
|
STREAM_FLUSH_THRESHOLD = 12
|
|
46
46
|
|
|
47
|
+
# How many cold-case files are listed one by one before they are summarised instead.
|
|
48
|
+
# A suite part-way through adoption has hundreds -- one real app had 1,277 -- and
|
|
49
|
+
# printing every one buries the warnings that actually need a decision under four
|
|
50
|
+
# thousand lines that say the same thing.
|
|
51
|
+
COLD_CASE_LIST_LIMIT = 10
|
|
52
|
+
|
|
47
53
|
DEFAULT_SLOWEST = 5
|
|
48
54
|
|
|
49
55
|
# Column the expanded stream right-aligns durations into. Descriptions are never
|
|
@@ -609,6 +615,7 @@ module Constable
|
|
|
609
615
|
end
|
|
610
616
|
|
|
611
617
|
def section_warnings(warnings)
|
|
618
|
+
warnings = collapse_cold_cases(warnings)
|
|
612
619
|
return if warnings.empty?
|
|
613
620
|
|
|
614
621
|
section("WARNINGS")
|
|
@@ -624,6 +631,25 @@ module Constable
|
|
|
624
631
|
end
|
|
625
632
|
end
|
|
626
633
|
|
|
634
|
+
# Cold cases are a fact about the suite, not a list of problems: every one says the
|
|
635
|
+
# same sentence about a different file. Past the limit they become one line that
|
|
636
|
+
# still carries the number -- which is the part that is supposed to shrink over time,
|
|
637
|
+
# and the reason the warning exists at all.
|
|
638
|
+
def collapse_cold_cases(warnings)
|
|
639
|
+
cold, rest = warnings.partition { |w| w[:kind] == :cold_case }
|
|
640
|
+
return warnings if cold.size <= COLD_CASE_LIST_LIMIT
|
|
641
|
+
|
|
642
|
+
tests = cold.sum { |w| w[:tests].to_i }
|
|
643
|
+
total = tests.positive? ? ", #{tests} #{pluralize(tests, "test")}" : ""
|
|
644
|
+
|
|
645
|
+
[{
|
|
646
|
+
kind: :cold_case,
|
|
647
|
+
location: nil,
|
|
648
|
+
message: "#{cold.size} files running as cold cases#{total} — not yet under " \
|
|
649
|
+
"native rules. `constable test --unsafe` runs just these."
|
|
650
|
+
}] + rest
|
|
651
|
+
end
|
|
652
|
+
|
|
627
653
|
# A warning carries the author's own words -- an unsafe block's reason, a cold case's
|
|
628
654
|
# count -- and those are easily ninety columns. Wrapped to the frame, but respecting
|
|
629
655
|
# any line breaks the message already chose.
|
|
@@ -707,7 +733,11 @@ module Constable
|
|
|
707
733
|
w = warning.to_h.transform_keys(&:to_sym)
|
|
708
734
|
return nil if w[:message].nil?
|
|
709
735
|
|
|
710
|
-
|
|
736
|
+
# `tests` rides along so cold cases can be totalled when there are too many to list.
|
|
737
|
+
# Normalizing is about the three fields the reporter needs, not about discarding
|
|
738
|
+
# everything a warning chose to carry.
|
|
739
|
+
{ message: w[:message].to_s, location: w[:location], kind: (w[:kind] || :unsafe).to_sym,
|
|
740
|
+
tests: w[:tests] }.compact
|
|
711
741
|
end
|
|
712
742
|
|
|
713
743
|
def normalize_coverage(coverage)
|
data/lib/constable/runner.rb
CHANGED
|
@@ -283,6 +283,9 @@ module Constable
|
|
|
283
283
|
#
|
|
284
284
|
# When we cannot shard, we run serially and say why. Slow is a trade-off; wrong is not.
|
|
285
285
|
def parallel_safe?
|
|
286
|
+
# An explicit opt-out. No attempt, and no warning about one -- the user has already
|
|
287
|
+
# told us they know.
|
|
288
|
+
return false if @config.worker_databases == :off
|
|
286
289
|
return true unless WorkerDatabases.active_record?
|
|
287
290
|
return true if WorkerDatabases.shardable?
|
|
288
291
|
|
|
@@ -342,7 +345,7 @@ module Constable
|
|
|
342
345
|
# A raise here dumps a full stack trace per worker and leaves the parent
|
|
343
346
|
# reporting a run that never happened.
|
|
344
347
|
begin
|
|
345
|
-
WorkerDatabases.after_fork!(worker_index)
|
|
348
|
+
WorkerDatabases.after_fork!(worker_index, mode: @config.worker_databases)
|
|
346
349
|
rescue Constable::Error => e
|
|
347
350
|
write_message(writer, :worker_error, e.message)
|
|
348
351
|
writer.close
|
|
@@ -400,8 +403,9 @@ module Constable
|
|
|
400
403
|
"no parallel worker could build its own test database, so the suite ran serially " \
|
|
401
404
|
"instead. This usually means the app's schema cannot rebuild the database by " \
|
|
402
405
|
"itself -- Postgres custom types, functions and triggers are the common reason, " \
|
|
403
|
-
"and `rails test` parallelization fails the same way. Set
|
|
404
|
-
"to
|
|
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}",
|
|
405
409
|
kind: :parallel
|
|
406
410
|
)
|
|
407
411
|
|
data/lib/constable/version.rb
CHANGED
|
@@ -46,24 +46,115 @@ module Constable
|
|
|
46
46
|
false
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Child side, immediately after the fork and before any test runs.
|
|
50
|
-
# `<database>_<index
|
|
49
|
+
# Child side, immediately after the fork and before any test runs. Gives this process
|
|
50
|
+
# a database of its own, named `<database>_<index>`.
|
|
51
51
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
52
|
+
# `mode` is the `worker_databases` setting:
|
|
53
|
+
#
|
|
54
|
+
# :schema rebuild it from schema every run, which is what Rails does. Correct by
|
|
55
|
+
# construction -- no drift is possible -- and the right default.
|
|
56
|
+
# :reuse connect to it when it is already there, build it from schema when it is
|
|
57
|
+
# not. Skips reloading a large schema on every run, and is the only thing
|
|
58
|
+
# that works when the schema cannot rebuild the database by itself.
|
|
59
|
+
def after_fork!(index, mode: :schema)
|
|
55
60
|
return false unless shardable?
|
|
61
|
+
return reuse!(index) if mode.to_sym == :reuse
|
|
56
62
|
|
|
57
63
|
::ActiveRecord::TestDatabases.create_and_load_schema(index, env_name: env_name)
|
|
58
64
|
true
|
|
59
65
|
rescue StandardError => e
|
|
60
|
-
# A worker that cannot build its own database
|
|
61
|
-
#
|
|
62
|
-
#
|
|
66
|
+
# A worker that cannot build its own database must never fall back to sharing the
|
|
67
|
+
# parent's -- that is the corruption this module exists to prevent. The runner turns
|
|
68
|
+
# this into a serial run rather than letting it kill the process.
|
|
63
69
|
raise Constable::Error, "worker #{index} could not create its own test database: " \
|
|
64
70
|
"#{e.class}: #{e.message}"
|
|
65
71
|
end
|
|
66
72
|
|
|
73
|
+
# `constable prepare`. Builds worker `index`'s databases from the parent process --
|
|
74
|
+
# the same work :reuse would do lazily inside a fork, done once, deliberately, where
|
|
75
|
+
# the output is visible and a failure is not four stack traces at once.
|
|
76
|
+
def prepare!(index)
|
|
77
|
+
raise Constable::Error, "this app has no ActiveRecord databases to prepare" unless shardable?
|
|
78
|
+
|
|
79
|
+
# Renaming is destructive, and here it happens in the parent rather than in a fork
|
|
80
|
+
# that is about to die. Without restoring the names afterwards this command would
|
|
81
|
+
# leave the process -- and, worse, anything else in it -- pointed at
|
|
82
|
+
# `<database>_<index>` instead of the real test database.
|
|
83
|
+
original = database_names
|
|
84
|
+
begin
|
|
85
|
+
reuse!(index)
|
|
86
|
+
ensure
|
|
87
|
+
restore_database_names(original)
|
|
88
|
+
::ActiveRecord::Base.establish_connection
|
|
89
|
+
end
|
|
90
|
+
rescue Constable::Error
|
|
91
|
+
raise
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
raise Constable::Error, "could not prepare worker #{index}: #{e.class}: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def database_names
|
|
97
|
+
::ActiveRecord::Base.configurations
|
|
98
|
+
.configs_for(env_name: env_name, include_hidden: true)
|
|
99
|
+
.map(&:database)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def restore_database_names(names)
|
|
103
|
+
::ActiveRecord::Base.configurations
|
|
104
|
+
.configs_for(env_name: env_name, include_hidden: true)
|
|
105
|
+
.zip(names).each { |config, name| config._database = name if name }
|
|
106
|
+
end
|
|
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.
|
|
110
|
+
#
|
|
111
|
+
# "There" means present *and* populated: an empty database is not a prepared one, and
|
|
112
|
+
# connecting to it would hand the worker a suite with no tables. Deciding that per
|
|
113
|
+
# database rather than per worker matters for a multi-database app -- Caseflow has a
|
|
114
|
+
# primary and an ETL database -- where one may be prepared and the other not.
|
|
115
|
+
#
|
|
116
|
+
# Keeping these current is the user's job once they opt in, which is the trade the
|
|
117
|
+
# setting exists to let them make.
|
|
118
|
+
def reuse!(index)
|
|
119
|
+
built = []
|
|
120
|
+
|
|
121
|
+
each_worker_config(index) do |db_config|
|
|
122
|
+
next if populated?(db_config)
|
|
123
|
+
|
|
124
|
+
::ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
|
|
125
|
+
built << db_config.database
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
built
|
|
129
|
+
ensure
|
|
130
|
+
# Rails does this after its own schema load: the pool has to be re-established
|
|
131
|
+
# against the renamed configuration before any test asks for a connection.
|
|
132
|
+
::ActiveRecord::Base.establish_connection
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Every database config for this environment, renamed to its per-worker sibling.
|
|
136
|
+
# `_database=` is exactly how Rails' own TestDatabases does the renaming, and this
|
|
137
|
+
# runs in a forked child, so the mutation dies with the worker.
|
|
138
|
+
def each_worker_config(index)
|
|
139
|
+
configs = ::ActiveRecord::Base.configurations.configs_for(env_name: env_name,
|
|
140
|
+
include_hidden: true)
|
|
141
|
+
configs.each do |db_config|
|
|
142
|
+
db_config._database = "#{db_config.database}_#{index}"
|
|
143
|
+
next unless db_config.database_tasks?
|
|
144
|
+
|
|
145
|
+
yield db_config
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Present and holding tables. A database that exists but is empty is not prepared, and
|
|
150
|
+
# silently running a suite against no tables is the worst of the available outcomes.
|
|
151
|
+
def populated?(db_config)
|
|
152
|
+
::ActiveRecord::Base.establish_connection(db_config)
|
|
153
|
+
::ActiveRecord::Base.connection.tables.any?
|
|
154
|
+
rescue StandardError
|
|
155
|
+
false
|
|
156
|
+
end
|
|
157
|
+
|
|
67
158
|
def env_name
|
|
68
159
|
if defined?(::ActiveRecord::ConnectionHandling::DEFAULT_ENV)
|
|
69
160
|
::ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
|
data/lib/constable.rb
CHANGED
|
@@ -97,8 +97,8 @@ module Constable
|
|
|
97
97
|
|
|
98
98
|
# Warnings are never silent and never fatal by default. They accumulate through a run
|
|
99
99
|
# and always get their own section in the summary.
|
|
100
|
-
def warn!(message, location: nil, kind: :unsafe)
|
|
101
|
-
warnings << { message: message, location: location, kind: kind }
|
|
100
|
+
def warn!(message, location: nil, kind: :unsafe, **extra)
|
|
101
|
+
warnings << { message: message, location: location, kind: kind, **extra }
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def warnings
|
|
@@ -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
|
-
output tiers
|
|
152
|
+
worker_databases output tiers
|
|
153
153
|
].freeze
|
|
154
154
|
|
|
155
155
|
# `storage` is the one setting that cannot live here, and the reason is ordering, not
|
|
@@ -156,6 +156,7 @@ end
|
|
|
156
156
|
#
|
|
157
157
|
# c.cold_cases = [] # RSpec/Minitest globs to run as cold cases
|
|
158
158
|
# c.parallel_workers = "auto" # or an integer. Each worker gets its own database
|
|
159
|
+
# c.worker_databases = :schema # or :reuse (keep them between runs) / :off
|
|
159
160
|
# c.output = :concise # or :expanded -- a line per test, with timings
|
|
160
161
|
# c.fail_on_warnings = false # CI: fail when the warning count is not trending down
|
|
161
162
|
# c.warrants = false # rerun a failure in isolation before believing it
|
|
@@ -73,6 +73,21 @@ fail_on_warnings: false # CI: fail the build when the warning count doesn't tre
|
|
|
73
73
|
|
|
74
74
|
parallel_workers: auto # or an explicit integer
|
|
75
75
|
|
|
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
|
|
90
|
+
|
|
76
91
|
# How much the live stream says while the suite is running. The summary is identical
|
|
77
92
|
# either way -- this only changes what you watch on the way there.
|
|
78
93
|
#
|