audition 0.3.0 → 0.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/README.md +77 -33
- data/lib/audition/bundle_sweep.rb +24 -15
- data/lib/audition/cli.rb +127 -140
- data/lib/audition/config.rb +30 -4
- data/lib/audition/dynamic/harness.rb +219 -20
- data/lib/audition/dynamic/prober.rb +150 -44
- data/lib/audition/finding.rb +10 -2
- data/lib/audition/progress.rb +418 -0
- data/lib/audition/report/github.rb +4 -3
- data/lib/audition/report/json.rb +5 -1
- data/lib/audition/report/sweep.rb +182 -0
- data/lib/audition/report/text.rb +13 -2
- data/lib/audition/report.rb +8 -1
- data/lib/audition/rewriters.rb +4 -1
- data/lib/audition/static/analyzer.rb +86 -17
- data/lib/audition/static/checks/dependency_class_state.rb +160 -0
- data/lib/audition/static/checks/mutable_constants.rb +71 -0
- data/lib/audition/static/checks/unshareable_reads.rb +259 -0
- data/lib/audition/static/checks.rb +5 -2
- data/lib/audition/static/gem_calls.rb +1770 -0
- data/lib/audition/static/graph_audit.rb +1050 -7
- data/lib/audition/static/literal_classifier.rb +118 -18
- data/lib/audition/static/native_extensions.rb +28 -16
- data/lib/audition/static/work_split.rb +54 -0
- data/lib/audition/target.rb +82 -13
- data/lib/audition/version.rb +1 -1
- data/lib/audition.rb +2 -0
- metadata +10 -4
data/lib/audition/cli.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Audition
|
|
|
34
34
|
|
|
35
35
|
args = options[:args]
|
|
36
36
|
if args.empty?
|
|
37
|
-
@stderr.puts(USAGE)
|
|
37
|
+
@stderr.puts(style(options, io: @stderr).dim(USAGE))
|
|
38
38
|
return 2
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -53,7 +53,7 @@ module Audition
|
|
|
53
53
|
audit(target, options)
|
|
54
54
|
end
|
|
55
55
|
rescue Error => e
|
|
56
|
-
|
|
56
|
+
complain(e.message, options)
|
|
57
57
|
2
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ module Audition
|
|
|
65
65
|
dynamic_only: false, fix: false, unsafe: false,
|
|
66
66
|
dry_run: false, capabilities: false, plain: false,
|
|
67
67
|
timeout: 30, write_baseline: false, no_baseline: false,
|
|
68
|
-
deps: false, explicit: []
|
|
68
|
+
deps: false, progress: nil, workers: nil, explicit: []
|
|
69
69
|
}
|
|
70
70
|
parser = OptionParser.new do |o|
|
|
71
71
|
o.banner = USAGE
|
|
@@ -129,9 +129,20 @@ module Audition
|
|
|
129
129
|
options[:timeout] = v
|
|
130
130
|
options[:explicit] << :timeout
|
|
131
131
|
end
|
|
132
|
+
o.on("-j", "--workers COUNT", Integer,
|
|
133
|
+
"scan Ractors (default: cores, capped by " \
|
|
134
|
+
"RUBY_MAX_CPU)") do |v|
|
|
135
|
+
options[:workers] = v
|
|
136
|
+
options[:explicit] << :workers
|
|
137
|
+
end
|
|
132
138
|
o.on("--plain", "disable colors and hyperlinks") do
|
|
133
139
|
options[:plain] = true
|
|
134
140
|
end
|
|
141
|
+
o.on("--[no-]progress",
|
|
142
|
+
"narrate scan phases on stderr (default: on for a " \
|
|
143
|
+
"large tree on a terminal)") do |v|
|
|
144
|
+
options[:progress] = v
|
|
145
|
+
end
|
|
135
146
|
o.on("-v", "--version") do
|
|
136
147
|
@stdout.puts(VERSION)
|
|
137
148
|
return 0
|
|
@@ -144,11 +155,19 @@ module Audition
|
|
|
144
155
|
options[:args] = parser.parse(argv)
|
|
145
156
|
options
|
|
146
157
|
rescue OptionParser::ParseError => e
|
|
147
|
-
|
|
148
|
-
@stderr.puts(USAGE)
|
|
158
|
+
complain(e.message)
|
|
159
|
+
@stderr.puts(style(nil, io: @stderr).dim(USAGE))
|
|
149
160
|
2
|
|
150
161
|
end
|
|
151
162
|
|
|
163
|
+
def complain(message, options = nil)
|
|
164
|
+
s = style(options, io: @stderr)
|
|
165
|
+
@stderr.puts(
|
|
166
|
+
"#{s.red(s.glyph(:error))} #{s.bold("Audition")}: " \
|
|
167
|
+
"#{message}"
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
152
171
|
def audit(target, options)
|
|
153
172
|
# Read the comparison report up front: a missing or broken
|
|
154
173
|
# file should be a fast usage error, not a post-audit crash.
|
|
@@ -158,7 +177,7 @@ module Audition
|
|
|
158
177
|
directives = Directives.new
|
|
159
178
|
findings = []
|
|
160
179
|
unless options[:dynamic_only]
|
|
161
|
-
findings = filter(static_findings(target, config),
|
|
180
|
+
findings = filter(static_findings(target, config, options),
|
|
162
181
|
directives, config)
|
|
163
182
|
findings = run_fix(target, findings, options) if options[:fix]
|
|
164
183
|
end
|
|
@@ -175,9 +194,11 @@ module Audition
|
|
|
175
194
|
|
|
176
195
|
if options[:write_baseline]
|
|
177
196
|
recorded = Baseline.write(target.root, all)
|
|
197
|
+
s = style(options)
|
|
178
198
|
@stdout.puts(
|
|
179
|
-
"
|
|
180
|
-
"#{
|
|
199
|
+
"#{s.green(s.glyph(:pass))} baseline written: " \
|
|
200
|
+
"#{s.bold("#{recorded} finding(s)")} recorded in " \
|
|
201
|
+
"#{s.cyan(Baseline.path_for(target.root))}"
|
|
181
202
|
)
|
|
182
203
|
return 0
|
|
183
204
|
end
|
|
@@ -239,7 +260,10 @@ module Audition
|
|
|
239
260
|
"#{s.red("#{introduced.size} introduced")}"
|
|
240
261
|
)
|
|
241
262
|
introduced.each do |f|
|
|
242
|
-
@stdout.puts(
|
|
263
|
+
@stdout.puts(
|
|
264
|
+
" #{s.red("+")} #{f.message} " \
|
|
265
|
+
"#{s.cyan("(#{f.location})")}"
|
|
266
|
+
)
|
|
243
267
|
end
|
|
244
268
|
end
|
|
245
269
|
|
|
@@ -275,17 +299,55 @@ module Audition
|
|
|
275
299
|
baseline.filter(findings, root: target.root)
|
|
276
300
|
end
|
|
277
301
|
|
|
278
|
-
def static_findings(target, config)
|
|
302
|
+
def static_findings(target, config, options)
|
|
279
303
|
files = target.ruby_files.reject do |file|
|
|
280
304
|
config.excluded?(file.delete_prefix("#{target.root}/"))
|
|
281
305
|
end
|
|
282
306
|
compiled = target.compiled_files.reject do |file|
|
|
283
307
|
config.excluded?(file.delete_prefix("#{target.root}/"))
|
|
284
308
|
end
|
|
285
|
-
|
|
286
|
-
|
|
309
|
+
stubs = target.stub_files.reject do |file|
|
|
310
|
+
config.excluded?(file.delete_prefix("#{target.root}/"))
|
|
311
|
+
end
|
|
312
|
+
progress = Progress.for(units: files.size,
|
|
313
|
+
wanted: options[:progress], format: options[:format],
|
|
314
|
+
io: @stderr, style: options[:plain] ? plain_style : nil)
|
|
315
|
+
scan(target, files, stubs, compiled, progress,
|
|
316
|
+
workers: options[:workers]).map do |f|
|
|
317
|
+
if target.test_file?(f.path, dirs: config.test_dirs)
|
|
318
|
+
f.with(test: true)
|
|
319
|
+
else
|
|
320
|
+
f
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Phases are named for what a waiting reader wants to know;
|
|
326
|
+
# progress shares stderr with fix chatter, for the same reason.
|
|
327
|
+
def scan(target, files, stubs, compiled, progress, workers: nil)
|
|
328
|
+
progress.phase("learning", total: files.size + stubs.size) do |p|
|
|
329
|
+
Static::Checks::UnshareableReads.learn(files + stubs,
|
|
330
|
+
progress: p)
|
|
331
|
+
Static::Checks::DependencyClassState.learn(stubs)
|
|
332
|
+
end
|
|
333
|
+
per_file = progress.phase("checking", total: files.size) do |p|
|
|
334
|
+
Static::Analyzer.new.analyze_paths(files, workers: workers,
|
|
335
|
+
progress: p)
|
|
336
|
+
end
|
|
337
|
+
gem_calls = progress.phase("gem calls") do |p|
|
|
338
|
+
Static::GemCalls.new(root: target.root, stubs: stubs)
|
|
339
|
+
.analyze_paths(files, progress: p)
|
|
340
|
+
end
|
|
341
|
+
graph = progress.phase("graph") do |p|
|
|
342
|
+
Static::GraphAudit.new.analyze_paths(files,
|
|
343
|
+
constant_findings: per_file + gem_calls,
|
|
344
|
+
workers: workers, progress: p)
|
|
345
|
+
end
|
|
346
|
+
per_file + gem_calls + graph +
|
|
287
347
|
Static::NativeExtensions.new.analyze(target,
|
|
288
348
|
compiled_files: compiled)
|
|
349
|
+
ensure
|
|
350
|
+
progress.finish
|
|
289
351
|
end
|
|
290
352
|
|
|
291
353
|
# Fix chatter goes to stderr: stdout carries the report, which
|
|
@@ -299,16 +361,19 @@ module Audition
|
|
|
299
361
|
|
|
300
362
|
applied = fixer.apply(findings)
|
|
301
363
|
total = applied.values.sum
|
|
364
|
+
s = style(options, io: @stderr)
|
|
302
365
|
if total.zero?
|
|
303
|
-
@stderr.puts("nothing to fix")
|
|
366
|
+
@stderr.puts(s.dim("nothing to fix"))
|
|
304
367
|
return findings
|
|
305
368
|
end
|
|
306
369
|
|
|
307
370
|
@stderr.puts(
|
|
308
|
-
"
|
|
371
|
+
"#{s.cyan(s.glyph(:fix))} #{s.green("fixed #{total} " \
|
|
372
|
+
"finding(s)")} in #{s.bold("#{applied.size} file(s)")}"
|
|
309
373
|
)
|
|
310
|
-
|
|
311
|
-
|
|
374
|
+
config = Config.load(target.root)
|
|
375
|
+
filter(static_findings(target, config, options),
|
|
376
|
+
Directives.new, config)
|
|
312
377
|
end
|
|
313
378
|
|
|
314
379
|
def render_preview(previews, options)
|
|
@@ -316,7 +381,7 @@ module Audition
|
|
|
316
381
|
previews.each do |preview|
|
|
317
382
|
@stderr.puts(s.bold(preview[:path]))
|
|
318
383
|
preview[:hunks].each do |hunk|
|
|
319
|
-
@stderr.puts(" @ line #{hunk[:line]}")
|
|
384
|
+
@stderr.puts(s.cyan(" @ line #{hunk[:line]}"))
|
|
320
385
|
hunk[:old].each_line do |line|
|
|
321
386
|
@stderr.puts(s.red(" - #{line.chomp}"))
|
|
322
387
|
end
|
|
@@ -325,7 +390,7 @@ module Audition
|
|
|
325
390
|
end
|
|
326
391
|
end
|
|
327
392
|
end
|
|
328
|
-
@stderr.puts("dry run: no files were changed")
|
|
393
|
+
@stderr.puts(s.dim("dry run: no files were changed"))
|
|
329
394
|
end
|
|
330
395
|
|
|
331
396
|
def prober(options)
|
|
@@ -356,26 +421,32 @@ module Audition
|
|
|
356
421
|
|
|
357
422
|
File.open(path, "a") { |f| f.puts(markdown) }
|
|
358
423
|
rescue SystemCallError => e
|
|
359
|
-
|
|
360
|
-
"audition: cannot write step summary: #{e.message}"
|
|
361
|
-
)
|
|
424
|
+
complain("cannot write step summary: #{e.message}")
|
|
362
425
|
end
|
|
363
426
|
|
|
427
|
+
# Diagnostics can precede option parsing, so --plain is
|
|
428
|
+
# honored only once there is something to honor it from.
|
|
364
429
|
def style(options, io: @stdout)
|
|
365
|
-
if options
|
|
366
|
-
|
|
430
|
+
if options&.fetch(:plain, false)
|
|
431
|
+
plain_style
|
|
367
432
|
else
|
|
368
433
|
Report::Style.detect(io: io)
|
|
369
434
|
end
|
|
370
435
|
end
|
|
371
436
|
|
|
437
|
+
def plain_style
|
|
438
|
+
Report::Style.new(color: false, hyperlinks: false)
|
|
439
|
+
end
|
|
440
|
+
|
|
372
441
|
def exit_code(report, options)
|
|
373
442
|
return 0 if options[:fail_on] == :never
|
|
374
443
|
|
|
375
444
|
threshold = SEVERITIES.fetch(options[:fail_on])
|
|
445
|
+
# Test findings never load in a production boot; they are
|
|
446
|
+
# reported but do not fail the run, matching the verdict.
|
|
376
447
|
failed =
|
|
377
448
|
report.findings.any? do |f|
|
|
378
|
-
f.severity_rank >= threshold
|
|
449
|
+
f.severity_rank >= threshold && !f.test?
|
|
379
450
|
end || report.dynamic_results.any? { |r| !r.passed }
|
|
380
451
|
failed ? 1 : 0
|
|
381
452
|
end
|
|
@@ -391,30 +462,41 @@ module Audition
|
|
|
391
462
|
Target.detect(lockfile)
|
|
392
463
|
end
|
|
393
464
|
|
|
394
|
-
VERDICT_CELLS = {
|
|
395
|
-
:not_ready => "not ready", :blocked => "blocked",
|
|
396
|
-
:risky => "risky", :ready => "ready", nil => "-"
|
|
397
|
-
}.freeze
|
|
398
|
-
|
|
399
465
|
def sweep(target, options)
|
|
400
466
|
sweeper = BundleSweep.new(
|
|
401
467
|
lockfile: target.entry[:lockfile],
|
|
402
468
|
static_only: options[:static_only],
|
|
403
469
|
timeout: options[:timeout]
|
|
404
470
|
)
|
|
405
|
-
rows = sweeper
|
|
406
|
-
|
|
407
|
-
case options[:format]
|
|
408
|
-
when :json then emit_sweep_json(rows)
|
|
409
|
-
when :github then emit_sweep_github(rows)
|
|
410
|
-
else emit_sweep_table(rows, options)
|
|
411
|
-
end
|
|
471
|
+
rows = sweep_rows(sweeper, options)
|
|
472
|
+
emit_sweep(Report::Sweep.new(rows, style(options)), options)
|
|
412
473
|
return 0 if options[:fail_on] == :never
|
|
413
474
|
|
|
414
475
|
threshold = SEVERITIES.fetch(options[:fail_on])
|
|
415
476
|
(rows.any? { |r| row_failed?(r, threshold) }) ? 1 : 0
|
|
416
477
|
end
|
|
417
478
|
|
|
479
|
+
# No unit count: every gem is a scan of its own, so even a
|
|
480
|
+
# short lockfile runs long enough to narrate.
|
|
481
|
+
def sweep_rows(sweeper, options)
|
|
482
|
+
progress = Progress.for(wanted: options[:progress],
|
|
483
|
+
format: options[:format], io: @stderr,
|
|
484
|
+
style: options[:plain] ? plain_style : nil)
|
|
485
|
+
sweeper.rows(progress: progress)
|
|
486
|
+
ensure
|
|
487
|
+
progress.finish
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def emit_sweep(sweep, options)
|
|
491
|
+
case options[:format]
|
|
492
|
+
when :json then @stdout.puts(sweep.json)
|
|
493
|
+
when :github
|
|
494
|
+
@stdout.puts(sweep.annotations)
|
|
495
|
+
append_step_summary(sweep.markdown)
|
|
496
|
+
else @stdout.puts(sweep.render(**table_opts(options)))
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
418
500
|
# Same contract as a direct audit: a row fails when it carries
|
|
419
501
|
# findings at or above --fail-on; not-ready rows always fail.
|
|
420
502
|
def row_failed?(row, threshold)
|
|
@@ -426,14 +508,6 @@ module Audition
|
|
|
426
508
|
hits.positive?
|
|
427
509
|
end
|
|
428
510
|
|
|
429
|
-
def sweep_progress
|
|
430
|
-
return nil unless @stderr.respond_to?(:tty?) && @stderr.tty?
|
|
431
|
-
|
|
432
|
-
lambda do |row, done, total|
|
|
433
|
-
@stderr.puts("audited #{row.name} (#{done}/#{total})")
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
|
|
437
511
|
# Cells arrive preformatted: coercion would render a version
|
|
438
512
|
# like "3.2" as 3.200. Color follows the CLI's own detection
|
|
439
513
|
# because the table gem reads the global $stdout and cannot
|
|
@@ -446,116 +520,29 @@ module Audition
|
|
|
446
520
|
}
|
|
447
521
|
end
|
|
448
522
|
|
|
449
|
-
def emit_sweep_table(rows, options)
|
|
450
|
-
ready = rows.count { |r| r.verdict == :ready }
|
|
451
|
-
table = rows.map do |r|
|
|
452
|
-
{
|
|
453
|
-
"gem" => r.name,
|
|
454
|
-
"version" => r.version,
|
|
455
|
-
"verdict" => VERDICT_CELLS.fetch(r.verdict),
|
|
456
|
-
"errors" => r.errors,
|
|
457
|
-
"dep errors" => r.dep_errors,
|
|
458
|
-
"warnings" => r.warnings,
|
|
459
|
-
"fixable" => r.fixable,
|
|
460
|
-
"status" => r.status
|
|
461
|
-
}
|
|
462
|
-
end
|
|
463
|
-
@stdout.puts(TableTennis.new(
|
|
464
|
-
table, zebra: true, **table_opts(options)
|
|
465
|
-
).to_s)
|
|
466
|
-
@stdout.puts(
|
|
467
|
-
"#{ready} of #{rows.size} gems ractor-ready"
|
|
468
|
-
)
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
# Sweep rows carry no file or line, so the annotations land on
|
|
472
|
-
# the run summary rather than a diff; the markdown table goes
|
|
473
|
-
# to the job summary page when Actions provides one.
|
|
474
|
-
def emit_sweep_github(rows)
|
|
475
|
-
ready = rows.count { |r| r.verdict == :ready }
|
|
476
|
-
rows.each do |row|
|
|
477
|
-
level = sweep_annotation_level(row)
|
|
478
|
-
next unless level
|
|
479
|
-
|
|
480
|
-
@stdout.puts(
|
|
481
|
-
"::#{level} title=audition::gem #{row.name} " \
|
|
482
|
-
"#{row.version}: #{row.errors} errors, " \
|
|
483
|
-
"#{row.dep_errors} dependency errors, " \
|
|
484
|
-
"#{row.warnings} warnings " \
|
|
485
|
-
"(#{VERDICT_CELLS.fetch(row.verdict)})"
|
|
486
|
-
)
|
|
487
|
-
end
|
|
488
|
-
@stdout.puts("#{ready} of #{rows.size} gems ractor-ready")
|
|
489
|
-
append_step_summary(sweep_summary_markdown(rows, ready))
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
def sweep_annotation_level(row)
|
|
493
|
-
if row.verdict == :not_ready ||
|
|
494
|
-
(row.errors + row.dep_errors).positive?
|
|
495
|
-
"error"
|
|
496
|
-
elsif row.warnings.positive?
|
|
497
|
-
"warning"
|
|
498
|
-
end
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def sweep_summary_markdown(rows, ready)
|
|
502
|
-
lines = [
|
|
503
|
-
"## audition bundle sweep", "",
|
|
504
|
-
"| gem | version | verdict | errors | dep errors " \
|
|
505
|
-
"| warnings | fixable |",
|
|
506
|
-
"| --- | --- | --- | --- | --- | --- | --- |"
|
|
507
|
-
]
|
|
508
|
-
rows.each do |r|
|
|
509
|
-
lines << "| #{r.name} | #{r.version} | " \
|
|
510
|
-
"#{VERDICT_CELLS.fetch(r.verdict)} | #{r.errors} | " \
|
|
511
|
-
"#{r.dep_errors} | #{r.warnings} | #{r.fixable} |"
|
|
512
|
-
end
|
|
513
|
-
lines << ""
|
|
514
|
-
lines << "#{ready} of #{rows.size} gems ractor-ready"
|
|
515
|
-
lines.join("\n")
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
def emit_sweep_json(rows)
|
|
519
|
-
@stdout.puts(JSON.pretty_generate(
|
|
520
|
-
"audition" => VERSION,
|
|
521
|
-
"ruby" => RUBY_VERSION,
|
|
522
|
-
"bundle" => rows.map do |r|
|
|
523
|
-
{
|
|
524
|
-
"gem" => r.name,
|
|
525
|
-
"version" => r.version,
|
|
526
|
-
"verdict" => r.verdict&.to_s,
|
|
527
|
-
"errors" => r.errors,
|
|
528
|
-
"dependency_errors" => r.dep_errors,
|
|
529
|
-
"warnings" => r.warnings,
|
|
530
|
-
"infos" => r.infos,
|
|
531
|
-
"fixable" => r.fixable,
|
|
532
|
-
"status" => r.status
|
|
533
|
-
}
|
|
534
|
-
end
|
|
535
|
-
))
|
|
536
|
-
end
|
|
537
|
-
|
|
538
523
|
def print_capabilities(options)
|
|
539
524
|
result = prober(options).probe(mode: :capabilities)
|
|
540
525
|
caps = result.raw["capabilities"]
|
|
541
526
|
unless caps
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
527
|
+
complain("capabilities probe failed: #{result.raw}",
|
|
528
|
+
options)
|
|
545
529
|
return 2
|
|
546
530
|
end
|
|
547
531
|
|
|
532
|
+
s = style(options)
|
|
548
533
|
rows = caps.map do |probe, info|
|
|
549
534
|
{
|
|
550
535
|
"works in Ractor" => probe,
|
|
551
|
-
"ok" => info["ok"] ?
|
|
552
|
-
"raises" => info["error"]
|
|
536
|
+
"ok" => s.glyph(info["ok"] ? :pass : :error),
|
|
537
|
+
"raises" => info["error"]
|
|
553
538
|
}
|
|
554
539
|
end
|
|
555
|
-
@stdout.puts("ruby #{RUBY_VERSION} at #{RbConfig.ruby}")
|
|
556
540
|
@stdout.puts(
|
|
557
|
-
|
|
541
|
+
"#{s.glyph(:section)} #{s.bold("ruby #{RUBY_VERSION}")} " \
|
|
542
|
+
"#{s.dim("at #{RbConfig.ruby}")}"
|
|
558
543
|
)
|
|
544
|
+
@stdout.puts(TableTennis.new(rows,
|
|
545
|
+
title: "Audition capabilities", **table_opts(options)).to_s)
|
|
559
546
|
0
|
|
560
547
|
end
|
|
561
548
|
end
|
data/lib/audition/config.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Audition
|
|
|
10
10
|
# exclude:
|
|
11
11
|
# - legacy/**
|
|
12
12
|
# - db/schema.rb
|
|
13
|
+
# test_dirs:
|
|
14
|
+
# - qa
|
|
13
15
|
# checks:
|
|
14
16
|
# disable:
|
|
15
17
|
# - at-exit
|
|
@@ -20,7 +22,7 @@ module Audition
|
|
|
20
22
|
|
|
21
23
|
EMPTY = Ractor.make_shareable(
|
|
22
24
|
{fail_on: nil, timeout: nil, exclude: [],
|
|
23
|
-
disabled_checks: []}
|
|
25
|
+
disabled_checks: [], test_dirs: nil}
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
FAIL_ON_LEVELS = %w[error warning info never].freeze
|
|
@@ -42,7 +44,8 @@ module Audition
|
|
|
42
44
|
timeout: data["timeout"],
|
|
43
45
|
exclude: Array(data["exclude"]).map(&:to_s),
|
|
44
46
|
disabled_checks:
|
|
45
|
-
Array(data.dig("checks", "disable")).map(&:to_s)
|
|
47
|
+
Array(data.dig("checks", "disable")).map(&:to_s),
|
|
48
|
+
test_dirs: data["test_dirs"]&.map(&:to_s)
|
|
46
49
|
)
|
|
47
50
|
rescue Psych::Exception => e
|
|
48
51
|
raise Error, "#{path}: #{e.message}"
|
|
@@ -54,6 +57,7 @@ module Audition
|
|
|
54
57
|
"#{path}: expected a YAML mapping, got #{data.class}"
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
validate_test_dirs!(path, data["test_dirs"])
|
|
57
61
|
fail_on = data["fail_on"]
|
|
58
62
|
return if fail_on.nil? ||
|
|
59
63
|
FAIL_ON_LEVELS.include?(fail_on.to_s)
|
|
@@ -62,13 +66,35 @@ module Audition
|
|
|
62
66
|
"#{path}: fail_on must be one of error, warning, " \
|
|
63
67
|
"info, or never (got #{fail_on.inspect})"
|
|
64
68
|
end
|
|
65
|
-
private_class_method :validate!
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
# Each name is matched against one path segment, so a nested
|
|
71
|
+
# path would silently match nothing.
|
|
72
|
+
def self.validate_test_dirs!(path, dirs)
|
|
73
|
+
return if dirs.nil? || (dirs.is_a?(Array) &&
|
|
74
|
+
dirs.none? { |dir| dir.to_s.include?("/") })
|
|
75
|
+
|
|
76
|
+
raise Error,
|
|
77
|
+
"#{path}: test_dirs must be a list of directory names " \
|
|
78
|
+
"without slashes (got #{dirs.inspect})"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :validate!, :validate_test_dirs!
|
|
81
|
+
|
|
82
|
+
def initialize(fail_on:, timeout:, exclude:, disabled_checks:,
|
|
83
|
+
test_dirs: nil)
|
|
68
84
|
@fail_on = fail_on
|
|
69
85
|
@timeout = timeout
|
|
70
86
|
@exclude = exclude
|
|
71
87
|
@disabled_checks = disabled_checks
|
|
88
|
+
@test_dirs = test_dirs
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Directories whose findings are tagged as test code rather
|
|
92
|
+
# than code a production boot loads. An explicit empty list
|
|
93
|
+
# leaves only the `_test.rb`/`_spec.rb` suffixes.
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<String>] Target::TEST_DIRS unless configured
|
|
96
|
+
def test_dirs
|
|
97
|
+
@test_dirs || Target::TEST_DIRS
|
|
72
98
|
end
|
|
73
99
|
|
|
74
100
|
# Globs follow .gitignore-style expectations: `*` stays within
|