branchproof 0.7.0 → 0.8.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 +35 -0
- data/README.md +212 -11
- data/doc/Branchproof/Analyzer.md +2 -2
- data/doc/Branchproof/CLI.md +4 -0
- data/doc/Branchproof/ComparisonReport.md +4 -0
- data/doc/Branchproof/Constraints/Solver.md +21 -0
- data/doc/Branchproof/Constraints.md +98 -0
- data/doc/Branchproof/CoverageIndex.md +3 -0
- data/doc/Branchproof/DecisionSyntax.md +6 -4
- data/doc/Branchproof/DecisionTable.md +153 -0
- data/doc/Branchproof/FocusedReport.md +5 -2
- data/doc/Branchproof/Loader.md +0 -4
- data/doc/Branchproof/MinitestAdapter.md +0 -3
- data/doc/Branchproof/Report.md +18 -0
- data/doc/Branchproof/Runtime.md +4 -0
- data/doc/Branchproof/SavedReport.md +15 -0
- data/doc/Branchproof.md +4 -1
- data/doc/CHANGELOG.md +35 -0
- data/doc/README.md +212 -11
- data/lib/branchproof/analyzer.rb +80 -44
- data/lib/branchproof/cli.rb +28 -19
- data/lib/branchproof/comparison.rb +207 -19
- data/lib/branchproof/comparison_report.rb +49 -1
- data/lib/branchproof/constraints.rb +363 -0
- data/lib/branchproof/coverage_index.rb +30 -1
- data/lib/branchproof/decision_syntax.rb +31 -15
- data/lib/branchproof/decision_table.rb +377 -0
- data/lib/branchproof/evidence.rb +51 -31
- data/lib/branchproof/flow_instrumentation.rb +17 -17
- data/lib/branchproof/focused_report.rb +77 -9
- data/lib/branchproof/instrumenter.rb +45 -34
- data/lib/branchproof/limits.rb +4 -1
- data/lib/branchproof/loader.rb +18 -6
- data/lib/branchproof/minimizer.rb +18 -13
- data/lib/branchproof/minitest_adapter.rb +11 -16
- data/lib/branchproof/records.rb +2 -0
- data/lib/branchproof/report.rb +301 -68
- data/lib/branchproof/runtime.rb +22 -30
- data/lib/branchproof/saved_report.rb +200 -14
- data/lib/branchproof/source.rb +90 -48
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +1 -4
- data/lib/branchproof.rb +2 -0
- data/llms.txt +4 -1
- data/sig/branchproof.rbs +47 -1
- metadata +6 -1
|
@@ -7,13 +7,14 @@ module Branchproof
|
|
|
7
7
|
# Compares two complete report documents without loading or executing the project.
|
|
8
8
|
class Comparison
|
|
9
9
|
SCHEMA_VERSION = "1.0"
|
|
10
|
-
SUPPORTED_REPORT_SCHEMAS = %w[1.0 1.1 1.2].freeze
|
|
10
|
+
SUPPORTED_REPORT_SCHEMAS = %w[1.0 1.1 1.2 1.3].freeze
|
|
11
11
|
CRITERION_VERSION = "masking_occurrence_v1"
|
|
12
12
|
|
|
13
13
|
def initialize(before:, after:)
|
|
14
14
|
@before = before
|
|
15
15
|
@after = after
|
|
16
16
|
@contexts = {}.compare_by_identity
|
|
17
|
+
@decision_table_indexes = {}.compare_by_identity
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def call
|
|
@@ -27,10 +28,17 @@ module Branchproof
|
|
|
27
28
|
before_conditions = condition_map(@before)
|
|
28
29
|
after_conditions = condition_map(@after)
|
|
29
30
|
matched_ids = before_conditions.keys & after_conditions.keys
|
|
30
|
-
matched_ids.select!
|
|
31
|
+
matched_ids.select! do |id|
|
|
32
|
+
compatible_condition?(before_conditions[id], after_conditions[id], changed_paths, before_sources,
|
|
33
|
+
after_sources)
|
|
34
|
+
end
|
|
31
35
|
changes = matched_ids.filter_map { |id| condition_change(id, before_conditions[id], after_conditions[id]) }
|
|
36
|
+
comparable_decisions = comparable_decision_ids(changed_paths, before_sources, after_sources)
|
|
37
|
+
table_changes = decision_table_changes(comparable_decisions)
|
|
38
|
+
table_context = decision_table_context_changes(comparable_decisions, changed_paths)
|
|
32
39
|
reasons = comparability_reasons(changed_paths)
|
|
33
40
|
reasons.concat(metadata_requirements)
|
|
41
|
+
reasons.concat(decision_table_comparability_reasons(comparable_decisions))
|
|
34
42
|
reasons << "legacy report is missing comparison context" if [@before, @after].any? do |document|
|
|
35
43
|
value(document, :schema_version).to_s == "1.0" && value(document, :run_metadata).nil?
|
|
36
44
|
end
|
|
@@ -72,8 +80,13 @@ module Branchproof
|
|
|
72
80
|
"changes" => changes.sort_by do |change|
|
|
73
81
|
[change["relative_path"].to_s, change["line"].to_i, change["condition_id"]]
|
|
74
82
|
end,
|
|
83
|
+
"decision_table_changes" => table_changes,
|
|
84
|
+
"decision_table_context_changes" => table_context,
|
|
85
|
+
"decision_table_matching" => decision_table_matching(comparable_decisions, table_changes),
|
|
86
|
+
"decision_table_regressions" => table_changes.count { |change| change["change"] == "rule coverage lost" },
|
|
75
87
|
"regressions" => lost,
|
|
76
|
-
"regression" => lost.positive?
|
|
88
|
+
"regression" => (lost.positive? || table_changes.any? { |change| change["change"] == "rule coverage lost" }) &&
|
|
89
|
+
status == "complete"
|
|
77
90
|
}
|
|
78
91
|
end
|
|
79
92
|
|
|
@@ -101,7 +114,9 @@ module Branchproof
|
|
|
101
114
|
"matching" => { "before_conditions" => 0, "after_conditions" => 0,
|
|
102
115
|
"matched_conditions" => 0, "denominator" => 0 },
|
|
103
116
|
"changed_sources" => [], "newly_in_report" => [], "no_longer_in_report" => [],
|
|
104
|
-
"changes" => [], "
|
|
117
|
+
"changes" => [], "decision_table_changes" => [], "decision_table_context_changes" => [],
|
|
118
|
+
"decision_table_matching" => { "matched_rules" => 0, "compared_decisions" => 0 },
|
|
119
|
+
"decision_table_regressions" => 0, "regressions" => 0, "regression" => false }
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def sources(document)
|
|
@@ -127,13 +142,13 @@ module Branchproof
|
|
|
127
142
|
end
|
|
128
143
|
end
|
|
129
144
|
|
|
130
|
-
def compatible_condition?(before, after, changed_paths)
|
|
145
|
+
def compatible_condition?(before, after, changed_paths, before_sources, after_sources)
|
|
131
146
|
before_path = source_path(@before, before[:decision])
|
|
132
147
|
after_path = source_path(@after, after[:decision])
|
|
133
148
|
return false if before_path.nil? || changed_paths.include?(before_path) || before_path != after_path
|
|
134
149
|
|
|
135
|
-
before_digest = digest(
|
|
136
|
-
after_digest = digest(
|
|
150
|
+
before_digest = digest(before_sources[before_path])
|
|
151
|
+
after_digest = digest(after_sources[after_path])
|
|
137
152
|
return false unless before_digest && before_digest == after_digest
|
|
138
153
|
|
|
139
154
|
%i[index expression byte_start byte_length].all? do |key|
|
|
@@ -159,7 +174,6 @@ module Branchproof
|
|
|
159
174
|
else
|
|
160
175
|
"unknown"
|
|
161
176
|
end
|
|
162
|
-
return if kind.nil?
|
|
163
177
|
|
|
164
178
|
decision = after[:decision]
|
|
165
179
|
source_path = source_path(@after, decision)
|
|
@@ -175,6 +189,180 @@ module Branchproof
|
|
|
175
189
|
row
|
|
176
190
|
end
|
|
177
191
|
|
|
192
|
+
# Decision identity already covers context, byte range, and Boolean tree, so
|
|
193
|
+
# a structurally changed decision never matches an old one. Comparison is
|
|
194
|
+
# additionally restricted to sources whose digest did not move.
|
|
195
|
+
def comparable_decision_ids(changed_paths, before_sources, after_sources)
|
|
196
|
+
before = context(@before)[:decisions]
|
|
197
|
+
after = context(@after)[:decisions]
|
|
198
|
+
(before.keys & after.keys).select do |id|
|
|
199
|
+
path = source_path(@before, before[id])
|
|
200
|
+
next false if path.nil? || changed_paths.include?(path)
|
|
201
|
+
next false unless path == source_path(@after, after[id])
|
|
202
|
+
|
|
203
|
+
digest(before_sources[path]) && digest(before_sources[path]) == digest(after_sources[path])
|
|
204
|
+
end.sort
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def decision_tables(document, decision_ids)
|
|
208
|
+
@decision_table_indexes[document] ||= begin
|
|
209
|
+
results = context(document)[:results_by_decision]
|
|
210
|
+
results.each_with_object({}) do |(id, decision), index|
|
|
211
|
+
table = value(decision, :decision_table)
|
|
212
|
+
next unless table && value(table, :status).to_s == "calculated"
|
|
213
|
+
|
|
214
|
+
index[id] = Array(value(table, :rules)).to_h { |rule| [value(rule, :id).to_s, rule] }
|
|
215
|
+
end
|
|
216
|
+
end.slice(*decision_ids)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Coverage movement and analysis movement are reported as distinct kinds:
|
|
220
|
+
# a rule that became statically impossible is not a coverage gain.
|
|
221
|
+
def decision_table_changes(decision_ids)
|
|
222
|
+
before = decision_tables(@before, decision_ids)
|
|
223
|
+
after = decision_tables(@after, decision_ids)
|
|
224
|
+
decision_table_comparable_ids(decision_ids, before, after).flat_map do |decision_id|
|
|
225
|
+
(before[decision_id].keys & after[decision_id].keys).sort.filter_map do |rule_id|
|
|
226
|
+
decision_table_change(decision_id, before[decision_id][rule_id], after[decision_id][rule_id])
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def decision_table_comparable_ids(decision_ids, before, after)
|
|
232
|
+
decision_ids.select do |id|
|
|
233
|
+
before.key?(id) && after.key?(id) &&
|
|
234
|
+
supported_decision_table_schema?(before_document_table(id)) &&
|
|
235
|
+
supported_decision_table_schema?(after_document_table(id))
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def before_document_table(decision_id)
|
|
240
|
+
value(context(@before)[:results_by_decision][decision_id], :decision_table)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def after_document_table(decision_id)
|
|
244
|
+
value(context(@after)[:results_by_decision][decision_id], :decision_table)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def decision_table_schema(table)
|
|
248
|
+
value(table, :schema_version)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def supported_decision_table_schema?(table)
|
|
252
|
+
decision_table_schema(table) == Branchproof::DecisionTable::SCHEMA_VERSION
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def decision_table_comparability_reasons(decision_ids)
|
|
256
|
+
decision_ids.filter_map do |id|
|
|
257
|
+
before = before_document_table(id)
|
|
258
|
+
after = after_document_table(id)
|
|
259
|
+
next if !before || !after ||
|
|
260
|
+
(supported_decision_table_schema?(before) && supported_decision_table_schema?(after))
|
|
261
|
+
|
|
262
|
+
"decision-table schema is incompatible for decision #{id}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def decision_table_metadata_changes(decision_id)
|
|
267
|
+
before = before_document_table(decision_id)
|
|
268
|
+
after = after_document_table(decision_id)
|
|
269
|
+
return [] unless before && after
|
|
270
|
+
|
|
271
|
+
changes = []
|
|
272
|
+
if value(before, :schema_version) != value(after, :schema_version)
|
|
273
|
+
changes << "decision-table schema version changed"
|
|
274
|
+
end
|
|
275
|
+
unless [before, after].all? { |table| supported_decision_table_schema?(table) }
|
|
276
|
+
changes << "decision-table schema version unsupported"
|
|
277
|
+
end
|
|
278
|
+
if value(before, :constraint_analysis_version) != value(after, :constraint_analysis_version)
|
|
279
|
+
changes << "constraint analysis version changed"
|
|
280
|
+
end
|
|
281
|
+
before_mode = value(before, :reachability_analyzed)
|
|
282
|
+
after_mode = value(after, :reachability_analyzed)
|
|
283
|
+
if !before_mode.nil? && !after_mode.nil? && before_mode != after_mode
|
|
284
|
+
changes << "reachability analysis mode changed"
|
|
285
|
+
end
|
|
286
|
+
changes
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def decision_table_change(decision_id, before, after)
|
|
290
|
+
previous = { "coverage" => value(before, :coverage).to_s, "reachability" => value(before, :reachability).to_s,
|
|
291
|
+
"reachability_reason" => value(before, :reachability_reason) }
|
|
292
|
+
current = { "coverage" => value(after, :coverage).to_s, "reachability" => value(after, :reachability).to_s,
|
|
293
|
+
"reachability_reason" => value(after, :reachability_reason) }
|
|
294
|
+
kind = if previous["coverage"] == "covered" && current["coverage"] != "covered"
|
|
295
|
+
"rule coverage lost"
|
|
296
|
+
elsif previous["coverage"] != "covered" && current["coverage"] == "covered"
|
|
297
|
+
"rule coverage gained"
|
|
298
|
+
elsif previous["reachability"] != current["reachability"]
|
|
299
|
+
"rule reachability changed"
|
|
300
|
+
else
|
|
301
|
+
"unchanged"
|
|
302
|
+
end
|
|
303
|
+
decision = context(@after)[:decisions][decision_id]
|
|
304
|
+
{ "decision_id" => decision_id, "rule_id" => value(after, :id).to_s, "label" => value(after, :label).to_s,
|
|
305
|
+
"change" => kind, "previous" => previous, "current" => current,
|
|
306
|
+
"conditions" => Array(value(after, :conditions)), "outcome" => value(after, :outcome),
|
|
307
|
+
"relative_path" => source_path(@after, decision), "line" => value(decision, :line),
|
|
308
|
+
"expression" => value(decision, :expression) }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def decision_table_context_changes(decision_ids, changed_paths)
|
|
312
|
+
before = decision_tables(@before, decision_ids)
|
|
313
|
+
after = decision_tables(@after, decision_ids)
|
|
314
|
+
changes = decision_ids.each_with_object([]) do |id, result|
|
|
315
|
+
next unless before.key?(id) || after.key?(id)
|
|
316
|
+
|
|
317
|
+
decision = context(@after)[:decisions][id] || context(@before)[:decisions][id]
|
|
318
|
+
location = { "decision_id" => id, "relative_path" => source_path(@after, decision),
|
|
319
|
+
"line" => value(decision, :line), "expression" => value(decision, :expression) }
|
|
320
|
+
decision_table_metadata_changes(id).each { |reason| result << location.merge("reason" => reason) }
|
|
321
|
+
if before.key?(id) != after.key?(id) ||
|
|
322
|
+
(before.key?(id) && after.key?(id) && before[id].keys.sort != after[id].keys.sort)
|
|
323
|
+
result << location.merge("reason" => "decision-table rule set changed")
|
|
324
|
+
end
|
|
325
|
+
next unless before.key?(id) && after.key?(id) && decision_table_comparable_ids([id], before, after).include?(id)
|
|
326
|
+
|
|
327
|
+
before[id].each do |rule_id, previous|
|
|
328
|
+
current = after[id][rule_id]
|
|
329
|
+
next unless current
|
|
330
|
+
next unless value(previous, :coverage).to_s != value(current, :coverage).to_s &&
|
|
331
|
+
value(previous, :reachability).to_s != value(current, :reachability).to_s
|
|
332
|
+
|
|
333
|
+
previous_reachability = value(previous, :reachability).to_s
|
|
334
|
+
current_reachability = value(current, :reachability).to_s
|
|
335
|
+
previous_coverage = value(previous, :coverage).to_s
|
|
336
|
+
current_coverage = value(current, :coverage).to_s
|
|
337
|
+
analysis_change = [previous_reachability, current_reachability].include?("statically_impossible") ||
|
|
338
|
+
[previous_coverage, current_coverage].include?("excluded")
|
|
339
|
+
next unless analysis_change
|
|
340
|
+
|
|
341
|
+
result << location.merge("reason" => "rule reachability changed alongside coverage",
|
|
342
|
+
"rule_id" => rule_id)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
changed_paths.sort.each do |path|
|
|
346
|
+
changes << { "decision_id" => nil, "reason" => "source changed / decision-table comparison unavailable",
|
|
347
|
+
"relative_path" => path, "line" => nil, "expression" => nil }
|
|
348
|
+
end
|
|
349
|
+
if !metadata(@before, :reachability).nil? && !metadata(@after, :reachability).nil? &&
|
|
350
|
+
metadata(@before, :reachability) != metadata(@after, :reachability)
|
|
351
|
+
changes << { "decision_id" => nil, "reason" => "reachability analysis mode changed",
|
|
352
|
+
"relative_path" => nil, "line" => nil, "expression" => nil }
|
|
353
|
+
end
|
|
354
|
+
changes
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def decision_table_matching(decision_ids, table_changes)
|
|
358
|
+
before = decision_tables(@before, decision_ids)
|
|
359
|
+
after = decision_tables(@after, decision_ids)
|
|
360
|
+
{ "compared_decisions" => decision_table_comparable_ids(decision_ids, before, after).length,
|
|
361
|
+
"matched_rules" => table_changes.length,
|
|
362
|
+
"before_covered_rules" => table_changes.count { |change| change["previous"]["coverage"] == "covered" },
|
|
363
|
+
"after_covered_rules" => table_changes.count { |change| change["current"]["coverage"] == "covered" } }
|
|
364
|
+
end
|
|
365
|
+
|
|
178
366
|
def context(document)
|
|
179
367
|
@contexts[document] ||= begin
|
|
180
368
|
index = CoverageIndex.new(document: document)
|
|
@@ -188,15 +376,18 @@ module Branchproof
|
|
|
188
376
|
results = Array(value(value(document, :analysis), :decisions)).flat_map do |decision|
|
|
189
377
|
Array(value(decision, :condition_results))
|
|
190
378
|
end
|
|
379
|
+
vectors = Array(value(value(document, :observations), :vectors))
|
|
191
380
|
{ conditions: index.conditions.to_h { |row| [row[:id].to_s, row] }, tests: tests, test_rows: rows,
|
|
192
381
|
test_keys: keys, key_owners: keys.keys.group_by { |id| keys[id] },
|
|
193
|
-
vectors:
|
|
194
|
-
|
|
195
|
-
end,
|
|
382
|
+
vectors: vectors.to_h { |vector| [value(vector, :id).to_s, vector] },
|
|
383
|
+
vectors_by_decision: vectors.group_by { |vector| value(vector, :decision_id).to_s },
|
|
196
384
|
decisions: Array(value(value(document, :source_inventory), :decisions)).to_h do |decision|
|
|
197
385
|
[value(decision, :id).to_s, decision]
|
|
198
386
|
end,
|
|
199
|
-
results: results.to_h { |result| [value(result, :condition_id).to_s, result] }
|
|
387
|
+
results: results.to_h { |result| [value(result, :condition_id).to_s, result] },
|
|
388
|
+
results_by_decision: Array(value(value(document, :analysis), :decisions)).to_h do |decision|
|
|
389
|
+
[value(decision, :decision_id).to_s, decision]
|
|
390
|
+
end }
|
|
200
391
|
end
|
|
201
392
|
end
|
|
202
393
|
|
|
@@ -211,10 +402,9 @@ module Branchproof
|
|
|
211
402
|
key = before[:test_keys][id.to_s]
|
|
212
403
|
unique = key && before[:key_owners][key]&.length == 1 && after[:key_owners][key]&.length == 1
|
|
213
404
|
current_id = unique ? after[:key_owners][key].first : nil
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
:values) == value(vector, :values) && value(current, :outcome) == value(vector, :outcome) &&
|
|
405
|
+
decision_vectors = after[:vectors_by_decision][value(decision, :id).to_s]
|
|
406
|
+
observed = current_id && Array(decision_vectors).any? do |current|
|
|
407
|
+
value(current, :values) == value(vector, :values) && value(current, :outcome) == value(vector, :outcome) &&
|
|
218
408
|
Array(value(current, :test_ids)).include?(current_id)
|
|
219
409
|
end
|
|
220
410
|
test_status = current_id ? "present in current run" : "not observed in current run (no unique test match)"
|
|
@@ -264,9 +454,7 @@ module Branchproof
|
|
|
264
454
|
def comparability_reasons(changed_paths)
|
|
265
455
|
reasons = []
|
|
266
456
|
reasons << "source files changed: #{changed_paths.sort.join(", ")}" unless changed_paths.empty?
|
|
267
|
-
%i[
|
|
268
|
-
next if key == :criterion_version
|
|
269
|
-
|
|
457
|
+
%i[project_kind runner_args source_patterns test_patterns limits runtime].each do |key|
|
|
270
458
|
left = metadata(@before, key)
|
|
271
459
|
right = metadata(@after, key)
|
|
272
460
|
if key == :runner_args
|
|
@@ -7,6 +7,8 @@ require "json"
|
|
|
7
7
|
module Branchproof
|
|
8
8
|
# Renders the offline document returned by Comparison.
|
|
9
9
|
class ComparisonReport
|
|
10
|
+
RULE_LABELS = Branchproof::DecisionTable::VALUE_LABELS.freeze
|
|
11
|
+
|
|
10
12
|
def initialize(document:)
|
|
11
13
|
raise ArgumentError, "comparison document must be a Hash" unless document.is_a?(Hash)
|
|
12
14
|
|
|
@@ -23,7 +25,7 @@ module Branchproof
|
|
|
23
25
|
|
|
24
26
|
def exit_code(fail_on_regression: false)
|
|
25
27
|
return 2 if status != "complete"
|
|
26
|
-
return 1 if fail_on_regression && regression?
|
|
28
|
+
return 1 if fail_on_regression && (regression? || decision_table_regression?)
|
|
27
29
|
|
|
28
30
|
0
|
|
29
31
|
end
|
|
@@ -72,9 +74,51 @@ module Branchproof
|
|
|
72
74
|
lines << " #{context.empty? ? "not observed in current run" : context.join(", ")}"
|
|
73
75
|
end
|
|
74
76
|
end
|
|
77
|
+
render_decision_tables(lines)
|
|
75
78
|
lines.join("\n") << "\n"
|
|
76
79
|
end
|
|
77
80
|
|
|
81
|
+
# Coverage movement and analysis movement stay separate: a rule that became
|
|
82
|
+
# statically impossible is an analysis change, never a coverage gain.
|
|
83
|
+
def render_decision_tables(lines)
|
|
84
|
+
matching = decision_table_matching
|
|
85
|
+
lines << "Decision-table rules: #{matching.fetch("matched_rules", 0)} matched across " \
|
|
86
|
+
"#{matching.fetch("compared_decisions", 0)} decisions"
|
|
87
|
+
lines << "Decision-table rules covered: before #{matching.fetch("before_covered_rules",
|
|
88
|
+
0)}; after #{matching.fetch(
|
|
89
|
+
"after_covered_rules", 0
|
|
90
|
+
)}"
|
|
91
|
+
decision_table_context_changes.each do |change|
|
|
92
|
+
location = change["relative_path"].to_s.empty? ? "location unavailable" : change["relative_path"].to_s
|
|
93
|
+
lines << "Decision-table context: #{location} #{change["reason"]}"
|
|
94
|
+
end
|
|
95
|
+
rendered = decision_table_changes.reject { |change| change["change"] == "unchanged" }
|
|
96
|
+
return lines << "No comparable decision-table rule changes." if rendered.empty?
|
|
97
|
+
|
|
98
|
+
rendered.each { |change| render_decision_table_change(lines, change) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_decision_table_change(lines, change)
|
|
102
|
+
location = if change["relative_path"].to_s.empty?
|
|
103
|
+
"location unavailable"
|
|
104
|
+
else
|
|
105
|
+
"#{change["relative_path"]}:#{change["line"] || "decision line unavailable"}"
|
|
106
|
+
end
|
|
107
|
+
signature = Array(change["conditions"]).map { |item| RULE_LABELS.fetch(item.to_s, item.to_s) }.join
|
|
108
|
+
lines << "#{change["change"].capitalize}: #{location} rule #{change["label"]} " \
|
|
109
|
+
"[#{signature}] => #{change["outcome"] ? "T" : "F"} #{change["expression"]}"
|
|
110
|
+
lines << " Previous: #{state_label(change["previous"])}"
|
|
111
|
+
lines << " Current: #{state_label(change["current"])}"
|
|
112
|
+
reason = change.dig("current", "reachability_reason")
|
|
113
|
+
lines << " Reason: #{reason}" unless reason.to_s.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def state_label(state)
|
|
117
|
+
state ||= {}
|
|
118
|
+
"#{state["coverage"] == "covered" ? "covered" : "uncovered"}, " \
|
|
119
|
+
"reachability #{state["reachability"]}"
|
|
120
|
+
end
|
|
121
|
+
|
|
78
122
|
def status = value(:status).to_s
|
|
79
123
|
|
|
80
124
|
def signs(values)
|
|
@@ -94,6 +138,10 @@ module Branchproof
|
|
|
94
138
|
end
|
|
95
139
|
|
|
96
140
|
def regression? = value(:regression) == true || value(:regressions).to_i.positive?
|
|
141
|
+
def decision_table_regression? = value(:decision_table_regressions).to_i.positive?
|
|
142
|
+
def decision_table_changes = Array(value(:decision_table_changes))
|
|
143
|
+
def decision_table_context_changes = Array(value(:decision_table_context_changes))
|
|
144
|
+
def decision_table_matching = value(:decision_table_matching) || {}
|
|
97
145
|
def reasons = Array(value(:reasons))
|
|
98
146
|
def matching = value(:matching) || {}
|
|
99
147
|
def before = value(:before) || {}
|