branchproof 0.6.0 → 0.7.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 +10 -0
- data/README.md +74 -23
- data/doc/Branchproof/CoverageIndex.md +3 -0
- data/doc/Branchproof/DecisionSyntax.md +20 -0
- data/doc/Branchproof/FlowInstrumentation.md +7 -0
- data/doc/Branchproof/Instrumenter.md +1 -0
- data/doc/Branchproof/Runtime.md +19 -0
- data/doc/Branchproof/RuntimeFlow.md +27 -0
- data/doc/Branchproof/SavedReport.md +6 -0
- data/doc/Branchproof/Source.md +1 -0
- data/doc/Branchproof.md +6 -2
- data/doc/CHANGELOG.md +10 -0
- data/doc/README.md +74 -23
- data/lib/branchproof/analyzer.rb +153 -21
- data/lib/branchproof/comparison.rb +1 -1
- data/lib/branchproof/coverage_index.rb +90 -2
- data/lib/branchproof/decision_syntax.rb +294 -0
- data/lib/branchproof/evidence.rb +54 -9
- data/lib/branchproof/flow_instrumentation.rb +107 -0
- data/lib/branchproof/focused_report.rb +85 -18
- data/lib/branchproof/instrumenter.rb +28 -13
- data/lib/branchproof/report.rb +195 -24
- data/lib/branchproof/runtime.rb +3 -0
- data/lib/branchproof/runtime_flow.rb +58 -0
- data/lib/branchproof/saved_report.rb +245 -6
- data/lib/branchproof/source.rb +191 -35
- data/lib/branchproof/version.rb +1 -1
- data/llms.txt +6 -2
- data/sig/branchproof.rbs +7 -0
- metadata +7 -1
data/lib/branchproof/analyzer.rb
CHANGED
|
@@ -36,6 +36,7 @@ module Branchproof
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def pair?(decision_id:, condition_index:, left:, right:)
|
|
39
|
+
return false if alternative_decision_for_id?(decision_id)
|
|
39
40
|
return false unless compatible_vectors?(left, right, decision_id)
|
|
40
41
|
return false unless observed?(left, condition_index) && observed?(right, condition_index)
|
|
41
42
|
return false if value(left, condition_index) == value(right, condition_index)
|
|
@@ -52,6 +53,8 @@ module Branchproof
|
|
|
52
53
|
decision = records(@inventory, :decisions).find { id(_1, :id) == decision_id }
|
|
53
54
|
return @missing_cache[cache_key] = nil unless decision
|
|
54
55
|
|
|
56
|
+
return @missing_cache[cache_key] = nil if alternative_decision?(decision)
|
|
57
|
+
|
|
55
58
|
support_status = id(decision, :support_status).to_s
|
|
56
59
|
return @missing_cache[cache_key] = nil unless support_status.empty? || support_status.upcase == "SUPPORTED"
|
|
57
60
|
|
|
@@ -94,6 +97,8 @@ module Branchproof
|
|
|
94
97
|
private
|
|
95
98
|
|
|
96
99
|
def analyze_decision(decision)
|
|
100
|
+
return analyze_alternative_decision(decision) if alternative_decision?(decision)
|
|
101
|
+
|
|
97
102
|
decision_id = id(decision, :id)
|
|
98
103
|
unless id(decision, :support_status).to_s.empty? || id(decision, :support_status).to_s.upcase == "SUPPORTED"
|
|
99
104
|
return {
|
|
@@ -159,6 +164,73 @@ module Branchproof
|
|
|
159
164
|
}
|
|
160
165
|
end
|
|
161
166
|
|
|
167
|
+
def analyze_alternative_decision(decision)
|
|
168
|
+
decision_id = id(decision, :id)
|
|
169
|
+
unless id(decision, :support_status).to_s.empty? || id(decision, :support_status).to_s.upcase == "SUPPORTED"
|
|
170
|
+
return {
|
|
171
|
+
decision_id: decision_id, kind: id(decision, :kind), effective_masks_by_vector: {},
|
|
172
|
+
condition_results: [], witness_buckets: {},
|
|
173
|
+
conditions: [], alternatives: alternatives(decision), unsupported: true,
|
|
174
|
+
coverage: unsupported_alternative_coverage,
|
|
175
|
+
completeness: { observation: true, attribution: true, analysis: true },
|
|
176
|
+
diagnostics: [diagnostic("unsupported_decision", "warning", decision_id, nil)]
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
vectors = @vectors.filter_map { |vector| valid_vector(vector, decision) }
|
|
181
|
+
rows = alternatives(decision).map do |alternative|
|
|
182
|
+
index = id(alternative, :index).to_i
|
|
183
|
+
selected = vectors.select { |vector| value(vector, index) == true }
|
|
184
|
+
not_selected = vectors.select { |vector| value(vector, index) == false }
|
|
185
|
+
skipped = vectors.select { |vector| value(vector, index).nil? }
|
|
186
|
+
{
|
|
187
|
+
alternative_id: id(alternative, :id), index: id(alternative, :index),
|
|
188
|
+
expression: id(alternative, :expression),
|
|
189
|
+
selected: alternative_evidence_bucket(selected),
|
|
190
|
+
not_selected: alternative_evidence_bucket(not_selected),
|
|
191
|
+
skipped: alternative_evidence_bucket(skipped)
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
missing = rows.filter_map { |row| row[:alternative_id] unless row[:selected][:observed] }
|
|
195
|
+
covered = rows.count { |row| row[:selected][:observed] }
|
|
196
|
+
{
|
|
197
|
+
decision_id: decision_id, kind: id(decision, :kind), effective_masks_by_vector: {},
|
|
198
|
+
condition_results: [], witness_buckets: {},
|
|
199
|
+
conditions: [], alternatives: alternatives(decision), unsupported: false,
|
|
200
|
+
coverage: { alternative: { status: coverage_status(covered, rows.length), covered_alternatives: covered,
|
|
201
|
+
required_alternatives: rows.length, alternatives: rows,
|
|
202
|
+
missing_alternatives: missing },
|
|
203
|
+
mcdc: { status: "not_applicable" } },
|
|
204
|
+
completeness: { observation: true, attribution: true, analysis: !@analysis_invalid }, diagnostics: []
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def unsupported_alternative_coverage
|
|
209
|
+
{ alternative: { status: "unsupported", covered_alternatives: 0, required_alternatives: 0,
|
|
210
|
+
alternatives: [], missing_alternatives: [] }, mcdc: { status: "unsupported" } }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def alternative_evidence_bucket(vectors)
|
|
214
|
+
evidence = vectors.map { |vector| provenance(vector) }
|
|
215
|
+
{ observed: !vectors.empty?, vector_ids: evidence.map { |item| item[:vector_id] }.uniq.sort,
|
|
216
|
+
test_ids: evidence.flat_map { |item| item[:test_ids] }.uniq.sort,
|
|
217
|
+
unattributed_count: evidence.sum { |item| item[:unattributed_count] } }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def alternative_decision?(decision)
|
|
221
|
+
kind = id(decision, :kind).to_s
|
|
222
|
+
!kind.empty? && kind != "boolean"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def alternative_decision_for_id?(decision_id)
|
|
226
|
+
decision = records(@inventory, :decisions).find { |item| id(item, :id) == decision_id }
|
|
227
|
+
decision && alternative_decision?(decision)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def alternatives(decision)
|
|
231
|
+
records(decision, :alternatives)
|
|
232
|
+
end
|
|
233
|
+
|
|
162
234
|
def unsupported_coverage
|
|
163
235
|
{ decision: { status: "unsupported", true_observed: false, false_observed: false,
|
|
164
236
|
covered_outcomes: 0, required_outcomes: 2, outcomes: [], missing_outcomes: [] },
|
|
@@ -254,29 +326,39 @@ module Branchproof
|
|
|
254
326
|
|
|
255
327
|
def aggregate_coverage(decisions)
|
|
256
328
|
supported = decisions.reject { |decision| decision[:unsupported] }
|
|
257
|
-
|
|
329
|
+
boolean_supported = supported.reject { |decision| alternative_decision?(decision) }
|
|
330
|
+
decision_covered = boolean_supported.count do |decision|
|
|
258
331
|
decision[:coverage][:decision][:status] == "covered"
|
|
259
332
|
end
|
|
260
|
-
condition_decision_covered =
|
|
333
|
+
condition_decision_covered = boolean_supported.count do |decision|
|
|
261
334
|
decision[:coverage][:condition_decision][:status] == "covered"
|
|
262
335
|
end
|
|
263
|
-
condition_count =
|
|
264
|
-
condition_values =
|
|
265
|
-
covered_conditions =
|
|
336
|
+
condition_count = boolean_supported.sum { |decision| decision[:coverage][:condition][:condition_count] }
|
|
337
|
+
condition_values = boolean_supported.sum { |decision| decision[:coverage][:condition][:covered_values] }
|
|
338
|
+
covered_conditions = boolean_supported.sum do |decision|
|
|
266
339
|
decision[:coverage][:condition][:covered_conditions]
|
|
267
340
|
end
|
|
268
|
-
proven =
|
|
341
|
+
proven = boolean_supported.sum { |decision| decision[:coverage][:mcdc][:proven_conditions] }
|
|
269
342
|
percentage = ->(covered, required) { required.zero? ? nil : (covered.to_f / required * 100).round(2) }
|
|
270
|
-
{ decision: { covered_decisions: decision_covered, supported_decisions:
|
|
271
|
-
percentage: percentage.call(decision_covered,
|
|
343
|
+
{ decision: { covered_decisions: decision_covered, supported_decisions: boolean_supported.length,
|
|
344
|
+
percentage: percentage.call(decision_covered, boolean_supported.length) },
|
|
272
345
|
condition: { covered_values: condition_values, required_values: condition_count * 2,
|
|
273
346
|
covered_conditions: covered_conditions, condition_count: condition_count,
|
|
274
347
|
percentage: percentage.call(condition_values, condition_count * 2) },
|
|
275
348
|
condition_decision: { covered_decisions: condition_decision_covered,
|
|
276
|
-
supported_decisions:
|
|
277
|
-
percentage: percentage.call(condition_decision_covered,
|
|
349
|
+
supported_decisions: boolean_supported.length,
|
|
350
|
+
percentage: percentage.call(condition_decision_covered, boolean_supported.length) },
|
|
278
351
|
mcdc: { proven_conditions: proven, supported_conditions: condition_count,
|
|
279
|
-
percentage: percentage.call(proven, condition_count) }
|
|
352
|
+
percentage: percentage.call(proven, condition_count) },
|
|
353
|
+
alternative: alternative_aggregate(decisions) }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def alternative_aggregate(decisions)
|
|
357
|
+
flow = decisions.select { |decision| !decision[:unsupported] && alternative_decision?(decision) }
|
|
358
|
+
required = flow.sum { |decision| decision.dig(:coverage, :alternative, :required_alternatives).to_i }
|
|
359
|
+
covered = flow.sum { |decision| decision.dig(:coverage, :alternative, :covered_alternatives).to_i }
|
|
360
|
+
{ covered_alternatives: covered, required_alternatives: required,
|
|
361
|
+
supported_decisions: flow.length, percentage: required.zero? ? nil : (covered.to_f / required * 100).round(2) }
|
|
280
362
|
end
|
|
281
363
|
|
|
282
364
|
def valid_vector(vector, decision)
|
|
@@ -308,7 +390,11 @@ module Branchproof
|
|
|
308
390
|
return nil
|
|
309
391
|
end
|
|
310
392
|
|
|
311
|
-
|
|
393
|
+
if alternative_decision?(decision)
|
|
394
|
+
handle_alternative_vector(vector, decision)
|
|
395
|
+
else
|
|
396
|
+
effective_mask(vector, id(decision, :id), decision[:tree])
|
|
397
|
+
end
|
|
312
398
|
decision_source.nil? ? vector : vector.merge(source_id: decision_source)
|
|
313
399
|
rescue ArgumentError
|
|
314
400
|
@analysis_invalid = true
|
|
@@ -316,6 +402,36 @@ module Branchproof
|
|
|
316
402
|
nil
|
|
317
403
|
end
|
|
318
404
|
|
|
405
|
+
# rubocop:disable-next Naming/PredicateMethod -- raises on malformed flow vectors
|
|
406
|
+
def handle_alternative_vector(vector, decision)
|
|
407
|
+
values = values_for(vector)
|
|
408
|
+
expected = alternatives(decision).length
|
|
409
|
+
raise ArgumentError, "invalid alternative vector shape" unless values.length == expected
|
|
410
|
+
|
|
411
|
+
observations = values.each_with_index.filter_map { |item, index| [index, item] unless item.nil? }
|
|
412
|
+
raise ArgumentError, "invalid alternative trace" unless valid_alternative_trace?(decision, observations,
|
|
413
|
+
outcome(vector))
|
|
414
|
+
|
|
415
|
+
true
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def valid_alternative_trace?(decision, observations, result)
|
|
419
|
+
return false unless result == true
|
|
420
|
+
|
|
421
|
+
expected = alternatives(decision).length
|
|
422
|
+
return false unless expected.positive?
|
|
423
|
+
if id(decision, :kind).to_s == "implicit"
|
|
424
|
+
return expected == 2 && observations.length == 2 &&
|
|
425
|
+
observations.map(&:first) == [0, 1] && observations.map(&:last).count(true) == 1
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
return false unless observations.length.between?(1, expected)
|
|
429
|
+
|
|
430
|
+
observations.each_with_index.all? do |(index, value), position|
|
|
431
|
+
index == position && value == (position == observations.length - 1)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
319
435
|
def effective_mask(vector, decision_id, tree = nil)
|
|
320
436
|
decision = records(@inventory, :decisions).find { id(_1, :id) == decision_id } unless tree
|
|
321
437
|
tree ||= decision && decision[:tree]
|
|
@@ -341,6 +457,10 @@ module Branchproof
|
|
|
341
457
|
|
|
342
458
|
return [value, 1 << index, offset + 1]
|
|
343
459
|
end
|
|
460
|
+
if type == :not
|
|
461
|
+
child_result, child_mask, consumed = replay(node.fetch(:child), values, offset)
|
|
462
|
+
return [!child_result, child_mask, consumed]
|
|
463
|
+
end
|
|
344
464
|
left_result, left_mask, consumed = replay(node.fetch(:left), values, offset)
|
|
345
465
|
return [left_result, left_mask, consumed] if (type == :and && !left_result) || (type == :or && left_result)
|
|
346
466
|
|
|
@@ -440,6 +560,8 @@ module Branchproof
|
|
|
440
560
|
return { entry: entry, true_exits: [edges[0]], false_exits: [edges[1]], all_bits: 1 << index, edges: edges }
|
|
441
561
|
end
|
|
442
562
|
type = id(node, :type).to_sym
|
|
563
|
+
return build_graph(node.fetch(:child), false_destination, true_destination) if type == :not
|
|
564
|
+
|
|
443
565
|
right = build_graph(node[:right], true_destination, false_destination)
|
|
444
566
|
if type == :and
|
|
445
567
|
left = build_graph(node[:left], right[:entry], false_destination)
|
|
@@ -455,7 +577,9 @@ module Branchproof
|
|
|
455
577
|
end
|
|
456
578
|
|
|
457
579
|
def impose_path(node, target, target_value, constraints)
|
|
458
|
-
|
|
580
|
+
type = id(node, :type).to_sym
|
|
581
|
+
return true if type == :atom && id(node, :index) == target
|
|
582
|
+
return impose_path(node.fetch(:child), target, target_value, constraints) if type == :not
|
|
459
583
|
|
|
460
584
|
left = node[:left]
|
|
461
585
|
right = node[:right]
|
|
@@ -480,6 +604,8 @@ module Branchproof
|
|
|
480
604
|
|
|
481
605
|
constraints << { condition_index: id(node, :index), value: desired }
|
|
482
606
|
true
|
|
607
|
+
elsif id(node, :type).to_sym == :not
|
|
608
|
+
impose_subtree(node.fetch(:child), !desired, constraints)
|
|
483
609
|
elsif desired == (id(node, :type).to_sym == :and)
|
|
484
610
|
impose_subtree(node[:left], desired, constraints) && impose_subtree(node[:right], desired, constraints)
|
|
485
611
|
else
|
|
@@ -488,7 +614,9 @@ module Branchproof
|
|
|
488
614
|
end
|
|
489
615
|
|
|
490
616
|
def evaluate(node, values)
|
|
491
|
-
|
|
617
|
+
type = id(node, :type).to_sym
|
|
618
|
+
return values[id(node, :index)] if type == :atom
|
|
619
|
+
return !evaluate(node.fetch(:child), values) if type == :not
|
|
492
620
|
|
|
493
621
|
left = evaluate(node[:left], values)
|
|
494
622
|
return left if id(node, :type).to_sym == :and && !left
|
|
@@ -498,11 +626,16 @@ module Branchproof
|
|
|
498
626
|
end
|
|
499
627
|
|
|
500
628
|
def evaluate_with_trace(node, values, trace = [])
|
|
501
|
-
|
|
629
|
+
type = id(node, :type).to_sym
|
|
630
|
+
if type == :atom
|
|
502
631
|
value = !values[id(node, :index)].nil? && values[id(node, :index)] != false
|
|
503
632
|
trace << [id(node, :index), value]
|
|
504
633
|
return [value, trace]
|
|
505
634
|
end
|
|
635
|
+
if type == :not
|
|
636
|
+
child, = evaluate_with_trace(node.fetch(:child), values, trace)
|
|
637
|
+
return [!child, trace]
|
|
638
|
+
end
|
|
506
639
|
left, = evaluate_with_trace(node[:left], values, trace)
|
|
507
640
|
return [left, trace] if id(node, :type).to_sym == :and && !left
|
|
508
641
|
return [left, trace] if id(node, :type).to_sym == :or && left
|
|
@@ -511,13 +644,12 @@ module Branchproof
|
|
|
511
644
|
end
|
|
512
645
|
|
|
513
646
|
def contains?(node, target)
|
|
514
|
-
if id(node,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
647
|
+
if id(node, :type).to_sym == :atom
|
|
648
|
+
id(node, :index) == target
|
|
649
|
+
elsif id(node, :type).to_sym == :not
|
|
650
|
+
contains?(node.fetch(:child), target)
|
|
518
651
|
else
|
|
519
|
-
contains?(node[:left],
|
|
520
|
-
target) || contains?(node[:right], target)
|
|
652
|
+
contains?(node[:left], target) || contains?(node[:right], target)
|
|
521
653
|
end
|
|
522
654
|
end
|
|
523
655
|
|
|
@@ -7,7 +7,7 @@ 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].freeze
|
|
10
|
+
SUPPORTED_REPORT_SCHEMAS = %w[1.0 1.1 1.2].freeze
|
|
11
11
|
CRITERION_VERSION = "masking_occurrence_v1"
|
|
12
12
|
|
|
13
13
|
def initialize(before:, after:)
|
|
@@ -7,7 +7,7 @@ require "pathname"
|
|
|
7
7
|
module Branchproof
|
|
8
8
|
# Derives condition- and test-oriented rows from one report document.
|
|
9
9
|
class CoverageIndex
|
|
10
|
-
attr_reader :conditions, :tests
|
|
10
|
+
attr_reader :conditions, :alternatives, :tests
|
|
11
11
|
|
|
12
12
|
def initialize(document:)
|
|
13
13
|
@document = symbolize(document || {})
|
|
@@ -27,6 +27,7 @@ module Branchproof
|
|
|
27
27
|
@tests_by_id[id] = (@tests_by_id[id] || {}).merge(test) unless id.empty?
|
|
28
28
|
end
|
|
29
29
|
@conditions = build_conditions.freeze
|
|
30
|
+
@alternatives = build_alternatives.freeze
|
|
30
31
|
@tests = build_tests.freeze
|
|
31
32
|
end
|
|
32
33
|
|
|
@@ -62,7 +63,7 @@ module Branchproof
|
|
|
62
63
|
id: condition[:id], decision_id: decision[:id], index: condition[:index],
|
|
63
64
|
expression: condition[:expression],
|
|
64
65
|
line: condition[:line], column: condition[:column], relative_path: relative_path(source[:relative_path]),
|
|
65
|
-
decision_expression: decision[:expression],
|
|
66
|
+
decision_expression: decision[:expression], kind: decision_kind(decision), context: decision[:context],
|
|
66
67
|
status: @document[:analysis] ? (result[:status] || "NOT_PROVEN") : "NOT CALCULATED",
|
|
67
68
|
observed_true: test_ids(observations[true]), observed_false: test_ids(observations[false]),
|
|
68
69
|
short_circuited: test_ids(observations[:short_circuited]),
|
|
@@ -80,12 +81,69 @@ module Branchproof
|
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
def build_alternatives
|
|
85
|
+
records(@inventory, :decisions).flat_map do |decision|
|
|
86
|
+
next [] if decision[:support_status].to_s == "UNSUPPORTED"
|
|
87
|
+
next [] if boolean_decision?(decision)
|
|
88
|
+
|
|
89
|
+
source = @source_units_by_id[decision[:source_id].to_s] || {}
|
|
90
|
+
coverage = symbolize(analysis_for(decision)[:coverage] || {})[:alternative] || {}
|
|
91
|
+
coverage_rows = records(coverage, :alternatives).to_h { |row| [row[:alternative_id].to_s, row] }
|
|
92
|
+
vectors = @vectors_by_decision[decision[:id].to_s] || []
|
|
93
|
+
records(decision, :alternatives).map do |alternative|
|
|
94
|
+
alternative = symbolize(alternative)
|
|
95
|
+
evidence = { selected: [], not_selected: [], skipped: [] }
|
|
96
|
+
vectors.each do |vector|
|
|
97
|
+
value = vector_values(vector)[alternative[:index].to_i]
|
|
98
|
+
state = if value.nil?
|
|
99
|
+
:skipped
|
|
100
|
+
else
|
|
101
|
+
(value ? :selected : :not_selected)
|
|
102
|
+
end
|
|
103
|
+
evidence[state] << vector
|
|
104
|
+
end
|
|
105
|
+
row = symbolize(coverage_rows[alternative[:id].to_s] || {})
|
|
106
|
+
{
|
|
107
|
+
id: alternative[:id], alternative_id: alternative[:id], decision_id: decision[:id],
|
|
108
|
+
index: alternative[:index], expression: alternative[:expression],
|
|
109
|
+
line: alternative[:line], column: alternative[:column],
|
|
110
|
+
relative_path: relative_path(source[:relative_path]),
|
|
111
|
+
decision_expression: decision[:expression], kind: decision_kind(decision),
|
|
112
|
+
context: decision[:context],
|
|
113
|
+
status: @document[:analysis] ? alternative_status(row, evidence) : "NOT CALCULATED",
|
|
114
|
+
selected: row[:selected] || evidence_bucket(evidence[:selected]),
|
|
115
|
+
not_selected: row[:not_selected] || evidence_bucket(evidence[:not_selected]),
|
|
116
|
+
skipped: row[:skipped] || evidence_bucket(evidence[:skipped]),
|
|
117
|
+
vectors: evidence, missing: missing_alternative?(coverage, alternative)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
83
123
|
def build_tests
|
|
84
124
|
by_id = @tests_by_id.transform_values do |test|
|
|
85
125
|
{ id: test[:id], name: display_name(test), relative_path: test_location(test)[0], line: test_location(test)[1],
|
|
86
126
|
status: test[:status], phases: Array(test[:phase_counts]).to_h.keys.sort,
|
|
87
127
|
observations: [], owns_witness: false }
|
|
88
128
|
end
|
|
129
|
+
@alternatives.each do |alternative|
|
|
130
|
+
alternative[:vectors].each do |kind, vectors|
|
|
131
|
+
vectors.each do |vector|
|
|
132
|
+
Array(vector[:test_ids]).each do |id|
|
|
133
|
+
row = by_id[id.to_s] ||= { id: id, name: id, relative_path: nil, line: nil, status: "unknown",
|
|
134
|
+
phases: [], observations: [], owns_witness: false }
|
|
135
|
+
phase_map = symbolize(vector[:phases_by_test] || {})
|
|
136
|
+
phases = phase_map[id.to_s] || phase_map[id.to_sym] || []
|
|
137
|
+
row[:phases] |= Array(phases).map(&:to_s)
|
|
138
|
+
row[:observations] << { alternative_id: alternative[:id], expression: alternative[:expression],
|
|
139
|
+
relative_path: alternative[:relative_path], line: alternative[:line],
|
|
140
|
+
value: kind == :skipped ? nil : (kind == :selected), kind: :alternative,
|
|
141
|
+
alternative_state: kind.to_s, phases: phases.map(&:to_s).sort,
|
|
142
|
+
owns_witness: false }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
89
147
|
@conditions.each do |condition|
|
|
90
148
|
condition[:vectors].each do |kind, vectors|
|
|
91
149
|
next if kind == :unattributed
|
|
@@ -118,6 +176,36 @@ module Branchproof
|
|
|
118
176
|
@analysis_by_decision[decision[:id].to_s] || {}
|
|
119
177
|
end
|
|
120
178
|
|
|
179
|
+
def boolean_decision?(decision)
|
|
180
|
+
decision_kind(decision) == "boolean"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def decision_kind(decision)
|
|
184
|
+
kind = decision[:kind].to_s
|
|
185
|
+
kind.empty? ? "boolean" : kind
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def evidence_bucket(vectors)
|
|
189
|
+
evidence = vectors.map do |vector|
|
|
190
|
+
{ vector_id: vector[:id].to_s, test_ids: Array(vector[:test_ids]).map(&:to_s).uniq.sort,
|
|
191
|
+
unattributed_count: vector[:unattributed_count].to_i }
|
|
192
|
+
end
|
|
193
|
+
{ observed: !vectors.empty?, vector_ids: evidence.map { |item| item[:vector_id] }.uniq.sort,
|
|
194
|
+
test_ids: evidence.flat_map { |item| item[:test_ids] }.uniq.sort,
|
|
195
|
+
unattributed_count: evidence.sum { |item| item[:unattributed_count] } }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def missing_alternative?(coverage, alternative)
|
|
199
|
+
Array(coverage[:missing_alternatives]).map(&:to_s).include?(alternative[:id].to_s)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def alternative_status(row, evidence)
|
|
203
|
+
status = row[:status].to_s
|
|
204
|
+
return status unless status.empty?
|
|
205
|
+
|
|
206
|
+
evidence[:selected].empty? ? "unexecuted" : "covered"
|
|
207
|
+
end
|
|
208
|
+
|
|
121
209
|
def supporting_set_for(decision_id)
|
|
122
210
|
Array(@document[:minima] || @document["minima"]).filter_map do |minimum|
|
|
123
211
|
minimum = symbolize(minimum)
|