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
data/lib/branchproof/analyzer.rb
CHANGED
|
@@ -8,20 +8,32 @@ module Branchproof
|
|
|
8
8
|
class Analyzer
|
|
9
9
|
CRITERION = "masking_occurrence_v1"
|
|
10
10
|
|
|
11
|
-
def initialize(inventory:, evidence:, limits:)
|
|
11
|
+
def initialize(inventory:, evidence:, limits:, reachability: true)
|
|
12
12
|
@inventory = inventory || {}
|
|
13
13
|
@evidence = evidence || {}
|
|
14
14
|
@limits = limits || {}
|
|
15
|
+
@reachability = reachability
|
|
15
16
|
@vectors = records(@evidence, :vectors)
|
|
17
|
+
@vectors_by_decision = @vectors.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |vector, hash|
|
|
18
|
+
hash[id(vector, :decision_id)] << vector
|
|
19
|
+
end
|
|
20
|
+
@decisions = records(@inventory, :decisions)
|
|
21
|
+
@decisions_by_id = @decisions.each_with_object({}) do |decision, hash|
|
|
22
|
+
key = id(decision, :id)
|
|
23
|
+
hash[key] = decision unless hash.key?(key)
|
|
24
|
+
end
|
|
16
25
|
@constraint_states = Hash.new(0)
|
|
17
26
|
@missing_cache = {}
|
|
18
27
|
@invalid_diagnostics = []
|
|
19
28
|
@analysis_invalid = false
|
|
20
29
|
@diagnostic_keys = Set.new
|
|
30
|
+
# Keyed by vector object identity (not content), so a validated vector's
|
|
31
|
+
# values are checked once and reused on every later read.
|
|
32
|
+
@values_cache = {}.compare_by_identity
|
|
21
33
|
end
|
|
22
34
|
|
|
23
35
|
def call
|
|
24
|
-
decisions =
|
|
36
|
+
decisions = @decisions.map { analyze_decision(_1) }
|
|
25
37
|
proven = decisions.sum { |decision| decision[:condition_results].count { |result| result[:status] == "PROVEN" } }
|
|
26
38
|
eligible = decisions.sum { |decision| decision[:unsupported] ? 0 : decision[:conditions].length }
|
|
27
39
|
{
|
|
@@ -35,22 +47,21 @@ module Branchproof
|
|
|
35
47
|
}.freeze
|
|
36
48
|
end
|
|
37
49
|
|
|
38
|
-
def pair?(decision_id:, condition_index:, left:, right:)
|
|
50
|
+
def pair?(decision_id:, condition_index:, left:, right:, masks: nil)
|
|
39
51
|
return false if alternative_decision_for_id?(decision_id)
|
|
40
52
|
return false unless compatible_vectors?(left, right, decision_id)
|
|
41
53
|
return false unless observed?(left, condition_index) && observed?(right, condition_index)
|
|
42
54
|
return false if value(left, condition_index) == value(right, condition_index)
|
|
43
55
|
return false if outcome(left) == outcome(right)
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
masks.all? { |mask| mask&.anybits?(1 << condition_index) }
|
|
57
|
+
[left, right].all? { |vector| masked_bits(vector, decision_id, masks)&.anybits?(1 << condition_index) }
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
def missing(decision_id:, condition_index:)
|
|
50
61
|
cache_key = [decision_id, condition_index]
|
|
51
62
|
return @missing_cache[cache_key] if @missing_cache.key?(cache_key)
|
|
52
63
|
|
|
53
|
-
decision =
|
|
64
|
+
decision = @decisions_by_id[decision_id]
|
|
54
65
|
return @missing_cache[cache_key] = nil unless decision
|
|
55
66
|
|
|
56
67
|
return @missing_cache[cache_key] = nil if alternative_decision?(decision)
|
|
@@ -58,7 +69,7 @@ module Branchproof
|
|
|
58
69
|
support_status = id(decision, :support_status).to_s
|
|
59
70
|
return @missing_cache[cache_key] = nil unless support_status.empty? || support_status.upcase == "SUPPORTED"
|
|
60
71
|
|
|
61
|
-
relevant = @
|
|
72
|
+
relevant = @vectors_by_decision[decision_id]
|
|
62
73
|
valid = relevant.filter_map { valid_vector(_1, decision) }
|
|
63
74
|
if valid.empty?
|
|
64
75
|
return @missing_cache[cache_key] = {
|
|
@@ -67,14 +78,15 @@ module Branchproof
|
|
|
67
78
|
feasibility_statement: "No completed structural observation exists."
|
|
68
79
|
}
|
|
69
80
|
end
|
|
70
|
-
|
|
81
|
+
masks = valid.to_h { |vector| [id(vector, :id), effective_mask(vector, decision_id)] }
|
|
82
|
+
return @missing_cache[cache_key] = nil if first_pair(valid, condition_index, decision_id, masks)
|
|
71
83
|
|
|
72
|
-
existing = valid.find { |vector|
|
|
84
|
+
existing = valid.find { |vector| masks[id(vector, :id)].anybits?(1 << condition_index) }
|
|
73
85
|
signs = existing ? [!value(existing, condition_index)] : [true, false]
|
|
74
86
|
candidates = signs.filter_map { counterpart(decision, condition_index, _1) }
|
|
75
87
|
candidates.select! do |candidate|
|
|
76
88
|
!existing || pair?(decision_id: decision_id, condition_index: condition_index, left: existing,
|
|
77
|
-
right: candidate[:vector])
|
|
89
|
+
right: candidate[:vector], masks: masks)
|
|
78
90
|
end
|
|
79
91
|
limited = @constraint_states[id(decision, :id)] > constraint_limit
|
|
80
92
|
if limited
|
|
@@ -102,14 +114,16 @@ module Branchproof
|
|
|
102
114
|
decision_id = id(decision, :id)
|
|
103
115
|
unless id(decision, :support_status).to_s.empty? || id(decision, :support_status).to_s.upcase == "SUPPORTED"
|
|
104
116
|
return {
|
|
105
|
-
decision_id: decision_id, effective_masks_by_vector: {}, condition_results: [],
|
|
117
|
+
decision_id: decision_id, effective_masks_by_vector: {}, condition_results: [],
|
|
106
118
|
conditions: [], unsupported: true, coverage: unsupported_coverage,
|
|
119
|
+
decision_table: DecisionTable.build(decision: decision, vectors: [], limits: @limits,
|
|
120
|
+
reachability: @reachability),
|
|
107
121
|
completeness: { observation: true, attribution: true, analysis: true },
|
|
108
122
|
diagnostics: [diagnostic("unsupported_decision", "warning", decision_id, nil)]
|
|
109
123
|
}
|
|
110
124
|
end
|
|
111
125
|
|
|
112
|
-
vectors = @
|
|
126
|
+
vectors = @vectors_by_decision[decision_id].filter_map { |vector| valid_vector(vector, decision) }
|
|
113
127
|
masks = vectors.to_h { |vector| [id(vector, :id), effective_mask(vector, decision_id)] }
|
|
114
128
|
buckets = {}
|
|
115
129
|
conditions(decision).each do |condition|
|
|
@@ -132,7 +146,7 @@ module Branchproof
|
|
|
132
146
|
true_ids = buckets[index][:true]
|
|
133
147
|
false_ids = buckets[index][:false]
|
|
134
148
|
# rubocop:enable Lint/BooleanSymbol
|
|
135
|
-
pair = first_pair(vectors, index, decision_id)
|
|
149
|
+
pair = first_pair(vectors, index, decision_id, masks)
|
|
136
150
|
{
|
|
137
151
|
condition_id: id(condition, :id),
|
|
138
152
|
status: pair ? "PROVEN" : "NOT_PROVEN",
|
|
@@ -149,18 +163,20 @@ module Branchproof
|
|
|
149
163
|
decision_coverage = decision_coverage(vectors)
|
|
150
164
|
condition_coverage_summary = condition_summary(results)
|
|
151
165
|
mcdc = mcdc_coverage(results)
|
|
166
|
+
table = DecisionTable.build(decision: decision, vectors: vectors, limits: @limits,
|
|
167
|
+
reachability: @reachability)
|
|
152
168
|
{
|
|
153
169
|
decision_id: decision_id,
|
|
154
170
|
effective_masks_by_vector: masks,
|
|
155
171
|
condition_results: results,
|
|
156
|
-
witness_buckets: buckets,
|
|
157
172
|
edge_table: edge_table(decision[:tree]),
|
|
158
173
|
conditions: conditions(decision),
|
|
159
174
|
coverage: { decision: decision_coverage, condition: condition_coverage_summary,
|
|
160
175
|
condition_decision: conjunction_coverage(decision_coverage, condition_coverage_summary),
|
|
161
|
-
mcdc: mcdc },
|
|
176
|
+
mcdc: mcdc, decision_table: DecisionTable.coverage_entry(table) },
|
|
177
|
+
decision_table: table,
|
|
162
178
|
completeness: { observation: true, attribution: true, analysis: !@analysis_invalid },
|
|
163
|
-
diagnostics: []
|
|
179
|
+
diagnostics: table[:diagnostics]
|
|
164
180
|
}
|
|
165
181
|
end
|
|
166
182
|
|
|
@@ -169,7 +185,7 @@ module Branchproof
|
|
|
169
185
|
unless id(decision, :support_status).to_s.empty? || id(decision, :support_status).to_s.upcase == "SUPPORTED"
|
|
170
186
|
return {
|
|
171
187
|
decision_id: decision_id, kind: id(decision, :kind), effective_masks_by_vector: {},
|
|
172
|
-
condition_results: [],
|
|
188
|
+
condition_results: [],
|
|
173
189
|
conditions: [], alternatives: alternatives(decision), unsupported: true,
|
|
174
190
|
coverage: unsupported_alternative_coverage,
|
|
175
191
|
completeness: { observation: true, attribution: true, analysis: true },
|
|
@@ -177,7 +193,7 @@ module Branchproof
|
|
|
177
193
|
}
|
|
178
194
|
end
|
|
179
195
|
|
|
180
|
-
vectors = @
|
|
196
|
+
vectors = @vectors_by_decision[decision_id].filter_map { |vector| valid_vector(vector, decision) }
|
|
181
197
|
rows = alternatives(decision).map do |alternative|
|
|
182
198
|
index = id(alternative, :index).to_i
|
|
183
199
|
selected = vectors.select { |vector| value(vector, index) == true }
|
|
@@ -195,7 +211,7 @@ module Branchproof
|
|
|
195
211
|
covered = rows.count { |row| row[:selected][:observed] }
|
|
196
212
|
{
|
|
197
213
|
decision_id: decision_id, kind: id(decision, :kind), effective_masks_by_vector: {},
|
|
198
|
-
condition_results: [],
|
|
214
|
+
condition_results: [],
|
|
199
215
|
conditions: [], alternatives: alternatives(decision), unsupported: false,
|
|
200
216
|
coverage: { alternative: { status: coverage_status(covered, rows.length), covered_alternatives: covered,
|
|
201
217
|
required_alternatives: rows.length, alternatives: rows,
|
|
@@ -223,7 +239,7 @@ module Branchproof
|
|
|
223
239
|
end
|
|
224
240
|
|
|
225
241
|
def alternative_decision_for_id?(decision_id)
|
|
226
|
-
decision =
|
|
242
|
+
decision = @decisions_by_id[decision_id]
|
|
227
243
|
decision && alternative_decision?(decision)
|
|
228
244
|
end
|
|
229
245
|
|
|
@@ -237,7 +253,8 @@ module Branchproof
|
|
|
237
253
|
condition: { status: "unsupported", covered_values: 0, required_values: 0,
|
|
238
254
|
covered_conditions: 0, condition_count: 0 },
|
|
239
255
|
condition_decision: { status: "unsupported" },
|
|
240
|
-
mcdc: { status: "unsupported", proven_conditions: 0, condition_count: 0 }
|
|
256
|
+
mcdc: { status: "unsupported", proven_conditions: 0, condition_count: 0 },
|
|
257
|
+
decision_table: DecisionTable.unsupported_coverage_entry }
|
|
241
258
|
end
|
|
242
259
|
|
|
243
260
|
def provenance(vector)
|
|
@@ -339,26 +356,44 @@ module Branchproof
|
|
|
339
356
|
decision[:coverage][:condition][:covered_conditions]
|
|
340
357
|
end
|
|
341
358
|
proven = boolean_supported.sum { |decision| decision[:coverage][:mcdc][:proven_conditions] }
|
|
342
|
-
percentage = ->(covered, required) { required.zero? ? nil : (covered.to_f / required * 100).round(2) }
|
|
343
359
|
{ decision: { covered_decisions: decision_covered, supported_decisions: boolean_supported.length,
|
|
344
|
-
percentage: percentage
|
|
360
|
+
percentage: DecisionTable.percentage(decision_covered, boolean_supported.length) },
|
|
345
361
|
condition: { covered_values: condition_values, required_values: condition_count * 2,
|
|
346
362
|
covered_conditions: covered_conditions, condition_count: condition_count,
|
|
347
|
-
percentage: percentage
|
|
363
|
+
percentage: DecisionTable.percentage(condition_values, condition_count * 2) },
|
|
348
364
|
condition_decision: { covered_decisions: condition_decision_covered,
|
|
349
365
|
supported_decisions: boolean_supported.length,
|
|
350
|
-
percentage: percentage
|
|
366
|
+
percentage: DecisionTable.percentage(condition_decision_covered,
|
|
367
|
+
boolean_supported.length) },
|
|
351
368
|
mcdc: { proven_conditions: proven, supported_conditions: condition_count,
|
|
352
|
-
percentage: percentage
|
|
369
|
+
percentage: DecisionTable.percentage(proven, condition_count) },
|
|
370
|
+
decision_table: decision_table_aggregate(boolean_supported),
|
|
353
371
|
alternative: alternative_aggregate(decisions) }
|
|
354
372
|
end
|
|
355
373
|
|
|
374
|
+
# Rule coverage and fully covered decisions stay separate metrics: a
|
|
375
|
+
# decision can hold most of its rules and still fail the criterion.
|
|
376
|
+
def decision_table_aggregate(boolean_supported)
|
|
377
|
+
analyzed = boolean_supported.select { |decision| decision.dig(:decision_table, :status) == "calculated" }
|
|
378
|
+
not_calculated = boolean_supported.length - analyzed.length
|
|
379
|
+
covered = analyzed.sum { |decision| decision.dig(:decision_table, :covered_rules).to_i }
|
|
380
|
+
required = analyzed.sum { |decision| decision.dig(:decision_table, :required_rules).to_i }
|
|
381
|
+
generated = analyzed.sum { |decision| decision.dig(:decision_table, :generated_rules).to_i }
|
|
382
|
+
impossible = analyzed.sum { |decision| decision.dig(:decision_table, :impossible_rules).to_i }
|
|
383
|
+
fully_covered = analyzed.count { |decision| decision.dig(:decision_table, :coverage_status) == "covered" }
|
|
384
|
+
{ decisions_analyzed: analyzed.length, not_calculated_decisions: not_calculated,
|
|
385
|
+
fully_covered_decisions: fully_covered, covered_rules: covered, required_rules: required,
|
|
386
|
+
generated_rules: generated, impossible_rules: impossible,
|
|
387
|
+
percentage: DecisionTable.percentage(covered, required),
|
|
388
|
+
decision_percentage: DecisionTable.percentage(fully_covered, analyzed.length) }
|
|
389
|
+
end
|
|
390
|
+
|
|
356
391
|
def alternative_aggregate(decisions)
|
|
357
392
|
flow = decisions.select { |decision| !decision[:unsupported] && alternative_decision?(decision) }
|
|
358
393
|
required = flow.sum { |decision| decision.dig(:coverage, :alternative, :required_alternatives).to_i }
|
|
359
394
|
covered = flow.sum { |decision| decision.dig(:coverage, :alternative, :covered_alternatives).to_i }
|
|
360
395
|
{ covered_alternatives: covered, required_alternatives: required,
|
|
361
|
-
supported_decisions: flow.length, percentage:
|
|
396
|
+
supported_decisions: flow.length, percentage: DecisionTable.percentage(covered, required) }
|
|
362
397
|
end
|
|
363
398
|
|
|
364
399
|
def valid_vector(vector, decision)
|
|
@@ -433,7 +468,7 @@ module Branchproof
|
|
|
433
468
|
end
|
|
434
469
|
|
|
435
470
|
def effective_mask(vector, decision_id, tree = nil)
|
|
436
|
-
decision =
|
|
471
|
+
decision = @decisions_by_id[decision_id] unless tree
|
|
437
472
|
tree ||= decision && decision[:tree]
|
|
438
473
|
raise ArgumentError, "unknown decision" unless tree
|
|
439
474
|
|
|
@@ -448,6 +483,15 @@ module Branchproof
|
|
|
448
483
|
mask
|
|
449
484
|
end
|
|
450
485
|
|
|
486
|
+
# Returns the effective mask for a vector, reusing a precomputed
|
|
487
|
+
# vector-id => mask hash when the caller has one, instead of
|
|
488
|
+
# replaying the whole Boolean tree again.
|
|
489
|
+
def masked_bits(vector, decision_id, masks)
|
|
490
|
+
return effective_mask(vector, decision_id) unless masks
|
|
491
|
+
|
|
492
|
+
masks[id(vector, :id)] || effective_mask(vector, decision_id)
|
|
493
|
+
end
|
|
494
|
+
|
|
451
495
|
def replay(node, values, offset)
|
|
452
496
|
type = id(node, :type).to_sym
|
|
453
497
|
if type == :atom
|
|
@@ -472,9 +516,9 @@ module Branchproof
|
|
|
472
516
|
end
|
|
473
517
|
end
|
|
474
518
|
|
|
475
|
-
def first_pair(vectors, index, decision_id)
|
|
519
|
+
def first_pair(vectors, index, decision_id, masks = nil)
|
|
476
520
|
observed_vectors = vectors.select do |vector|
|
|
477
|
-
observed?(vector, index) &&
|
|
521
|
+
observed?(vector, index) && masked_bits(vector, decision_id, masks)&.anybits?(1 << index)
|
|
478
522
|
end
|
|
479
523
|
observed_vectors.sort_by! { |vector| id(vector, :id).to_s }
|
|
480
524
|
grouped = observed_vectors.group_by do |vector|
|
|
@@ -491,7 +535,9 @@ module Branchproof
|
|
|
491
535
|
|
|
492
536
|
left = left_vectors.first
|
|
493
537
|
right = right_vectors.first
|
|
494
|
-
|
|
538
|
+
if pair?(decision_id: decision_id, condition_index: index, left: left, right: right, masks: masks)
|
|
539
|
+
return [left, right]
|
|
540
|
+
end
|
|
495
541
|
end
|
|
496
542
|
nil
|
|
497
543
|
end
|
|
@@ -613,18 +659,6 @@ module Branchproof
|
|
|
613
659
|
end
|
|
614
660
|
end
|
|
615
661
|
|
|
616
|
-
def evaluate(node, values)
|
|
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
|
|
620
|
-
|
|
621
|
-
left = evaluate(node[:left], values)
|
|
622
|
-
return left if id(node, :type).to_sym == :and && !left
|
|
623
|
-
return left if id(node, :type).to_sym == :or && left
|
|
624
|
-
|
|
625
|
-
evaluate(node[:right], values)
|
|
626
|
-
end
|
|
627
|
-
|
|
628
662
|
def evaluate_with_trace(node, values, trace = [])
|
|
629
663
|
type = id(node, :type).to_sym
|
|
630
664
|
if type == :atom
|
|
@@ -654,13 +688,15 @@ module Branchproof
|
|
|
654
688
|
end
|
|
655
689
|
|
|
656
690
|
def values_for(vector)
|
|
691
|
+
return @values_cache[vector] if @values_cache.key?(vector)
|
|
692
|
+
|
|
657
693
|
raw = vector[:values] || vector["values"]
|
|
658
694
|
raise ArgumentError, "observation must contain booleans or nil" unless raw.is_a?(Array)
|
|
659
695
|
unless raw.all? { |value| value.nil? || value == true || value == false }
|
|
660
696
|
raise ArgumentError, "observation must contain booleans or nil"
|
|
661
697
|
end
|
|
662
698
|
|
|
663
|
-
raw
|
|
699
|
+
@values_cache[vector] = raw
|
|
664
700
|
end
|
|
665
701
|
|
|
666
702
|
def outcome(vector) = vector[:outcome].nil? ? vector["outcome"] : vector[:outcome]
|
data/lib/branchproof/cli.rb
CHANGED
|
@@ -15,6 +15,9 @@ require "time"
|
|
|
15
15
|
module Branchproof
|
|
16
16
|
# Coordinates source inventory, isolated test execution, and report output.
|
|
17
17
|
class CLI
|
|
18
|
+
VIEWS = Report::VIEWS.to_h { |view| [view.to_s.tr("_", "-"), view] }
|
|
19
|
+
.merge("decision_tables" => :decision_tables).freeze
|
|
20
|
+
|
|
18
21
|
def initialize(stdout:, stderr:)
|
|
19
22
|
@stdout = stdout
|
|
20
23
|
@stderr = stderr
|
|
@@ -25,7 +28,7 @@ module Branchproof
|
|
|
25
28
|
return help if [["--help"], ["help"], ["analyze", "--help"]].include?(argv)
|
|
26
29
|
return offline(argv) if %w[report compare].include?(argv.first)
|
|
27
30
|
|
|
28
|
-
options = parse(
|
|
31
|
+
options = parse(argv)
|
|
29
32
|
return usage_error("expected analyze, report, or compare; use branchproof --help") unless options
|
|
30
33
|
|
|
31
34
|
inventory = build_inventory(options)
|
|
@@ -49,30 +52,29 @@ module Branchproof
|
|
|
49
52
|
message: merge_status[:reason].to_s }]
|
|
50
53
|
end
|
|
51
54
|
end
|
|
55
|
+
snapshot = evidence.snapshot
|
|
52
56
|
if value(baseline, :status).to_s == "PASSED"
|
|
53
|
-
analysis = Analyzer.new(inventory: inventory, evidence:
|
|
57
|
+
analysis = Analyzer.new(inventory: inventory, evidence: snapshot, limits: options[:limits],
|
|
58
|
+
reachability: options[:reachability]).call
|
|
54
59
|
baseline[:analysis] = analysis
|
|
55
60
|
if options[:level] >= 2
|
|
56
61
|
ids = Array(value(inventory, :decisions)).map { |decision| value(decision, :id) }
|
|
62
|
+
minimizer = Minimizer.new(analysis: analysis, evidence: snapshot, limits: options[:limits])
|
|
57
63
|
baseline[:minima] = ids.filter_map do |decision_id|
|
|
58
|
-
|
|
64
|
+
minimizer.call(objective: :vectors, decision_ids: [decision_id])
|
|
59
65
|
end
|
|
60
66
|
baseline[:minima] += ids.filter_map do |decision_id|
|
|
61
|
-
|
|
62
|
-
objective: :tests, decision_ids: [decision_id]
|
|
63
|
-
)
|
|
67
|
+
minimizer.call(objective: :tests, decision_ids: [decision_id])
|
|
64
68
|
end
|
|
65
|
-
baseline[:minima] <<
|
|
66
|
-
objective: :tests, decision_ids: ids
|
|
67
|
-
)
|
|
69
|
+
baseline[:minima] << minimizer.call(objective: :tests, decision_ids: ids)
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
diagnostics = Array(value(inventory, :diagnostics)) + Array(value(baseline, :diagnostics)) +
|
|
71
|
-
Array(value(
|
|
73
|
+
Array(value(snapshot,
|
|
72
74
|
:diagnostics)) + Array(value(value(baseline, :analysis), :diagnostics))
|
|
73
75
|
analysis = value(baseline, :analysis)
|
|
74
76
|
minima = options[:level] == 1 ? [] : Array(value(baseline, :minima))
|
|
75
|
-
report = Report.new(inventory: inventory, evidence: value(baseline, :evidence) ||
|
|
77
|
+
report = Report.new(inventory: inventory, evidence: value(baseline, :evidence) || snapshot,
|
|
76
78
|
analysis: analysis, minima: minima, baseline: baseline, diagnostics: diagnostics,
|
|
77
79
|
level: options[:level], missing_only: options[:missing_only], view: options[:view],
|
|
78
80
|
run_metadata: run_metadata(options, baseline))
|
|
@@ -92,21 +94,24 @@ module Branchproof
|
|
|
92
94
|
@stdout.write(<<~HELP)
|
|
93
95
|
Usage:
|
|
94
96
|
branchproof analyze [SOURCE_GLOB ...] [--test TEST_GLOB] [--project auto|ruby|rails]
|
|
95
|
-
[--view decisions|conditions|tests] [--level 1|2|3] [--missing-only]
|
|
96
|
-
[--format terminal|json] [--output PATH] [--limits PATH] [-- RUNNER_ARGS]
|
|
97
|
-
branchproof report SNAPSHOT [--view decisions|conditions|tests
|
|
98
|
-
[--missing-only] [--format terminal|json] [--output PATH]
|
|
97
|
+
[--view decisions|conditions|tests|decision-tables] [--level 1|2|3] [--missing-only]
|
|
98
|
+
[--format terminal|json] [--output PATH] [--limits PATH] [--no-reachability] [-- RUNNER_ARGS]
|
|
99
|
+
branchproof report SNAPSHOT [--view decisions|conditions|tests|decision-tables]
|
|
100
|
+
[--level 1|2|3] [--missing-only] [--format terminal|json] [--output PATH]
|
|
99
101
|
branchproof compare BEFORE AFTER [--format terminal|json] [--output PATH] [--fail-on-regression]
|
|
100
102
|
mcdc accepts the same commands as a compatibility alias.
|
|
101
103
|
JSON always contains full evidence; --view requires terminal output.
|
|
104
|
+
decision_tables is also accepted as an alias for the decision-tables view.
|
|
105
|
+
--no-reachability keeps every generated decision-table rule as a coverage obligation.
|
|
102
106
|
HELP
|
|
103
107
|
0
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
def parse_view(view)
|
|
107
|
-
|
|
111
|
+
resolved = VIEWS[view.to_s]
|
|
112
|
+
raise ArgumentError, "view must be decisions, conditions, tests, or decision-tables" unless resolved
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
resolved
|
|
110
115
|
end
|
|
111
116
|
|
|
112
117
|
def validate_view!(options)
|
|
@@ -204,7 +209,8 @@ module Branchproof
|
|
|
204
209
|
project_root: root, source_patterns: options[:source_patterns].map { |path| relative_path(path, root) },
|
|
205
210
|
test_patterns: options[:test_patterns].map { |path| relative_path(path, root) },
|
|
206
211
|
test_files: options[:tests].map { |path| relative_path(path, root) }, runner_args: options[:runner_args],
|
|
207
|
-
seed: value(baseline, :seed), limits: options[:limits], test_locations: locations
|
|
212
|
+
seed: value(baseline, :seed), limits: options[:limits], test_locations: locations,
|
|
213
|
+
reachability: options[:reachability] }
|
|
208
214
|
end
|
|
209
215
|
|
|
210
216
|
def relative_path(path, root)
|
|
@@ -221,7 +227,8 @@ module Branchproof
|
|
|
221
227
|
runner_args = delimiter ? args[(delimiter + 1)..] : []
|
|
222
228
|
args = args[0...delimiter] if delimiter
|
|
223
229
|
options = { level: 3, format: :terminal, output: nil, tests: [], source_paths: [], limits: Limits.default,
|
|
224
|
-
runner_args: runner_args, project: nil, missing_only: false, view: :decisions
|
|
230
|
+
runner_args: runner_args, project: nil, missing_only: false, view: :decisions,
|
|
231
|
+
reachability: true }
|
|
225
232
|
until args.empty?
|
|
226
233
|
token = args.shift
|
|
227
234
|
case token
|
|
@@ -230,6 +237,8 @@ module Branchproof
|
|
|
230
237
|
options[:explicit_view] = true
|
|
231
238
|
when "--missing-only"
|
|
232
239
|
options[:missing_only] = true
|
|
240
|
+
when "--no-reachability"
|
|
241
|
+
options[:reachability] = false
|
|
233
242
|
when "--level"
|
|
234
243
|
level = Integer(args.shift.to_s, 10)
|
|
235
244
|
raise ArgumentError, "level must be 1, 2, or 3" unless (1..3).cover?(level)
|