constable-rails 1.1.0 → 1.2.0
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 +52 -1
- data/lib/constable/runner.rb +45 -3
- data/lib/constable/selection.rb +27 -0
- data/lib/constable/version.rb +1 -1
- 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: 65a31ecd5db11b141e18762227af70518749d77042d45fad1626cc1713e06289
|
|
4
|
+
data.tar.gz: 6fe6e92175d4d306e47626a4d66e6e1ed029817c80af644f6c70f88f4c15c9df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 717a3d9604ef554b4216a0fc497626b7bbff207767904c7c73d66805d70b7e3e60c932d030c728123e414823a346040817ddeb715fe84260ff46c8038bb02181
|
|
7
|
+
data.tar.gz: 4f22e2278306e93af27767a71ae63931ea4b1752dc403ef5b33d604cb6ca8b4c136267f3c3d95f78e74bfdaa581eab2d8f343a65bc49bd8e1180a434bbf1ebeb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,56 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.2.0]
|
|
9
|
+
|
|
10
|
+
Two bugs found installing 1.1.0 into a large real Postgres app (~3,000-line schema,
|
|
11
|
+
custom types, a factory directory). Both are the same shape as the ones 1.0.0 fixed:
|
|
12
|
+
Constable was confidently wrong and said nothing useful about it.
|
|
13
|
+
|
|
14
|
+
### A factory named `*_case.rb` was loaded as a test
|
|
15
|
+
|
|
16
|
+
`spec/**/*_case.rb` is a generous net, and a real app has things in it that merely share
|
|
17
|
+
the suffix. Caseflow has a FactoryBot factory at `spec/factories/distributed_case.rb`.
|
|
18
|
+
Constable loaded it as a case file, FactoryBot raised `DuplicateDefinitionError` because
|
|
19
|
+
the factory was already registered, and the run reported a failing test in a file that
|
|
20
|
+
contains no tests:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
✗ spec/factories/distributed_case.rb
|
|
24
|
+
"could not be loaded"
|
|
25
|
+
FactoryBot::DuplicateDefinitionError: Factory already registered: distributed_case
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A filename is not evidence. A file outside the conventional `test/cases/` and
|
|
29
|
+
`spec/cases/` directories now has to look like a case before it is loaded — a class
|
|
30
|
+
declaration, or the DSL. Files under those directories are still taken at their word,
|
|
31
|
+
since that is what they are for and an empty one there is a case somebody is part-way
|
|
32
|
+
through writing.
|
|
33
|
+
|
|
34
|
+
### An app that cannot be sharded now runs anyway
|
|
35
|
+
|
|
36
|
+
Per-worker databases (1.0.0) are built by loading `schema.rb` into `<database>_<index>`.
|
|
37
|
+
Not every app can do that: one with Postgres custom types, functions or triggers cannot
|
|
38
|
+
rebuild itself from `schema.rb` at all, which is exactly why such apps keep a
|
|
39
|
+
`structure.sql`. Rails' own `parallelize` fails the same way.
|
|
40
|
+
|
|
41
|
+
Constable handled it about as badly as possible. Each worker raised, printing a full
|
|
42
|
+
stack trace — four workers, four traces, several hundred lines — and the parent then
|
|
43
|
+
reported a run that had never happened:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
CONSTABLE 1 test · 1 case · 10.8s
|
|
47
|
+
✓ 0 passed ✗ 1 failed
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A worker that cannot build its database now reports that home rather than raising. If no
|
|
51
|
+
worker got started, nothing has run yet, so the parent simply runs the suite serially and
|
|
52
|
+
says why in one sentence — including the real error and how to skip the attempt
|
|
53
|
+
(`parallel_workers: 1`). The rule from 1.0.0 is unchanged: a worker never falls back to
|
|
54
|
+
sharing the parent's database, because that is the corruption this whole mechanism
|
|
55
|
+
exists to prevent.
|
|
56
|
+
|
|
57
|
+
|
|
8
58
|
## [1.1.0]
|
|
9
59
|
|
|
10
60
|
Three things that were documented and did not work, plus the command for a docket
|
|
@@ -358,7 +408,8 @@ Initial release.
|
|
|
358
408
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
359
409
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
360
410
|
|
|
361
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.
|
|
411
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.2.0...HEAD
|
|
412
|
+
[1.2.0]: https://github.com/Ray-Hughes/constable/compare/v1.1.0...v1.2.0
|
|
362
413
|
[1.1.0]: https://github.com/Ray-Hughes/constable/compare/v1.0.0...v1.1.0
|
|
363
414
|
[1.0.0]: https://github.com/Ray-Hughes/constable/compare/v0.1.0...v1.0.0
|
|
364
415
|
[0.1.0]: https://github.com/Ray-Hughes/constable/releases/tag/v0.1.0
|
data/lib/constable/runner.rb
CHANGED
|
@@ -337,9 +337,17 @@ module Constable
|
|
|
337
337
|
reader.close
|
|
338
338
|
|
|
339
339
|
# Before a single test runs: build this worker's own database and point the
|
|
340
|
-
# process at it.
|
|
341
|
-
#
|
|
342
|
-
|
|
340
|
+
# process at it. Never falls back to the shared one -- that is the bug this
|
|
341
|
+
# exists to prevent -- but the failure is reported home rather than raised.
|
|
342
|
+
# A raise here dumps a full stack trace per worker and leaves the parent
|
|
343
|
+
# reporting a run that never happened.
|
|
344
|
+
begin
|
|
345
|
+
WorkerDatabases.after_fork!(worker_index)
|
|
346
|
+
rescue Constable::Error => e
|
|
347
|
+
write_message(writer, :worker_error, e.message)
|
|
348
|
+
writer.close
|
|
349
|
+
exit!(0)
|
|
350
|
+
end
|
|
343
351
|
|
|
344
352
|
bucket.each do |item|
|
|
345
353
|
run_item(item).each { |result| write_message(writer, :result, result.to_h) }
|
|
@@ -365,6 +373,17 @@ module Constable
|
|
|
365
373
|
collected = drain(readers)
|
|
366
374
|
pids.each { |pid| Process.waitpid(pid) rescue nil } # rubocop:disable Style/RescueModifier
|
|
367
375
|
|
|
376
|
+
# No worker could build itself a database, so no test ran. Not every app can be
|
|
377
|
+
# sharded: an app whose schema.rb cannot rebuild the database on its own -- Postgres
|
|
378
|
+
# custom types, functions and triggers are the usual reason, and are exactly why
|
|
379
|
+
# such apps use structure.sql -- will fail here every time. Rails' own `parallelize`
|
|
380
|
+
# fails the same way; the difference is that this is not the user's fault and they
|
|
381
|
+
# should not have to read four stack traces to find that out.
|
|
382
|
+
#
|
|
383
|
+
# Nothing has run yet, so falling back to a serial run costs a restart, not
|
|
384
|
+
# correctness.
|
|
385
|
+
return run_serially_after_worker_failure(items) if collected.empty? && worker_errors.any?
|
|
386
|
+
|
|
368
387
|
# A warning raised inside a worker only ever reached that worker's memory, so the
|
|
369
388
|
# results carry them home. Nothing that bends the rules is allowed to go missing
|
|
370
389
|
# just because it happened in a subprocess.
|
|
@@ -372,6 +391,25 @@ module Constable
|
|
|
372
391
|
collected
|
|
373
392
|
end
|
|
374
393
|
|
|
394
|
+
def worker_errors = (@worker_errors ||= [])
|
|
395
|
+
|
|
396
|
+
def run_serially_after_worker_failure(items)
|
|
397
|
+
reason = worker_errors.first.to_s
|
|
398
|
+
|
|
399
|
+
Constable.warn!(
|
|
400
|
+
"no parallel worker could build its own test database, so the suite ran serially " \
|
|
401
|
+
"instead. This usually means the app's schema cannot rebuild the database by " \
|
|
402
|
+
"itself -- Postgres custom types, functions and triggers are the common reason, " \
|
|
403
|
+
"and `rails test` parallelization fails the same way. Set `parallel_workers: 1` " \
|
|
404
|
+
"to skip the attempt. The first worker said: #{reason}",
|
|
405
|
+
kind: :parallel
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# The blotter handle was closed before forking, and the pool was cleared. Both come
|
|
409
|
+
# back on their next use, so there is nothing to reopen by hand.
|
|
410
|
+
run_serial(items)
|
|
411
|
+
end
|
|
412
|
+
|
|
375
413
|
# Every message on the pipe is tagged, because results are not the only thing a worker
|
|
376
414
|
# has to send home.
|
|
377
415
|
def write_message(writer, kind, body)
|
|
@@ -416,6 +454,10 @@ module Constable
|
|
|
416
454
|
@reporter.record(result)
|
|
417
455
|
when :coverage
|
|
418
456
|
@worker_coverage = Constable::Coverage.merge_raw(@worker_coverage, body)
|
|
457
|
+
when :worker_error
|
|
458
|
+
# A worker that could not start. Collected rather than raised, so the parent
|
|
459
|
+
# decides what to do once it knows whether any worker got going at all.
|
|
460
|
+
worker_errors << body
|
|
419
461
|
end
|
|
420
462
|
end
|
|
421
463
|
end
|
data/lib/constable/selection.rb
CHANGED
|
@@ -17,6 +17,22 @@ module Constable
|
|
|
17
17
|
def to_s = line ? "#{path}:#{line}" : path.to_s
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# A filename is not evidence. `spec/**/*_case.rb` is a generous net, and in a real app
|
|
21
|
+
# it catches things that merely share the suffix -- a FactoryBot factory named
|
|
22
|
+
# spec/factories/distributed_case.rb, say. Loading one of those runs somebody's code
|
|
23
|
+
# twice and reports the resulting explosion as a failing test in a file that contains
|
|
24
|
+
# no tests.
|
|
25
|
+
#
|
|
26
|
+
# So the file has to look like a case before we load it: a class declaration, or the
|
|
27
|
+
# DSL. Deliberately a cheap read rather than a parse -- this runs over every candidate
|
|
28
|
+
# on every run, and anything a parse would catch that this misses would also have to
|
|
29
|
+
# be a file that defines a case without mentioning one.
|
|
30
|
+
NATIVE_MARKERS = /
|
|
31
|
+
<\s*(?:Constable::Case|\w*Case)\b # class FooCase < UnitCase
|
|
32
|
+
| ^\s*investigate\s*[("] # or the DSL, for a reopened class
|
|
33
|
+
| ^\s*tier\s+:
|
|
34
|
+
/x
|
|
35
|
+
|
|
20
36
|
NATIVE_GLOBS = [
|
|
21
37
|
"test/cases/**/*.rb",
|
|
22
38
|
"spec/cases/**/*.rb",
|
|
@@ -183,6 +199,17 @@ module Constable
|
|
|
183
199
|
|
|
184
200
|
def native_files
|
|
185
201
|
@native_files ||= glob(NATIVE_GLOBS).reject { |f| @config.cold_case?(f) }
|
|
202
|
+
.select { |f| native_by_content?(f) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def native_by_content?(path)
|
|
206
|
+
# Files under the conventional case directories are taken at their word: that is
|
|
207
|
+
# what the directory is for, and an empty one there is a case file being written.
|
|
208
|
+
return true if path.match?(%r{/(?:test|spec)/cases/})
|
|
209
|
+
|
|
210
|
+
File.read(path).match?(NATIVE_MARKERS)
|
|
211
|
+
rescue StandardError
|
|
212
|
+
false
|
|
186
213
|
end
|
|
187
214
|
|
|
188
215
|
def cold_files
|
data/lib/constable/version.rb
CHANGED