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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fd3b3f867d958ed8f1306a51fb6a001145d88bd4c79839dd6d603f0b565248b
4
- data.tar.gz: 1dee6cf2bece623c102744cb44e271461907d386715ed435fcc871ee131dd92c
3
+ metadata.gz: 807163563a2d149b1f1267c2dd873ebc19e9948664b8311f4185676ac9698753
4
+ data.tar.gz: d7c59c367bf69908c0909b2bda248f2d5d0c320a8238bbea628ac203134105a5
5
5
  SHA512:
6
- metadata.gz: 1f6c418f0dd90a2e6e45733b237e8aa3c8e2e0d7c62b55d145dc7596bf047569983f243ba92558050612328f78834971cf5fef19e52efe4eb4deee0f5a751506
7
- data.tar.gz: 74e6fd7f76215036de95d36ccdd84a068a71b999368e97f998e5e832c2230d3624eaf6aba4aafe0ff5edf2aa0245b15a050e85fb5784347c6407a01d72842711
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.0...HEAD
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)
@@ -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
- bucket.each do |item|
356
- run_item(item).each { |result| write_message(writer, :result, result.to_h) }
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.each { |pid| Process.waitpid(pid) rescue nil } # rubocop:disable Style/RescueModifier
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
- return run_serially_after_worker_failure(items) if collected.empty? && worker_errors.any?
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
- "no parallel worker could build its own test database, so the suite ran serially " \
404
- "instead. This usually means the app's schema cannot rebuild the database by " \
405
- "itself -- Postgres custom types, functions and triggers are the common reason, " \
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}",
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Constable
4
- VERSION = "1.4.0"
4
+ VERSION = "1.4.1"
5
5
  end
@@ -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 reuse!(index) if mode.to_sym == :reuse
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. Points every database this environment declares at its `_<index>`
176
- # sibling, and only builds the ones that are not there yet.
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) | postgres | mysql
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. Sharing one is not a speed/safety
85
- # trade but a correctness bug: on SQLite the run dissolves into "database is locked", and
86
- # on a client/server database tests quietly see each other's rows.
87
- #
88
- # schema rebuild <database>_<index> from schema on every run. What Rails does for
89
- # `rails test`, and correct by construction -- no drift is possible.
90
- # reuse connect to <database>_<index> when it is already there, and build it from
91
- # schema only when it is not. Faster, because a large schema is not reloaded
92
- # every run -- and the only option that works at all when the schema cannot
93
- # rebuild the database by itself, which is true of any app with Postgres custom
94
- # types (`CREATE TYPE` has no schema.rb representation, so a from-scratch load
95
- # fails). Keeping those databases current becomes your job.
96
- # off do not shard, so do not fork. An explicit serial run: no attempt, no warning.
97
- worker_databases: schema # schema | reuse | off
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 identical
100
- # either way -- this only changes what you watch on the way there.
101
- #
102
- # concise one glyph per test, grouped into a run per case. A thousand tests stay
103
- # inside one screen, and a wall of green is the point.
104
- # expanded a line per test: glyph, name, duration. Slower to read in bulk, but you
105
- # can see which test is hanging while it hangs, rather than after.
106
- output: concise # concise | expanded
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
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.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Hughes