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
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "flow_instrumentation"
|
|
4
|
+
|
|
3
5
|
module Branchproof
|
|
4
6
|
# Applies the smallest possible source edits around inventoried expressions.
|
|
5
7
|
# The edits are deliberately textual: Prism owns the ranges, while this class
|
|
6
8
|
# never evaluates application code or introduces a Ruby scope.
|
|
7
9
|
class Instrumenter
|
|
10
|
+
include FlowInstrumentation
|
|
11
|
+
|
|
8
12
|
RUNTIME = "::Branchproof::Runtime"
|
|
9
13
|
|
|
10
14
|
def rewrite(unit:)
|
|
@@ -17,8 +21,7 @@ module Branchproof
|
|
|
17
21
|
decisions = Array(unit[:decisions]).select { |decision| supported?(decision) }
|
|
18
22
|
edits = decisions.filter_map do |decision|
|
|
19
23
|
next if decisions.any? do |outer|
|
|
20
|
-
|
|
21
|
-
decision[:byte_length])
|
|
24
|
+
encloses_decision?(outer, decision)
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
decision_edit(bytes, decision, decisions)
|
|
@@ -57,13 +60,13 @@ module Branchproof
|
|
|
57
60
|
return nil unless valid_range?(bytes, start, length)
|
|
58
61
|
|
|
59
62
|
nested = all_decisions.select do |candidate|
|
|
60
|
-
|
|
63
|
+
encloses_decision?(decision, candidate)
|
|
61
64
|
end
|
|
62
|
-
expression =
|
|
65
|
+
expression = render_decision(bytes, decision, nested)
|
|
63
66
|
{
|
|
64
67
|
start: start,
|
|
65
68
|
finish: start + length,
|
|
66
|
-
text:
|
|
69
|
+
text: expression
|
|
67
70
|
}
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -90,24 +93,36 @@ module Branchproof
|
|
|
90
93
|
contains?(start, length, child[:byte_start], child[:byte_length])
|
|
91
94
|
end
|
|
92
95
|
children = children.reject do |child|
|
|
93
|
-
|
|
94
|
-
candidate != child && contains?(candidate[:byte_start], candidate[:byte_length], child[:byte_start],
|
|
95
|
-
child[:byte_length])
|
|
96
|
-
end
|
|
96
|
+
children.any? { |candidate| encloses_decision?(candidate, child) }
|
|
97
97
|
end
|
|
98
98
|
children.sort_by! { |child| -child[:byte_start] }
|
|
99
99
|
output = bytes.byteslice(start, length)
|
|
100
100
|
children.each do |child|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end)
|
|
104
|
-
child_text = frame(child[:id], child_text)
|
|
101
|
+
descendants = nested.select { |candidate| encloses_decision?(child, candidate) }
|
|
102
|
+
child_text = render_decision(bytes, child, descendants)
|
|
105
103
|
offset = child[:byte_start] - start
|
|
106
104
|
output[offset, child[:byte_length]] = child_text
|
|
107
105
|
end
|
|
108
106
|
output
|
|
109
107
|
end
|
|
110
108
|
|
|
109
|
+
def encloses_decision?(outer, inner)
|
|
110
|
+
return false if outer.equal?(inner) || outer == inner
|
|
111
|
+
return false unless contains?(outer[:byte_start], outer[:byte_length], inner[:byte_start], inner[:byte_length])
|
|
112
|
+
return true unless outer[:byte_start] == inner[:byte_start] && outer[:byte_length] == inner[:byte_length]
|
|
113
|
+
|
|
114
|
+
(outer[:kind] || "boolean") == "boolean" && inner[:kind] != "boolean" && !inner[:kind].nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_decision(bytes, decision, nested)
|
|
118
|
+
if decision[:instrumentation]
|
|
119
|
+
render_flow(bytes, decision, nested)
|
|
120
|
+
else
|
|
121
|
+
expression = render_range(bytes, decision[:byte_start], decision[:byte_length], decision, nested)
|
|
122
|
+
frame(decision[:id], expression)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
111
126
|
def condition_wrapper(decision_id, index, expression)
|
|
112
127
|
"#{RUNTIME}.condition(#{decision_id.inspect}, #{index}, (#{expression}))"
|
|
113
128
|
end
|
data/lib/branchproof/report.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
|
4
|
+
|
|
3
5
|
require "json"
|
|
4
6
|
require "pathname"
|
|
5
7
|
|
|
6
8
|
module Branchproof
|
|
7
9
|
# Renders versioned terminal and JSON analysis reports.
|
|
8
10
|
class Report
|
|
9
|
-
SCHEMA_VERSION = "1.
|
|
11
|
+
SCHEMA_VERSION = "1.2"
|
|
10
12
|
CRITERION_VERSION = "masking_occurrence_v1"
|
|
11
13
|
|
|
12
14
|
def initialize(inventory:, evidence:, analysis:, minima:, baseline:, diagnostics:, level: 3, missing_only: false,
|
|
@@ -103,7 +105,9 @@ module Branchproof
|
|
|
103
105
|
return 1 if status == "FAILED"
|
|
104
106
|
return 2 unless status == "PASSED" && value(@baseline, :finalized) == true
|
|
105
107
|
return 2 if @saved_document && (value(@saved_document, :completeness) || {}).values.include?(false)
|
|
106
|
-
|
|
108
|
+
|
|
109
|
+
eligible = metrics[:eligible_conditions].positive? || metrics[:eligible_alternatives].positive?
|
|
110
|
+
return 2 unless eligible
|
|
107
111
|
return 2 unless valid_for_requested_level?
|
|
108
112
|
|
|
109
113
|
0
|
|
@@ -118,7 +122,7 @@ module Branchproof
|
|
|
118
122
|
tool_version: (defined?(Branchproof::VERSION) ? Branchproof::VERSION : "unknown"),
|
|
119
123
|
criterion_version: CRITERION_VERSION, runtime: RUBY_DESCRIPTION,
|
|
120
124
|
run_ids: Array(value(@evidence, :run_ids)),
|
|
121
|
-
source_inventory:
|
|
125
|
+
source_inventory: inventory_with_default_kinds, baseline: @baseline, observations: @evidence,
|
|
122
126
|
analysis: @analysis, minima: @minima, metrics: metrics,
|
|
123
127
|
diagnostics: @diagnostics, completeness: completeness,
|
|
124
128
|
run_metadata: @run_metadata)
|
|
@@ -137,12 +141,13 @@ module Branchproof
|
|
|
137
141
|
"Analysis: #{terminal_analysis_status}",
|
|
138
142
|
"Decisions: #{metrics[:supported]} supported, #{metrics[:unsupported]} excluded, " \
|
|
139
143
|
"#{metrics[:unexecuted]} unexecuted (#{metrics[:discovered]} discovered)",
|
|
144
|
+
decision_kind_summary_line,
|
|
140
145
|
"Observations: #{metrics[:completed]} completed, #{metrics[:aborted]} aborted, " \
|
|
141
146
|
"#{metrics[:unattributed]} unattributed",
|
|
142
|
-
|
|
147
|
+
values_legend]
|
|
143
148
|
lines.concat(coverage_ladder_lines)
|
|
144
149
|
lines << missing_summary_line if @missing_only
|
|
145
|
-
lines << "Scope: supported decisions and
|
|
150
|
+
lines << "Scope: supported decisions, conditions, and alternatives"
|
|
146
151
|
lines << ""
|
|
147
152
|
decisions_to_render.each { |decision| render_decision(lines, decision) }
|
|
148
153
|
render_minima(lines) unless @missing_only
|
|
@@ -159,18 +164,23 @@ module Branchproof
|
|
|
159
164
|
decision_label = "Decision #{short_id(value(decision, :id))} #{filename}:#{value(decision, :line)}"
|
|
160
165
|
lines << decision_label
|
|
161
166
|
lines << " Decision: #{value(decision, :expression)}"
|
|
167
|
+
lines << " Kind: #{decision_kind(decision)}"
|
|
168
|
+
context = value(decision, :context)
|
|
169
|
+
lines << " Context: #{context}" unless context.nil? || context.to_s.empty?
|
|
162
170
|
lines << " Status: #{value(decision, :support_status) || "SUPPORTED"}"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
unless nonboolean_decision?(decision)
|
|
172
|
+
conditions_to_render(decision).each do |condition|
|
|
173
|
+
result = condition_result(decision, condition)
|
|
174
|
+
detail = condition_detail(decision, condition, result)
|
|
175
|
+
status = if @analysis.nil? || (@level == 1 && !coverage_available?)
|
|
176
|
+
"NOT CALCULATED"
|
|
177
|
+
else
|
|
178
|
+
value(result, :status) || "NOT_PROVEN"
|
|
179
|
+
end
|
|
180
|
+
lines << " Condition #{value(condition, :index)}: #{value(condition, :expression)}"
|
|
181
|
+
lines << " #{status}#{detail}"
|
|
182
|
+
render_condition_coverage(lines, decision, condition, result) if @level >= 2 && coverage_available?
|
|
183
|
+
end
|
|
174
184
|
end
|
|
175
185
|
render_decision_coverage(lines, decision) if coverage_available?
|
|
176
186
|
vectors_to_render(decision).each do |vector|
|
|
@@ -183,8 +193,23 @@ module Branchproof
|
|
|
183
193
|
end.join
|
|
184
194
|
owners = Array(value(vector, :test_ids)).map { |test_id| test_label(test_id) }
|
|
185
195
|
owners << "unattributed" if value(vector, :unattributed_count).to_i.positive?
|
|
186
|
-
vector_label =
|
|
187
|
-
|
|
196
|
+
vector_label = if nonboolean_decision?(decision)
|
|
197
|
+
selected = Array(value(vector, :values)).each_with_index.filter_map do |item, index|
|
|
198
|
+
next unless item
|
|
199
|
+
|
|
200
|
+
alternative = Array(value(decision, :alternatives))[index]
|
|
201
|
+
value(alternative, :expression) || "alternative #{index}"
|
|
202
|
+
end
|
|
203
|
+
" Vector #{short_id(value(vector, :id))} [#{values}] => selected: #{selected.join(", ")}"
|
|
204
|
+
else
|
|
205
|
+
" Vector #{short_id(value(vector,
|
|
206
|
+
:id))} [#{values}] => " + (if value(vector,
|
|
207
|
+
:outcome)
|
|
208
|
+
"T"
|
|
209
|
+
else
|
|
210
|
+
"F"
|
|
211
|
+
end).to_s
|
|
212
|
+
end
|
|
188
213
|
lines << "#{vector_label} owners=#{owners.join(", ")}"
|
|
189
214
|
end
|
|
190
215
|
lines << ""
|
|
@@ -197,6 +222,11 @@ module Branchproof
|
|
|
197
222
|
end
|
|
198
223
|
|
|
199
224
|
def render_decision_coverage(lines, decision)
|
|
225
|
+
if nonboolean_decision?(decision)
|
|
226
|
+
render_alternative_coverage(lines, decision)
|
|
227
|
+
return
|
|
228
|
+
end
|
|
229
|
+
|
|
200
230
|
decision_coverage = value(value(analysis_for(decision), :coverage), :decision)
|
|
201
231
|
all_coverage = value(analysis_for(decision), :coverage) || {}
|
|
202
232
|
statuses = [["D", :decision], ["C", :condition], ["C/D", :condition_decision],
|
|
@@ -226,6 +256,51 @@ module Branchproof
|
|
|
226
256
|
end.join(", ")}"
|
|
227
257
|
end
|
|
228
258
|
|
|
259
|
+
def render_alternative_coverage(lines, decision)
|
|
260
|
+
coverage = value(value(analysis_for(decision), :coverage), :alternative) || {}
|
|
261
|
+
status = value(coverage, :status)
|
|
262
|
+
required = value(coverage, :required_alternatives) || Array(value(decision, :alternatives)).length
|
|
263
|
+
covered = value(coverage, :covered_alternatives) || 0
|
|
264
|
+
lines << " Alternatives: #{status || "NOT_CALCULATED"} (#{covered}/#{required})"
|
|
265
|
+
lines << " MC/DC: N/A (not applicable)"
|
|
266
|
+
rows = Array(value(coverage, :alternatives))
|
|
267
|
+
rows_by_id = rows.to_h { |row| [value(row, :alternative_id).to_s, row] }
|
|
268
|
+
alternatives_to_render(decision).each do |alternative|
|
|
269
|
+
id = value(alternative, :id)
|
|
270
|
+
row = rows_by_id[id.to_s] || {}
|
|
271
|
+
lines << " Alternative #{value(alternative, :index)}: #{value(alternative, :expression)}"
|
|
272
|
+
render_alternative_state(lines, "Selected", value(row, :selected)) if @level >= 2
|
|
273
|
+
render_alternative_state(lines, "Not selected", value(row, :not_selected)) if @level >= 2
|
|
274
|
+
render_alternative_state(lines, "Skipped", value(row, :skipped)) if @level >= 2
|
|
275
|
+
end
|
|
276
|
+
missing = Array(value(coverage, :missing_alternatives))
|
|
277
|
+
return if missing.empty?
|
|
278
|
+
|
|
279
|
+
labels = missing.map do |id|
|
|
280
|
+
alternative = Array(value(decision, :alternatives)).find { |item| value(item, :id).to_s == id.to_s }
|
|
281
|
+
value(alternative, :expression) || short_id(id)
|
|
282
|
+
end
|
|
283
|
+
label = labels.join(", ")
|
|
284
|
+
lines << " Missing alternatives: #{label}"
|
|
285
|
+
lines << " Need selection of: #{label}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def render_alternative_state(lines, label, evidence)
|
|
289
|
+
evidence ||= {}
|
|
290
|
+
owners = Array(value(evidence, :test_ids)).map { |id| test_label(id) }
|
|
291
|
+
owners << "unattributed" if value(evidence, :unattributed_count).to_i.positive?
|
|
292
|
+
owners = owners.uniq
|
|
293
|
+
status = if value(evidence, :observed)
|
|
294
|
+
"observed"
|
|
295
|
+
elsif label == "Selected"
|
|
296
|
+
"missing"
|
|
297
|
+
else
|
|
298
|
+
"none recorded"
|
|
299
|
+
end
|
|
300
|
+
detail = owners.empty? ? "none recorded" : owners.join(", ")
|
|
301
|
+
lines << " #{label}: #{status}; tests: #{detail}"
|
|
302
|
+
end
|
|
303
|
+
|
|
229
304
|
def render_condition_coverage(lines, _decision, condition, result)
|
|
230
305
|
coverage = value(result, :coverage)
|
|
231
306
|
return unless coverage
|
|
@@ -520,23 +595,73 @@ module Branchproof
|
|
|
520
595
|
vectors.find { |vector| value(vector, :id).to_s == id }
|
|
521
596
|
end
|
|
522
597
|
|
|
598
|
+
def values_legend
|
|
599
|
+
return "Values: T=true, F=false, -=short-circuited" unless inventory_decisions.any? do |decision|
|
|
600
|
+
nonboolean_decision?(decision)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
"Values: Boolean T=true/F=false; flow T=selected, F=not-selected, -=skipped"
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def inventory_with_default_kinds
|
|
607
|
+
decisions = inventory_decisions.map do |decision|
|
|
608
|
+
next decision if value(decision, :kind)
|
|
609
|
+
|
|
610
|
+
decision.respond_to?(:key?) ? decision.merge(kind: "boolean") : decision
|
|
611
|
+
end
|
|
612
|
+
return @inventory unless value(@inventory, :decisions)
|
|
613
|
+
|
|
614
|
+
@inventory.merge(decisions: decisions)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def decision_kind(decision)
|
|
618
|
+
kind = value(decision, :kind).to_s
|
|
619
|
+
kind.empty? ? "boolean" : kind
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def nonboolean_decision?(decision)
|
|
623
|
+
%w[implicit multiway pattern exception].include?(decision_kind(decision))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def decision_kind_summary_line
|
|
627
|
+
counts = metrics[:kind_counts]
|
|
628
|
+
return "Decision kinds: none" if counts.empty?
|
|
629
|
+
|
|
630
|
+
contexts = metrics[:context_counts]
|
|
631
|
+
suffix = if contexts.empty?
|
|
632
|
+
""
|
|
633
|
+
else
|
|
634
|
+
"; contexts: #{contexts.map do |context, count|
|
|
635
|
+
"#{context}=#{count}"
|
|
636
|
+
end.join(", ")}"
|
|
637
|
+
end
|
|
638
|
+
"Decision kinds: #{counts.map { |kind, count| "#{kind}=#{count}" }.join(", ")}#{suffix}"
|
|
639
|
+
end
|
|
640
|
+
|
|
523
641
|
def metrics
|
|
524
642
|
decisions = inventory_decisions
|
|
525
643
|
unsupported, supported = decisions.partition { |decision| unsupported?(decision) }
|
|
526
644
|
eligible = supported.sum { |decision| Array(value(decision, :conditions)).length }
|
|
527
645
|
observed = vectors.map { |vector| value(vector, :decision_id).to_s }.uniq
|
|
528
646
|
proven = @analysis ? value(@analysis, :proven_count).to_i : 0
|
|
647
|
+
kind_counts = decisions.group_by { |decision| decision_kind(decision) }.transform_values(&:length)
|
|
648
|
+
context_counts = decisions.group_by { |decision| value(decision, :context).to_s }
|
|
649
|
+
context_counts = context_counts.reject { |context, _| context.empty? }.transform_values(&:length)
|
|
529
650
|
{ discovered: decisions.length, supported: supported.length, unsupported: unsupported.length,
|
|
530
651
|
unsupported_conditions: unsupported.sum do |decision|
|
|
531
652
|
discovered_conditions(decision)
|
|
532
653
|
end, eligible_conditions: eligible,
|
|
533
654
|
opaque: decisions.sum { |decision| Array(value(decision, :opaque_ranges)).length },
|
|
534
655
|
unexecuted: supported.count { |decision| !observed.include?(value(decision, :id).to_s) },
|
|
656
|
+
eligible_alternatives: supported.sum do |decision|
|
|
657
|
+
nonboolean_decision?(decision) ? Array(value(decision, :alternatives)).length : 0
|
|
658
|
+
end,
|
|
535
659
|
completed: vectors.sum do |vector|
|
|
536
660
|
value(vector, :count).to_i
|
|
537
661
|
end, aborted: numeric_hash_value(@evidence, :abort_counts),
|
|
538
662
|
unattributed: vectors.sum { |vector| value(vector, :unattributed_count).to_i }, limited: incomplete? ? 1 : 0,
|
|
539
|
-
proven: proven, percentage: percentage(eligible, proven)
|
|
663
|
+
proven: proven, percentage: percentage(eligible, proven), kind_counts: kind_counts,
|
|
664
|
+
decision_kinds: kind_counts, context_counts: context_counts }
|
|
540
665
|
end
|
|
541
666
|
|
|
542
667
|
def completeness
|
|
@@ -594,18 +719,36 @@ module Branchproof
|
|
|
594
719
|
|
|
595
720
|
inventory_decisions
|
|
596
721
|
.reject { |decision| unsupported?(decision) }
|
|
597
|
-
.select
|
|
722
|
+
.select do |decision|
|
|
723
|
+
nonboolean_decision?(decision) ? missing_alternatives_for(decision).any? : conditions_to_render(decision).any?
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def alternatives_to_render(decision)
|
|
728
|
+
alternatives = Array(value(decision, :alternatives))
|
|
729
|
+
return alternatives unless @missing_only && analysis_available?
|
|
730
|
+
|
|
731
|
+
missing_ids = missing_alternatives_for(decision).map(&:to_s)
|
|
732
|
+
alternatives.select { |alternative| missing_ids.include?(value(alternative, :id).to_s) }
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def missing_alternatives_for(decision)
|
|
736
|
+
coverage = value(value(analysis_for(decision), :coverage), :alternative)
|
|
737
|
+
Array(value(coverage, :missing_alternatives))
|
|
598
738
|
end
|
|
599
739
|
|
|
600
740
|
def conditions_to_render(decision)
|
|
601
741
|
conditions = Array(value(decision, :conditions))
|
|
602
742
|
return conditions unless @missing_only && analysis_available?
|
|
603
743
|
|
|
604
|
-
conditions.reject
|
|
744
|
+
conditions.reject do |condition|
|
|
745
|
+
value(condition_result(decision, condition), :status).to_s.upcase == "PROVEN"
|
|
746
|
+
end
|
|
605
747
|
end
|
|
606
748
|
|
|
607
749
|
def vectors_to_render(decision)
|
|
608
750
|
return vectors_for(decision) unless @missing_only
|
|
751
|
+
return [] if nonboolean_decision?(decision)
|
|
609
752
|
|
|
610
753
|
[]
|
|
611
754
|
end
|
|
@@ -650,6 +793,14 @@ module Branchproof
|
|
|
650
793
|
end
|
|
651
794
|
lines << " #{label} (#{description}): #{shown} (#{numerator}/#{denominator} #{denominator_label})#{qualifier}"
|
|
652
795
|
end
|
|
796
|
+
alternative = value(aggregate, :alternative)
|
|
797
|
+
if alternative
|
|
798
|
+
covered = value(alternative, :covered_alternatives) || 0
|
|
799
|
+
required = value(alternative, :required_alternatives) || 0
|
|
800
|
+
percentage = value(alternative, :percentage)
|
|
801
|
+
shown = percentage.nil? ? "N/A" : "#{percentage}%"
|
|
802
|
+
lines << " Alternative coverage: #{covered}/#{required} alternatives (#{shown})"
|
|
803
|
+
end
|
|
653
804
|
lines << ""
|
|
654
805
|
lines
|
|
655
806
|
end
|
|
@@ -683,11 +834,29 @@ module Branchproof
|
|
|
683
834
|
def missing_summary_line
|
|
684
835
|
return "Missing conditions: Cannot identify missing conditions (analysis unavailable)" unless analysis_available?
|
|
685
836
|
return "Missing conditions: Cannot identify missing conditions (analysis incomplete)" unless analysis_complete?
|
|
686
|
-
return "No
|
|
687
|
-
|
|
837
|
+
return "No missing conditions or alternatives" if analysis_complete? && missing_condition_count.zero? &&
|
|
838
|
+
missing_alternatives_count.zero?
|
|
839
|
+
return "No eligible conditions" if metrics[:eligible_conditions].zero? && metrics[:eligible_alternatives].zero?
|
|
688
840
|
|
|
689
841
|
count = decisions_to_render.length
|
|
690
|
-
|
|
842
|
+
if missing_alternatives_count.zero?
|
|
843
|
+
label = count == 1 ? "decision" : "decisions"
|
|
844
|
+
return "Missing conditions: #{missing_condition_count} across #{count} #{label}"
|
|
845
|
+
end
|
|
846
|
+
if missing_condition_count.zero?
|
|
847
|
+
label = count == 1 ? "decision" : "decisions"
|
|
848
|
+
return "Missing alternatives: #{missing_alternatives_count} across #{count} #{label}"
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
label = count == 1 ? "decision" : "decisions"
|
|
852
|
+
conditions = missing_condition_count
|
|
853
|
+
alternatives = missing_alternatives_count
|
|
854
|
+
"Missing conditions: #{conditions}; alternatives: #{alternatives} " \
|
|
855
|
+
"across #{count} #{label}"
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def missing_alternatives_count
|
|
859
|
+
decisions_to_render.sum { |decision| missing_alternatives_for(decision).length }
|
|
691
860
|
end
|
|
692
861
|
|
|
693
862
|
def analysis_for(decision)
|
|
@@ -752,3 +921,5 @@ module Branchproof
|
|
|
752
921
|
public :condition_coverage_evidence, :coverage_ladder_lines, :coverage_status_label
|
|
753
922
|
end
|
|
754
923
|
end
|
|
924
|
+
|
|
925
|
+
# rubocop:enable Metrics/BlockLength
|
data/lib/branchproof/runtime.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require_relative "runtime_flow"
|
|
4
5
|
|
|
5
6
|
module Branchproof
|
|
6
7
|
# Process-local execution recorder. It deliberately never coerces or stores
|
|
7
8
|
# application values: Ruby's conditional expression is used for truthiness.
|
|
8
9
|
# Captures condition evaluations while preserving application values.
|
|
9
10
|
module Runtime
|
|
11
|
+
extend RuntimeFlow
|
|
12
|
+
|
|
10
13
|
class << self
|
|
11
14
|
def boot(evidence:)
|
|
12
15
|
return nil if defined?(@booted) && @booted && @evidence.equal?(evidence) && Process.pid == @process_id
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Branchproof
|
|
4
|
+
# Records native Ruby path selection without evaluating application values twice.
|
|
5
|
+
module RuntimeFlow
|
|
6
|
+
def flow_candidate(decision_id, index)
|
|
7
|
+
frame = current_frame(decision_id)
|
|
8
|
+
frame[:observations] << [index, false] if frame
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def flow_selected(decision_id)
|
|
13
|
+
frame = current_frame(decision_id)
|
|
14
|
+
if frame && !frame[:observations].empty?
|
|
15
|
+
frame[:observations].last[1] = true
|
|
16
|
+
frame[:outcome] = true
|
|
17
|
+
frame[:finished] = true
|
|
18
|
+
end
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def flow_select(decision_id, index)
|
|
23
|
+
frame = current_frame(decision_id)
|
|
24
|
+
if frame
|
|
25
|
+
frame[:observations] = (0..index).map { |position| [position, position == index] }
|
|
26
|
+
frame[:outcome] = true
|
|
27
|
+
frame[:finished] = true
|
|
28
|
+
end
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def flow_path(decision_id, index)
|
|
33
|
+
frame = current_frame(decision_id)
|
|
34
|
+
if frame
|
|
35
|
+
frame[:observations] = [[0, index.zero?], [1, index == 1]]
|
|
36
|
+
frame[:outcome] = true
|
|
37
|
+
frame[:finished] = true
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flow_finish(decision_id, value, default_path = nil)
|
|
43
|
+
frame = current_frame(decision_id)
|
|
44
|
+
flow_path(decision_id, default_path) if frame && !frame[:finished] && !default_path.nil?
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def flow_receiver(decision_id, receiver)
|
|
49
|
+
enter(decision_id)
|
|
50
|
+
begin
|
|
51
|
+
flow_path(decision_id, nil.equal?(receiver) ? 0 : 1)
|
|
52
|
+
ensure
|
|
53
|
+
leave(decision_id)
|
|
54
|
+
end
|
|
55
|
+
receiver
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|