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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4612ebf00dac193c1557bbd887beccfbd7e7864f157dd88a77386e476897a6e
4
- data.tar.gz: f208f84658349658dccef504a21f6a62833d27b9b772d1d91ef5f26216b60ae5
3
+ metadata.gz: 65a31ecd5db11b141e18762227af70518749d77042d45fad1626cc1713e06289
4
+ data.tar.gz: 6fe6e92175d4d306e47626a4d66e6e1ed029817c80af644f6c70f88f4c15c9df
5
5
  SHA512:
6
- metadata.gz: 296f8609681632e8427d481ecc94a15773b581c90dd7a88fa28c66eb584cd492c1706eec3faebc56aec385709ff0e72320db299c3901cec4659b333d8b94f97a
7
- data.tar.gz: aeb4696fc4029685e7ffcab4c4b7d3e0287b6a153b86f6c331864353e56546d6b9a9eb0aef640a59dce748ed08e27f6f5e80251b7e521143f1490ed18356d67f
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.1.0...HEAD
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
@@ -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. Raises rather than falling back to the shared one, because a
341
- # silent fallback is the bug we are here to prevent.
342
- WorkerDatabases.after_fork!(worker_index)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Constable
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: constable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Hughes