constable-rails 1.4.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 807163563a2d149b1f1267c2dd873ebc19e9948664b8311f4185676ac9698753
4
- data.tar.gz: d7c59c367bf69908c0909b2bda248f2d5d0c320a8238bbea628ac203134105a5
3
+ metadata.gz: 04f058e6367c5b8a7739a80141601c3db8fc4f088ff1a040353cad17c435e4f8
4
+ data.tar.gz: 18ab049c33174d1028c94352c20612fb79c39ed6aee70bce6a462321a4916d32
5
5
  SHA512:
6
- metadata.gz: 16458c866e5975ed485a03793c52c4d918b1191f8760ee45fd814d824857c943f5a848d268888e2d29334850b360d6f1df56e4cc1f619ad347f79c94a2bec0ae
7
- data.tar.gz: 0e7fc08cf6105a1fea100ebbda9dc3d316444982fe5ed467b09e45c27f8d36577711f8f175dc020773724f1973f2748b9a0fd76b0d791d24f48e1afb15a31ed7
6
+ metadata.gz: 3f1e99fc5bbf329fc0bff1bfd9b35afb396ae1ac2f23a1db8f75be493507dce586ff604bf016749975e384f764637c79dfa53e44092f417f582fd63d6009e38f
7
+ data.tar.gz: 18ff4465d65894ee18dbe1e5d116ae1a4128798ed759d41dd894135dd0b30554ab95329301c2559079e35066ce8cfc489ec88fe40760ef6eeedcbfa7f547972c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@ 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
+
8
41
  ## [1.4.1]
9
42
 
10
43
  ### A parallel run that ran nothing is no longer a pass
@@ -712,7 +745,8 @@ Initial release.
712
745
  - Diff-based coverage gate — only lines changed in the current diff are held to the
713
746
  threshold. `constable beat` for the full picture, `--html` for a browsable report.
714
747
 
715
- [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.1...HEAD
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
716
750
  [1.4.1]: https://github.com/Ray-Hughes/constable/compare/v1.4.0...v1.4.1
717
751
  [1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
718
752
  [1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
@@ -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=` rather than plain Kernel#require: it is RSpec's own accessor, and
310
- # it puts `lib` and the default path (`spec`) on the load path first, which is
311
- # what makes a bare `require "rails_helper"` resolve.
312
- configuration.requires = requires
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Constable
4
- VERSION = "1.4.1"
4
+ VERSION = "1.4.2"
5
5
  end
@@ -236,13 +236,32 @@ module Constable
236
236
  # Every database config for this environment, renamed to its per-worker sibling.
237
237
  # `_database=` is exactly how Rails' own TestDatabases does the renaming, and this
238
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
+
239
257
  def each_worker_config(index)
240
258
  configs = ::ActiveRecord::Base.configurations.configs_for(env_name: env_name,
241
259
  include_hidden: true)
242
260
  configs.each do |db_config|
243
- db_config._database = "#{db_config.database}_#{index}"
244
261
  next unless db_config.database_tasks?
262
+ next unless shardable_adapter?(db_config)
245
263
 
264
+ db_config._database = "#{db_config.database}_#{index}"
246
265
  yield db_config
247
266
  end
248
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
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.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Hughes