branchproof 0.10.0 → 0.11.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 +23 -0
- data/README.md +153 -10
- data/doc/Branchproof/Configuration.md +28 -0
- data/doc/Branchproof/CoveragePolicy.md +27 -0
- data/doc/Branchproof/FocusedReport.md +1 -1
- data/doc/Branchproof/Report.md +8 -2
- data/doc/Branchproof/ReportSelection.md +42 -0
- data/doc/Branchproof.md +4 -1
- data/doc/CHANGELOG.md +23 -0
- data/doc/README.md +153 -10
- data/lib/branchproof/cli.rb +120 -13
- data/lib/branchproof/comparison.rb +6 -1
- data/lib/branchproof/configuration.rb +111 -0
- data/lib/branchproof/coverage_policy.rb +169 -0
- data/lib/branchproof/evidence.rb +37 -2
- data/lib/branchproof/focused_report.rb +144 -27
- data/lib/branchproof/report.rb +97 -9
- data/lib/branchproof/report_selection.rb +158 -0
- data/lib/branchproof/saved_report.rb +29 -3
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof.rb +3 -0
- data/llms.txt +4 -1
- data/sig/branchproof.rbs +35 -3
- metadata +7 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
|
4
|
+
|
|
5
|
+
module Branchproof
|
|
6
|
+
# Evaluates configured coverage thresholds against the aggregate Analyzer
|
|
7
|
+
# coverage counts without relying on rounded percentage fields.
|
|
8
|
+
class CoveragePolicy
|
|
9
|
+
CRITERIA = {
|
|
10
|
+
"decision" => { numerator: "covered_decisions", denominator: "supported_decisions" },
|
|
11
|
+
"condition" => { numerator: "covered_values", denominator: "required_values" },
|
|
12
|
+
"condition_decision" => { numerator: "covered_decisions", denominator: "supported_decisions" },
|
|
13
|
+
"mcdc" => { numerator: "proven_conditions", denominator: "supported_conditions" },
|
|
14
|
+
"decision_table" => { numerator: "covered_rules", denominator: "required_rules" }
|
|
15
|
+
}.freeze
|
|
16
|
+
COMPLETENESS_FIELDS = %w[observation attribution analysis].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def normalize(minimum)
|
|
20
|
+
raise ArgumentError, "minimum must be a hash" unless minimum.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
minimum.each_with_object({}) do |(key, threshold), normalized|
|
|
23
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
24
|
+
raise ArgumentError, "unknown coverage criterion: #{key.inspect}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
criterion = key.to_s
|
|
28
|
+
raise ArgumentError, "unknown coverage criterion: #{criterion}" unless CRITERIA.key?(criterion)
|
|
29
|
+
raise ArgumentError, "duplicate coverage criterion: #{criterion}" if normalized.key?(criterion)
|
|
30
|
+
|
|
31
|
+
validate_threshold!(criterion, threshold)
|
|
32
|
+
normalized[criterion] = threshold
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def validate_threshold!(criterion, threshold)
|
|
39
|
+
valid_type = threshold.is_a?(Integer) || threshold.is_a?(Float)
|
|
40
|
+
valid_float = !threshold.is_a?(Float) || threshold.finite?
|
|
41
|
+
unless valid_type && valid_float
|
|
42
|
+
raise ArgumentError, "coverage minimum for #{criterion} must be a finite number"
|
|
43
|
+
end
|
|
44
|
+
return if (0..100).cover?(threshold)
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "coverage minimum for #{criterion} must be between 0 and 100"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(minimum:)
|
|
51
|
+
@minimum = self.class.normalize(minimum)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(document:)
|
|
55
|
+
return { minimum: @minimum.dup, status: "passed", gates: [] } if @minimum.empty?
|
|
56
|
+
|
|
57
|
+
global_reason = availability_reason(document)
|
|
58
|
+
gates = @minimum.map do |criterion, threshold|
|
|
59
|
+
evaluate_gate(criterion, threshold, document, global_reason)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
status = if gates.any? { |gate| gate[:status] == "unavailable" }
|
|
63
|
+
"unavailable"
|
|
64
|
+
elsif gates.any? { |gate| gate[:status] == "failed" }
|
|
65
|
+
"failed"
|
|
66
|
+
else
|
|
67
|
+
"passed"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
{ minimum: @minimum.dup, status: status, gates: gates }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def availability_reason(document)
|
|
76
|
+
return "document_unavailable" unless document.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
baseline = value(document, :baseline)
|
|
79
|
+
baseline_passed = baseline.is_a?(Hash) && value(baseline, :status).to_s.upcase == "PASSED"
|
|
80
|
+
return "baseline_unavailable" unless baseline_passed && value(baseline, :finalized) == true
|
|
81
|
+
return "incomplete_document" unless complete?(value(document, :completeness))
|
|
82
|
+
|
|
83
|
+
observations = value(document, :observations) || value(document, :evidence)
|
|
84
|
+
return "incomplete_observations" if incomplete_section?(observations)
|
|
85
|
+
|
|
86
|
+
analysis = value(document, :analysis)
|
|
87
|
+
return "incomplete_analysis" if incomplete_section?(analysis)
|
|
88
|
+
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def complete?(completeness)
|
|
93
|
+
return false unless completeness.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
COMPLETENESS_FIELDS.all? { |field| value(completeness, field) == true }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def incomplete_section?(section)
|
|
99
|
+
section.is_a?(Hash) && key_present?(section, :completeness) && !complete?(value(section, :completeness))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def evaluate_gate(criterion, threshold, document, global_reason)
|
|
103
|
+
return gate(criterion, threshold, nil, nil, "unavailable", global_reason) if global_reason
|
|
104
|
+
|
|
105
|
+
coverage = value(value(document, :analysis), :coverage)
|
|
106
|
+
row = value(coverage, criterion)
|
|
107
|
+
return gate(criterion, threshold, nil, nil, "unavailable", "missing_coverage_count") unless row.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
mapping = CRITERIA.fetch(criterion)
|
|
110
|
+
numerator = value(row, mapping.fetch(:numerator))
|
|
111
|
+
denominator = value(row, mapping.fetch(:denominator))
|
|
112
|
+
|
|
113
|
+
if criterion == "decision_table"
|
|
114
|
+
not_calculated = value(row, :not_calculated_decisions)
|
|
115
|
+
unless not_calculated.nil?
|
|
116
|
+
unless valid_count?(not_calculated)
|
|
117
|
+
return gate(criterion, threshold, numerator, denominator, "unavailable", "invalid_coverage_count")
|
|
118
|
+
end
|
|
119
|
+
if not_calculated.positive?
|
|
120
|
+
return gate(criterion, threshold, numerator, denominator, "unavailable", "decision_table_not_calculated")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if numerator.nil? || denominator.nil?
|
|
126
|
+
return unavailable_gate(criterion, threshold, numerator, denominator, "missing_coverage_count")
|
|
127
|
+
end
|
|
128
|
+
unless valid_count?(numerator) && valid_count?(denominator)
|
|
129
|
+
return unavailable_gate(criterion, threshold, numerator, denominator, "invalid_coverage_count")
|
|
130
|
+
end
|
|
131
|
+
return unavailable_gate(criterion, threshold, numerator, denominator, "zero_denominator") if denominator.zero?
|
|
132
|
+
if numerator > denominator
|
|
133
|
+
return unavailable_gate(criterion, threshold, numerator, denominator, "invalid_coverage_count")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
status = numerator * 100 >= denominator * Rational(threshold.to_s) ? "passed" : "failed"
|
|
137
|
+
gate(criterion, threshold, numerator, denominator, status, nil)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def valid_count?(value)
|
|
141
|
+
value.is_a?(Integer) && value >= 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def gate(criterion, threshold, numerator, denominator, status, reason)
|
|
145
|
+
{ criterion: criterion, numerator: numerator, denominator: denominator, minimum: threshold,
|
|
146
|
+
status: status, reason: reason }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def unavailable_gate(criterion, threshold, numerator, denominator, reason)
|
|
150
|
+
gate(criterion, threshold, numerator, denominator, "unavailable", reason)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def value(hash, key)
|
|
154
|
+
return nil unless hash.respond_to?(:key?)
|
|
155
|
+
return hash[key] if hash.key?(key)
|
|
156
|
+
|
|
157
|
+
alternate = key.is_a?(Symbol) ? key.to_s : key.to_sym
|
|
158
|
+
return hash[alternate] if hash.key?(alternate)
|
|
159
|
+
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def key_present?(hash, key)
|
|
164
|
+
hash.key?(key) || hash.key?(key.to_s) || hash.key?(key.to_sym)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
data/lib/branchproof/evidence.rb
CHANGED
|
@@ -50,7 +50,9 @@ module Branchproof
|
|
|
50
50
|
current = @tests[id] || { id: id, adapter: "unknown", name: id, source: nil, class_name: nil,
|
|
51
51
|
method_name: nil, status: "unknown", phase_counts: {} }
|
|
52
52
|
merged = current.merge(value).merge(id: id)
|
|
53
|
-
merged[:phase_counts] = (current[:phase_counts]
|
|
53
|
+
merged[:phase_counts] = normalize_phase_counts(current[:phase_counts]).merge(
|
|
54
|
+
normalize_phase_counts(value[:phase_counts])
|
|
55
|
+
)
|
|
54
56
|
@tests[id] = deep_dup(merged)
|
|
55
57
|
status("registered", nil)
|
|
56
58
|
end
|
|
@@ -172,7 +174,7 @@ module Branchproof
|
|
|
172
174
|
@run_payloads[id] = Branchproof::Records.id(payload)
|
|
173
175
|
end
|
|
174
176
|
@run_ids |= incoming_runs
|
|
175
|
-
incoming.fetch(:tests, []).each {
|
|
177
|
+
incoming.fetch(:tests, []).each { merge_test(_1) }
|
|
176
178
|
incoming.fetch(:vectors, []).each { merge_vector(_1) }
|
|
177
179
|
@diagnostics.concat(incoming.fetch(:diagnostics, []))
|
|
178
180
|
@abort_counts.merge!(incoming.fetch(:abort_counts, {})) { |_k, a, b| a.to_i + b.to_i }
|
|
@@ -337,6 +339,39 @@ module Branchproof
|
|
|
337
339
|
end
|
|
338
340
|
end
|
|
339
341
|
|
|
342
|
+
def merge_test(raw)
|
|
343
|
+
value = symbolize(raw)
|
|
344
|
+
value[:phase_counts] = normalize_phase_counts(value[:phase_counts])
|
|
345
|
+
id = (value[:id] || test_id(value)).to_s
|
|
346
|
+
unless @tests.key?(id)
|
|
347
|
+
register_test(test: value)
|
|
348
|
+
return unless @tests.key?(id)
|
|
349
|
+
|
|
350
|
+
return
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
current = @tests.fetch(id)
|
|
354
|
+
merged = current.merge(value).merge(id: id)
|
|
355
|
+
merged[:phase_counts] = sum_phase_counts(current[:phase_counts], value[:phase_counts])
|
|
356
|
+
@tests[id] = deep_dup(merged)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def normalize_phase_counts(value)
|
|
360
|
+
(value || {}).each_with_object({}) do |(phase, count), result|
|
|
361
|
+
key = phase.to_s
|
|
362
|
+
result[key] = result.fetch(key, 0) + count
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def sum_phase_counts(*counts)
|
|
367
|
+
counts.each_with_object({}) do |phase_counts, result|
|
|
368
|
+
normalize_phase_counts(phase_counts).each do |phase, count|
|
|
369
|
+
result[phase] = result.fetch(phase, 0) + count
|
|
370
|
+
end
|
|
371
|
+
result
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
340
375
|
def merge_error(incoming)
|
|
341
376
|
return "unsupported schema" unless incoming[:schema_version].to_s == SCHEMA_VERSION
|
|
342
377
|
return "criterion mismatch" unless incoming[:criterion_version].to_s == CRITERION_VERSION
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
4
4
|
require "pathname"
|
|
5
|
+
require_relative "coverage_index"
|
|
6
|
+
require_relative "report_selection"
|
|
5
7
|
|
|
6
8
|
module Branchproof
|
|
7
9
|
# Terminal renderings grouped around conditions or tests.
|
|
8
10
|
class FocusedReport
|
|
9
11
|
DECISION_TABLE_LABELS = { "true" => "T", "false" => "F", "dont_care" => "-" }.freeze
|
|
10
12
|
|
|
11
|
-
def initialize(document:, view:, level:, missing_only: false, coordinator: nil
|
|
13
|
+
def initialize(document:, view:, level:, missing_only: false, coordinator: nil, focus: nil, top: nil,
|
|
14
|
+
selection: nil)
|
|
12
15
|
@document = document || {}
|
|
13
16
|
@view = view.to_sym
|
|
14
17
|
unless (Report::VIEWS - [:decisions]).include?(@view)
|
|
@@ -17,12 +20,14 @@ module Branchproof
|
|
|
17
20
|
|
|
18
21
|
@level = level.to_i
|
|
19
22
|
@missing_only = missing_only ? true : false
|
|
23
|
+
@selection = selection || ReportSelection.new(focus: focus, top: top)
|
|
20
24
|
@index = CoverageIndex.new(document: @document)
|
|
21
25
|
@tests = @index.tests.to_h { |test| [test[:id].to_s, test] }
|
|
22
26
|
@test_name_counts = @index.tests.each_with_object(Hash.new(0)) do |test, counts|
|
|
23
27
|
counts[[test[:name], test[:relative_path], test[:line]]] += 1
|
|
24
28
|
end
|
|
25
29
|
@coordinator = coordinator || Report.from_document(document: @document, level: @level)
|
|
30
|
+
@matching_decision_ids = @selection.focus_active? ? @selection.matching_decision_ids(@document) : nil
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def render
|
|
@@ -45,20 +50,19 @@ module Branchproof
|
|
|
45
50
|
end
|
|
46
51
|
lines << "Empty groups mean no recorded completed observation."
|
|
47
52
|
lines << ""
|
|
53
|
+
render_focus_notice(lines)
|
|
48
54
|
lines.concat(@coordinator.coverage_ladder_lines)
|
|
55
|
+
lines.concat(@coordinator.coverage_policy_lines)
|
|
49
56
|
case @view
|
|
50
57
|
when :conditions
|
|
51
58
|
render_conditions(lines)
|
|
52
|
-
render_alternatives(lines)
|
|
53
59
|
when :decision_tables
|
|
54
60
|
render_decision_tables(lines)
|
|
55
61
|
else
|
|
56
62
|
render_tests(lines)
|
|
57
63
|
end
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
render_unsupported(lines)
|
|
61
|
-
end
|
|
64
|
+
render_unowned(lines)
|
|
65
|
+
render_unsupported(lines)
|
|
62
66
|
Array(fetch(@document, :diagnostics)).each do |diagnostic|
|
|
63
67
|
lines << "Diagnostic: #{@coordinator.diagnostic_message(diagnostic)}"
|
|
64
68
|
end
|
|
@@ -68,8 +72,8 @@ module Branchproof
|
|
|
68
72
|
private
|
|
69
73
|
|
|
70
74
|
def render_conditions(lines)
|
|
71
|
-
rows =
|
|
72
|
-
|
|
75
|
+
rows, alternatives, hidden = condition_rows_for_display
|
|
76
|
+
append_limit_summary(lines, hidden, "condition/alternative row")
|
|
73
77
|
rows.each do |row|
|
|
74
78
|
lines << "Condition: #{row[:expression]}"
|
|
75
79
|
lines << "Location: #{location(row[:relative_path], row[:line], unavailable: "condition line unavailable")}"
|
|
@@ -108,7 +112,8 @@ module Branchproof
|
|
|
108
112
|
end
|
|
109
113
|
lines << ""
|
|
110
114
|
end
|
|
111
|
-
lines << "No missing conditions" if @missing_only && rows.empty?
|
|
115
|
+
lines << "No missing conditions" if @missing_only && rows.empty? && focus_match_exists?
|
|
116
|
+
render_alternatives(lines, alternatives)
|
|
112
117
|
end
|
|
113
118
|
|
|
114
119
|
def coverage_available?
|
|
@@ -133,11 +138,12 @@ module Branchproof
|
|
|
133
138
|
# --missing-only only uncovered, non-impossible rules remain: a rule proven
|
|
134
139
|
# impossible is not a missing obligation.
|
|
135
140
|
def render_decision_tables(lines)
|
|
136
|
-
|
|
141
|
+
all_rows = filtered_decision_tables
|
|
142
|
+
rows, hidden = limit_rows(all_rows)
|
|
143
|
+
append_limit_summary(lines, hidden, "decision")
|
|
137
144
|
rendered = 0
|
|
138
|
-
impossible =
|
|
145
|
+
impossible = @index.decision_tables.sum { |row| row[:impossible_rules].to_i }
|
|
139
146
|
rows.each do |row|
|
|
140
|
-
impossible += row[:impossible_rules].to_i
|
|
141
147
|
rules = @missing_only ? row[:rules].select { |rule| rule[:coverage].to_s == "missing" } : row[:rules]
|
|
142
148
|
next if @missing_only && rules.empty? && row[:status].to_s == "calculated"
|
|
143
149
|
|
|
@@ -187,9 +193,8 @@ module Branchproof
|
|
|
187
193
|
lines << " Reason: #{Constraints.message(rule[:reachability_reason])}"
|
|
188
194
|
end
|
|
189
195
|
|
|
190
|
-
def render_alternatives(lines)
|
|
191
|
-
rows
|
|
192
|
-
rows = rows.select { |row| row[:missing] || row[:status].to_s != "covered" } if @missing_only && @level > 1
|
|
196
|
+
def render_alternatives(lines, rows = nil)
|
|
197
|
+
rows ||= filtered_alternatives
|
|
193
198
|
rows.each do |row|
|
|
194
199
|
lines << "Alternative #{row[:index]}: #{row[:expression]}"
|
|
195
200
|
lines << "Location: #{location(row[:relative_path], row[:line],
|
|
@@ -208,7 +213,7 @@ module Branchproof
|
|
|
208
213
|
end
|
|
209
214
|
lines << ""
|
|
210
215
|
end
|
|
211
|
-
lines << "No missing alternatives" if @missing_only && rows.empty?
|
|
216
|
+
lines << "No missing alternatives" if @missing_only && rows.empty? && focus_match_exists?
|
|
212
217
|
end
|
|
213
218
|
|
|
214
219
|
def render_alternative_group(lines, heading, evidence)
|
|
@@ -219,22 +224,22 @@ module Branchproof
|
|
|
219
224
|
end
|
|
220
225
|
|
|
221
226
|
def render_tests(lines)
|
|
222
|
-
rows =
|
|
227
|
+
rows = filtered_tests
|
|
223
228
|
missing_ids = @index.conditions.reject { |row| row[:status].to_s.upcase == "PROVEN" }.map { |row| row[:id] }
|
|
224
229
|
missing_alternative_ids = @index.alternatives.select { |row| row[:missing] || row[:status].to_s != "covered" }
|
|
225
230
|
.map { |row| row[:alternative_id] }
|
|
231
|
+
rows = rows.select do |row|
|
|
232
|
+
!@missing_only || test_observations(row, missing_ids, missing_alternative_ids).any?
|
|
233
|
+
end
|
|
234
|
+
rows, hidden = limit_rows(rows)
|
|
235
|
+
append_limit_summary(lines, hidden, "test")
|
|
226
236
|
rows.each { |row| render_test_row(lines, row, missing_ids, missing_alternative_ids) }
|
|
227
237
|
end
|
|
228
238
|
|
|
229
239
|
def render_test_row(lines, row, missing_ids, missing_alternative_ids = [])
|
|
230
|
-
observations = row
|
|
231
|
-
if @missing_only
|
|
232
|
-
|
|
233
|
-
missing_ids.include?(observation[:condition_id]) ||
|
|
234
|
-
missing_alternative_ids.include?(observation[:alternative_id])
|
|
235
|
-
end
|
|
236
|
-
return if observations.empty?
|
|
237
|
-
end
|
|
240
|
+
observations = test_observations(row, missing_ids, missing_alternative_ids)
|
|
241
|
+
return if @missing_only && observations.empty?
|
|
242
|
+
|
|
238
243
|
lines << "Test: #{row[:name]}"
|
|
239
244
|
lines << "Location: #{location(row[:relative_path], row[:line], unavailable: "location unavailable")}"
|
|
240
245
|
lines << "Status: #{row[:status] || "unknown"}"
|
|
@@ -291,6 +296,11 @@ module Branchproof
|
|
|
291
296
|
groups.each do |heading, conditions|
|
|
292
297
|
next if conditions.empty?
|
|
293
298
|
|
|
299
|
+
if @selection.active?
|
|
300
|
+
lines << "#{heading}: #{conditions.length} (run-wide)"
|
|
301
|
+
next
|
|
302
|
+
end
|
|
303
|
+
|
|
294
304
|
lines << "#{heading}:"
|
|
295
305
|
conditions.each do |row|
|
|
296
306
|
unavailable = row[:alternative_id] ? "alternative location unavailable" : "condition line unavailable"
|
|
@@ -307,6 +317,12 @@ module Branchproof
|
|
|
307
317
|
end
|
|
308
318
|
return if excluded.empty?
|
|
309
319
|
|
|
320
|
+
if @selection.active?
|
|
321
|
+
noun = excluded.length == 1 ? "decision" : "decisions"
|
|
322
|
+
lines << "Unsupported conditions (MC/DC unavailable): #{excluded.length} #{noun} (run-wide)"
|
|
323
|
+
return
|
|
324
|
+
end
|
|
325
|
+
|
|
310
326
|
lines << "Unsupported conditions (MC/DC unavailable):"
|
|
311
327
|
excluded.each do |decision|
|
|
312
328
|
source = sources[fetch(decision, :source_id)] || {}
|
|
@@ -321,6 +337,107 @@ module Branchproof
|
|
|
321
337
|
end
|
|
322
338
|
end
|
|
323
339
|
|
|
340
|
+
def render_focus_notice(lines)
|
|
341
|
+
return unless @selection.focus_active?
|
|
342
|
+
|
|
343
|
+
lines << if @matching_decision_ids.empty?
|
|
344
|
+
"Focus: no matching decisions for #{@selection.focus_label}"
|
|
345
|
+
else
|
|
346
|
+
"Focus: #{@selection.focus_label}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def append_limit_summary(lines, hidden, unit)
|
|
351
|
+
return unless @selection.top
|
|
352
|
+
|
|
353
|
+
noun = hidden == 1 ? unit : "#{unit}s"
|
|
354
|
+
hidden_noun = hidden == 1 ? unit : "#{unit}s"
|
|
355
|
+
lines << "Display limit: top #{@selection.top} #{noun}; hidden #{hidden} #{hidden_noun} " \
|
|
356
|
+
"(not risk-ranked)"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def focus_match_exists?
|
|
360
|
+
!@selection.focus_active? || !@matching_decision_ids.empty?
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def filtered_conditions
|
|
364
|
+
rows = @index.conditions
|
|
365
|
+
rows = rows.select { |row| @matching_decision_ids.include?(row[:decision_id].to_s) } if @selection.focus_active?
|
|
366
|
+
rows = rows.reject { |row| row[:status].to_s.upcase == "PROVEN" } if @missing_only && @level > 1
|
|
367
|
+
rows.sort_by do |row|
|
|
368
|
+
[row[:relative_path].to_s, row[:line].to_i, row[:column].to_i,
|
|
369
|
+
row[:decision_id].to_s, row[:index].to_i]
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def filtered_alternatives
|
|
374
|
+
rows = @index.alternatives
|
|
375
|
+
rows = rows.select { |row| @matching_decision_ids.include?(row[:decision_id].to_s) } if @selection.focus_active?
|
|
376
|
+
rows = rows.select { |row| row[:missing] || row[:status].to_s != "covered" } if @missing_only && @level > 1
|
|
377
|
+
rows.sort_by do |row|
|
|
378
|
+
[row[:relative_path].to_s, row[:line].to_i, row[:column].to_i,
|
|
379
|
+
row[:decision_id].to_s, row[:index].to_i]
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def condition_rows_for_display
|
|
384
|
+
conditions = filtered_conditions
|
|
385
|
+
alternatives = filtered_alternatives
|
|
386
|
+
return [conditions, alternatives, 0] unless @selection.top
|
|
387
|
+
|
|
388
|
+
combined = (conditions.map { |row| [:condition, row] } + alternatives.map { |row| [:alternative, row] })
|
|
389
|
+
.sort_by do |kind, row|
|
|
390
|
+
[row[:relative_path].to_s, row[:line].to_i, row[:column].to_i, kind == :condition ? 0 : 1,
|
|
391
|
+
row[:decision_id].to_s, row[:index].to_i]
|
|
392
|
+
end
|
|
393
|
+
visible, hidden = @selection.limit(combined)
|
|
394
|
+
[visible.filter_map { |kind, row| row if kind == :condition },
|
|
395
|
+
visible.filter_map { |kind, row| row if kind == :alternative }, hidden]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def filtered_decision_tables
|
|
399
|
+
rows = @index.decision_tables
|
|
400
|
+
rows = rows.select { |row| @matching_decision_ids.include?(row[:decision_id].to_s) } if @selection.focus_active?
|
|
401
|
+
rows = rows.select do |row|
|
|
402
|
+
!@missing_only || row[:status].to_s != "calculated" || row[:rules].any? do |rule|
|
|
403
|
+
rule[:coverage].to_s == "missing"
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
rows.sort_by { |row| [row[:relative_path].to_s, row[:line].to_i, row[:decision_id].to_s] }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def filtered_tests
|
|
410
|
+
rows = @index.tests
|
|
411
|
+
return rows unless @selection.active?
|
|
412
|
+
|
|
413
|
+
if @selection.focus_active?
|
|
414
|
+
condition_ids = @index.conditions.select { |row| @matching_decision_ids.include?(row[:decision_id].to_s) }
|
|
415
|
+
.map { |row| row[:id].to_s }
|
|
416
|
+
alternative_ids = @index.alternatives.select { |row| @matching_decision_ids.include?(row[:decision_id].to_s) }
|
|
417
|
+
.map { |row| row[:alternative_id].to_s }
|
|
418
|
+
rows = rows.select do |row|
|
|
419
|
+
row[:observations].any? do |observation|
|
|
420
|
+
condition_ids.include?(observation[:condition_id].to_s) ||
|
|
421
|
+
alternative_ids.include?(observation[:alternative_id].to_s)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
rows.sort_by { |row| [row[:relative_path].to_s, row[:line].to_i, row[:name].to_s, row[:id].to_s] }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def limit_rows(rows)
|
|
429
|
+
@selection.limit(rows)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_observations(row, missing_ids, missing_alternative_ids)
|
|
433
|
+
return row[:observations] unless @missing_only
|
|
434
|
+
|
|
435
|
+
row[:observations].select do |observation|
|
|
436
|
+
missing_ids.include?(observation[:condition_id]) ||
|
|
437
|
+
missing_alternative_ids.include?(observation[:alternative_id])
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
324
441
|
def location(path, line, unavailable: "location unavailable")
|
|
325
442
|
return "location unavailable" if path.to_s.empty?
|
|
326
443
|
return "#{path}: #{unavailable}" if line.nil?
|
|
@@ -356,4 +473,4 @@ module Branchproof
|
|
|
356
473
|
end
|
|
357
474
|
end
|
|
358
475
|
|
|
359
|
-
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
476
|
+
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|