constable-rails 0.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 +7 -0
- data/CHANGELOG.md +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +515 -0
- data/exe/constable +7 -0
- data/lib/constable/case.rb +336 -0
- data/lib/constable/cli.rb +475 -0
- data/lib/constable/cold_case/minitest.rb +342 -0
- data/lib/constable/cold_case/rspec.rb +334 -0
- data/lib/constable/cold_case.rb +280 -0
- data/lib/constable/config.rb +125 -0
- data/lib/constable/coverage.rb +951 -0
- data/lib/constable/diff.rb +212 -0
- data/lib/constable/dsl.rb +833 -0
- data/lib/constable/identity.rb +121 -0
- data/lib/constable/importer/modernizer.rb +860 -0
- data/lib/constable/importer/reopener.rb +468 -0
- data/lib/constable/importer.rb +51 -0
- data/lib/constable/investigation.rb +67 -0
- data/lib/constable/isolation.rb +171 -0
- data/lib/constable/jail.rb +399 -0
- data/lib/constable/log_router.rb +197 -0
- data/lib/constable/matchers.rb +834 -0
- data/lib/constable/order_audit.rb +130 -0
- data/lib/constable/rails_support.rb +213 -0
- data/lib/constable/railtie.rb +36 -0
- data/lib/constable/registry.rb +57 -0
- data/lib/constable/reporter.rb +625 -0
- data/lib/constable/result.rb +149 -0
- data/lib/constable/runner.rb +697 -0
- data/lib/constable/selection.rb +205 -0
- data/lib/constable/storage/adapter.rb +91 -0
- data/lib/constable/storage/mysql_adapter.rb +125 -0
- data/lib/constable/storage/postgres_adapter.rb +125 -0
- data/lib/constable/storage/sqlite_adapter.rb +84 -0
- data/lib/constable/storage.rb +847 -0
- data/lib/constable/version.rb +5 -0
- data/lib/constable/warrants.rb +290 -0
- data/lib/constable-rails.rb +16 -0
- data/lib/constable.rb +151 -0
- data/lib/generators/constable/base.rb +99 -0
- data/lib/generators/constable/channel/channel_generator.rb +20 -0
- data/lib/generators/constable/channel/templates/channel_case.rb.tt +29 -0
- data/lib/generators/constable/controller/controller_generator.rb +25 -0
- data/lib/generators/constable/controller/templates/controller_case.rb.tt +32 -0
- data/lib/generators/constable/generator/generator_generator.rb +31 -0
- data/lib/generators/constable/generator/templates/generator_case.rb.tt +28 -0
- data/lib/generators/constable/helper/helper_generator.rb +23 -0
- data/lib/generators/constable/helper/templates/helper_case.rb.tt +19 -0
- data/lib/generators/constable/import_generator.rb +137 -0
- data/lib/generators/constable/install_generator.rb +188 -0
- data/lib/generators/constable/integration/integration_generator.rb +27 -0
- data/lib/generators/constable/integration/templates/request_case.rb.tt +22 -0
- data/lib/generators/constable/job/job_generator.rb +20 -0
- data/lib/generators/constable/job/templates/job_case.rb.tt +33 -0
- data/lib/generators/constable/mailbox/mailbox_generator.rb +20 -0
- data/lib/generators/constable/mailbox/templates/mailbox_case.rb.tt +26 -0
- data/lib/generators/constable/mailer/mailer_generator.rb +32 -0
- data/lib/generators/constable/mailer/templates/mailer_case.rb.tt +34 -0
- data/lib/generators/constable/mailer/templates/preview.rb.tt +14 -0
- data/lib/generators/constable/model/model_generator.rb +31 -0
- data/lib/generators/constable/model/templates/model_case.rb.tt +37 -0
- data/lib/generators/constable/resource/resource_generator.rb +27 -0
- data/lib/generators/constable/scaffold/scaffold_generator.rb +42 -0
- data/lib/generators/constable/scaffold/templates/api_controller_case.rb.tt +54 -0
- data/lib/generators/constable/scaffold/templates/controller_case.rb.tt +70 -0
- data/lib/generators/constable/scaffold/templates/system_case.rb.tt +53 -0
- data/lib/generators/constable/system/system_generator.rb +20 -0
- data/lib/generators/constable/system/templates/system_case.rb.tt +18 -0
- data/lib/generators/constable/templates/authenticatable.rb.tt +31 -0
- data/lib/generators/constable/templates/case_helper.rb.tt +179 -0
- data/lib/generators/constable/templates/config.yml.tt +67 -0
- data/lib/generators/constable/templates/example_case.rb.tt +56 -0
- data/lib/generators/constable/templates/matchers.rb.tt +36 -0
- data/lib/generators/constable/templates/rubocop.yml.tt +12 -0
- metadata +209 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Constable
|
|
6
|
+
# The command line. Every command answers one question and prints the answer -- stdout is
|
|
7
|
+
# reserved for results, so anything a Rails app would normally shout into the terminal
|
|
8
|
+
# goes to log/test.log instead.
|
|
9
|
+
class CLI < Thor
|
|
10
|
+
EXIT_CLEAN = 0
|
|
11
|
+
EXIT_FAILED = 1
|
|
12
|
+
EXIT_USAGE = 2
|
|
13
|
+
|
|
14
|
+
def self.exit_on_failure? = true
|
|
15
|
+
|
|
16
|
+
# Thor's own exit status handling doesn't distinguish "tests failed" from "command was
|
|
17
|
+
# wrong", and CI needs to. Commands return a status; this turns it into one.
|
|
18
|
+
def self.dispatch!(argv)
|
|
19
|
+
start(argv)
|
|
20
|
+
rescue Thor::Error => e
|
|
21
|
+
warn e.message
|
|
22
|
+
exit(EXIT_USAGE)
|
|
23
|
+
rescue Interrupt
|
|
24
|
+
warn "\ninterrupted"
|
|
25
|
+
exit(EXIT_FAILED)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class_option :"no-color", type: :boolean, default: false, desc: "Disable ANSI color"
|
|
29
|
+
|
|
30
|
+
desc "test [PATH[:LINE]]", "Run the suite -- native and cold cases side by side"
|
|
31
|
+
long_desc <<~DESC
|
|
32
|
+
With no arguments, runs only the cases touched by your current git diff. CI should
|
|
33
|
+
always pass --full. PATH runs one file; PATH:LINE runs the single investigation at
|
|
34
|
+
that line.
|
|
35
|
+
DESC
|
|
36
|
+
option :full, type: :boolean, default: false, desc: "Run the whole suite (always use this in CI)"
|
|
37
|
+
option :unsafe, type: :boolean, default: false, desc: "Run cold cases only"
|
|
38
|
+
option :jail, type: :boolean, default: false, desc: "Jail failures instead of failing the build"
|
|
39
|
+
option :warrants, type: :boolean, desc: "Turn the flaky detector on for this run"
|
|
40
|
+
option :coverage, type: :boolean, desc: "Record coverage for this run"
|
|
41
|
+
option :seed, type: :numeric, desc: "Replay a previous run's order"
|
|
42
|
+
option :workers, type: :numeric, desc: "Parallel workers (default: config, or auto)"
|
|
43
|
+
option :verbose, type: :boolean, default: false, desc: "Stream log/test.log to stdout"
|
|
44
|
+
option :tier, type: :string, desc: "Run one tier only: unit, integration or system"
|
|
45
|
+
def test(*paths)
|
|
46
|
+
config = load_config
|
|
47
|
+
LogRouter.route!(verbose: options[:verbose])
|
|
48
|
+
|
|
49
|
+
selection = Selection.new(
|
|
50
|
+
paths,
|
|
51
|
+
config: config,
|
|
52
|
+
root: Constable.root,
|
|
53
|
+
full: options[:full],
|
|
54
|
+
unsafe_only: options[:unsafe],
|
|
55
|
+
tier: options[:tier]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
runner = Runner.new(
|
|
59
|
+
selection: selection,
|
|
60
|
+
config: config,
|
|
61
|
+
reporter: reporter(config),
|
|
62
|
+
storage: Constable.storage,
|
|
63
|
+
seed: options[:seed],
|
|
64
|
+
jail_mode: options[:jail],
|
|
65
|
+
warrants: options[:warrants],
|
|
66
|
+
coverage: options[:coverage],
|
|
67
|
+
workers: options[:workers],
|
|
68
|
+
verbose: options[:verbose]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
exit(runner.call)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc "watchlist", "Everything under supervision right now: jailed, paroled and warranted"
|
|
75
|
+
def watchlist
|
|
76
|
+
config = load_config
|
|
77
|
+
storage = Constable.storage
|
|
78
|
+
jail = Jail.new(config: config, storage: storage)
|
|
79
|
+
warrants = Warrants.new(config: config, storage: storage)
|
|
80
|
+
|
|
81
|
+
say_table("JAILED", jail.jailed) do |entry|
|
|
82
|
+
[entry.location, entry.label, entry.reason, jailed_on(entry)]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
say_table("ON PAROLE", jail.paroled) do |entry|
|
|
86
|
+
["#{entry.parole_day}/#{config.parole_period} clean"].then do |clean|
|
|
87
|
+
[entry.location, entry.label, clean.first, jailed_on(entry)]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
say_table("WARRANTS", warrants.entries) do |entry|
|
|
92
|
+
[entry.location, entry.label, "issued #{short_date(entry.issued_at)}"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
exit(EXIT_CLEAN)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "status", "How the suite is doing over time"
|
|
99
|
+
long_desc <<~DESC
|
|
100
|
+
The trend view: how much of the suite is still running as cold cases, how the last
|
|
101
|
+
runs went, and which tests have been the slowest historically. For what is flagged
|
|
102
|
+
right now -- jailed, paroled, warranted -- use `constable watchlist` instead.
|
|
103
|
+
DESC
|
|
104
|
+
def status
|
|
105
|
+
storage = Constable.storage
|
|
106
|
+
runs = storage.runs(limit: 20)
|
|
107
|
+
|
|
108
|
+
if runs.empty?
|
|
109
|
+
say "No runs recorded yet. Run: constable test --full"
|
|
110
|
+
exit(EXIT_CLEAN)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
print_adoption(storage, runs)
|
|
114
|
+
print_recent_runs(runs)
|
|
115
|
+
print_historical_slowest(storage)
|
|
116
|
+
exit(EXIT_CLEAN)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
desc "beat", "Coverage: overall %, per-file breakdown and the unpatrolled list"
|
|
120
|
+
option :html, type: :boolean, default: false, desc: "Write a browsable HTML report"
|
|
121
|
+
def beat
|
|
122
|
+
config = load_config
|
|
123
|
+
LogRouter.route!(verbose: false)
|
|
124
|
+
|
|
125
|
+
# The beat is walked, not remembered: a stored snapshot carries percentages but not
|
|
126
|
+
# the per-line detail the breakdown and the HTML report are made of. So this runs
|
|
127
|
+
# the full suite with coverage on and reports on what it finds.
|
|
128
|
+
selection = Selection.new([], config: config, root: Constable.root, full: true)
|
|
129
|
+
runner = Runner.new(
|
|
130
|
+
selection: selection, config: config, reporter: reporter(config),
|
|
131
|
+
storage: Constable.storage, coverage: true
|
|
132
|
+
)
|
|
133
|
+
runner.call
|
|
134
|
+
report = runner.coverage_report
|
|
135
|
+
|
|
136
|
+
unless report
|
|
137
|
+
say "No coverage was recorded. Is there anything to run?"
|
|
138
|
+
exit(EXIT_FAILED)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
say ""
|
|
142
|
+
say report.beat_report
|
|
143
|
+
|
|
144
|
+
if options[:html] || config.coverage_html?
|
|
145
|
+
path = Constable::Coverage.write_html(report)
|
|
146
|
+
say "\nHTML report: #{path}"
|
|
147
|
+
end
|
|
148
|
+
exit(EXIT_CLEAN)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
desc "import", "Adopt an existing suite as cold cases -- verbatim, nothing rewritten"
|
|
152
|
+
option :from, type: :string, required: true, enum: %w[rspec minitest], desc: "Source framework"
|
|
153
|
+
option :strategy, type: :string, default: "auto", enum: %w[auto config superclass],
|
|
154
|
+
desc: "auto: widest clean glob, else a superclass swap. " \
|
|
155
|
+
"config: no file changes at all. superclass: one line per file"
|
|
156
|
+
option :"dry-run", type: :boolean, default: false, desc: "Show what would change"
|
|
157
|
+
def import
|
|
158
|
+
result = Importer.run(
|
|
159
|
+
from: options[:from].to_sym,
|
|
160
|
+
config: load_config,
|
|
161
|
+
dry_run: options[:"dry-run"],
|
|
162
|
+
strategy: options[:strategy].to_sym
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
say result.summary
|
|
166
|
+
say "\nNothing was rewritten -- cold cases run through their own engine, unchanged." if result.any_changes?
|
|
167
|
+
exit(EXIT_CLEAN)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
desc "modernize PATH [PATH...]", "Opt-in AST rewrite into the native DSL"
|
|
171
|
+
long_desc <<~DESC
|
|
172
|
+
Reports by default and writes nothing. --alongside writes a new *_case.rb next to the
|
|
173
|
+
original; --in-place overwrites it. Anything the rewrite cannot decide safely is
|
|
174
|
+
flagged for a human rather than guessed at, and never converted.
|
|
175
|
+
DESC
|
|
176
|
+
option :alongside, type: :boolean, default: false, desc: "Write PATH_case.rb beside the original"
|
|
177
|
+
option :"in-place", type: :boolean, default: false, desc: "Overwrite the file"
|
|
178
|
+
option :"show-source", type: :boolean, default: false, desc: "Print the rewritten source"
|
|
179
|
+
def modernize(*paths)
|
|
180
|
+
if paths.empty?
|
|
181
|
+
warn "modernize needs at least one path"
|
|
182
|
+
exit(EXIT_USAGE)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
mode = if options[:"in-place"] then :in_place
|
|
186
|
+
elsif options[:alongside] then :alongside
|
|
187
|
+
else :none
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
run = Importer.modernize(paths, config: load_config, write: mode)
|
|
191
|
+
|
|
192
|
+
run.results.each do |result|
|
|
193
|
+
if result.error
|
|
194
|
+
say "#{result.relative_path}: #{result.error}"
|
|
195
|
+
next
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
counts = result.counts
|
|
199
|
+
say "#{result.relative_path} — #{describe_counts(counts)}"
|
|
200
|
+
say(result.source) if options[:"show-source"]
|
|
201
|
+
|
|
202
|
+
result.flags.each { |flag| say " flagged #{flag[:location]} #{flag[:reason]}" }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
say "\nWrote #{run.written.size} file(s)." if run.written.any?
|
|
206
|
+
say "Report: #{run.report_path}" if run.report_path
|
|
207
|
+
say "\nNothing was written. Re-run with --alongside or --in-place." if mode == :none
|
|
208
|
+
exit(run.ok? ? EXIT_CLEAN : EXIT_FAILED)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc "version", "Print the version"
|
|
212
|
+
def version
|
|
213
|
+
say "constable #{Constable::VERSION} (gem: constable-rails)"
|
|
214
|
+
exit(EXIT_CLEAN)
|
|
215
|
+
end
|
|
216
|
+
map %w[-v --version] => :version
|
|
217
|
+
|
|
218
|
+
# --- subcommands -----------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
# The jail docket. Jailing isn't hiding -- it swaps "blocks the build" for "tracked and
|
|
221
|
+
# skipped", and nothing leaves the docket without someone deciding it should.
|
|
222
|
+
class JailCommand < Thor
|
|
223
|
+
def self.exit_on_failure? = true
|
|
224
|
+
|
|
225
|
+
default_task :list
|
|
226
|
+
|
|
227
|
+
desc "list", "Every jailed test: reason, file:line, date jailed"
|
|
228
|
+
def list
|
|
229
|
+
config = Constable.config
|
|
230
|
+
jail = Jail.new(config: config, storage: Constable.storage)
|
|
231
|
+
entries = jail.entries
|
|
232
|
+
|
|
233
|
+
if entries.empty?
|
|
234
|
+
say "The docket is empty."
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
entries.each do |entry|
|
|
239
|
+
state = entry.paroled? ? "on parole" : "jailed"
|
|
240
|
+
say format("%-9s %s", state, entry.location)
|
|
241
|
+
say " #{entry.label}"
|
|
242
|
+
say " #{entry.reason}"
|
|
243
|
+
say " jailed #{short_date(entry.jailed_at)}#{repeat_note(entry)}"
|
|
244
|
+
say ""
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Thor reserves `run` as a method name, so the command is named for the user and the
|
|
249
|
+
# method is named for Ruby.
|
|
250
|
+
map "run" => :rerun
|
|
251
|
+
|
|
252
|
+
desc "run [PATH:LINE]", "Re-run jailed tests -- sequentially, for clean attribution"
|
|
253
|
+
option :full, type: :boolean, default: false,
|
|
254
|
+
desc: "Re-run the whole docket in one parallel batch (faster, coarser)"
|
|
255
|
+
def rerun(path = nil)
|
|
256
|
+
config = Constable.config
|
|
257
|
+
storage = Constable.storage
|
|
258
|
+
jail = Jail.new(config: config, storage: storage)
|
|
259
|
+
targets = path ? [path] : jail.entries.map(&:location)
|
|
260
|
+
|
|
261
|
+
if targets.empty?
|
|
262
|
+
say "The docket is empty."
|
|
263
|
+
return
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
selection = Selection.new(targets, config: config, root: Constable.root, full: true)
|
|
267
|
+
runner = Runner.new(
|
|
268
|
+
selection: selection,
|
|
269
|
+
config: config,
|
|
270
|
+
storage: storage,
|
|
271
|
+
jail_run: true,
|
|
272
|
+
# Sequential by default: one test at a time gives clean attribution for a
|
|
273
|
+
# docket nobody trusts yet. --full trades that for speed.
|
|
274
|
+
workers: options[:full] ? nil : 1
|
|
275
|
+
)
|
|
276
|
+
status = runner.call
|
|
277
|
+
|
|
278
|
+
# A pass here is a candidate, never a release. One green run doesn't prove anything.
|
|
279
|
+
passed = runner.results.select(&:passed?)
|
|
280
|
+
if passed.any?
|
|
281
|
+
say "\nCandidates for parole (a pass here is not a release):"
|
|
282
|
+
passed.each { |r| say " constable jail parole #{r.location} # #{r.display_label}" }
|
|
283
|
+
end
|
|
284
|
+
exit(status)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
desc "parole PATH:LINE", "Move a jailed test to parole -- runs again, but watched"
|
|
288
|
+
def parole(locator)
|
|
289
|
+
act(locator) { |jail, identity| jail.parole(identity) }
|
|
290
|
+
say "Paroled. It runs normally now; one failure sends it straight back."
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
desc "release PATH:LINE", "Fully release a test, no supervision"
|
|
294
|
+
def release(locator)
|
|
295
|
+
act(locator) { |jail, identity| jail.release(identity) }
|
|
296
|
+
say "Released."
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
no_commands do
|
|
300
|
+
def act(locator)
|
|
301
|
+
jail = Jail.new(config: Constable.config, storage: Constable.storage)
|
|
302
|
+
identity = jail.identity_for(locator)
|
|
303
|
+
unless identity
|
|
304
|
+
warn "Nothing on the docket at #{locator}"
|
|
305
|
+
exit(EXIT_USAGE)
|
|
306
|
+
end
|
|
307
|
+
yield jail, identity
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def repeat_note(entry)
|
|
311
|
+
count = entry.times_jailed
|
|
312
|
+
count > 1 ? " (this is its #{Reporter.ordinalize(count)} time in jail)" : ""
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def short_date(value) = value.to_s[0, 10]
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Warrants answer a different question from jail: not "does this block the build" but
|
|
320
|
+
# "is this failure even real."
|
|
321
|
+
class WarrantsCommand < Thor
|
|
322
|
+
def self.exit_on_failure? = true
|
|
323
|
+
|
|
324
|
+
default_task :list
|
|
325
|
+
|
|
326
|
+
desc "list", "Every test currently under a warrant"
|
|
327
|
+
def list
|
|
328
|
+
warrants = Warrants.new(config: Constable.config, storage: Constable.storage)
|
|
329
|
+
entries = warrants.entries
|
|
330
|
+
|
|
331
|
+
if entries.empty?
|
|
332
|
+
say "No warrants outstanding."
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
entries.each do |entry|
|
|
337
|
+
say entry.location
|
|
338
|
+
say " #{entry.label}"
|
|
339
|
+
say " issued #{entry.issued_at.to_s[0, 10]}, last seen #{entry.last_seen_at.to_s[0, 10]}"
|
|
340
|
+
say ""
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
desc "release PATH:LINE", "Clear a warrant by hand"
|
|
345
|
+
def release(locator)
|
|
346
|
+
warrants = Warrants.new(config: Constable.config, storage: Constable.storage)
|
|
347
|
+
identity = warrants.identity_for(locator)
|
|
348
|
+
unless identity
|
|
349
|
+
warn "No warrant at #{locator}"
|
|
350
|
+
exit(EXIT_USAGE)
|
|
351
|
+
end
|
|
352
|
+
warrants.clear(identity)
|
|
353
|
+
say "Warrant cleared."
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Flake history is keyed by a content hash of the investigate block, so renames carry
|
|
358
|
+
# over on their own. This is for the case where a rename shipped with a real edit.
|
|
359
|
+
class HistoryCommand < Thor
|
|
360
|
+
def self.exit_on_failure? = true
|
|
361
|
+
|
|
362
|
+
default_task :show
|
|
363
|
+
|
|
364
|
+
desc "show", "Recent runs"
|
|
365
|
+
def show
|
|
366
|
+
storage = Constable.storage
|
|
367
|
+
storage.runs(limit: 20).each do |run|
|
|
368
|
+
say format("%s seed %-6s %s %d passed, %d failed",
|
|
369
|
+
run[:started_at], run[:seed], run[:mode], run[:passed].to_i, run[:failed].to_i)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
desc "relink OLD_HASH NEW_HASH", "Carry a test's history across a real body change"
|
|
374
|
+
def relink(old_hash, new_hash)
|
|
375
|
+
Constable.storage.relink(old_hash, new_hash)
|
|
376
|
+
say "Relinked #{old_hash} -> #{new_hash}. History carried over."
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
desc "jail SUBCOMMAND", "The jail docket"
|
|
381
|
+
subcommand "jail", JailCommand
|
|
382
|
+
|
|
383
|
+
desc "warrants SUBCOMMAND", "Outstanding warrants"
|
|
384
|
+
subcommand "warrants", WarrantsCommand
|
|
385
|
+
|
|
386
|
+
desc "history SUBCOMMAND", "Flake history"
|
|
387
|
+
subcommand "history", HistoryCommand
|
|
388
|
+
|
|
389
|
+
no_commands do
|
|
390
|
+
def load_config
|
|
391
|
+
Constable.config
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def color?
|
|
395
|
+
return false if options[:"no-color"]
|
|
396
|
+
return false unless $stdout.tty?
|
|
397
|
+
return false unless ENV["NO_COLOR"].to_s.empty?
|
|
398
|
+
|
|
399
|
+
true
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def reporter(config)
|
|
403
|
+
Reporter.new(io: $stdout, config: config, color: color?)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def say_table(heading, entries)
|
|
407
|
+
say heading
|
|
408
|
+
say "─" * heading.length
|
|
409
|
+
if entries.empty?
|
|
410
|
+
say " (none)"
|
|
411
|
+
else
|
|
412
|
+
entries.each { |entry| say " #{yield(entry).compact.join(" ")}" }
|
|
413
|
+
end
|
|
414
|
+
say ""
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def jailed_on(entry)
|
|
418
|
+
"jailed #{short_date(entry.jailed_at)}"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def short_date(value)
|
|
422
|
+
value.to_s[0, 10]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# The adoption number: what share of the suite is still opted out of native rules.
|
|
426
|
+
# It only means anything as a direction of travel, so it is shown against the run
|
|
427
|
+
# the blotter remembers furthest back.
|
|
428
|
+
def print_adoption(storage, runs)
|
|
429
|
+
totals = storage.kind_totals(limit: runs.size)
|
|
430
|
+
return if totals.empty?
|
|
431
|
+
|
|
432
|
+
latest = totals.first
|
|
433
|
+
total = latest[:native] + latest[:cold]
|
|
434
|
+
return if total.zero?
|
|
435
|
+
|
|
436
|
+
native_pct = (latest[:native] * 100.0 / total).round
|
|
437
|
+
say_table("ADOPTION", [latest]) do |entry|
|
|
438
|
+
["#{native_pct}% native", "#{entry[:native]} native", "#{entry[:cold]} cold"]
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
oldest = totals.last
|
|
442
|
+
oldest_total = oldest[:native] + oldest[:cold]
|
|
443
|
+
return if oldest_total.zero? || totals.size < 2
|
|
444
|
+
|
|
445
|
+
was = (oldest[:native] * 100.0 / oldest_total).round
|
|
446
|
+
say " #{was}% native #{totals.size} runs ago -> #{native_pct}% now"
|
|
447
|
+
say ""
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def print_recent_runs(runs)
|
|
451
|
+
say_table("RECENT RUNS", runs.first(10)) do |run|
|
|
452
|
+
[
|
|
453
|
+
short_date(run[:started_at]),
|
|
454
|
+
format("%-5s", run[:mode]),
|
|
455
|
+
"#{run[:passed].to_i} passed",
|
|
456
|
+
"#{run[:failed].to_i} failed",
|
|
457
|
+
("#{run[:jailed].to_i} jailed" if run[:jailed].to_i.positive?)
|
|
458
|
+
]
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def print_historical_slowest(storage)
|
|
463
|
+
slowest = storage.slowest(limit: 10)
|
|
464
|
+
say_table("SLOWEST, HISTORICALLY", slowest) do |entry|
|
|
465
|
+
[format("%6.2fs", entry[:average].to_f), entry[:label] || entry[:identity]]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def describe_counts(counts)
|
|
470
|
+
parts = counts.filter_map { |kind, count| "#{count} #{kind}" if count.to_i.positive? }
|
|
471
|
+
parts.empty? ? "nothing to convert" : parts.join(", ")
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|