branchproof 0.11.0 → 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 +89 -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 +6 -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 +89 -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 +25 -4
- 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 +38 -22
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
4
|
+
require_relative "coverage_index"
|
|
5
|
+
require_relative "report_selection"
|
|
6
|
+
|
|
7
|
+
module Branchproof
|
|
8
|
+
# Ranks supported decisions and source files by missing coverage obligations.
|
|
9
|
+
#
|
|
10
|
+
# Order: unexecuted decisions first, then more missing obligations
|
|
11
|
+
# (decision-table rules, unproven MC/DC conditions, missing alternatives),
|
|
12
|
+
# then source path, line, column, and ID. The order is a count, not a risk
|
|
13
|
+
# estimate. Unsupported decisions are counted but never ranked.
|
|
14
|
+
class SummaryRanking
|
|
15
|
+
Decision = Struct.new(:id, :relative_path, :line, :column, :expression, :kind, :context, :unexecuted,
|
|
16
|
+
:missing_rules, :required_rules, :table_calculated, :unproven_conditions,
|
|
17
|
+
:conditions, :missing_alternatives, :alternatives, :test_ids,
|
|
18
|
+
keyword_init: true) do
|
|
19
|
+
def missing_total = missing_rules.length + unproven_conditions.length + missing_alternatives.length
|
|
20
|
+
def gap? = unexecuted || missing_total.positive?
|
|
21
|
+
|
|
22
|
+
# One new observation covers at most one missing rule, so missing rules
|
|
23
|
+
# are an exact case count. Without missing rules (no table, or only
|
|
24
|
+
# impossible rules left), each unproven condition or alternative counts.
|
|
25
|
+
def cases_to_test
|
|
26
|
+
return missing_rules.length if rule_cases?
|
|
27
|
+
|
|
28
|
+
unproven_conditions.length + missing_alternatives.length
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rule_cases? = table_calculated && !missing_rules.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
SourceFile = Struct.new(:relative_path, :decisions, keyword_init: true) do
|
|
35
|
+
def gaps = decisions.count(&:gap?)
|
|
36
|
+
def unexecuted = decisions.count(&:unexecuted)
|
|
37
|
+
def missing_rules = decisions.sum { |decision| decision.missing_rules.length }
|
|
38
|
+
def required_rules = decisions.sum(&:required_rules)
|
|
39
|
+
def unproven_conditions = decisions.sum { |decision| decision.unproven_conditions.length }
|
|
40
|
+
def conditions = decisions.sum { |decision| decision.conditions.length }
|
|
41
|
+
def missing_alternatives = decisions.sum { |decision| decision.missing_alternatives.length }
|
|
42
|
+
def alternatives = decisions.sum { |decision| decision.alternatives.length }
|
|
43
|
+
def missing_total = decisions.sum(&:missing_total)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader :index
|
|
47
|
+
|
|
48
|
+
def initialize(document:, selection: ReportSelection.new)
|
|
49
|
+
@document = document || {}
|
|
50
|
+
@selection = selection
|
|
51
|
+
@index = CoverageIndex.new(document: @document)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def available?
|
|
55
|
+
analysis = fetch(@document, :analysis)
|
|
56
|
+
analysis.respond_to?(:key?) && !fetch(analysis, :decisions).nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# All supported decisions in the selection, ranked.
|
|
60
|
+
def decisions
|
|
61
|
+
@decisions ||= build_decisions.sort_by { |decision| decision_key(decision) }.freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def gaps
|
|
65
|
+
decisions.select(&:gap?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def files
|
|
69
|
+
@files ||= begin
|
|
70
|
+
files = decisions.group_by(&:relative_path).map do |path, items|
|
|
71
|
+
SourceFile.new(relative_path: path, decisions: items)
|
|
72
|
+
end
|
|
73
|
+
files.sort_by { |file| file_key(file) }.freeze
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def unsupported_count
|
|
78
|
+
inventory_decisions.count { |decision| fetch(decision, :support_status).to_s == "UNSUPPORTED" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def build_decisions
|
|
84
|
+
conditions = @index.conditions.group_by { |row| row[:decision_id].to_s }
|
|
85
|
+
alternatives = @index.alternatives.group_by { |row| row[:decision_id].to_s }
|
|
86
|
+
tables = @index.decision_tables.to_h { |row| [row[:decision_id].to_s, row] }
|
|
87
|
+
sources = inventory_sources
|
|
88
|
+
selected_ids = @selection.decision_filter_active? ? @selection.selected_decision_ids(@document) : nil
|
|
89
|
+
selected_id_set = selected_ids&.to_h { |id| [id, true] }
|
|
90
|
+
inventory_decisions.filter_map do |decision|
|
|
91
|
+
id = fetch(decision, :id).to_s
|
|
92
|
+
next if fetch(decision, :support_status).to_s == "UNSUPPORTED"
|
|
93
|
+
next if selected_id_set && !selected_id_set.key?(id)
|
|
94
|
+
|
|
95
|
+
condition_rows = conditions.fetch(id, [])
|
|
96
|
+
alternative_rows = alternatives.fetch(id, [])
|
|
97
|
+
table = tables[id]
|
|
98
|
+
calculated = table && table[:status].to_s == "calculated"
|
|
99
|
+
source = sources[fetch(decision, :source_id).to_s] || {}
|
|
100
|
+
Decision.new(
|
|
101
|
+
id: id, relative_path: display_path(fetch(source, :relative_path)),
|
|
102
|
+
line: fetch(decision, :line), column: fetch(decision, :column),
|
|
103
|
+
expression: fetch(decision, :expression).to_s, kind: kind(decision), context: fetch(decision, :context),
|
|
104
|
+
unexecuted: !executed_decision_ids.include?(id),
|
|
105
|
+
missing_rules: calculated ? table[:rules].select { |rule| rule[:coverage].to_s == "missing" } : [],
|
|
106
|
+
required_rules: calculated ? table[:required_rules].to_i : 0, table_calculated: calculated ? true : false,
|
|
107
|
+
unproven_conditions: condition_rows.reject { |row| row[:status].to_s.upcase == "PROVEN" },
|
|
108
|
+
conditions: condition_rows,
|
|
109
|
+
missing_alternatives: alternative_rows.select { |row| row[:missing] }, alternatives: alternative_rows,
|
|
110
|
+
test_ids: reaching_test_ids(condition_rows, alternative_rows)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def reaching_test_ids(condition_rows, alternative_rows)
|
|
116
|
+
ids = condition_rows.flat_map { |row| row[:observed_true] + row[:observed_false] + row[:short_circuited] }
|
|
117
|
+
ids += alternative_rows.flat_map do |row|
|
|
118
|
+
%i[selected not_selected skipped].flat_map { |state| Array((row[state] || {})[:test_ids]) }
|
|
119
|
+
end
|
|
120
|
+
ids.map(&:to_s).uniq.sort
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def decision_key(decision)
|
|
124
|
+
[decision.unexecuted ? 0 : 1, -decision.missing_total, decision.relative_path.to_s,
|
|
125
|
+
decision.line.to_i, decision.column.to_i, decision.id]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def file_key(file)
|
|
129
|
+
[-file.unexecuted, -file.missing_total, file.relative_path.to_s]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def executed_decision_ids
|
|
133
|
+
@executed_decision_ids ||= Array(fetch(fetch(@document, :observations) || {}, :vectors))
|
|
134
|
+
.to_set { |vector| fetch(vector, :decision_id).to_s }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def kind(decision)
|
|
138
|
+
value = fetch(decision, :kind).to_s
|
|
139
|
+
value.empty? ? "boolean" : value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def display_path(path)
|
|
143
|
+
return nil if path.to_s.empty? || Pathname.new(path.to_s).absolute?
|
|
144
|
+
|
|
145
|
+
path.to_s
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def inventory
|
|
149
|
+
fetch(@document, :source_inventory) || fetch(@document, :inventory) || {}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def inventory_decisions = Array(fetch(inventory, :decisions))
|
|
153
|
+
|
|
154
|
+
def inventory_sources
|
|
155
|
+
Array(fetch(inventory, :source_units)).to_h { |unit| [fetch(unit, :source_id).to_s, unit] }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def fetch(hash, key)
|
|
159
|
+
return nil unless hash.respond_to?(:key?)
|
|
160
|
+
|
|
161
|
+
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
4
|
+
require_relative "summary_ranking"
|
|
5
|
+
|
|
6
|
+
module Branchproof
|
|
7
|
+
# Terminal view that lists the largest coverage gaps first.
|
|
8
|
+
class SummaryReport
|
|
9
|
+
RULE_LABELS = { "true" => "T", "false" => "F", "dont_care" => "-" }.freeze
|
|
10
|
+
ORDER_NOTE = "Order: unexecuted decisions first, then most missing obligations " \
|
|
11
|
+
"(decision-table rules, unproven MC/DC conditions, missing alternatives); " \
|
|
12
|
+
"a count, not a risk estimate."
|
|
13
|
+
SHOWN_TESTS = 3
|
|
14
|
+
EXPRESSION_WIDTH = 100
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Shared wording for terminal rows and GitHub output.
|
|
18
|
+
def gap_summary(decision)
|
|
19
|
+
parts = []
|
|
20
|
+
parts << "unexecuted" if decision.unexecuted
|
|
21
|
+
if decision.table_calculated
|
|
22
|
+
parts << "DT #{decision.missing_rules.length}/#{decision.required_rules} rules missing"
|
|
23
|
+
end
|
|
24
|
+
unless decision.conditions.empty?
|
|
25
|
+
parts << "MC/DC #{decision.unproven_conditions.length}/#{decision.conditions.length} conditions unproven"
|
|
26
|
+
end
|
|
27
|
+
unless decision.alternatives.empty?
|
|
28
|
+
parts << "#{decision.missing_alternatives.length}/#{decision.alternatives.length} alternatives missing"
|
|
29
|
+
end
|
|
30
|
+
parts.join("; ")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One line per missing case: decision-table rules when some are missing,
|
|
34
|
+
# otherwise unproven conditions; then missing alternatives.
|
|
35
|
+
def case_lines(decision, coordinator)
|
|
36
|
+
lines = []
|
|
37
|
+
if decision.rule_cases?
|
|
38
|
+
expressions = decision.conditions.sort_by { |row| row[:index].to_i }.map { |row| row[:expression] }
|
|
39
|
+
heading = coordinator.decision_table_expected_heading({ context: decision.context })
|
|
40
|
+
decision.missing_rules.each do |rule|
|
|
41
|
+
lines << "#{rule_text(rule, expressions, coordinator)}; " \
|
|
42
|
+
"#{heading.downcase.delete_suffix(":")} #{rule[:outcome] ? "true" : "false"}"
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
decision.unproven_conditions.each do |row|
|
|
46
|
+
explanation = coordinator.condition_explanation(decision_id: decision.id, condition_id: row[:id])
|
|
47
|
+
lines << "Condition #{row[:index]}: #{row[:expression]} NOT_PROVEN#{explanation}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
if decision.rule_cases? && decision.unproven_conditions.any?
|
|
51
|
+
row = decision.unproven_conditions.first
|
|
52
|
+
explanation = coordinator.condition_explanation(decision_id: decision.id, condition_id: row[:id])
|
|
53
|
+
lines << "MC/DC evidence: Condition #{row[:index]}: #{row[:expression]} NOT_PROVEN#{explanation}"
|
|
54
|
+
remaining = decision.unproven_conditions.length - 1
|
|
55
|
+
lines << "Additional unproven conditions: #{remaining}" if remaining.positive?
|
|
56
|
+
end
|
|
57
|
+
decision.missing_alternatives.each { |row| lines << "Need selection of: #{row[:expression]}" }
|
|
58
|
+
lines
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def one_line(text, width = EXPRESSION_WIDTH)
|
|
62
|
+
text = text.to_s.gsub(/\s+/, " ").strip
|
|
63
|
+
text.length > width ? "#{text[0, width - 3]}..." : text
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rule_text(rule, expressions, coordinator)
|
|
67
|
+
signature = Array(rule[:conditions]).map { |item| RULE_LABELS.fetch(item.to_s, item.to_s) }.join
|
|
68
|
+
requirements = Array(rule[:conditions]).each_with_index.filter_map do |item, index|
|
|
69
|
+
requirement = coordinator.decision_table_requirement(item)
|
|
70
|
+
"#{expressions[index] || "condition #{index}"} #{requirement}" if requirement
|
|
71
|
+
end
|
|
72
|
+
"#{rule[:label]} [#{signature}]: #{requirements.join(", ")}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(document:, level:, coordinator:, missing_only: false, selection: ReportSelection.new)
|
|
77
|
+
@document = document || {}
|
|
78
|
+
@level = level.to_i
|
|
79
|
+
@coordinator = coordinator
|
|
80
|
+
@missing_only = missing_only ? true : false
|
|
81
|
+
@selection = selection
|
|
82
|
+
@ranking = SummaryRanking.new(document: @document, selection: selection)
|
|
83
|
+
@tests = @ranking.index.tests.to_h { |test| [test[:id].to_s, test] }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render
|
|
87
|
+
baseline = fetch(@document, :baseline) || {}
|
|
88
|
+
completeness = fetch(@document, :completeness) || {}
|
|
89
|
+
lines = ["Branchproof summary view", "Tests: #{fetch(baseline, :status) || "INCOMPLETE"}"]
|
|
90
|
+
if completeness.values.include?(false) || fetch(baseline, :status).to_s != "PASSED"
|
|
91
|
+
lines << "Warning: failed or incomplete run; counts are lower bounds."
|
|
92
|
+
end
|
|
93
|
+
lines << ""
|
|
94
|
+
render_focus_notice(lines)
|
|
95
|
+
lines << "Whole-run coverage and policy:" if @selection.decision_ids
|
|
96
|
+
lines.concat(@coordinator.coverage_ladder_lines)
|
|
97
|
+
lines.concat(@coordinator.coverage_policy_lines)
|
|
98
|
+
lines.concat(@coordinator.changed_scope_lines) if @selection.decision_ids
|
|
99
|
+
lines.concat(@coordinator.collation_lines)
|
|
100
|
+
if @ranking.available?
|
|
101
|
+
render_ranking(lines)
|
|
102
|
+
else
|
|
103
|
+
lines << "Ranking unavailable: the report has no analysis."
|
|
104
|
+
end
|
|
105
|
+
unsupported = @ranking.unsupported_count
|
|
106
|
+
if unsupported.positive?
|
|
107
|
+
lines << "Unsupported decisions: #{unsupported} (not ranked; MC/DC unavailable) (run-wide)"
|
|
108
|
+
end
|
|
109
|
+
Array(fetch(@document, :diagnostics)).each do |diagnostic|
|
|
110
|
+
prefix = @selection.decision_ids ? "Run-wide diagnostic" : "Diagnostic"
|
|
111
|
+
lines << "#{prefix}: #{@coordinator.diagnostic_message(diagnostic)}"
|
|
112
|
+
end
|
|
113
|
+
lines.join("\n") << "\n"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def render_ranking(lines)
|
|
119
|
+
lines << ORDER_NOTE
|
|
120
|
+
lines << ""
|
|
121
|
+
files = @ranking.files
|
|
122
|
+
files = files.select { |file| file.gaps.positive? } if @missing_only
|
|
123
|
+
files, hidden_files = @selection.limit(files)
|
|
124
|
+
gaps, hidden_gaps = @selection.limit(@ranking.gaps)
|
|
125
|
+
if @selection.top
|
|
126
|
+
lines << "Display limit: top #{@selection.top} per list; " \
|
|
127
|
+
"hidden #{hidden_files} #{hidden_files == 1 ? "file" : "files"}, " \
|
|
128
|
+
"#{hidden_gaps} #{hidden_gaps == 1 ? "decision" : "decisions"}"
|
|
129
|
+
lines << ""
|
|
130
|
+
end
|
|
131
|
+
render_files(lines, files)
|
|
132
|
+
render_gaps(lines, gaps)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def render_files(lines, files)
|
|
136
|
+
lines << "Files:"
|
|
137
|
+
lines << " none" if files.empty?
|
|
138
|
+
files.each_with_index do |file, position|
|
|
139
|
+
parts = ["#{file.gaps}/#{file.decisions.length} decisions with gaps"]
|
|
140
|
+
parts << "#{file.unexecuted} unexecuted" if file.unexecuted.positive?
|
|
141
|
+
parts << "DT #{file.missing_rules}/#{file.required_rules} rules missing" if file.required_rules.positive?
|
|
142
|
+
parts << "MC/DC #{file.unproven_conditions}/#{file.conditions} conditions unproven" if file.conditions.positive?
|
|
143
|
+
parts << "#{file.missing_alternatives}/#{file.alternatives} alternatives missing" if file.alternatives.positive?
|
|
144
|
+
path = if file.relative_path
|
|
145
|
+
@selection.decision_ids ? @coordinator.display_scope_path(file.relative_path) : file.relative_path
|
|
146
|
+
else
|
|
147
|
+
"location unavailable"
|
|
148
|
+
end
|
|
149
|
+
lines << " #{position + 1}. #{path}: #{parts.join("; ")}"
|
|
150
|
+
end
|
|
151
|
+
lines << ""
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def render_gaps(lines, gaps)
|
|
155
|
+
lines << "Where to start:"
|
|
156
|
+
if gaps.empty?
|
|
157
|
+
if @selection.decision_ids
|
|
158
|
+
message = if !focus_match? || empty_changed_scope? || !@coordinator.changed_coverage_available?
|
|
159
|
+
"No changed-scope gaps can be reported from the available evidence"
|
|
160
|
+
else
|
|
161
|
+
"No changed-scope gaps found"
|
|
162
|
+
end
|
|
163
|
+
lines << " #{message}"
|
|
164
|
+
else
|
|
165
|
+
lines << (focus_match? ? " No missing coverage" : " none")
|
|
166
|
+
end
|
|
167
|
+
lines << ""
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
gaps.each_with_index do |decision, position|
|
|
171
|
+
lines << " #{position + 1}. #{location(decision)} #{self.class.one_line(decision.expression)}"
|
|
172
|
+
lines << " #{self.class.gap_summary(decision)}"
|
|
173
|
+
if @level >= 2
|
|
174
|
+
lines << " Cases to test: #{decision.cases_to_test}"
|
|
175
|
+
self.class.case_lines(decision, @coordinator).each { |line| lines << " #{line}" }
|
|
176
|
+
end
|
|
177
|
+
render_reaching_tests(lines, decision) if @level >= 3
|
|
178
|
+
lines << ""
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def render_reaching_tests(lines, decision)
|
|
183
|
+
ids = decision.test_ids
|
|
184
|
+
if ids.empty?
|
|
185
|
+
lines << " Tests reaching this decision: none recorded"
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
lines << " Tests reaching this decision:"
|
|
189
|
+
ids.map { |id| test_label(id) }.sort.first(SHOWN_TESTS).each { |label| lines << " #{label}" }
|
|
190
|
+
lines << " #{ids.length - SHOWN_TESTS} more" if ids.length > SHOWN_TESTS
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def render_focus_notice(lines)
|
|
194
|
+
return unless @selection.focus_active?
|
|
195
|
+
|
|
196
|
+
label = @selection.focus_label
|
|
197
|
+
no_match = @selection.decision_ids ? "no matching changed decisions" : "no matching decisions"
|
|
198
|
+
label = @coordinator.display_scope_path(label) if @selection.decision_ids
|
|
199
|
+
lines << (focus_match? ? "Focus: #{label}" : "Focus: #{no_match} for #{label}")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def focus_match?
|
|
203
|
+
return true unless @selection.focus_active?
|
|
204
|
+
|
|
205
|
+
ids = if @selection.decision_ids
|
|
206
|
+
@selection.selected_decision_ids(@document)
|
|
207
|
+
else
|
|
208
|
+
@selection.matching_decision_ids(@document)
|
|
209
|
+
end
|
|
210
|
+
!ids.empty?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def empty_changed_scope?
|
|
214
|
+
@selection.decision_ids&.empty?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def location(decision)
|
|
218
|
+
path = @selection.decision_ids ? @coordinator.display_scope_path(decision.relative_path) : decision.relative_path
|
|
219
|
+
return "location unavailable" if decision.relative_path.nil?
|
|
220
|
+
|
|
221
|
+
decision.line ? "#{path}:#{decision.line}" : path
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_label(id)
|
|
225
|
+
test = @tests[id.to_s]
|
|
226
|
+
return id.to_s unless test
|
|
227
|
+
|
|
228
|
+
label = test[:name].to_s
|
|
229
|
+
label += " (#{test[:relative_path]}:#{test[:line]})" if test[:relative_path] && test[:line]
|
|
230
|
+
command = @coordinator.rerun_command(id)
|
|
231
|
+
command ? "#{label} (rerun: #{command})" : label
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def fetch(hash, key)
|
|
235
|
+
return nil unless hash.respond_to?(:key?)
|
|
236
|
+
|
|
237
|
+
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
data/lib/branchproof/version.rb
CHANGED
data/lib/branchproof/worker.rb
CHANGED
|
@@ -65,7 +65,10 @@ module Branchproof
|
|
|
65
65
|
else
|
|
66
66
|
"worker"
|
|
67
67
|
end
|
|
68
|
-
|
|
68
|
+
if payload
|
|
69
|
+
evidence_snapshot = runtime&.snapshot if %w[minitest_missing minitest_unsupported_version].include?(code)
|
|
70
|
+
write_failure(payload, code, { message: e.message }, evidence: evidence_snapshot)
|
|
71
|
+
end
|
|
69
72
|
2
|
|
70
73
|
end
|
|
71
74
|
|
|
@@ -83,9 +86,8 @@ module Branchproof
|
|
|
83
86
|
require_relative "rspec_adapter"
|
|
84
87
|
Branchproof::RSpecAdapter.new(runtime: runtime)
|
|
85
88
|
else
|
|
86
|
-
require "minitest"
|
|
87
|
-
require "minitest/test"
|
|
88
89
|
require_relative "minitest_adapter"
|
|
90
|
+
Branchproof::MinitestAdapter.load_framework!
|
|
89
91
|
Branchproof::MinitestAdapter.new(runtime: runtime)
|
|
90
92
|
end
|
|
91
93
|
end
|
|
@@ -131,8 +133,8 @@ module Branchproof
|
|
|
131
133
|
framework = project[:framework].to_s.empty? ? "minitest" : project[:framework].to_s
|
|
132
134
|
version = if framework == "rspec" && defined?(RSpec::Core::Version::STRING)
|
|
133
135
|
RSpec::Core::Version::STRING
|
|
134
|
-
elsif framework == "minitest"
|
|
135
|
-
Minitest::VERSION
|
|
136
|
+
elsif framework == "minitest"
|
|
137
|
+
Gem.loaded_specs["minitest"]&.version&.to_s || (Minitest::VERSION if defined?(Minitest::VERSION))
|
|
136
138
|
end
|
|
137
139
|
metadata = { kind: project[:kind].to_s, root: project[:root].to_s,
|
|
138
140
|
framework: framework, framework_version: version,
|
|
@@ -169,12 +171,15 @@ module Branchproof
|
|
|
169
171
|
nil
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
def write_failure(payload, code, details)
|
|
174
|
+
def write_failure(payload, code, details, evidence: nil)
|
|
173
175
|
return unless payload["result_path"]
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
diagnostic = { code: code, severity: "error", message: details.to_s }
|
|
178
|
+
result = { status: "ERROR", executed_tests: 0, failed_tests: 0, skipped_tests: 0,
|
|
179
|
+
finalized: false, exit_status: 2, project: project_metadata_for(payload),
|
|
180
|
+
diagnostics: [diagnostic] }
|
|
181
|
+
result[:evidence] = incomplete_evidence(evidence, [diagnostic]) if evidence
|
|
182
|
+
write_completion(payload, result)
|
|
178
183
|
end
|
|
179
184
|
|
|
180
185
|
def project_metadata_for(payload)
|
data/lib/branchproof.rb
CHANGED
|
@@ -11,8 +11,10 @@ module Branchproof
|
|
|
11
11
|
class Error < StandardError; end
|
|
12
12
|
autoload :Configuration, "branchproof/configuration"
|
|
13
13
|
autoload :CoveragePolicy, "branchproof/coverage_policy"
|
|
14
|
+
autoload :CoverageSummary, "branchproof/coverage_summary"
|
|
14
15
|
autoload :ReportSelection, "branchproof/report_selection"
|
|
15
16
|
autoload :CLI, "branchproof/cli"
|
|
17
|
+
autoload :Doctor, "branchproof/doctor"
|
|
16
18
|
autoload :Instrumenter, "branchproof/instrumenter"
|
|
17
19
|
autoload :Loader, "branchproof/loader"
|
|
18
20
|
autoload :Runtime, "branchproof/runtime"
|
|
@@ -26,9 +28,19 @@ module Branchproof
|
|
|
26
28
|
autoload :Report, "branchproof/report"
|
|
27
29
|
autoload :CoverageIndex, "branchproof/coverage_index"
|
|
28
30
|
autoload :FocusedReport, "branchproof/focused_report"
|
|
31
|
+
autoload :SummaryRanking, "branchproof/summary_ranking"
|
|
32
|
+
autoload :SummaryReport, "branchproof/summary_report"
|
|
33
|
+
autoload :HtmlReport, "branchproof/html_report"
|
|
34
|
+
autoload :GithubReport, "branchproof/github_report"
|
|
29
35
|
autoload :SavedReport, "branchproof/saved_report"
|
|
30
36
|
autoload :Comparison, "branchproof/comparison"
|
|
37
|
+
autoload :Collation, "branchproof/collation"
|
|
38
|
+
autoload :CollationManifest, "branchproof/collation_manifest"
|
|
31
39
|
autoload :ComparisonReport, "branchproof/comparison_report"
|
|
40
|
+
autoload :ChangedScope, "branchproof/changed_scope"
|
|
41
|
+
autoload :GitChanges, "branchproof/git_changes"
|
|
42
|
+
autoload :ChangedDecisionMap, "branchproof/changed_decision_map"
|
|
43
|
+
autoload :ChangedCoverage, "branchproof/changed_coverage"
|
|
32
44
|
autoload :Worker, "branchproof/worker"
|
|
33
45
|
end
|
|
34
46
|
|
data/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
4
|
| --- | --- |
|
|
5
|
-
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/minimizer.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/value_syntax.rb, lib/branchproof/configuration.rb, lib/branchproof/rails_support.rb, lib/branchproof/rspec_adapter.rb, lib/branchproof/value_runtime.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/default_syntax.rb, lib/branchproof/focused_report.rb, lib/branchproof/coverage_policy.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/default_runtime.rb, lib/branchproof/exception_syntax.rb, lib/branchproof/iteration_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/report_selection.rb, lib/branchproof/comparison_report.rb, lib/branchproof/exception_runtime.rb, lib/branchproof/iteration_runtime.rb, lib/branchproof/flow_instrumentation.rb, lib/branchproof/value_instrumentation.rb, lib/branchproof/default_instrumentation.rb, lib/branchproof/exception_instrumentation.rb, lib/branchproof/iteration_instrumentation.rb, lib/branchproof/extended_alternative_runtime.rb |
|
|
5
|
+
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/doctor.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/collation.rb, lib/branchproof/minimizer.rb, lib/branchproof/rake_task.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/git_changes.rb, lib/branchproof/html_report.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/value_syntax.rb, lib/branchproof/changed_scope.rb, lib/branchproof/configuration.rb, lib/branchproof/github_report.rb, lib/branchproof/rails_support.rb, lib/branchproof/rspec_adapter.rb, lib/branchproof/value_runtime.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/default_syntax.rb, lib/branchproof/focused_report.rb, lib/branchproof/summary_report.rb, lib/branchproof/coverage_policy.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/default_runtime.rb, lib/branchproof/fallback_syntax.rb, lib/branchproof/summary_ranking.rb, lib/branchproof/changed_coverage.rb, lib/branchproof/coverage_summary.rb, lib/branchproof/exception_syntax.rb, lib/branchproof/fallback_runtime.rb, lib/branchproof/iteration_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/report_selection.rb, lib/branchproof/comparison_report.rb, lib/branchproof/exception_runtime.rb, lib/branchproof/iteration_runtime.rb, lib/branchproof/collation_manifest.rb, lib/branchproof/changed_decision_map.rb, lib/branchproof/flow_instrumentation.rb, lib/branchproof/value_instrumentation.rb, lib/branchproof/default_instrumentation.rb, lib/branchproof/fallback_instrumentation.rb, lib/branchproof/exception_instrumentation.rb, lib/branchproof/iteration_instrumentation.rb, lib/branchproof/extended_alternative_runtime.rb |
|
|
6
6
|
|
|
7
7
|
Keep source-boundary and parameter-binding rules together for auditing.
|
|
8
8
|
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity,
|
|
@@ -16,6 +16,12 @@ Not documented.
|
|
|
16
16
|
|
|
17
17
|
- [Branchproof/Analyzer.md](doc/Branchproof/Analyzer.md)
|
|
18
18
|
- [Branchproof/CLI.md](doc/Branchproof/CLI.md)
|
|
19
|
+
- [Branchproof/ChangedCoverage.md](doc/Branchproof/ChangedCoverage.md)
|
|
20
|
+
- [Branchproof/ChangedCoveragePolicy.md](doc/Branchproof/ChangedCoveragePolicy.md)
|
|
21
|
+
- [Branchproof/ChangedDecisionMap.md](doc/Branchproof/ChangedDecisionMap.md)
|
|
22
|
+
- [Branchproof/ChangedScope.md](doc/Branchproof/ChangedScope.md)
|
|
23
|
+
- [Branchproof/Collation.md](doc/Branchproof/Collation.md)
|
|
24
|
+
- [Branchproof/CollationManifest.md](doc/Branchproof/CollationManifest.md)
|
|
19
25
|
- [Branchproof/Comparison.md](doc/Branchproof/Comparison.md)
|
|
20
26
|
- [Branchproof/ComparisonReport.md](doc/Branchproof/ComparisonReport.md)
|
|
21
27
|
- [Branchproof/Configuration.md](doc/Branchproof/Configuration.md)
|
|
@@ -23,19 +29,28 @@ Not documented.
|
|
|
23
29
|
- [Branchproof/Constraints.md](doc/Branchproof/Constraints.md)
|
|
24
30
|
- [Branchproof/CoverageIndex.md](doc/Branchproof/CoverageIndex.md)
|
|
25
31
|
- [Branchproof/CoveragePolicy.md](doc/Branchproof/CoveragePolicy.md)
|
|
32
|
+
- [Branchproof/CoverageSummary.md](doc/Branchproof/CoverageSummary.md)
|
|
26
33
|
- [Branchproof/DecisionSyntax.md](doc/Branchproof/DecisionSyntax.md)
|
|
27
34
|
- [Branchproof/DecisionTable.md](doc/Branchproof/DecisionTable.md)
|
|
28
35
|
- [Branchproof/DefaultInstrumentation.md](doc/Branchproof/DefaultInstrumentation.md)
|
|
29
36
|
- [Branchproof/DefaultRuntime.md](doc/Branchproof/DefaultRuntime.md)
|
|
30
37
|
- [Branchproof/DefaultSyntax.md](doc/Branchproof/DefaultSyntax.md)
|
|
38
|
+
- [Branchproof/Doctor/TerminalRenderer.md](doc/Branchproof/Doctor/TerminalRenderer.md)
|
|
39
|
+
- [Branchproof/Doctor.md](doc/Branchproof/Doctor.md)
|
|
31
40
|
- [Branchproof/Error.md](doc/Branchproof/Error.md)
|
|
32
41
|
- [Branchproof/Evidence.md](doc/Branchproof/Evidence.md)
|
|
33
42
|
- [Branchproof/ExceptionInstrumentation.md](doc/Branchproof/ExceptionInstrumentation.md)
|
|
34
43
|
- [Branchproof/ExceptionRuntime.md](doc/Branchproof/ExceptionRuntime.md)
|
|
35
44
|
- [Branchproof/ExceptionSyntax.md](doc/Branchproof/ExceptionSyntax.md)
|
|
36
45
|
- [Branchproof/ExtendedAlternativeRuntime.md](doc/Branchproof/ExtendedAlternativeRuntime.md)
|
|
46
|
+
- [Branchproof/FallbackInstrumentation.md](doc/Branchproof/FallbackInstrumentation.md)
|
|
47
|
+
- [Branchproof/FallbackRuntime.md](doc/Branchproof/FallbackRuntime.md)
|
|
48
|
+
- [Branchproof/FallbackSyntax.md](doc/Branchproof/FallbackSyntax.md)
|
|
37
49
|
- [Branchproof/FlowInstrumentation.md](doc/Branchproof/FlowInstrumentation.md)
|
|
38
50
|
- [Branchproof/FocusedReport.md](doc/Branchproof/FocusedReport.md)
|
|
51
|
+
- [Branchproof/GitChanges.md](doc/Branchproof/GitChanges.md)
|
|
52
|
+
- [Branchproof/GithubReport.md](doc/Branchproof/GithubReport.md)
|
|
53
|
+
- [Branchproof/HtmlReport.md](doc/Branchproof/HtmlReport.md)
|
|
39
54
|
- [Branchproof/Instrumenter.md](doc/Branchproof/Instrumenter.md)
|
|
40
55
|
- [Branchproof/IterationInstrumentation.md](doc/Branchproof/IterationInstrumentation.md)
|
|
41
56
|
- [Branchproof/IterationRuntime.md](doc/Branchproof/IterationRuntime.md)
|
|
@@ -54,6 +69,7 @@ Not documented.
|
|
|
54
69
|
- [Branchproof/RSpecAdapter.md](doc/Branchproof/RSpecAdapter.md)
|
|
55
70
|
- [Branchproof/RailsSupport/Error.md](doc/Branchproof/RailsSupport/Error.md)
|
|
56
71
|
- [Branchproof/RailsSupport.md](doc/Branchproof/RailsSupport.md)
|
|
72
|
+
- [Branchproof/RakeTask.md](doc/Branchproof/RakeTask.md)
|
|
57
73
|
- [Branchproof/Records.md](doc/Branchproof/Records.md)
|
|
58
74
|
- [Branchproof/Report.md](doc/Branchproof/Report.md)
|
|
59
75
|
- [Branchproof/ReportSelection.md](doc/Branchproof/ReportSelection.md)
|
|
@@ -61,6 +77,10 @@ Not documented.
|
|
|
61
77
|
- [Branchproof/RuntimeFlow.md](doc/Branchproof/RuntimeFlow.md)
|
|
62
78
|
- [Branchproof/SavedReport.md](doc/Branchproof/SavedReport.md)
|
|
63
79
|
- [Branchproof/Source.md](doc/Branchproof/Source.md)
|
|
80
|
+
- [Branchproof/SummaryRanking/Decision.md](doc/Branchproof/SummaryRanking/Decision.md)
|
|
81
|
+
- [Branchproof/SummaryRanking/SourceFile.md](doc/Branchproof/SummaryRanking/SourceFile.md)
|
|
82
|
+
- [Branchproof/SummaryRanking.md](doc/Branchproof/SummaryRanking.md)
|
|
83
|
+
- [Branchproof/SummaryReport.md](doc/Branchproof/SummaryReport.md)
|
|
64
84
|
- [Branchproof/ValueInstrumentation.md](doc/Branchproof/ValueInstrumentation.md)
|
|
65
85
|
- [Branchproof/ValueRuntime.md](doc/Branchproof/ValueRuntime.md)
|
|
66
86
|
- [Branchproof/ValueSyntax.md](doc/Branchproof/ValueSyntax.md)
|