constable-rails 1.0.0 → 1.1.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 +119 -1
- data/README.md +45 -0
- data/lib/constable/cli.rb +52 -0
- data/lib/constable/config.rb +42 -3
- data/lib/constable/jail.rb +38 -0
- data/lib/constable/reporter.rb +6 -3
- data/lib/constable/runner.rb +22 -0
- data/lib/constable/version.rb +1 -1
- data/lib/constable/warrants.rb +33 -0
- data/lib/constable.rb +47 -1
- data/lib/generators/constable/install_generator.rb +61 -1
- data/lib/generators/constable/templates/case_helper.rb.tt +42 -9
- data/lib/generators/constable/templates/config.yml.tt +23 -7
- 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: c4612ebf00dac193c1557bbd887beccfbd7e7864f157dd88a77386e476897a6e
|
|
4
|
+
data.tar.gz: f208f84658349658dccef504a21f6a62833d27b9b772d1d91ef5f26216b60ae5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 296f8609681632e8427d481ecc94a15773b581c90dd7a88fa28c66eb584cd492c1706eec3faebc56aec385709ff0e72320db299c3901cec4659b333d8b94f97a
|
|
7
|
+
data.tar.gz: aeb4696fc4029685e7ffcab4c4b7d3e0287b6a153b86f6c331864353e56546d6b9a9eb0aef640a59dce748ed08e27f6f5e80251b7e521143f1490ed18356d67f
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,123 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.1.0]
|
|
9
|
+
|
|
10
|
+
Three things that were documented and did not work, plus the command for a docket
|
|
11
|
+
that has gone stale.
|
|
12
|
+
|
|
13
|
+
### `Constable.configure` actually configures things now
|
|
14
|
+
|
|
15
|
+
The generated `test/case_helper.rb` told you to write `c.parallel_workers = 4`, explained
|
|
16
|
+
when you would want to, and then **nothing in the codebase ever read it**. Four accessors,
|
|
17
|
+
all inert.
|
|
18
|
+
|
|
19
|
+
They work now, and every setting `.constable/config.yml` understands is settable in Ruby
|
|
20
|
+
alongside them — `cold_cases`, `storage`, `warrants`, `warrant_retries`, `auto_relink`,
|
|
21
|
+
`parole_period`, `coverage`, `coverage_threshold`, `coverage_html`, `fail_on_warnings`,
|
|
22
|
+
`parallel_workers`, `output`, `tiers`. A test asserts the two halves stay in step, so a
|
|
23
|
+
setting cannot be added to one and forgotten in the other.
|
|
24
|
+
|
|
25
|
+
Precedence, and the reasoning:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
a CLI flag --workers 4, --expanded one run, most specific
|
|
29
|
+
Constable.configure test/case_helper.rb code you deliberately ran
|
|
30
|
+
.constable/config.yml the project's declared default
|
|
31
|
+
Constable's defaults
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Which to use? A setting that differs per machine or per branch belongs in the YAML, where
|
|
35
|
+
it is obvious and greppable. A setting that has to be *computed* belongs in Ruby, because
|
|
36
|
+
YAML cannot do this:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Constable.configure do |c|
|
|
40
|
+
c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
|
|
41
|
+
c.coverage = ENV["CI"] == "true"
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Values set in Ruby go through the same clamping as values set in the file, so a typo is no
|
|
46
|
+
more dangerous in one than the other.
|
|
47
|
+
|
|
48
|
+
**`storage` is the one exception, and it now says so.** The blotter is opened before
|
|
49
|
+
`case_helper.rb` loads, so that `constable jail`, `warrants`, `watchlist` and `status` can
|
|
50
|
+
read the docket without booting the app — a broken app should not stop you reading the
|
|
51
|
+
docket. Setting it in Ruby would have been silently ignored, which is the exact failure
|
|
52
|
+
this release exists to stop, so it raises and explains why.
|
|
53
|
+
|
|
54
|
+
The docs now lead with Ruby. The generated `case_helper.rb` lists **every** setting at its
|
|
55
|
+
default in one compact block — a test asserts the list stays complete, and that it never
|
|
56
|
+
offers `storage`. The long-form explanation of each stays in `config.yml`, so the two files
|
|
57
|
+
are a reference and a place to write code rather than two competing references.
|
|
58
|
+
|
|
59
|
+
Neither is `config/initializers/`, and the reason is concrete: initializers run on every
|
|
60
|
+
boot including production, where a test-only gem is not in the bundle, so an initializer
|
|
61
|
+
calling `Constable.configure` takes the app down with a NoMethodError. Same reason RSpec,
|
|
62
|
+
SimpleCov, WebMock and Capybara all configure from the test helper.
|
|
63
|
+
|
|
64
|
+
### `.constable/config.yml` is now genuinely optional
|
|
65
|
+
|
|
66
|
+
You can delete it. `rails generate constable:install --skip-config` never writes it.
|
|
67
|
+
|
|
68
|
+
The one thing keeping it mandatory was `storage`, which cannot be set in Ruby for the
|
|
69
|
+
ordering reason above. It now reads from the environment as well, which is the right shape
|
|
70
|
+
for CI anyway, where the value is a secret and differs per machine:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
CONSTABLE_STORAGE_URL=postgres://user:pass@host/constable_metadata
|
|
74
|
+
CONSTABLE_STORAGE_PATH=/var/lib/constable/blotter.sqlite3
|
|
75
|
+
CONSTABLE_STORAGE_ADAPTER=postgres
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
An adapter is inferred from the URL scheme when it is not given, and an empty variable
|
|
79
|
+
means unset rather than "connect to the empty string". The `.constable/` **directory**
|
|
80
|
+
still exists to hold the blotter — deliberately not `tmp/`, which `rails tmp:clear` and
|
|
81
|
+
most deploys would wipe, taking weeks of flake history with it.
|
|
82
|
+
|
|
83
|
+
One bug found while checking this end to end, in the override layer added above: settings
|
|
84
|
+
were applied *after* the selection had already been asked for its targets, so
|
|
85
|
+
`c.cold_cases` in Ruby was silently ignored and a suite of 192 ran 35. Overrides are now
|
|
86
|
+
applied between requiring the helper and asking the selection anything.
|
|
87
|
+
|
|
88
|
+
### Settings added after you install are no longer invisible
|
|
89
|
+
|
|
90
|
+
`.constable/config.yml` doubles as the reference — every key at its default, with the
|
|
91
|
+
reasoning above it — which only works if it stays current. A gem upgrade cannot rewrite it
|
|
92
|
+
without clobbering your settings, and Thor's only other answer is to skip the file, so
|
|
93
|
+
**`output` shipped in 1.0.0 and never appeared in any existing config.** The first anyone
|
|
94
|
+
knew was running `bundle update` and finding the setting missing.
|
|
95
|
+
|
|
96
|
+
`rails generate constable:install` now appends only the settings your file does not
|
|
97
|
+
mention, each with its explanation, under a header saying where they came from. Your
|
|
98
|
+
values and your own comments are never touched. Re-run it after any upgrade.
|
|
99
|
+
|
|
100
|
+
### `constable prune`
|
|
101
|
+
|
|
102
|
+
A test's key is a content hash of its body, so editing a jailed test gives it a new
|
|
103
|
+
identity and leaves the old row behind — pointing at a `file:line` that may now hold
|
|
104
|
+
something else. That is identity working as designed, and it was the one known limitation
|
|
105
|
+
left open in 1.0.0. This is the broom:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
$ constable prune --dry-run # list what would go
|
|
109
|
+
$ constable prune # forget it
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
It loads the whole suite first, because which tests still exist is only knowable once
|
|
113
|
+
every case file has been read, and it is deliberately conservative in two directions:
|
|
114
|
+
|
|
115
|
+
- **A known identity is never pruned**, even when the file recorded beside it is gone. The
|
|
116
|
+
path is a display label; the identity is the truth. A test that moved file has an
|
|
117
|
+
out-of-date label, not a missing test.
|
|
118
|
+
- **A cold case is never pruned while its file exists.** Cold-case tests cannot be
|
|
119
|
+
enumerated without running their own engine, so their absence says nothing.
|
|
120
|
+
|
|
121
|
+
Flake history is left alone either way — only the docket and outstanding warrants are
|
|
122
|
+
touched.
|
|
123
|
+
|
|
124
|
+
|
|
8
125
|
## [1.0.0]
|
|
9
126
|
|
|
10
127
|
The first release anybody should install.
|
|
@@ -241,6 +358,7 @@ Initial release.
|
|
|
241
358
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
242
359
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
243
360
|
|
|
244
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.
|
|
361
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.1.0...HEAD
|
|
362
|
+
[1.1.0]: https://github.com/Ray-Hughes/constable/compare/v1.0.0...v1.1.0
|
|
245
363
|
[1.0.0]: https://github.com/Ray-Hughes/constable/compare/v0.1.0...v1.0.0
|
|
246
364
|
[0.1.0]: https://github.com/Ray-Hughes/constable/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -399,6 +399,7 @@ worse than one that resets.
|
|
|
399
399
|
| `constable status` | How the suite is doing over time |
|
|
400
400
|
| `constable beat [--html]` | Coverage: overall %, per-file, the unpatrolled list |
|
|
401
401
|
| `constable history relink OLD NEW` | Carry history across a real body change |
|
|
402
|
+
| `constable prune [--dry-run]` | Forget docket rows and warrants for tests that no longer exist |
|
|
402
403
|
| `constable import --from=rspec` | Adopt an existing suite as cold cases |
|
|
403
404
|
| `constable modernize PATH` | Opt-in AST rewrite into the native DSL |
|
|
404
405
|
|
|
@@ -557,6 +558,50 @@ storage:
|
|
|
557
558
|
|
|
558
559
|
### Configuration
|
|
559
560
|
|
|
561
|
+
Settings can be written in Ruby, in `test/case_helper.rb` — the same place RSpec puts
|
|
562
|
+
`RSpec.configure` — or in `.constable/config.yml`, or on the command line. The most
|
|
563
|
+
specific wins:
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
a CLI flag --workers 4, --expanded one run
|
|
567
|
+
Constable.configure test/case_helper.rb code you deliberately ran
|
|
568
|
+
.constable/config.yml the project's declared default
|
|
569
|
+
Constable's defaults
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Every key below can be set in either place. Put settings that differ per machine or per
|
|
573
|
+
branch in the YAML, where they are obvious and greppable; put settings that have to be
|
|
574
|
+
*computed* in Ruby, because YAML cannot:
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
# test/case_helper.rb
|
|
578
|
+
Constable.configure do |c|
|
|
579
|
+
c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
|
|
580
|
+
c.coverage = ENV["CI"] == "true"
|
|
581
|
+
c.output = :expanded
|
|
582
|
+
end
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Not `config/initializers/`, which is where a runtime gem like Devise goes. Initializers
|
|
586
|
+
run on **every** boot including production, where a test-only gem is not in the bundle, so
|
|
587
|
+
an initializer calling `Constable.configure` takes the app down. It is the same reason
|
|
588
|
+
RSpec, SimpleCov, WebMock and Capybara all configure from the test helper.
|
|
589
|
+
|
|
590
|
+
`config.yml` is optional, and one setting is the reason it exists: **`storage` can only be
|
|
591
|
+
set there.** The blotter is opened before `case_helper.rb` loads, so that `constable jail`,
|
|
592
|
+
`warrants`, `watchlist` and `status` can read the docket without booting the app — a
|
|
593
|
+
broken app should not stop you reading the docket. Setting it in Ruby raises rather than
|
|
594
|
+
being quietly ignored.
|
|
595
|
+
|
|
596
|
+
Beyond that it is a preference. A settings file is greppable and diffable without running
|
|
597
|
+
anything, which suits values that differ per project or per branch; Ruby suits anything
|
|
598
|
+
computed.
|
|
599
|
+
|
|
600
|
+
`config.yml` doubles as the reference, so re-run
|
|
601
|
+
`rails generate constable:install --skip` after an upgrade: it appends any settings your
|
|
602
|
+
file does not mention and leaves your own values and comments alone. (`--skip` so the
|
|
603
|
+
other generated files, which you have probably edited, are left as they are.)
|
|
604
|
+
|
|
560
605
|
```yaml
|
|
561
606
|
# .constable/config.yml
|
|
562
607
|
cold_cases:
|
data/lib/constable/cli.rb
CHANGED
|
@@ -399,6 +399,48 @@ module Constable
|
|
|
399
399
|
end
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
+
desc "prune", "Forget docket rows and warrants for tests that no longer exist"
|
|
403
|
+
long_desc <<~DESC
|
|
404
|
+
A test's key is a content hash of its body, so editing a jailed test gives it a new
|
|
405
|
+
identity and leaves the old row behind -- pointing at a file:line that may now hold
|
|
406
|
+
something else. That is identity working as designed; this is the broom.
|
|
407
|
+
|
|
408
|
+
Loads the whole suite first, because which tests still exist is only knowable once
|
|
409
|
+
every case file has been read. Cold cases are never pruned while their file exists:
|
|
410
|
+
their tests cannot be enumerated without running their own engine, so silence about
|
|
411
|
+
them means nothing.
|
|
412
|
+
DESC
|
|
413
|
+
option :dry_run, type: :boolean, default: false, desc: "List what would go, change nothing"
|
|
414
|
+
def prune
|
|
415
|
+
config = load_config
|
|
416
|
+
known = Runner.identities(config: config)
|
|
417
|
+
cold = ->(path) { config.cold_case?(path) }
|
|
418
|
+
|
|
419
|
+
jail = Jail.new(config: config, storage: Constable.storage)
|
|
420
|
+
warrants = Warrants.new(config: config, storage: Constable.storage)
|
|
421
|
+
|
|
422
|
+
stale_rows = jail.stale_entries(known, cold_case: cold)
|
|
423
|
+
stale_warrants = warrants.stale_entries(known, cold_case: cold)
|
|
424
|
+
|
|
425
|
+
if stale_rows.empty? && stale_warrants.empty?
|
|
426
|
+
say "Nothing to prune -- every row on the docket still names a test that exists."
|
|
427
|
+
return 0
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
report_prunable("docket", stale_rows)
|
|
431
|
+
report_prunable("warrants", stale_warrants)
|
|
432
|
+
|
|
433
|
+
if options[:dry_run]
|
|
434
|
+
say "Dry run: nothing was changed."
|
|
435
|
+
return 0
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
stale_rows.each { |entry| jail.forget(entry.identity) }
|
|
439
|
+
stale_warrants.each { |entry| warrants.release(entry.identity) }
|
|
440
|
+
say "Pruned #{stale_rows.size + stale_warrants.size} row(s). Flake history is left alone."
|
|
441
|
+
0
|
|
442
|
+
end
|
|
443
|
+
|
|
402
444
|
desc "jail SUBCOMMAND", "The jail docket"
|
|
403
445
|
subcommand "jail", JailCommand
|
|
404
446
|
|
|
@@ -409,6 +451,16 @@ module Constable
|
|
|
409
451
|
subcommand "history", HistoryCommand
|
|
410
452
|
|
|
411
453
|
no_commands do
|
|
454
|
+
def report_prunable(heading, entries)
|
|
455
|
+
return if entries.empty?
|
|
456
|
+
|
|
457
|
+
say "#{heading} (#{entries.size}):"
|
|
458
|
+
entries.sort_by { |entry| [entry.file.to_s, entry.line.to_i] }.each do |entry|
|
|
459
|
+
say " #{entry.location} #{entry.label}"
|
|
460
|
+
end
|
|
461
|
+
say ""
|
|
462
|
+
end
|
|
463
|
+
|
|
412
464
|
def load_config
|
|
413
465
|
Constable.config
|
|
414
466
|
end
|
data/lib/constable/config.rb
CHANGED
|
@@ -63,6 +63,16 @@ module Constable
|
|
|
63
63
|
@raw = deep_merge(@raw, stringify(overrides || {}))
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Merges values set in Ruby (Constable.configure) over the ones read from the file.
|
|
67
|
+
# Called once, after case_helper.rb has been loaded -- which is the first moment those
|
|
68
|
+
# values exist.
|
|
69
|
+
def apply_overrides!(overrides)
|
|
70
|
+
return self if overrides.nil? || overrides.empty?
|
|
71
|
+
|
|
72
|
+
@raw = deep_merge(@raw, stringify(overrides))
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
66
76
|
def cold_cases = Array(@raw["cold_cases"])
|
|
67
77
|
def warrants? = truthy(@raw["warrants"])
|
|
68
78
|
# Negative retries are a typo for "off", not an instruction to count backwards.
|
|
@@ -103,11 +113,33 @@ module Constable
|
|
|
103
113
|
def expanded_output? = output_mode == :expanded
|
|
104
114
|
def storage = @raw["storage"] || {}
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
# Storage is the one setting that cannot be written in Ruby -- the blotter is opened
|
|
117
|
+
# before test/case_helper.rb loads, so `constable jail` and `constable status` can read
|
|
118
|
+
# the docket without booting the app. That would leave .constable/config.yml mandatory
|
|
119
|
+
# for anyone not on the default SQLite, so the environment can say it instead:
|
|
120
|
+
#
|
|
121
|
+
# CONSTABLE_STORAGE_URL=postgres://user:pass@host/constable_metadata
|
|
122
|
+
# CONSTABLE_STORAGE_PATH=/var/lib/constable/blotter.sqlite3
|
|
123
|
+
#
|
|
124
|
+
# Which is also the right shape for CI, where the value is a secret and differs per
|
|
125
|
+
# machine. The environment wins over the file, as an environment usually should.
|
|
126
|
+
def storage_url = env_or("CONSTABLE_STORAGE_URL", storage["url"])
|
|
127
|
+
|
|
128
|
+
def storage_adapter
|
|
129
|
+
explicit = env_or("CONSTABLE_STORAGE_ADAPTER", storage["adapter"])
|
|
130
|
+
return explicit.to_s if explicit
|
|
131
|
+
|
|
132
|
+
# A URL with no adapter names its own: postgres://... can only mean postgres.
|
|
133
|
+
scheme = storage_url.to_s[%r{\A([a-z][a-z0-9+.-]*)://}, 1]
|
|
134
|
+
return "postgres" if %w[postgres postgresql].include?(scheme)
|
|
135
|
+
return "mysql" if %w[mysql mysql2].include?(scheme)
|
|
136
|
+
|
|
137
|
+
"sqlite"
|
|
138
|
+
end
|
|
108
139
|
|
|
109
140
|
def storage_path
|
|
110
|
-
path =
|
|
141
|
+
path = env_or("CONSTABLE_STORAGE_PATH", storage["path"]) ||
|
|
142
|
+
DEFAULTS["storage"]["path"]
|
|
111
143
|
File.absolute_path?(path) ? path : File.join(@root, path)
|
|
112
144
|
end
|
|
113
145
|
|
|
@@ -145,6 +177,13 @@ module Constable
|
|
|
145
177
|
|
|
146
178
|
private
|
|
147
179
|
|
|
180
|
+
# An empty environment variable is not a value. `CONSTABLE_STORAGE_URL=` in a CI
|
|
181
|
+
# config means "unset", not "connect to the empty string".
|
|
182
|
+
def env_or(name, fallback)
|
|
183
|
+
value = ENV.fetch(name, nil)
|
|
184
|
+
value.nil? || value.strip.empty? ? fallback : value.strip
|
|
185
|
+
end
|
|
186
|
+
|
|
148
187
|
def truthy(value)
|
|
149
188
|
return false if value.nil? || value == false
|
|
150
189
|
return false if value.to_s.strip.downcase == "false"
|
data/lib/constable/jail.rb
CHANGED
|
@@ -241,6 +241,11 @@ module Constable
|
|
|
241
241
|
# `constable jail release PATH:LINE`. Off the docket entirely, no supervision.
|
|
242
242
|
def release(identity) = @storage.release(identity.to_s) ? true : false
|
|
243
243
|
|
|
244
|
+
# Pruning is not release. Release says "this test is trusted again"; prune says "this
|
|
245
|
+
# row is about a test that no longer exists". The distinction matters because release
|
|
246
|
+
# is a judgement someone made and prune is bookkeeping.
|
|
247
|
+
def forget(identity) = @storage.release(identity.to_s) ? true : false
|
|
248
|
+
|
|
244
249
|
# `jail run` never auto-releases and never auto-paroles. One green run proves nothing;
|
|
245
250
|
# it only earns a mention. A human reads this list and decides.
|
|
246
251
|
#
|
|
@@ -360,6 +365,39 @@ module Constable
|
|
|
360
365
|
matches
|
|
361
366
|
end
|
|
362
367
|
|
|
368
|
+
# Rows whose test no longer exists.
|
|
369
|
+
#
|
|
370
|
+
# A test's key is a content hash of its body, so editing a jailed test gives it a new
|
|
371
|
+
# identity and leaves the old row behind -- pointing at a file:line that may now hold
|
|
372
|
+
# something else entirely. That is content-hash identity working as designed, but over
|
|
373
|
+
# a few months the docket fills with tests nobody can find.
|
|
374
|
+
#
|
|
375
|
+
# `known` is every identity the loaded suite registered, so this is only meaningful
|
|
376
|
+
# after a full load. Two things are deliberately conservative about it:
|
|
377
|
+
#
|
|
378
|
+
# * A known identity is never stale, even when the file recorded beside it is gone.
|
|
379
|
+
# The path is a display label; the identity is the truth. A test that moved file
|
|
380
|
+
# has an out-of-date label, not a missing test.
|
|
381
|
+
# * A cold case is never pruned while its file exists. Cold-case tests cannot be
|
|
382
|
+
# enumerated without running their own engine, so absence from `known` says
|
|
383
|
+
# nothing about them.
|
|
384
|
+
def stale_entries(known, cold_case: nil)
|
|
385
|
+
known = Array(known)
|
|
386
|
+
entries.reject { |entry| known.include?(entry.identity) }
|
|
387
|
+
.select { |entry| gone?(entry, cold_case) }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def gone?(entry, cold_case)
|
|
391
|
+
relative = entry.file.to_s
|
|
392
|
+
return true if relative.empty?
|
|
393
|
+
|
|
394
|
+
path = File.absolute_path?(relative) ? relative : File.join(Constable.root, relative)
|
|
395
|
+
return true unless File.exist?(path)
|
|
396
|
+
return false if cold_case&.call(relative)
|
|
397
|
+
|
|
398
|
+
true
|
|
399
|
+
end
|
|
400
|
+
|
|
363
401
|
# An identity String, or nil when nothing matches -- or when more than one does.
|
|
364
402
|
# Ambiguity is the caller's to report, with the candidates in hand.
|
|
365
403
|
def resolve(target)
|
data/lib/constable/reporter.rb
CHANGED
|
@@ -100,14 +100,16 @@ module Constable
|
|
|
100
100
|
0x1F900..0x1F9FF, 0x20000..0x3FFFD
|
|
101
101
|
].freeze
|
|
102
102
|
|
|
103
|
-
attr_reader :io, :config, :seed, :total
|
|
103
|
+
attr_reader :io, :config, :seed, :total
|
|
104
104
|
|
|
105
105
|
def initialize(io: $stdout, config: nil, color: nil, slowest: DEFAULT_SLOWEST, mode: nil)
|
|
106
106
|
@io = io
|
|
107
107
|
@config = config || Constable.config
|
|
108
108
|
@color = resolve_color(color)
|
|
109
109
|
@slowest = slowest.to_i
|
|
110
|
-
|
|
110
|
+
# Resolved lazily: the reporter is built before case_helper.rb has run, so reading
|
|
111
|
+
# the config now would miss anything Constable.configure sets.
|
|
112
|
+
@requested_mode = mode
|
|
111
113
|
@io.set_encoding(Encoding::UTF_8) if @io.respond_to?(:set_encoding)
|
|
112
114
|
|
|
113
115
|
reset_stream!
|
|
@@ -210,7 +212,8 @@ module Constable
|
|
|
210
212
|
def failed? = !success?
|
|
211
213
|
def color? = @color
|
|
212
214
|
def finished? = @finished
|
|
213
|
-
def
|
|
215
|
+
def mode = @mode ||= resolve_mode(@requested_mode)
|
|
216
|
+
def expanded? = mode == :expanded
|
|
214
217
|
|
|
215
218
|
private
|
|
216
219
|
|
data/lib/constable/runner.rb
CHANGED
|
@@ -64,6 +64,19 @@ module Constable
|
|
|
64
64
|
def jail_run? = @jail_run
|
|
65
65
|
def coverage? = @coverage_requested
|
|
66
66
|
|
|
67
|
+
# Loads every case file and hands back the identities the suite actually defines,
|
|
68
|
+
# without running anything. `constable prune` needs this: which tests still exist is
|
|
69
|
+
# only knowable once the whole suite has been loaded.
|
|
70
|
+
def self.identities(config: Constable.config)
|
|
71
|
+
selection = Selection.new([], config: config, root: Constable.root, full: true)
|
|
72
|
+
runner = new(selection: selection, config: config,
|
|
73
|
+
reporter: Reporter.new(io: StringIO.new, config: config, color: false),
|
|
74
|
+
storage: Constable.storage, workers: 1)
|
|
75
|
+
runner.send(:load_suite!)
|
|
76
|
+
Constable.registry.disambiguate_identities!
|
|
77
|
+
Constable.registry.investigations.map(&:identity)
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
# => Integer exit status (0 clean, 1 failures)
|
|
68
81
|
def call
|
|
69
82
|
# Before anything is loaded: Coverage only counts files required after it starts, so
|
|
@@ -143,6 +156,15 @@ module Constable
|
|
|
143
156
|
.find { |p| File.exist?(p) }
|
|
144
157
|
require helper if helper
|
|
145
158
|
|
|
159
|
+
# Between requiring the helper and asking the selection anything.
|
|
160
|
+
#
|
|
161
|
+
# Ordering is the whole point. case_helper.rb is where Constable.configure runs, so
|
|
162
|
+
# its settings do not exist until the line above. But Selection memoizes its targets
|
|
163
|
+
# the first time it is asked for them, and `cold_cases` is one of the settings people
|
|
164
|
+
# will most want to set in Ruby -- ask first and the override arrives too late to
|
|
165
|
+
# matter, silently. Requiring the helper needs no selection, so this fits between.
|
|
166
|
+
@config.apply_overrides!(Constable.configuration.overrides)
|
|
167
|
+
|
|
146
168
|
@selection.native_targets.each { |target| load_case_file(target.path) }
|
|
147
169
|
helper
|
|
148
170
|
end
|
data/lib/constable/version.rb
CHANGED
data/lib/constable/warrants.rb
CHANGED
|
@@ -244,6 +244,39 @@ module Constable
|
|
|
244
244
|
matches
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
+
# Rows whose test no longer exists.
|
|
248
|
+
#
|
|
249
|
+
# A test's key is a content hash of its body, so editing a jailed test gives it a new
|
|
250
|
+
# identity and leaves the old row behind -- pointing at a file:line that may now hold
|
|
251
|
+
# something else entirely. That is content-hash identity working as designed, but over
|
|
252
|
+
# a few months the docket fills with tests nobody can find.
|
|
253
|
+
#
|
|
254
|
+
# `known` is every identity the loaded suite registered, so this is only meaningful
|
|
255
|
+
# after a full load. Two things are deliberately conservative about it:
|
|
256
|
+
#
|
|
257
|
+
# * A known identity is never stale, even when the file recorded beside it is gone.
|
|
258
|
+
# The path is a display label; the identity is the truth. A test that moved file
|
|
259
|
+
# has an out-of-date label, not a missing test.
|
|
260
|
+
# * A cold case is never pruned while its file exists. Cold-case tests cannot be
|
|
261
|
+
# enumerated without running their own engine, so absence from `known` says
|
|
262
|
+
# nothing about them.
|
|
263
|
+
def stale_entries(known, cold_case: nil)
|
|
264
|
+
known = Array(known)
|
|
265
|
+
entries.reject { |entry| known.include?(entry.identity) }
|
|
266
|
+
.select { |entry| gone?(entry, cold_case) }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def gone?(entry, cold_case)
|
|
270
|
+
relative = entry.file.to_s
|
|
271
|
+
return true if relative.empty?
|
|
272
|
+
|
|
273
|
+
path = File.absolute_path?(relative) ? relative : File.join(Constable.root, relative)
|
|
274
|
+
return true unless File.exist?(path)
|
|
275
|
+
return false if cold_case&.call(relative)
|
|
276
|
+
|
|
277
|
+
true
|
|
278
|
+
end
|
|
279
|
+
|
|
247
280
|
def resolve(target)
|
|
248
281
|
matches = candidates(target)
|
|
249
282
|
return matches.first.identity if matches.size == 1
|
data/lib/constable.rb
CHANGED
|
@@ -130,14 +130,60 @@ module Constable
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# Code-level configuration set from test/case_helper.rb.
|
|
133
|
+
# The Ruby half of configuration, set from test/case_helper.rb.
|
|
134
|
+
#
|
|
135
|
+
# Anything that is *code* -- custom matchers, tier base classes, one-time global setup --
|
|
136
|
+
# can only live here. Everything that is merely a *setting* can live in either place, and
|
|
137
|
+
# this is the one that wins:
|
|
138
|
+
#
|
|
139
|
+
# a CLI flag for one run
|
|
140
|
+
# Constable.configure in case_helper.rb, because it is code and ran deliberately
|
|
141
|
+
# .constable/config.yml the declared default for the project
|
|
142
|
+
# Constable's defaults
|
|
143
|
+
#
|
|
144
|
+
# Until 1.1.0 the four accessors here were read by nothing at all: `c.parallel_workers = 4`
|
|
145
|
+
# was in the generated case_helper.rb, documented, and silently ignored.
|
|
133
146
|
class Configuration
|
|
134
|
-
|
|
147
|
+
# Every setting .constable/config.yml understands, settable in Ruby as well. A nil is
|
|
148
|
+
# "not set here" rather than "set to nothing", so leaving one alone defers to the file.
|
|
149
|
+
SETTINGS = %i[
|
|
150
|
+
cold_cases warrants warrant_retries auto_relink parole_period
|
|
151
|
+
coverage coverage_threshold coverage_html fail_on_warnings parallel_workers
|
|
152
|
+
output tiers
|
|
153
|
+
].freeze
|
|
154
|
+
|
|
155
|
+
# `storage` is the one setting that cannot live here, and the reason is ordering, not
|
|
156
|
+
# preference: the blotter handle is opened before test/case_helper.rb is loaded,
|
|
157
|
+
# because `constable jail`, `warrants`, `watchlist` and `status` all read the docket
|
|
158
|
+
# without booting the app at all. By the time this block runs, it is already open.
|
|
159
|
+
#
|
|
160
|
+
# Raising beats accepting the value and quietly using the old path -- silently
|
|
161
|
+
# ignoring a setting somebody wrote is the failure mode this whole class was fixed for.
|
|
162
|
+
SETTINGS_ONLY_IN_YAML = %i[storage].freeze
|
|
163
|
+
|
|
164
|
+
attr_accessor(*SETTINGS, :seed)
|
|
165
|
+
|
|
166
|
+
def storage=(_value)
|
|
167
|
+
raise ConfigurationError,
|
|
168
|
+
"storage must be set in .constable/config.yml, not Constable.configure. The " \
|
|
169
|
+
"blotter is opened before case_helper.rb loads, so that `constable jail` and " \
|
|
170
|
+
"`constable status` can read the docket without booting the app -- by the " \
|
|
171
|
+
"time this block runs the connection is already open."
|
|
172
|
+
end
|
|
135
173
|
|
|
136
174
|
def initialize
|
|
137
175
|
@before_suite_hooks = []
|
|
138
176
|
@after_suite_hooks = []
|
|
139
177
|
end
|
|
140
178
|
|
|
179
|
+
# Only the settings actually assigned, ready to merge over the file's values.
|
|
180
|
+
def overrides
|
|
181
|
+
SETTINGS.each_with_object({}) do |name, out|
|
|
182
|
+
value = public_send(name)
|
|
183
|
+
out[name.to_s] = value unless value.nil?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
141
187
|
def before_suite(&block) = @before_suite_hooks << block
|
|
142
188
|
def after_suite(&block) = @after_suite_hooks << block
|
|
143
189
|
|
|
@@ -45,6 +45,8 @@ module Constable
|
|
|
45
45
|
desc: "Don't write the example case under test/cases/"
|
|
46
46
|
class_option :skip_support, type: :boolean, default: false,
|
|
47
47
|
desc: "Don't write the example files under test/support/"
|
|
48
|
+
class_option :skip_config, type: :boolean, default: false,
|
|
49
|
+
desc: "Don't write .constable/config.yml -- configure in Ruby instead"
|
|
48
50
|
|
|
49
51
|
COLD_CASE_HEADER = <<~RUBY
|
|
50
52
|
# Cold cases: your existing RSpec/Minitest files, run verbatim through their own
|
|
@@ -72,6 +74,15 @@ module Constable
|
|
|
72
74
|
/.constable/*.sqlite3-*
|
|
73
75
|
TEXT
|
|
74
76
|
|
|
77
|
+
NEW_SETTINGS_HEADER = <<~TEXT
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Added by `rails generate constable:install` on a later upgrade. These settings
|
|
80
|
+
# did not exist when this file was written; each is shown at its default, so
|
|
81
|
+
# deleting any of them changes nothing.
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
TEXT
|
|
85
|
+
|
|
75
86
|
RUBOCOP_EXTENSION = "rubocop-constable"
|
|
76
87
|
|
|
77
88
|
def create_case_helper
|
|
@@ -85,8 +96,32 @@ module Constable
|
|
|
85
96
|
template "authenticatable.rb.tt", "test/support/authenticatable.rb"
|
|
86
97
|
end
|
|
87
98
|
|
|
99
|
+
# The config file doubles as the reference -- every key at its default, with the
|
|
100
|
+
# reasoning above it -- which only works if it stays current. A gem upgrade cannot
|
|
101
|
+
# rewrite it (that would clobber your settings) and Thor's only other answer is to
|
|
102
|
+
# skip the file entirely, so before 1.1.0 a setting added after you installed was
|
|
103
|
+
# invisible: `output` shipped in 1.0.0 and never appeared in an existing config.
|
|
104
|
+
#
|
|
105
|
+
# So: create it if it is missing, and otherwise append only the settings it does not
|
|
106
|
+
# already mention. Your edits and comments are never touched.
|
|
88
107
|
def create_config
|
|
89
|
-
|
|
108
|
+
if options[:skip_config]
|
|
109
|
+
say_status :skip, ".constable/config.yml (configure in test/case_helper.rb instead)", :blue
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
path = File.join(destination_root, ".constable/config.yml")
|
|
114
|
+
return template("config.yml.tt", ".constable/config.yml") unless File.exist?(path)
|
|
115
|
+
|
|
116
|
+
missing = missing_config_blocks(File.read(path))
|
|
117
|
+
if missing.empty?
|
|
118
|
+
say_status :identical, ".constable/config.yml (every setting is documented)", :blue
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
names = missing.flat_map { |block| config_keys_in(block) }
|
|
123
|
+
say_status :append, ".constable/config.yml (#{names.join(", ")})", :green
|
|
124
|
+
append_to_file ".constable/config.yml", "\n#{NEW_SETTINGS_HEADER}#{missing.join("\n\n")}\n"
|
|
90
125
|
end
|
|
91
126
|
|
|
92
127
|
def create_example_case
|
|
@@ -204,6 +239,31 @@ module Constable
|
|
|
204
239
|
end
|
|
205
240
|
end
|
|
206
241
|
|
|
242
|
+
# Blocks in the shipped reference whose settings the existing file never mentions.
|
|
243
|
+
# A "block" is a run of lines between blank ones: the comment and the setting it
|
|
244
|
+
# explains travel together, because a bare key with no reasoning is not a reference.
|
|
245
|
+
def missing_config_blocks(existing)
|
|
246
|
+
present = config_keys_in(existing)
|
|
247
|
+
|
|
248
|
+
reference_blocks.select do |block|
|
|
249
|
+
keys = config_keys_in(block)
|
|
250
|
+
keys.any? && (keys - present) == keys
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def reference_blocks
|
|
255
|
+
File.read(find_in_source_paths("config.yml.tt")).split(/\n{2,}/).map(&:rstrip).reject(&:empty?)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Top-level YAML keys only, ignoring comments and nested ones -- a commented-out
|
|
259
|
+
# example is a suggestion, not a declaration.
|
|
260
|
+
def config_keys_in(text)
|
|
261
|
+
text.lines.filter_map do |line|
|
|
262
|
+
match = line.match(/\A([a-z][a-z0-9_]*):/)
|
|
263
|
+
match && match[1]
|
|
264
|
+
end.uniq
|
|
265
|
+
end
|
|
266
|
+
|
|
207
267
|
def gemfile_contents
|
|
208
268
|
File.read(File.join(destination_root, "Gemfile"))
|
|
209
269
|
end
|
|
@@ -139,16 +139,49 @@ end
|
|
|
139
139
|
# Code-level configuration.
|
|
140
140
|
# -----------------------------------------------------------------------------
|
|
141
141
|
|
|
142
|
+
# Code only, by default: matchers and suite hooks can be written nowhere else.
|
|
143
|
+
#
|
|
144
|
+
# Settings can be written here too, and this file wins over
|
|
145
|
+
# .constable/config.yml -- a CLI flag wins over both, for one run. Use the file
|
|
146
|
+
# for values that differ per project or per branch, where being greppable
|
|
147
|
+
# without running anything is worth something. Use Ruby when the value has to be
|
|
148
|
+
# computed, which YAML cannot do:
|
|
149
|
+
#
|
|
150
|
+
# c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
|
|
151
|
+
# c.coverage = ENV["CI"] == "true"
|
|
152
|
+
#
|
|
153
|
+
# Every setting, at its default -- uncomment what you want and delete the rest.
|
|
154
|
+
# `.constable/config.yml` explains each one at length and is optional; between
|
|
155
|
+
# the two, this file can be the only place you configure Constable.
|
|
156
|
+
#
|
|
157
|
+
# c.cold_cases = [] # RSpec/Minitest globs to run as cold cases
|
|
158
|
+
# c.parallel_workers = "auto" # or an integer. Each worker gets its own database
|
|
159
|
+
# c.output = :concise # or :expanded -- a line per test, with timings
|
|
160
|
+
# c.fail_on_warnings = false # CI: fail when the warning count is not trending down
|
|
161
|
+
# c.warrants = false # rerun a failure in isolation before believing it
|
|
162
|
+
# c.warrant_retries = 5
|
|
163
|
+
# c.parole_period = 10 # clean runs before a paroled test releases itself
|
|
164
|
+
# c.auto_relink = false # auto-confirm rename detection
|
|
165
|
+
# c.coverage = false
|
|
166
|
+
# c.coverage_threshold = 90 # diff-based: only lines changed in this diff
|
|
167
|
+
# c.coverage_html = false
|
|
168
|
+
# c.tiers = { "unit" => "test/cases/models/**/*" }
|
|
169
|
+
#
|
|
170
|
+
# `storage` is the one setting that cannot go here: the blotter is opened before
|
|
171
|
+
# this file loads, so `constable jail` and `constable status` can read the docket
|
|
172
|
+
# without booting the app. Setting it here raises rather than being ignored. Use
|
|
173
|
+
# .constable/config.yml, or CONSTABLE_STORAGE_URL / _PATH / _ADAPTER.
|
|
174
|
+
#
|
|
175
|
+
# Not config/initializers/: initializers run on every boot including production,
|
|
176
|
+
# where a test-only gem is not in the bundle. Same reason RSpec, SimpleCov and
|
|
177
|
+
# WebMock all configure from the test helper rather than an initializer.
|
|
178
|
+
# -----------------------------------------------------------------------------
|
|
179
|
+
|
|
142
180
|
Constable.configure do |c|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
# One-time global setup, run once per process before the whole suite. This is
|
|
149
|
-
# for configuring the world -- drivers, adapters, formats -- and deliberately
|
|
150
|
-
# not for creating records that tests then share. See the note at the bottom
|
|
151
|
-
# of this file about why that distinction is the whole ballgame.
|
|
181
|
+
# One-time global setup, run once per process before the whole suite. For
|
|
182
|
+
# configuring the world -- drivers, adapters, formats -- and deliberately not
|
|
183
|
+
# for creating records that tests then share. See the note at the bottom of
|
|
184
|
+
# this file about why that distinction is the whole ballgame.
|
|
152
185
|
#
|
|
153
186
|
# c.before_suite do
|
|
154
187
|
# Capybara.default_driver = :rack_test
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
# .constable/config.yml
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# anything that is Ruby -- custom matchers, tier base classes, one-time global
|
|
5
|
-
# setup -- lives in test/case_helper.rb instead.
|
|
3
|
+
# Optional. Everything here can also be written in Ruby, in test/case_helper.rb:
|
|
6
4
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# Constable.configure do |c|
|
|
6
|
+
# c.parallel_workers = ENV.fetch("CI_WORKERS", 4).to_i
|
|
7
|
+
# c.output = :expanded
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# ...which is the better home for anything computed, and the more familiar one if you
|
|
11
|
+
# come from RSpec's spec_helper.rb. Ruby wins over this file; a CLI flag wins over both.
|
|
12
|
+
#
|
|
13
|
+
# Two reasons this file exists at all:
|
|
14
|
+
#
|
|
15
|
+
# 1. `storage` cannot go anywhere else. The blotter is opened before case_helper.rb
|
|
16
|
+
# loads, so that `constable jail`, `warrants`, `watchlist` and `status` can read the
|
|
17
|
+
# docket without booting the app -- a broken app should not stop you reading the
|
|
18
|
+
# docket. Setting it in Ruby raises rather than being quietly ignored.
|
|
19
|
+
#
|
|
20
|
+
# 2. A settings file is greppable and diffable without executing anything, which is
|
|
21
|
+
# what you want for the values that differ per project or per branch.
|
|
22
|
+
#
|
|
23
|
+
# Every key Constable understands is below at its default, so this doubles as the
|
|
24
|
+
# reference. Delete anything you have not changed; the behavior is identical either way.
|
|
25
|
+
# Re-run `rails generate constable:install --skip` after upgrading and any settings added
|
|
26
|
+
# since will be appended here, leaving your own values and comments alone.
|
|
11
27
|
|
|
12
28
|
# Glob paths to run as cold cases: original RSpec/Minitest files, driven verbatim
|
|
13
29
|
# through their own real engine, with pass/fail/timing fed into Constable's
|