constable-rails 1.3.3 → 1.4.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 +47 -1
- data/README.md +1 -1
- data/lib/constable/config.rb +14 -0
- data/lib/constable/jail.rb +8 -1
- data/lib/constable/version.rb +1 -1
- data/lib/constable/worker_databases.rb +71 -0
- data/lib/constable.rb +1 -1
- data/lib/generators/constable/templates/case_helper.rb.tt +1 -0
- data/lib/generators/constable/templates/config.yml.tt +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fd3b3f867d958ed8f1306a51fb6a001145d88bd4c79839dd6d603f0b565248b
|
|
4
|
+
data.tar.gz: 1dee6cf2bece623c102744cb44e271461907d386715ed435fcc871ee131dd92c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f6c418f0dd90a2e6e45733b237e8aa3c8e2e0d7c62b55d145dc7596bf047569983f243ba92558050612328f78834971cf5fef19e52efe4eb4deee0f5a751506
|
|
7
|
+
data.tar.gz: 74e6fd7f76215036de95d36ccdd84a068a71b999368e97f998e5e832c2230d3624eaf6aba4aafe0ff5edf2aa0245b15a050e85fb5784347c6407a01d72842711
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.4.0]
|
|
9
|
+
|
|
10
|
+
Two problems from the same 1,277-file suite: a docket nobody asked for, and 110 minutes.
|
|
11
|
+
|
|
12
|
+
### A flaky test is no longer jailed by itself
|
|
13
|
+
|
|
14
|
+
**This is the important one, because jailed means skipped.** A first `constable test` on a
|
|
15
|
+
real suite put **29 tests on a docket the user had never asked for** — each one recorded
|
|
16
|
+
as *"passed, then failed with no code change"* — and every one of them was silently
|
|
17
|
+
skipped from then on. A suite with order-dependent tests, which is most large suites and
|
|
18
|
+
exactly what Constable is pitched at, trips that constantly.
|
|
19
|
+
|
|
20
|
+
Automatic flake-jailing is now off by default:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
jail_flakes: false # was: always on
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`constable test --jail` still jails failures, because that is a thing you asked for.
|
|
27
|
+
Turn the automatic route back on when you want it. Nothing stops being *reported* — a
|
|
28
|
+
flaky test still fails, still shows up, still gets a warrant if warrants are on. It just
|
|
29
|
+
does not remove itself from the suite.
|
|
30
|
+
|
|
31
|
+
### Per-worker databases without a loadable schema
|
|
32
|
+
|
|
33
|
+
The same suite ran **serially for 110 minutes on a 12-core machine**, because its schema
|
|
34
|
+
cannot be loaded from `schema.rb` (Postgres custom types), so `worker_databases: off` was
|
|
35
|
+
the only setting that worked.
|
|
36
|
+
|
|
37
|
+
Postgres can copy a whole database in one statement, and that needs no schema at all:
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
CREATE DATABASE "caseflow_test_3" TEMPLATE "caseflow_test"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`worker_databases: reuse` now clones from the test database you already have, dropping a
|
|
44
|
+
stale copy and disconnecting the template first. It falls back to loading the schema when
|
|
45
|
+
there is nothing to clone, and to Postgres only — anything unexpected takes the old path
|
|
46
|
+
rather than failing. It is also simply faster than replaying a large schema once per
|
|
47
|
+
worker, so it is worth having on any Postgres app.
|
|
48
|
+
|
|
49
|
+
For a suite that could only run serially, this is the difference between one core and all
|
|
50
|
+
of them.
|
|
51
|
+
|
|
52
|
+
|
|
8
53
|
## [1.3.3]
|
|
9
54
|
|
|
10
55
|
**1.3.2 was tagged twice.** The console fix in it was rebuilt after the gem had already
|
|
@@ -619,7 +664,8 @@ Initial release.
|
|
|
619
664
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
620
665
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
621
666
|
|
|
622
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.
|
|
667
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.4.0...HEAD
|
|
668
|
+
[1.4.0]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...v1.4.0
|
|
623
669
|
[1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
|
|
624
670
|
[1.3.2]: https://github.com/Ray-Hughes/constable/compare/v1.3.1...v1.3.2
|
|
625
671
|
[1.3.1]: https://github.com/Ray-Hughes/constable/compare/v1.3.0...v1.3.1
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
**A strict Rails testing framework where fast and non-flaky are structural, not disciplinary.**
|
|
9
9
|
|
|
10
|
-
[](https://badge.fury.io/rb/constable-rails)
|
|
11
11
|
[](https://github.com/Ray-Hughes/constable/actions/workflows/ci.yml)
|
|
12
12
|
[](https://www.ruby-lang.org)
|
|
13
13
|
[](https://rubyonrails.org)
|
data/lib/constable/config.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Constable
|
|
|
22
22
|
"output" => "concise",
|
|
23
23
|
"parallel_workers" => "auto",
|
|
24
24
|
"worker_databases" => "schema",
|
|
25
|
+
"jail_flakes" => false,
|
|
25
26
|
"tiers" => {
|
|
26
27
|
"unit" => "test/cases/models/**/*",
|
|
27
28
|
"integration" => "test/cases/controllers/**/*",
|
|
@@ -76,6 +77,19 @@ module Constable
|
|
|
76
77
|
|
|
77
78
|
def cold_cases = Array(@raw["cold_cases"])
|
|
78
79
|
def warrants? = truthy(@raw["warrants"])
|
|
80
|
+
|
|
81
|
+
# Should a test that passed last run and failed this one be put on the docket by
|
|
82
|
+
# itself? Off by default, and the reason is what jailing *does*: a jailed test is
|
|
83
|
+
# skipped on every later run. Turning that on automatically means a suite quietly
|
|
84
|
+
# stops running tests nobody chose to stop running.
|
|
85
|
+
#
|
|
86
|
+
# Observed on a real suite: a first `constable test` put 29 tests on a docket the user
|
|
87
|
+
# had never asked for, and every one of them was skipped from then on. A suite with
|
|
88
|
+
# order-dependent tests -- which is most large suites, and exactly the kind Constable
|
|
89
|
+
# is pitched at -- trips this constantly.
|
|
90
|
+
#
|
|
91
|
+
# `constable test --jail` still jails failures, because that is a thing you asked for.
|
|
92
|
+
def jail_flakes? = truthy(@raw["jail_flakes"])
|
|
79
93
|
# Negative retries are a typo for "off", not an instruction to count backwards.
|
|
80
94
|
def warrant_retries = [@raw["warrant_retries"].to_i, 0].max
|
|
81
95
|
def auto_relink? = truthy(@raw["auto_relink"])
|
data/lib/constable/jail.rb
CHANGED
|
@@ -123,6 +123,13 @@ module Constable
|
|
|
123
123
|
|
|
124
124
|
# Consecutive clean runs that earn an automatic release. Floors at 1 -- a period of
|
|
125
125
|
# zero would mean "release on sight", which is not parole.
|
|
126
|
+
# Opt-in. See Config#jail_flakes? for why the default is off: jailing skips the test on
|
|
127
|
+
# every later run, and doing that to a test nobody nominated is how a suite quietly
|
|
128
|
+
# stops testing things.
|
|
129
|
+
def jail_flakes?
|
|
130
|
+
@config.respond_to?(:jail_flakes?) ? @config.jail_flakes? : false
|
|
131
|
+
end
|
|
132
|
+
|
|
126
133
|
def parole_period
|
|
127
134
|
period = @config.respond_to?(:parole_period) ? @config.parole_period.to_i : 0
|
|
128
135
|
period.positive? ? period : 10
|
|
@@ -290,7 +297,7 @@ module Constable
|
|
|
290
297
|
|
|
291
298
|
if jail_mode
|
|
292
299
|
jail_failure(result)
|
|
293
|
-
elsif flake_flip?(result)
|
|
300
|
+
elsif flake_flip?(result) && jail_flakes?
|
|
294
301
|
jail_for_flake(result)
|
|
295
302
|
else
|
|
296
303
|
result
|
data/lib/constable/version.rb
CHANGED
|
@@ -105,6 +105,73 @@ module Constable
|
|
|
105
105
|
.zip(names).each { |config, name| config._database = name if name }
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Postgres can copy a whole database in one statement:
|
|
109
|
+
#
|
|
110
|
+
# CREATE DATABASE "caseflow_test_3" TEMPLATE "caseflow_test"
|
|
111
|
+
#
|
|
112
|
+
# That matters because it needs no schema.rb at all. An app whose schema cannot
|
|
113
|
+
# rebuild the database by itself -- custom types, functions, triggers -- can still get
|
|
114
|
+
# per-worker databases this way, cloned from the test database it already has. It is
|
|
115
|
+
# also far faster than replaying a large schema once per worker.
|
|
116
|
+
#
|
|
117
|
+
# Returns true when it cloned, false when this is not Postgres or the source is not
|
|
118
|
+
# there, so the caller can fall back to loading the schema.
|
|
119
|
+
def clone_database(db_config, index)
|
|
120
|
+
return false unless postgres?(db_config)
|
|
121
|
+
|
|
122
|
+
source = db_config.database.to_s.sub(/_#{index}\z/, "")
|
|
123
|
+
target = db_config.database.to_s
|
|
124
|
+
return false if source.empty? || source == target
|
|
125
|
+
|
|
126
|
+
maintenance_connection(db_config) do |connection|
|
|
127
|
+
return false unless database_exists?(connection, source)
|
|
128
|
+
|
|
129
|
+
# A template cannot be copied while anything is connected to it.
|
|
130
|
+
disconnect_everyone_from!(connection, source)
|
|
131
|
+
connection.execute(%(DROP DATABASE IF EXISTS "#{target}"))
|
|
132
|
+
connection.execute(%(CREATE DATABASE "#{target}" TEMPLATE "#{source}"))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
true
|
|
136
|
+
rescue StandardError
|
|
137
|
+
# Cloning is the fast path, never the only one. Anything unexpected -- a permission,
|
|
138
|
+
# a Postgres version, a connection that will not drop -- falls back to the schema.
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def postgres?(db_config)
|
|
143
|
+
db_config.respond_to?(:adapter) && db_config.adapter.to_s.include?("postgre")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Postgres will not let you create a database while connected to the one you are
|
|
147
|
+
# copying, so the statements run against the cluster's own maintenance database.
|
|
148
|
+
def maintenance_connection(db_config)
|
|
149
|
+
previous = ::ActiveRecord::Base.connection_db_config
|
|
150
|
+
::ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(database: "postgres"))
|
|
151
|
+
yield ::ActiveRecord::Base.connection
|
|
152
|
+
ensure
|
|
153
|
+
::ActiveRecord::Base.establish_connection(previous)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def database_exists?(connection, name)
|
|
157
|
+
# Plain Ruby, not #present?: this runs inside a forked worker in somebody else's app,
|
|
158
|
+
# and quietly depending on ActiveSupport being loaded is how a fast path silently
|
|
159
|
+
# turns itself off.
|
|
160
|
+
value = connection.select_value("SELECT 1 FROM pg_database WHERE datname = #{connection.quote(name)}")
|
|
161
|
+
!value.nil?
|
|
162
|
+
rescue StandardError
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def disconnect_everyone_from!(connection, name)
|
|
167
|
+
connection.execute(
|
|
168
|
+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity " \
|
|
169
|
+
"WHERE datname = #{connection.quote(name)} AND pid <> pg_backend_pid()"
|
|
170
|
+
)
|
|
171
|
+
rescue StandardError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
108
175
|
# The :reuse half. Points every database this environment declares at its `_<index>`
|
|
109
176
|
# sibling, and only builds the ones that are not there yet.
|
|
110
177
|
#
|
|
@@ -121,6 +188,10 @@ module Constable
|
|
|
121
188
|
each_worker_config(index) do |db_config|
|
|
122
189
|
next if populated?(db_config)
|
|
123
190
|
|
|
191
|
+
# Clone first: it needs no schema.rb, which is the only thing that works for an
|
|
192
|
+
# app whose schema cannot rebuild the database, and it is faster besides.
|
|
193
|
+
next built << "#{db_config.database} (cloned)" if clone_database(db_config, index)
|
|
194
|
+
|
|
124
195
|
::ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
|
|
125
196
|
built << db_config.database
|
|
126
197
|
end
|
data/lib/constable.rb
CHANGED
|
@@ -149,7 +149,7 @@ module Constable
|
|
|
149
149
|
SETTINGS = %i[
|
|
150
150
|
cold_cases warrants warrant_retries auto_relink parole_period
|
|
151
151
|
coverage coverage_threshold coverage_html fail_on_warnings parallel_workers
|
|
152
|
-
worker_databases output tiers
|
|
152
|
+
worker_databases jail_flakes output tiers
|
|
153
153
|
].freeze
|
|
154
154
|
|
|
155
155
|
# `storage` is the one setting that cannot live here, and the reason is ordering, not
|
|
@@ -159,6 +159,7 @@ end
|
|
|
159
159
|
# c.worker_databases = :schema # or :reuse (keep them between runs) / :off
|
|
160
160
|
# c.output = :concise # or :expanded -- a line per test, with timings
|
|
161
161
|
# c.fail_on_warnings = false # CI: fail when the warning count is not trending down
|
|
162
|
+
# c.jail_flakes = false # auto-jail a pass->fail flip (jailed = skipped)
|
|
162
163
|
# c.warrants = false # rerun a failure in isolation before believing it
|
|
163
164
|
# c.warrant_retries = 5
|
|
164
165
|
# c.parole_period = 10 # clean runs before a paroled test releases itself
|
|
@@ -59,6 +59,14 @@ storage:
|
|
|
59
59
|
url: # postgres/mysql only, e.g.
|
|
60
60
|
# postgres://user:pass@host/constable_metadata
|
|
61
61
|
|
|
62
|
+
# Put a test on the docket by itself when it passed last run and failed this one, with no
|
|
63
|
+
# code change in between. Off by default, and the reason is what jailing *does*: a jailed
|
|
64
|
+
# test is skipped on every later run. Turning that on automatically means a suite quietly
|
|
65
|
+
# stops running tests nobody chose to stop running -- and a suite with order-dependent
|
|
66
|
+
# tests, which is most large ones, trips it constantly. `constable test --jail` still
|
|
67
|
+
# jails failures, because that is a thing you asked for.
|
|
68
|
+
jail_flakes: false
|
|
69
|
+
|
|
62
70
|
warrants: false # opt-in flaky detector -- rerun a failure in isolation before believing it
|
|
63
71
|
warrant_retries: 5 # reruns before declaring a warrant (flaky) or a genuine failure
|
|
64
72
|
auto_relink: false # auto-confirm high-confidence rename detection instead of suggesting it
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: constable-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ray Hughes
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|