constable-rails 1.4.2 → 2.0.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: 04f058e6367c5b8a7739a80141601c3db8fc4f088ff1a040353cad17c435e4f8
4
- data.tar.gz: 18ab049c33174d1028c94352c20612fb79c39ed6aee70bce6a462321a4916d32
3
+ metadata.gz: 4ee7e9d9303ba3b3e4977f529ab515059faa946e96209dae4e80dbef42d2930c
4
+ data.tar.gz: feadad150285bf9d68b11cc671b7c1bc13188c39a0b6c76317aef1a7a6ca9c70
5
5
  SHA512:
6
- metadata.gz: 3f1e99fc5bbf329fc0bff1bfd9b35afb396ae1ac2f23a1db8f75be493507dce586ff604bf016749975e384f764637c79dfa53e44092f417f582fd63d6009e38f
7
- data.tar.gz: 18ff4465d65894ee18dbe1e5d116ae1a4128798ed759d41dd894135dd0b30554ab95329301c2559079e35066ce8cfc489ec88fe40760ef6eeedcbfa7f547972c
6
+ metadata.gz: 71adc2fab90c1ce4b1239159f7000c2b2200dc7f6becc3d365f6c935dc1702a9c012557b4602c909de880f2728433cfdfc9b4bd1524b8ec25581459c4a4f415a
7
+ data.tar.gz: 70fd3761fed31d90e365e1b24940025ddd14dc6f80f0761d004cdfe6c69145b4f1806f4fe49753a61af4137677cc2313c9a3f7cd0ce2f284e121ada1e75d0fdf
data/CHANGELOG.md CHANGED
@@ -5,6 +5,90 @@ All notable changes to this project are documented here. This project adheres to
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.0.0]
9
+
10
+ A tidy-up release. The API you write — `investigate`, `witness`, `briefing`, `docket`,
11
+ `attest` — has not changed, and no case file or config key needs editing. The major
12
+ version is for one removed accessor and for the behaviour change in 1.4.0, both below.
13
+
14
+ ### `constable modernize --cold`
15
+
16
+ A conversion that comes back flagged is **not runnable**: the flagged constructs are left
17
+ verbatim, so `let!` stays `let!` and the class body raises the moment it loads. That is
18
+ deliberate — what a `let!` should become is a decision, not a rewrite — but it leaves a
19
+ file stranded in `spec/` when the goal is one tree.
20
+
21
+ ```console
22
+ $ constable modernize spec/models/tag_spec.rb --cold
23
+ ```
24
+
25
+ writes `spec/models/tag_case.rb` as a cold case: one line at the top, the body byte for
26
+ byte, run through real RSpec. Move it into `test/cases/` and the file is ported without
27
+ being converted. Verified end to end — a `let!`-using spec that cannot convert runs 9/9
28
+ from `test/` untouched.
29
+
30
+ So a port is now: `modernize` a file, convert it if it comes back clean, `--cold` it if it
31
+ does not, and revisit later. Nothing has to stay behind.
32
+
33
+ ### A case file that will not load says why
34
+
35
+ A half-converted case still containing `let!` reported:
36
+
37
+ ```
38
+ no tests matched test/cases/tmp
39
+ ```
40
+
41
+ A file that raises while loading registers no investigations, so the selection came back
42
+ empty and the empty-selection guard — added in 1.0.0 to catch mistyped paths — claimed
43
+ the path was wrong. For a file that was right there and broken. It now reports the real
44
+ error:
45
+
46
+ ```
47
+ NoMethodError: undefined method `let!' for TagCase:Class
48
+ This file never ran. Nothing in it was tested.
49
+ ```
50
+
51
+ ### `constable jail release --all`
52
+
53
+ Emptying the docket took one command per test, which is unusable at the scale a docket
54
+ reaches — before 1.4.0 a pass/fail flip jailed a test by itself, and one real suite put 29
55
+ on the docket from a single run.
56
+
57
+ Releasing is safe, and that is the point: a jailed test is *skipped*, so a docket full of
58
+ tests nobody nominated is silently reducing what the suite covers. A bare `release` with
59
+ no locator stays a usage error rather than an implicit "everything".
60
+
61
+ ### Removed: `Result#coverage`
62
+
63
+ **Breaking, and why the major.** It was never written or read anywhere in the gem, and it
64
+ did not survive `to_h`/`from_h` — so a value set on it vanished crossing the pipe from a
65
+ parallel worker, silently, and only in parallel. An attribute that loses data is worse
66
+ than no attribute. Coverage has always travelled separately and still does.
67
+
68
+ ### Also breaking, from 1.4.0
69
+
70
+ `jail_flakes` defaults to **false**: a test that passed last run and failed this one is no
71
+ longer jailed automatically. Jailing skips the test on every later run, and doing that to
72
+ a test nobody nominated is how a suite quietly stops testing things. `--jail` is
73
+ unaffected.
74
+
75
+ ### Test coverage
76
+
77
+ The suite is now **1,025 runs**, up from 738 at 1.0.0. This release adds permanent tests
78
+ for the three areas that had none and carried the most risk:
79
+
80
+ - **`Result`** — the wire format between a worker and the parent. Every attribute is
81
+ asserted to survive a real `Marshal` round trip, because anything that does not is data
82
+ lost only in parallel runs, which is the worst way to find out. That is how the
83
+ `coverage` bug above was found.
84
+ - **Cold-case shapes** — 17 real-world files run end to end through both engines: pending
85
+ and `xit`, `before`/`around` hooks, tags, aggregate failures, a raising hook, shared
86
+ examples, `RSpec.configure` inside a spec, deep nesting, the same file twice, and seven
87
+ Minitest arrangements. Every serious bug this project has had lived in these adapters.
88
+ - **`Identity`** — stability under reformatting and comments, distinctness on real change,
89
+ unicode, cold-case keys, and the disambiguation added in 1.0.0.
90
+
91
+
8
92
  ## [1.4.2]
9
93
 
10
94
  ### Cold cases run inside a parallel worker
@@ -745,7 +829,8 @@ Initial release.
745
829
  - Diff-based coverage gate — only lines changed in the current diff are held to the
746
830
  threshold. `constable beat` for the full picture, `--html` for a browsable report.
747
831
 
748
- [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.2...HEAD
832
+ [Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.3...HEAD
833
+ [1.4.3]: https://github.com/Ray-Hughes/constable/compare/v1.4.2...v1.4.3
749
834
  [1.4.2]: https://github.com/Ray-Hughes/constable/compare/v1.4.1...v1.4.2
750
835
  [1.4.1]: https://github.com/Ray-Hughes/constable/compare/v1.4.0...v1.4.1
751
836
  [1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
data/README.md CHANGED
@@ -393,7 +393,7 @@ worse than one that resets.
393
393
  | `constable test PATH[:LINE]` | One file, or one investigation at that line |
394
394
  | `constable test --unsafe` | Cold cases only |
395
395
  | `constable test --jail` | The full run, in jail mode |
396
- | `constable jail [run\|parole\|release]` | The docket |
396
+ | `constable jail [run\|parole\|release]` | The docket. `release --all` empties it |
397
397
  | `constable warrants [release]` | Outstanding warrants |
398
398
  | `constable watchlist` | Everything under supervision right now |
399
399
  | `constable status` | How the suite is doing over time |
@@ -402,7 +402,7 @@ worse than one that resets.
402
402
  | `constable prepare [--workers N]` | Build the per-worker test databases `worker_databases: reuse` needs |
403
403
  | `constable prune [--dry-run]` | Forget docket rows and warrants for tests that no longer exist |
404
404
  | `constable import --from=rspec` | Adopt an existing suite as cold cases |
405
- | `constable modernize PATH` | Opt-in AST rewrite into the native DSL |
405
+ | `constable modernize PATH [--cold]` | Opt-in AST rewrite into the native DSL. `--cold` moves it verbatim instead |
406
406
 
407
407
  Flags: `--full --unsafe --jail --warrants --coverage --seed N --workers N --verbose --tier T\n--expanded --concise --output MODE --no-color`.
408
408
 
data/lib/constable/cli.rb CHANGED
@@ -197,6 +197,8 @@ module Constable
197
197
  DESC
198
198
  option :alongside, type: :boolean, default: false, desc: "Write PATH_case.rb beside the original"
199
199
  option :"in-place", type: :boolean, default: false, desc: "Overwrite the file"
200
+ option :cold, type: :boolean, default: false,
201
+ desc: "Move it verbatim as a cold case instead of converting"
200
202
  option :"show-source", type: :boolean, default: false, desc: "Print the rewritten source"
201
203
  def modernize(*paths)
202
204
  if paths.empty?
@@ -205,6 +207,7 @@ module Constable
205
207
  end
206
208
 
207
209
  mode = if options[:"in-place"] then :in_place
210
+ elsif options[:cold] then :cold
208
211
  elsif options[:alongside] then :alongside
209
212
  else :none
210
213
  end
@@ -312,13 +315,48 @@ module Constable
312
315
  say "Paroled. It runs normally now; one failure sends it straight back."
313
316
  end
314
317
 
315
- desc "release PATH:LINE", "Fully release a test, no supervision"
316
- def release(locator)
318
+ desc "release [PATH:LINE]", "Fully release a test, no supervision"
319
+ long_desc <<~DESC
320
+ Takes one test off the docket. `--all` empties it.
321
+
322
+ Emptying is the right move after a docket that filled up on its own -- before
323
+ 1.4.0 a pass/fail flip jailed a test automatically, and a suite with
324
+ order-dependent tests could put dozens on it that nobody chose. Releasing is safe:
325
+ anything genuinely broken fails again on the next run, in the open.
326
+ DESC
327
+ option :all, type: :boolean, default: false, desc: "Release every test on the docket"
328
+ def release(locator = nil)
329
+ return release_all if options[:all]
330
+
331
+ if locator.nil?
332
+ CLI.complain("release needs a PATH:LINE, or --all to empty the docket")
333
+ exit(EXIT_USAGE)
334
+ end
335
+
317
336
  act(locator) { |jail, identity| jail.release(identity) }
318
337
  say "Released."
319
338
  end
320
339
 
321
340
  no_commands do
341
+ # Emptying the docket in one go. Right after a docket that filled up on its own --
342
+ # before 1.4.0 a pass/fail flip jailed a test automatically, and a suite with
343
+ # order-dependent tests could put dozens on it that nobody chose. Releasing is
344
+ # safe: anything genuinely broken fails again on the next run, in the open.
345
+ def release_all
346
+ jail = Jail.new(config: Constable.config, storage: Constable.storage)
347
+ entries = jail.entries
348
+
349
+ if entries.empty?
350
+ say "The docket is already empty."
351
+ return 0
352
+ end
353
+
354
+ entries.each { |entry| jail.release(entry.identity) }
355
+ say "Released #{entries.size} #{entries.size == 1 ? "test" : "tests"}. The docket is empty."
356
+ say "Anything genuinely broken will fail on the next run, where you can see it."
357
+ 0
358
+ end
359
+
322
360
  def act(locator)
323
361
  jail = Jail.new(config: Constable.config, storage: Constable.storage)
324
362
  refuse_ambiguous(locator, jail.candidates(locator), "on the docket")
@@ -28,7 +28,7 @@ module Constable
28
28
  # :none -- dry run. Report only. The default.
29
29
  # :alongside -- write foo_spec.rb's conversion to foo_case.rb, never clobbering.
30
30
  # :in_place -- overwrite the original file.
31
- WRITE_MODES = %i[none alongside in_place].freeze
31
+ WRITE_MODES = %i[none alongside in_place cold].freeze
32
32
 
33
33
  GROUP_METHODS = %i[describe context xdescribe xcontext fdescribe fcontext feature].freeze
34
34
  EXAMPLE_METHODS = %i[it specify example scenario].freeze
@@ -175,9 +175,60 @@ module Constable
175
175
  File.join(dir, "#{base}_case.rb")
176
176
  end
177
177
 
178
+ # `--cold`: move the file into the native tree without converting a line of it.
179
+ #
180
+ # A conversion that comes back flagged is not runnable -- the flagged constructs
181
+ # are left verbatim, so `let!` stays `let!` and the class body raises the moment it
182
+ # loads. That is deliberate: what a `let!` should become is a decision, not a
183
+ # rewrite. But it leaves a file stuck in spec/ when the goal is one tree.
184
+ #
185
+ # A cold case is the answer that already exists: one line at the top, the body
186
+ # untouched, run through real RSpec, results folded into the same report. This
187
+ # writes exactly that, so a port can move every file and convert the ones worth
188
+ # converting on its own schedule.
189
+ def cold_wrap(result, root)
190
+ target = alongside_path(result.path)
191
+ if File.exist?(target)
192
+ result.error = "refusing to overwrite #{target.delete_prefix("#{root}/")}"
193
+ return
194
+ end
195
+
196
+ FileUtils.mkdir_p(File.dirname(target))
197
+ File.write(target, cold_source(result))
198
+ result.written_to = target.delete_prefix("#{root}/")
199
+ end
200
+
201
+ # The original bytes, between a header line and a final `end`. Nothing inside is
202
+ # parsed, reindented or touched -- that is the whole promise of a cold case.
203
+ def cold_source(result)
204
+ <<~RUBY
205
+ # frozen_string_literal: true
206
+
207
+ # Moved verbatim from #{result.relative_path}. Runs through real RSpec, with its
208
+ # results folded into Constable's reporting, flake history and CI gate.
209
+ #
210
+ # Nothing inside has been converted, so every RSpec feature still works --
211
+ # `let!`, `before(:all)`, shared examples, rspec-mocks. Convert it with
212
+ # `constable modernize` when it is worth doing; there is no deadline.
213
+ class #{cold_class_name(result)} < Constable::ColdCase::RSpec
214
+ #{indent(result.original.to_s.rstrip)}
215
+ end
216
+ RUBY
217
+ end
218
+
219
+ def cold_class_name(result)
220
+ base = File.basename(result.path, ".rb").sub(/_(?:spec|test)\z/, "")
221
+ "Legacy#{base.split(%r{[_/]}).map(&:capitalize).join}Spec"
222
+ end
223
+
224
+ def indent(source)
225
+ source.lines.map { |line| line.strip.empty? ? line : " #{line}" }.join
226
+ end
227
+
178
228
  private
179
229
 
180
230
  def persist(result, root, write)
231
+ return cold_wrap(result, root) if write == :cold
181
232
  return unless result.ok? && result.changed?
182
233
 
183
234
  case write
@@ -18,7 +18,7 @@ module Constable
18
18
 
19
19
  attr_reader :identity, :case_name, :description, :file, :line, :kind, :tier
20
20
  attr_accessor :status, :duration, :failure, :warnings, :retries, :jail_reason,
21
- :parole_day, :times_jailed, :seed, :coverage
21
+ :parole_day, :times_jailed, :seed
22
22
 
23
23
  def initialize(identity:, case_name:, description:, file:, line:, kind: :native, tier: nil,
24
24
  status: :passed, duration: 0.0, failure: nil, warnings: [], retries: [])
@@ -231,6 +231,12 @@ module Constable
231
231
  def refuse_empty_selection!(items)
232
232
  return unless items.empty?
233
233
  return unless @selection.explicit?
234
+ # A file that raised while loading registers no investigations, so the selection
235
+ # comes back empty and this guard would report "no tests matched" -- burying the
236
+ # actual error, which is already captured and about to be reported as a failure.
237
+ # "Your path is wrong" is the wrong thing to say about a file that is right there
238
+ # and broken.
239
+ return if load_errors.any?
234
240
 
235
241
  raise Constable::Error, @selection.empty_selection_message
236
242
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Constable
4
- VERSION = "1.4.2"
4
+ VERSION = "2.0.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.4.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ray Hughes