branchproof 0.11.1 → 0.12.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 +82 -0
- data/README.md +440 -29
- data/doc/Branchproof/CLI.md +1 -1
- data/doc/Branchproof/ChangedCoverage.md +19 -0
- data/doc/Branchproof/ChangedCoveragePolicy.md +24 -0
- data/doc/Branchproof/ChangedDecisionMap.md +20 -0
- data/doc/Branchproof/ChangedScope.md +18 -0
- data/doc/Branchproof/Collation.md +23 -0
- data/doc/Branchproof/CollationManifest.md +29 -0
- data/doc/Branchproof/CoverageSummary.md +20 -0
- data/doc/Branchproof/Doctor/TerminalRenderer.md +15 -0
- data/doc/Branchproof/Doctor.md +34 -0
- data/doc/Branchproof/FallbackInstrumentation.md +8 -0
- data/doc/Branchproof/FallbackRuntime.md +12 -0
- data/doc/Branchproof/FallbackSyntax.md +13 -0
- data/doc/Branchproof/GitChanges.md +19 -0
- data/doc/Branchproof/GithubReport.md +25 -0
- data/doc/Branchproof/HtmlReport.md +22 -0
- data/doc/Branchproof/Instrumenter.md +1 -1
- data/doc/Branchproof/MinitestAdapter.md +4 -0
- data/doc/Branchproof/RakeTask.md +121 -0
- data/doc/Branchproof/Report.md +41 -3
- data/doc/Branchproof/ReportSelection.md +10 -1
- data/doc/Branchproof/Runtime.md +4 -1
- data/doc/Branchproof/Source.md +1 -1
- data/doc/Branchproof/SummaryRanking/Decision.md +86 -0
- data/doc/Branchproof/SummaryRanking/SourceFile.md +43 -0
- data/doc/Branchproof/SummaryRanking.md +36 -0
- data/doc/Branchproof/SummaryReport.md +42 -0
- data/doc/Branchproof/Worker.md +1 -1
- data/doc/Branchproof.md +21 -1
- data/doc/CHANGELOG.md +82 -0
- data/doc/README.md +440 -29
- data/lib/branchproof/analyzer.rb +2 -52
- data/lib/branchproof/changed_coverage.rb +177 -0
- data/lib/branchproof/changed_decision_map.rb +359 -0
- data/lib/branchproof/changed_scope.rb +93 -0
- data/lib/branchproof/cli.rb +242 -21
- data/lib/branchproof/collation.rb +525 -0
- data/lib/branchproof/collation_manifest.rb +129 -0
- data/lib/branchproof/comparison.rb +11 -3
- data/lib/branchproof/comparison_report.rb +2 -0
- data/lib/branchproof/configuration.rb +12 -2
- data/lib/branchproof/coverage_policy.rb +122 -0
- data/lib/branchproof/coverage_summary.rb +94 -0
- data/lib/branchproof/doctor.rb +241 -0
- data/lib/branchproof/evidence.rb +5 -1
- data/lib/branchproof/exception_instrumentation.rb +4 -3
- data/lib/branchproof/exception_syntax.rb +15 -1
- data/lib/branchproof/fallback_instrumentation.rb +25 -0
- data/lib/branchproof/fallback_runtime.rb +14 -0
- data/lib/branchproof/fallback_syntax.rb +90 -0
- data/lib/branchproof/focused_report.rb +81 -22
- data/lib/branchproof/git_changes.rb +188 -0
- data/lib/branchproof/github_report.rb +292 -0
- data/lib/branchproof/html_report.rb +412 -0
- data/lib/branchproof/instrumenter.rb +2 -0
- data/lib/branchproof/minimizer.rb +4 -2
- data/lib/branchproof/minitest_adapter.rb +45 -5
- data/lib/branchproof/rails_support.rb +2 -2
- data/lib/branchproof/rake_task.rb +186 -0
- data/lib/branchproof/report.rb +383 -71
- data/lib/branchproof/report_selection.rb +26 -5
- data/lib/branchproof/runtime.rb +2 -0
- data/lib/branchproof/saved_report.rb +323 -5
- data/lib/branchproof/source.rb +15 -3
- data/lib/branchproof/summary_ranking.rb +165 -0
- data/lib/branchproof/summary_report.rb +241 -0
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +14 -9
- data/lib/branchproof.rb +12 -0
- data/llms.txt +21 -1
- data/sig/branchproof.rbs +206 -5
- metadata +39 -23
data/lib/branchproof/cli.rb
CHANGED
|
@@ -18,20 +18,28 @@ module Branchproof
|
|
|
18
18
|
VIEWS = Report::VIEWS.to_h { |view| [view.to_s.tr("_", "-"), view] }
|
|
19
19
|
.merge("decision_tables" => :decision_tables).freeze
|
|
20
20
|
|
|
21
|
-
def initialize(stdout:, stderr:)
|
|
21
|
+
def initialize(stdout:, stderr:, env: ENV)
|
|
22
22
|
@stdout = stdout
|
|
23
23
|
@stderr = stderr
|
|
24
|
+
@env = env
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def call(argv)
|
|
27
28
|
argv = Array(argv)
|
|
28
|
-
return help if [["--help"], ["help"], ["analyze", "--help"]].include?(argv)
|
|
29
|
+
return help if [["--help"], ["help"], ["analyze", "--help"], ["collate", "--help"]].include?(argv)
|
|
30
|
+
return doctor_command(argv.drop(1)) if argv.first == "doctor"
|
|
31
|
+
return collate_command(argv.drop(1)) if argv.first == "collate"
|
|
29
32
|
return offline(argv) if %w[report compare].include?(argv.first)
|
|
33
|
+
return usage_error("mutation testing is not supported yet; use doctor, analyze, report, compare, or collate") if argv.first == "mutate"
|
|
30
34
|
|
|
31
35
|
options = parse(argv)
|
|
32
|
-
return usage_error("expected analyze, report, or
|
|
36
|
+
return usage_error("expected doctor, analyze, report, compare, or collate; use branchproof --help") unless options
|
|
33
37
|
|
|
34
38
|
inventory = build_inventory(options)
|
|
39
|
+
changed_scope = if options[:changed_since]
|
|
40
|
+
ChangedScope.new(root: options[:project][:root], ref: options[:changed_since])
|
|
41
|
+
.call(inventory: inventory)
|
|
42
|
+
end
|
|
35
43
|
evidence = empty_evidence(inventory, options)
|
|
36
44
|
baseline = if run_worker?(options)
|
|
37
45
|
run_worker(options, inventory, evidence)
|
|
@@ -78,8 +86,10 @@ module Branchproof
|
|
|
78
86
|
analysis: analysis, minima: minima, baseline: baseline, diagnostics: diagnostics,
|
|
79
87
|
level: options[:level], missing_only: options[:missing_only], view: options[:view],
|
|
80
88
|
run_metadata: run_metadata(options, baseline), minimum: options[:minimum],
|
|
81
|
-
focus: options[:focus], top: options[:top]
|
|
89
|
+
focus: options[:focus], top: options[:top], changed_scope: changed_scope,
|
|
90
|
+
minimum_changed: options[:minimum_changed])
|
|
82
91
|
output_report(report, options)
|
|
92
|
+
write_step_summary(report, options)
|
|
83
93
|
report.exit_code
|
|
84
94
|
rescue ArgumentError => e
|
|
85
95
|
usage_error(e.message)
|
|
@@ -94,34 +104,100 @@ module Branchproof
|
|
|
94
104
|
def help
|
|
95
105
|
@stdout.write(<<~HELP)
|
|
96
106
|
Usage:
|
|
107
|
+
branchproof doctor [SOURCE_GLOB ...] [--test TEST_GLOB] [--project auto|ruby|rails] [--framework auto|minitest|rspec]
|
|
108
|
+
[--config PATH|--no-config] [--format terminal|json]
|
|
97
109
|
branchproof analyze [SOURCE_GLOB ...] [--test TEST_GLOB] [--project auto|ruby|rails] [--framework auto|minitest|rspec]
|
|
98
|
-
[--view decisions|conditions|tests|decision-tables] [--level 1|2|3] [--missing-only] [--minimum CRITERION=THRESHOLD]
|
|
110
|
+
[--view decisions|conditions|tests|decision-tables|summary] [--level 1|2|3] [--missing-only] [--minimum CRITERION=THRESHOLD] [--minimum-changed CRITERION=THRESHOLD]
|
|
111
|
+
[--changed-since REF]
|
|
99
112
|
[--focus PATH[:LINE]] [--top N]
|
|
100
|
-
[--format terminal|json] [--output PATH] [--limits PATH] [--config PATH|--no-config]
|
|
113
|
+
[--format terminal|json|github] [--output PATH] [--limits PATH] [--config PATH|--no-config]
|
|
101
114
|
[--no-reachability] [-- RUNNER_ARGS]
|
|
102
|
-
branchproof report SNAPSHOT [--view decisions|conditions|tests|decision-tables]
|
|
103
|
-
[--level 1|2|3] [--missing-only] [--minimum CRITERION=THRESHOLD] [--focus PATH[:LINE]] [--top N]
|
|
104
|
-
[--format terminal|json] [--output PATH]
|
|
115
|
+
branchproof report SNAPSHOT [--view decisions|conditions|tests|decision-tables|summary]
|
|
116
|
+
[--level 1|2|3] [--missing-only] [--minimum CRITERION=THRESHOLD] [--minimum-changed CRITERION=THRESHOLD] [--focus PATH[:LINE]] [--top N]
|
|
117
|
+
[--format terminal|json|github|html] [--output PATH]
|
|
105
118
|
branchproof compare BEFORE AFTER [--format terminal|json] [--output PATH] [--fail-on-regression]
|
|
119
|
+
branchproof collate REPORT... [--manifest PATH] [--format terminal|json|github|html] [--output PATH]
|
|
106
120
|
mcdc accepts the same commands as a compatibility alias.
|
|
107
121
|
JSON always contains full evidence; --view requires terminal output.
|
|
122
|
+
github prints Actions annotations for the summary ranking and appends Markdown to $GITHUB_STEP_SUMMARY.
|
|
123
|
+
HTML report output is self-contained and offline; it uses an integrated layout, so --view is unavailable.
|
|
108
124
|
decision_tables is also accepted as an alias for the decision-tables view.
|
|
109
125
|
--no-reachability keeps every generated decision-table rule as a coverage obligation.
|
|
126
|
+
--changed-since REF captures tracked changes from a commit to the current worktree; --minimum-changed requires this scope and gates its coverage separately.
|
|
110
127
|
HELP
|
|
111
128
|
0
|
|
112
129
|
end
|
|
113
130
|
|
|
131
|
+
def doctor_command(args)
|
|
132
|
+
return help if args == ["--help"]
|
|
133
|
+
|
|
134
|
+
format = doctor_json_requested?(args) ? :json : :terminal
|
|
135
|
+
begin
|
|
136
|
+
analyze_args = doctor_analyze_args(args)
|
|
137
|
+
options = parse(analyze_args, doctor: true)
|
|
138
|
+
source_files = select_source_files(options).select { |path| File.file?(path) }
|
|
139
|
+
test_files = options[:tests].select { |path| File.file?(path) }
|
|
140
|
+
document = Doctor.new(options: options).document(source_files: source_files, test_files: test_files)
|
|
141
|
+
format = options[:format]
|
|
142
|
+
rescue ArgumentError, SystemCallError, IOError => e
|
|
143
|
+
document = Doctor.error(e.message)
|
|
144
|
+
end
|
|
145
|
+
output_doctor(document, format)
|
|
146
|
+
document[:status] == "ready" ? 0 : 2
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def doctor_json_requested?(args)
|
|
150
|
+
args.each_cons(2).any? { |flag, format| flag == "--format" && format == "json" }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def doctor_analyze_args(args)
|
|
154
|
+
allowed_with_value = %w[--test --project --framework --config --format]
|
|
155
|
+
allowed_without_value = ["--no-config"]
|
|
156
|
+
analyze = ["analyze"]
|
|
157
|
+
until args.empty?
|
|
158
|
+
token = args.shift
|
|
159
|
+
if allowed_with_value.include?(token)
|
|
160
|
+
value = args.shift
|
|
161
|
+
raise ArgumentError, "#{token} requires a value" if value.to_s.empty? || value.start_with?("-")
|
|
162
|
+
if token == "--format" && !%w[terminal json].include?(value)
|
|
163
|
+
raise ArgumentError, "format must be terminal or json"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
analyze.push(token, value)
|
|
167
|
+
elsif allowed_without_value.include?(token)
|
|
168
|
+
analyze << token
|
|
169
|
+
elsif token.start_with?("-")
|
|
170
|
+
raise ArgumentError, "#{token} is not supported by doctor"
|
|
171
|
+
else
|
|
172
|
+
analyze << token
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
analyze
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def output_doctor(document, format)
|
|
179
|
+
if format == :json
|
|
180
|
+
@stdout.write(JSON.generate(normalize(document)))
|
|
181
|
+
@stdout.write("\n")
|
|
182
|
+
else
|
|
183
|
+
@stdout.write(Doctor.terminal(document))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
114
187
|
def parse_view(view)
|
|
115
188
|
resolved = VIEWS[view.to_s]
|
|
116
|
-
raise ArgumentError, "view must be decisions, conditions, tests,
|
|
189
|
+
raise ArgumentError, "view must be decisions, conditions, tests, decision-tables, or summary" unless resolved
|
|
117
190
|
|
|
118
191
|
resolved
|
|
119
192
|
end
|
|
120
193
|
|
|
121
194
|
def validate_view!(options)
|
|
122
|
-
return unless options[:explicit_view]
|
|
195
|
+
return unless options[:explicit_view]
|
|
196
|
+
raise ArgumentError, "HTML uses an integrated layout; --view is unavailable" if options[:format] == :html
|
|
197
|
+
raise ArgumentError, "--view requires terminal format; JSON contains full evidence" if options[:format] == :json
|
|
198
|
+
return unless options[:format] == :github
|
|
123
199
|
|
|
124
|
-
raise ArgumentError, "--view requires terminal format;
|
|
200
|
+
raise ArgumentError, "--view requires terminal format; GitHub output uses the summary ranking"
|
|
125
201
|
end
|
|
126
202
|
|
|
127
203
|
def offline(argv)
|
|
@@ -141,27 +217,111 @@ module Branchproof
|
|
|
141
217
|
if options[:level] > 1 && !document["analysis"]
|
|
142
218
|
raise ArgumentError, "saved report has no analysis; use --level 1"
|
|
143
219
|
end
|
|
144
|
-
if options[:missing_only] && (options[:format]
|
|
145
|
-
raise ArgumentError, "--missing-only requires terminal format and level 2 or 3"
|
|
220
|
+
if options[:missing_only] && (!%i[terminal html].include?(options[:format]) || options[:level] == 1)
|
|
221
|
+
raise ArgumentError, "--missing-only requires terminal or HTML format and level 2 or 3"
|
|
146
222
|
end
|
|
147
223
|
|
|
148
224
|
report = Report.from_document(document: document, level: options[:level], view: options[:view],
|
|
149
225
|
missing_only: options[:missing_only], minimum: options[:minimum_overrides],
|
|
226
|
+
minimum_changed: options[:minimum_changed_overrides],
|
|
150
227
|
focus: options[:focus], top: options[:top])
|
|
151
228
|
output_report(report, options)
|
|
229
|
+
write_step_summary(report, options)
|
|
152
230
|
report.exit_code
|
|
153
231
|
end
|
|
154
232
|
end
|
|
155
233
|
|
|
234
|
+
def collate_command(args)
|
|
235
|
+
options, paths = parse_collate(args)
|
|
236
|
+
manifest = if options[:manifest]
|
|
237
|
+
require_relative "collation_manifest"
|
|
238
|
+
CollationManifest.new(path: options[:manifest], supplied_paths: paths)
|
|
239
|
+
end
|
|
240
|
+
protected_paths = paths.map { File.expand_path(_1) }
|
|
241
|
+
if manifest
|
|
242
|
+
protected_paths.concat(manifest.declared_paths)
|
|
243
|
+
protected_paths << File.expand_path(options[:manifest])
|
|
244
|
+
end
|
|
245
|
+
reject_collation_output_collision!(protected_paths, options[:output]) if options[:output]
|
|
246
|
+
|
|
247
|
+
supplied = if manifest
|
|
248
|
+
manifest.inputs
|
|
249
|
+
else
|
|
250
|
+
paths.map { |path| { id: nil, path: File.expand_path(path) } }
|
|
251
|
+
end
|
|
252
|
+
inputs = supplied.map do |input|
|
|
253
|
+
input.merge(document: SavedReport.read(input.fetch(:path)))
|
|
254
|
+
end
|
|
255
|
+
document = Collation.new(inputs: inputs, expected_shards: manifest&.expected_ids).call
|
|
256
|
+
report = Report.from_document(document: document, level: document.dig("run_metadata", "requested_level"))
|
|
257
|
+
output_report(report, options)
|
|
258
|
+
write_step_summary(report, options)
|
|
259
|
+
report.exit_code
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def parse_collate(args)
|
|
263
|
+
options = { format: :json }
|
|
264
|
+
paths = []
|
|
265
|
+
seen = {}
|
|
266
|
+
until args.empty?
|
|
267
|
+
token = args.shift
|
|
268
|
+
case token
|
|
269
|
+
when "--format"
|
|
270
|
+
argument = collate_option_value(args, seen, token)
|
|
271
|
+
raise ArgumentError, "format must be terminal, json, github, or html" unless %w[terminal json github html].include?(argument)
|
|
272
|
+
|
|
273
|
+
options[:format] = argument.to_sym
|
|
274
|
+
when "--output", "--manifest"
|
|
275
|
+
argument = collate_option_value(args, seen, token)
|
|
276
|
+
options[token == "--output" ? :output : :manifest] = argument
|
|
277
|
+
else
|
|
278
|
+
raise ArgumentError, "unknown option: #{token}" if token.start_with?("-")
|
|
279
|
+
|
|
280
|
+
paths << token
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
raise ArgumentError, "collate requires at least one saved report" if paths.empty?
|
|
284
|
+
|
|
285
|
+
[options, paths]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def collate_option_value(args, seen, token)
|
|
289
|
+
raise ArgumentError, "#{token} may be specified only once" if seen[token]
|
|
290
|
+
|
|
291
|
+
seen[token] = true
|
|
292
|
+
argument = args.shift
|
|
293
|
+
raise ArgumentError, "#{token} requires a value" if argument.to_s.empty? || argument.start_with?("-")
|
|
294
|
+
|
|
295
|
+
argument
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def reject_collation_output_collision!(protected_paths, output)
|
|
299
|
+
require_relative "collation_manifest"
|
|
300
|
+
output_path = File.expand_path(output)
|
|
301
|
+
output_identity = CollationManifest.path_identity(output_path)
|
|
302
|
+
collision = protected_paths.any? do |path|
|
|
303
|
+
File.expand_path(path) == output_path ||
|
|
304
|
+
CollationManifest.path_identity(path) == output_identity ||
|
|
305
|
+
(File.exist?(path) && File.exist?(output_path) && File.identical?(path, output_path))
|
|
306
|
+
end
|
|
307
|
+
raise ArgumentError, "output must not overwrite an input, declared report, or manifest" if collision
|
|
308
|
+
end
|
|
309
|
+
|
|
156
310
|
def parse_offline(command, args)
|
|
157
311
|
options = { format: :terminal, view: :decisions, missing_only: false, minimum_overrides: {} }
|
|
158
312
|
paths = []
|
|
159
313
|
until args.empty?
|
|
160
314
|
token = args.shift
|
|
161
315
|
case token
|
|
316
|
+
when "--changed-since"
|
|
317
|
+
raise ArgumentError, "--changed-since is available only for analyze"
|
|
162
318
|
when "--format"
|
|
163
319
|
format = args.shift
|
|
164
|
-
|
|
320
|
+
if command == "compare"
|
|
321
|
+
raise ArgumentError, "format must be terminal or json" unless %w[terminal json].include?(format)
|
|
322
|
+
else
|
|
323
|
+
raise ArgumentError, "format must be terminal, json, github, or html" unless %w[terminal json github html].include?(format)
|
|
324
|
+
end
|
|
165
325
|
|
|
166
326
|
options[:format] = format.to_sym
|
|
167
327
|
when "--output"
|
|
@@ -171,7 +331,7 @@ module Branchproof
|
|
|
171
331
|
raise ArgumentError, "--fail-on-regression requires compare" unless command == "compare"
|
|
172
332
|
|
|
173
333
|
options[:fail_on_regression] = true
|
|
174
|
-
when "--view", "--level", "--missing-only", "--minimum", "--focus", "--top"
|
|
334
|
+
when "--view", "--level", "--missing-only", "--minimum", "--minimum-changed", "--focus", "--top"
|
|
175
335
|
raise ArgumentError, "#{token} requires report" unless command == "report"
|
|
176
336
|
|
|
177
337
|
case token
|
|
@@ -183,6 +343,8 @@ module Branchproof
|
|
|
183
343
|
raise ArgumentError, "level must be 1, 2, or 3" unless (1..3).cover?(options[:level])
|
|
184
344
|
when "--minimum"
|
|
185
345
|
add_minimum_override!(options, args.shift)
|
|
346
|
+
when "--minimum-changed"
|
|
347
|
+
add_minimum_changed_override!(options, args.shift)
|
|
186
348
|
when "--focus"
|
|
187
349
|
options[:focus] = args.shift
|
|
188
350
|
raise ArgumentError, "--focus requires PATH or PATH:LINE" if options[:focus].nil? || options[:focus].start_with?("-")
|
|
@@ -256,7 +418,7 @@ module Branchproof
|
|
|
256
418
|
path.to_s.empty? ? path.to_s : relative_path(path, root)
|
|
257
419
|
end
|
|
258
420
|
|
|
259
|
-
def parse(argv)
|
|
421
|
+
def parse(argv, doctor: false)
|
|
260
422
|
return nil if argv.empty? || argv.first != "analyze"
|
|
261
423
|
|
|
262
424
|
args = argv.drop(1)
|
|
@@ -267,7 +429,7 @@ module Branchproof
|
|
|
267
429
|
runner_args: runner_args, project: nil, missing_only: false, view: :decisions,
|
|
268
430
|
reachability: true, project_mode: "auto", framework: "auto", explicit_tests: false,
|
|
269
431
|
explicit_project: false, explicit_framework: false, explicit_sources: false,
|
|
270
|
-
config_path: nil, config_disabled: false, minimum_overrides: {} }
|
|
432
|
+
config_path: nil, config_disabled: false, minimum_overrides: {}, minimum_changed_overrides: {} }
|
|
271
433
|
until args.empty?
|
|
272
434
|
token = args.shift
|
|
273
435
|
case token
|
|
@@ -278,6 +440,13 @@ module Branchproof
|
|
|
278
440
|
options[:missing_only] = true
|
|
279
441
|
when "--minimum"
|
|
280
442
|
add_minimum_override!(options, args.shift)
|
|
443
|
+
when "--minimum-changed"
|
|
444
|
+
add_minimum_changed_override!(options, args.shift)
|
|
445
|
+
when "--changed-since"
|
|
446
|
+
ref = args.shift
|
|
447
|
+
raise ArgumentError, "--changed-since requires a non-option REF" if ref.to_s.empty? || ref.start_with?("-")
|
|
448
|
+
|
|
449
|
+
options[:changed_since] = ref
|
|
281
450
|
when "--focus"
|
|
282
451
|
options[:focus] = args.shift
|
|
283
452
|
raise ArgumentError, "--focus requires PATH or PATH:LINE" if options[:focus].nil? || options[:focus].start_with?("-")
|
|
@@ -293,7 +462,7 @@ module Branchproof
|
|
|
293
462
|
options[:level] = level
|
|
294
463
|
when "--format"
|
|
295
464
|
format = args.shift.to_s
|
|
296
|
-
raise ArgumentError, "format must be terminal or
|
|
465
|
+
raise ArgumentError, "format must be terminal, json, or github" unless %w[terminal json github].include?(format)
|
|
297
466
|
|
|
298
467
|
options[:format] = format.to_sym
|
|
299
468
|
when "--output"
|
|
@@ -355,10 +524,16 @@ module Branchproof
|
|
|
355
524
|
end
|
|
356
525
|
options[:exclude] = Array(configuration[:exclude]).dup
|
|
357
526
|
options[:minimum] = configuration.fetch(:minimum, {}).dup.merge(options[:minimum_overrides])
|
|
527
|
+
options[:minimum_changed] = configuration.fetch(:minimum_changed, {}).dup.merge(options[:minimum_changed_overrides])
|
|
358
528
|
else
|
|
359
529
|
options[:exclude] = []
|
|
360
530
|
options[:minimum] = options[:minimum_overrides].dup
|
|
531
|
+
options[:minimum_changed] = options[:minimum_changed_overrides].dup
|
|
532
|
+
end
|
|
533
|
+
if !doctor && !options[:minimum_changed].empty? && !options[:changed_since]
|
|
534
|
+
raise ArgumentError, "--minimum-changed requires --changed-since REF"
|
|
361
535
|
end
|
|
536
|
+
|
|
362
537
|
options[:project] = Project.new(root: root, mode: options[:project_mode], framework: options[:framework]).to_h
|
|
363
538
|
validate_view!(options)
|
|
364
539
|
validate_selection!(options)
|
|
@@ -393,6 +568,20 @@ module Branchproof
|
|
|
393
568
|
raise ArgumentError, "minimum threshold must be a finite number from 0 to 100"
|
|
394
569
|
end
|
|
395
570
|
|
|
571
|
+
def add_minimum_changed_override!(options, argument)
|
|
572
|
+
match = argument.to_s.match(/\A([a-z_]+)=([0-9]+(?:\.[0-9]+)?)\z/)
|
|
573
|
+
raise ArgumentError, "minimum-changed must be CRITERION=THRESHOLD" unless match
|
|
574
|
+
|
|
575
|
+
criterion = match[1]
|
|
576
|
+
threshold = match[2].include?(".") ? Float(match[2]) : Integer(match[2], 10)
|
|
577
|
+
normalized = CoveragePolicy.normalize(criterion => threshold)
|
|
578
|
+
criterion = normalized.keys.first
|
|
579
|
+
options[:minimum_changed_overrides] ||= {}
|
|
580
|
+
raise ArgumentError, "duplicate coverage criterion: #{criterion}" if options[:minimum_changed_overrides].key?(criterion)
|
|
581
|
+
|
|
582
|
+
options[:minimum_changed_overrides][criterion] = normalized.fetch(criterion)
|
|
583
|
+
end
|
|
584
|
+
|
|
396
585
|
def validate_selection!(options)
|
|
397
586
|
return unless options[:focus] || options[:top]
|
|
398
587
|
raise ArgumentError, "focus and top filters are terminal-only" if options[:format] == :json
|
|
@@ -401,6 +590,12 @@ module Branchproof
|
|
|
401
590
|
end
|
|
402
591
|
|
|
403
592
|
def build_inventory(options)
|
|
593
|
+
root = options[:project][:root]
|
|
594
|
+
selected = select_source_files(options)
|
|
595
|
+
Source.new(root: root, limits: options[:limits]).inventory(paths: selected)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def select_source_files(options)
|
|
404
599
|
root = options[:project][:root]
|
|
405
600
|
test_paths = options[:tests].filter_map do |path|
|
|
406
601
|
File.realpath(path)
|
|
@@ -426,7 +621,7 @@ module Branchproof
|
|
|
426
621
|
end
|
|
427
622
|
options[:excluded_files] = excluded
|
|
428
623
|
options[:selected_source_files] = selected
|
|
429
|
-
|
|
624
|
+
selected
|
|
430
625
|
end
|
|
431
626
|
|
|
432
627
|
def empty_evidence(inventory, options)
|
|
@@ -500,6 +695,28 @@ module Branchproof
|
|
|
500
695
|
File.unlink(temporary) if created && temporary && File.file?(temporary)
|
|
501
696
|
end
|
|
502
697
|
|
|
698
|
+
# GitHub Actions reads Markdown appended to this file into the job summary.
|
|
699
|
+
def write_step_summary(report, options)
|
|
700
|
+
path = @env["GITHUB_STEP_SUMMARY"].to_s
|
|
701
|
+
return unless options[:format] == :github && !path.empty?
|
|
702
|
+
|
|
703
|
+
File.open(path, "a") { |file| file.write(report.step_summary) }
|
|
704
|
+
rescue SystemCallError, IOError => e
|
|
705
|
+
@stderr.write("branchproof: could not write GITHUB_STEP_SUMMARY: #{e.message}\n")
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Annotation paths must be relative to the repository (GITHUB_WORKSPACE),
|
|
709
|
+
# but report paths are relative to the project root (the current directory).
|
|
710
|
+
def github_path_prefix
|
|
711
|
+
workspace = @env["GITHUB_WORKSPACE"].to_s
|
|
712
|
+
return nil if workspace.empty?
|
|
713
|
+
|
|
714
|
+
relative = Pathname.new(File.realpath(Dir.pwd)).relative_path_from(Pathname.new(File.realpath(workspace))).to_s
|
|
715
|
+
relative == "." || relative.start_with?("..") ? nil : relative
|
|
716
|
+
rescue SystemCallError, ArgumentError
|
|
717
|
+
nil
|
|
718
|
+
end
|
|
719
|
+
|
|
503
720
|
def run_worker?(options)
|
|
504
721
|
!options[:tests].empty? || options[:project][:framework].to_s == "rspec"
|
|
505
722
|
end
|
|
@@ -515,7 +732,11 @@ module Branchproof
|
|
|
515
732
|
|
|
516
733
|
def report_string(report, format)
|
|
517
734
|
buffer = StringIO.new
|
|
518
|
-
|
|
735
|
+
if format == :github
|
|
736
|
+
report.write(io: buffer, format: format, path_prefix: github_path_prefix)
|
|
737
|
+
else
|
|
738
|
+
report.write(io: buffer, format: format)
|
|
739
|
+
end
|
|
519
740
|
buffer.string
|
|
520
741
|
end
|
|
521
742
|
|