branchproof 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +153 -10
- data/doc/Branchproof/Configuration.md +28 -0
- data/doc/Branchproof/CoveragePolicy.md +27 -0
- data/doc/Branchproof/FocusedReport.md +1 -1
- data/doc/Branchproof/Report.md +8 -2
- data/doc/Branchproof/ReportSelection.md +42 -0
- data/doc/Branchproof.md +4 -1
- data/doc/CHANGELOG.md +23 -0
- data/doc/README.md +153 -10
- data/lib/branchproof/cli.rb +120 -13
- data/lib/branchproof/comparison.rb +6 -1
- data/lib/branchproof/configuration.rb +111 -0
- data/lib/branchproof/coverage_policy.rb +169 -0
- data/lib/branchproof/evidence.rb +37 -2
- data/lib/branchproof/focused_report.rb +144 -27
- data/lib/branchproof/report.rb +97 -9
- data/lib/branchproof/report_selection.rb +158 -0
- data/lib/branchproof/saved_report.rb +29 -3
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof.rb +3 -0
- data/llms.txt +4 -1
- data/sig/branchproof.rbs +35 -3
- metadata +7 -1
data/lib/branchproof/report.rb
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
require "json"
|
|
6
6
|
require "pathname"
|
|
7
|
+
require_relative "coverage_policy"
|
|
8
|
+
require_relative "report_selection"
|
|
7
9
|
|
|
8
10
|
module Branchproof
|
|
9
11
|
# Renders versioned terminal and JSON analysis reports.
|
|
10
12
|
class Report
|
|
11
|
-
SCHEMA_VERSION = "1.
|
|
13
|
+
SCHEMA_VERSION = "1.4"
|
|
12
14
|
CRITERION_VERSION = "masking_occurrence_v1"
|
|
13
15
|
VIEWS = %i[decisions conditions tests decision_tables].freeze
|
|
14
16
|
DECISION_TABLE_LABELS = { "true" => "T", "false" => "F", "dont_care" => "-" }.freeze
|
|
@@ -16,7 +18,7 @@ module Branchproof
|
|
|
16
18
|
"excluded" => "EXCLUDED" }.freeze
|
|
17
19
|
|
|
18
20
|
def initialize(inventory:, evidence:, analysis:, minima:, baseline:, diagnostics:, level: 3, missing_only: false,
|
|
19
|
-
view: :decisions, run_metadata: {}, saved_document: nil)
|
|
21
|
+
view: :decisions, run_metadata: {}, saved_document: nil, focus: nil, top: nil, minimum: nil)
|
|
20
22
|
raise ArgumentError, "level must be 1, 2, or 3" unless [1, 2, 3].include?(level.to_i)
|
|
21
23
|
unless VIEWS.include?(view.to_sym)
|
|
22
24
|
raise ArgumentError, "view must be :decisions, :conditions, :tests, or :decision_tables"
|
|
@@ -33,21 +35,29 @@ module Branchproof
|
|
|
33
35
|
@view = view.to_sym
|
|
34
36
|
@run_metadata = run_metadata || {}
|
|
35
37
|
@saved_document = saved_document
|
|
38
|
+
raise ArgumentError, "minimum must be a hash" if !minimum.nil? && !minimum.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
@minimum_override = minimum.is_a?(Hash) && !minimum.empty? ? CoveragePolicy.normalize(minimum) : nil
|
|
41
|
+
@minimum = effective_minimum
|
|
42
|
+
@selection = ReportSelection.new(focus: focus, top: top)
|
|
36
43
|
end
|
|
37
44
|
|
|
38
|
-
def self.from_document(document:, level: nil, view: :decisions, missing_only: false
|
|
45
|
+
def self.from_document(document:, level: nil, view: :decisions, missing_only: false, focus: nil, top: nil,
|
|
46
|
+
minimum: nil)
|
|
39
47
|
data = document || {}
|
|
40
48
|
new(inventory: data[:source_inventory] || data["source_inventory"],
|
|
41
49
|
evidence: data[:observations] || data["observations"],
|
|
42
50
|
analysis: data[:analysis] || data["analysis"], minima: data[:minima] || data["minima"],
|
|
43
51
|
baseline: data[:baseline] || data["baseline"], diagnostics: data[:diagnostics] || data["diagnostics"],
|
|
44
52
|
level: level || (data[:analysis] || data["analysis"] ? 3 : 1), view: view, missing_only: missing_only,
|
|
45
|
-
|
|
53
|
+
focus: focus, top: top,
|
|
54
|
+
run_metadata: data[:run_metadata] || data["run_metadata"], saved_document: data, minimum: minimum)
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def write(io:, format:)
|
|
49
58
|
format = format.to_sym
|
|
50
59
|
raise ArgumentError, "format must be :terminal or :json" unless %i[terminal json].include?(format)
|
|
60
|
+
raise ArgumentError, "focus and top filters are terminal-only" if format == :json && @selection.active?
|
|
51
61
|
|
|
52
62
|
io.write(format == :json ? JSON.generate(json_document) : terminal_document)
|
|
53
63
|
nil
|
|
@@ -105,6 +115,10 @@ module Branchproof
|
|
|
105
115
|
def exit_code
|
|
106
116
|
return 2 unless usage_valid?
|
|
107
117
|
|
|
118
|
+
policy_status = value(coverage_policy, :status).to_s
|
|
119
|
+
return 2 if policy_status == "unavailable"
|
|
120
|
+
return 1 if policy_status == "failed"
|
|
121
|
+
|
|
108
122
|
status = value(@baseline, :status).to_s.upcase
|
|
109
123
|
return 2 if %w[ERROR INCOMPLETE].include?(status)
|
|
110
124
|
return 1 if status == "FAILED"
|
|
@@ -131,7 +145,11 @@ module Branchproof
|
|
|
131
145
|
private
|
|
132
146
|
|
|
133
147
|
def json_document
|
|
134
|
-
|
|
148
|
+
if @saved_document
|
|
149
|
+
document = normalize(@saved_document)
|
|
150
|
+
document["coverage_policy"] = normalize(coverage_policy) if @minimum_override
|
|
151
|
+
return document
|
|
152
|
+
end
|
|
135
153
|
|
|
136
154
|
normalize(schema_version: SCHEMA_VERSION,
|
|
137
155
|
tool_version: (defined?(Branchproof::VERSION) ? Branchproof::VERSION : "unknown"),
|
|
@@ -139,14 +157,15 @@ module Branchproof
|
|
|
139
157
|
run_ids: Array(value(@evidence, :run_ids)),
|
|
140
158
|
source_inventory: inventory_with_default_kinds, baseline: @baseline, observations: @evidence,
|
|
141
159
|
analysis: @analysis, minima: @minima, metrics: metrics,
|
|
142
|
-
diagnostics: @diagnostics, completeness: completeness,
|
|
160
|
+
diagnostics: @diagnostics, completeness: completeness, coverage_policy: coverage_policy,
|
|
143
161
|
run_metadata: @run_metadata)
|
|
144
162
|
end
|
|
145
163
|
|
|
146
164
|
def terminal_document
|
|
147
165
|
unless @view == :decisions
|
|
148
166
|
return FocusedReport.new(document: json_document, view: @view, level: @level,
|
|
149
|
-
missing_only: @missing_only, coordinator: self
|
|
167
|
+
missing_only: @missing_only, coordinator: self,
|
|
168
|
+
selection: @selection).render
|
|
150
169
|
end
|
|
151
170
|
|
|
152
171
|
@terminal_ids = terminal_ids
|
|
@@ -161,10 +180,13 @@ module Branchproof
|
|
|
161
180
|
"#{metrics[:unattributed]} unattributed",
|
|
162
181
|
values_legend]
|
|
163
182
|
lines.concat(coverage_ladder_lines)
|
|
183
|
+
lines.concat(coverage_policy_lines)
|
|
164
184
|
lines << missing_summary_line if @missing_only
|
|
185
|
+
selected_decisions, hidden = selected_decisions_for_display
|
|
186
|
+
append_selection_lines(lines, hidden, "decision")
|
|
165
187
|
lines << "Scope: supported decisions, conditions, and alternatives"
|
|
166
188
|
lines << ""
|
|
167
|
-
|
|
189
|
+
selected_decisions.each { |decision| render_decision(lines, decision) }
|
|
168
190
|
render_minima(lines) unless @missing_only
|
|
169
191
|
unless @diagnostics.empty?
|
|
170
192
|
lines << "Diagnostics:"
|
|
@@ -930,6 +952,33 @@ module Branchproof
|
|
|
930
952
|
end
|
|
931
953
|
end
|
|
932
954
|
|
|
955
|
+
def selected_decisions_for_display
|
|
956
|
+
decisions = decisions_to_render
|
|
957
|
+
return [decisions, 0] unless @selection.active?
|
|
958
|
+
|
|
959
|
+
inventory = value(@inventory, :source_units) ? @inventory : { source_units: [], decisions: inventory_decisions }
|
|
960
|
+
decisions = @selection.filter_decisions(decisions, inventory: inventory)
|
|
961
|
+
decisions = decisions.sort_by { |decision| @selection.sort_key(decision, inventory: inventory) }
|
|
962
|
+
@selection.limit(decisions)
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def append_selection_lines(lines, hidden, unit)
|
|
966
|
+
if @selection.focus_active?
|
|
967
|
+
matching = @selection.matching_decision_ids(json_document)
|
|
968
|
+
lines << if matching.empty?
|
|
969
|
+
"Focus: no matching decisions for #{@selection.focus_label}"
|
|
970
|
+
else
|
|
971
|
+
"Focus: #{@selection.focus_label}"
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
return unless @selection.top
|
|
975
|
+
|
|
976
|
+
noun = hidden == 1 ? unit : "#{unit}s"
|
|
977
|
+
hidden_noun = hidden == 1 ? unit : "#{unit}s"
|
|
978
|
+
lines << "Display limit: top #{@selection.top} #{noun}; hidden #{hidden} #{hidden_noun} " \
|
|
979
|
+
"(not risk-ranked)"
|
|
980
|
+
end
|
|
981
|
+
|
|
933
982
|
def alternatives_to_render(decision)
|
|
934
983
|
alternatives = Array(value(decision, :alternatives))
|
|
935
984
|
return alternatives unless @missing_only && analysis_available?
|
|
@@ -962,6 +1011,44 @@ module Branchproof
|
|
|
962
1011
|
!@analysis.nil?
|
|
963
1012
|
end
|
|
964
1013
|
|
|
1014
|
+
def coverage_policy
|
|
1015
|
+
@coverage_policy ||= CoveragePolicy.new(minimum: @minimum).call(document: policy_document)
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
def coverage_policy_lines
|
|
1019
|
+
policy = coverage_policy
|
|
1020
|
+
minimum = value(policy, :minimum) || {}
|
|
1021
|
+
return [] if minimum.empty?
|
|
1022
|
+
|
|
1023
|
+
lines = ["Coverage policy: #{value(policy, :status).to_s.upcase}"]
|
|
1024
|
+
Array(value(policy, :gates)).each do |gate|
|
|
1025
|
+
numerator = value(gate, :numerator)
|
|
1026
|
+
denominator = value(gate, :denominator)
|
|
1027
|
+
count = if denominator.nil? || (denominator.respond_to?(:zero?) && denominator.zero?)
|
|
1028
|
+
"N/A"
|
|
1029
|
+
else
|
|
1030
|
+
"#{numerator || "N/A"}/#{denominator}"
|
|
1031
|
+
end
|
|
1032
|
+
suffix = value(gate, :reason) ? "; reason: #{value(gate, :reason)}" : ""
|
|
1033
|
+
lines << " #{value(gate, :criterion)}: #{count}, threshold #{value(gate, :minimum)}, " \
|
|
1034
|
+
"#{value(gate, :status).to_s.upcase}#{suffix}"
|
|
1035
|
+
end
|
|
1036
|
+
lines << ""
|
|
1037
|
+
lines
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
def policy_document
|
|
1041
|
+
{ baseline: @baseline, completeness: value(@saved_document, :completeness) || completeness,
|
|
1042
|
+
observations: value(@saved_document, :observations) || @evidence,
|
|
1043
|
+
analysis: value(@saved_document, :analysis) || @analysis }
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def effective_minimum
|
|
1047
|
+
inherited = value(value(@saved_document, :coverage_policy), :minimum)
|
|
1048
|
+
base = inherited.is_a?(Hash) ? CoveragePolicy.normalize(inherited) : {}
|
|
1049
|
+
base.merge(@minimum_override || {})
|
|
1050
|
+
end
|
|
1051
|
+
|
|
965
1052
|
def coverage_available?
|
|
966
1053
|
analysis_available? && !value(@analysis, :coverage).nil?
|
|
967
1054
|
end
|
|
@@ -1170,7 +1257,8 @@ module Branchproof
|
|
|
1170
1257
|
def normalize_unknown(object)
|
|
1171
1258
|
object.to_s
|
|
1172
1259
|
end
|
|
1173
|
-
public :condition_coverage_evidence, :coverage_ladder_lines, :
|
|
1260
|
+
public :condition_coverage_evidence, :coverage_ladder_lines, :coverage_policy_lines, :coverage_policy,
|
|
1261
|
+
:coverage_status_label,
|
|
1174
1262
|
:decision_table_requirement, :decision_table_reachability, :decision_table_expected_heading
|
|
1175
1263
|
end
|
|
1176
1264
|
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Branchproof
|
|
6
|
+
# Validates and applies terminal-only report display filters.
|
|
7
|
+
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
8
|
+
class ReportSelection
|
|
9
|
+
attr_reader :focus, :top
|
|
10
|
+
|
|
11
|
+
def initialize(focus: nil, top: nil)
|
|
12
|
+
@focus = normalize_focus(focus)
|
|
13
|
+
@top = normalize_top(top)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def active?
|
|
17
|
+
!@focus.nil? || !@top.nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def focus_active?
|
|
21
|
+
!@focus.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def focus_label
|
|
25
|
+
return unless @focus
|
|
26
|
+
|
|
27
|
+
@focus[:line] ? "#{@focus[:path]}:#{@focus[:line]}" : @focus[:path]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def matching_decision_ids(document)
|
|
31
|
+
inventory = fetch(document, :source_inventory) || fetch(document, :inventory) || {}
|
|
32
|
+
sources = source_map(inventory)
|
|
33
|
+
Array(fetch(inventory, :decisions)).filter_map do |decision|
|
|
34
|
+
next unless matches_decision?(decision, sources)
|
|
35
|
+
|
|
36
|
+
fetch(decision, :id).to_s
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def filter_decisions(decisions, inventory: {})
|
|
41
|
+
return decisions unless focus_active?
|
|
42
|
+
|
|
43
|
+
sources = source_map(inventory)
|
|
44
|
+
decisions.select { |decision| matches_decision?(decision, sources) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def limit(items)
|
|
48
|
+
return [items, 0] unless @top
|
|
49
|
+
|
|
50
|
+
[items.first(@top), [items.length - @top, 0].max]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort_key(decision, inventory: {})
|
|
54
|
+
source = source_for(decision, source_map(inventory))
|
|
55
|
+
[normalize_source_path(fetch(source, :relative_path)).to_s,
|
|
56
|
+
fetch(decision, :line).to_i, fetch(decision, :column).to_i, fetch(decision, :id).to_s]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize_focus(focus)
|
|
62
|
+
return nil if focus.nil?
|
|
63
|
+
raise ArgumentError, "focus must be PATH or PATH:LINE" unless focus.is_a?(String)
|
|
64
|
+
|
|
65
|
+
raw = focus.strip
|
|
66
|
+
raise ArgumentError, "focus path must not be empty" if raw.empty?
|
|
67
|
+
|
|
68
|
+
path, line = if raw.match?(/:\d+\z/)
|
|
69
|
+
match = raw.match(/\A(.+):([0-9]+)\z/)
|
|
70
|
+
raise ArgumentError, "focus path must not be empty" unless match
|
|
71
|
+
|
|
72
|
+
[match[1], match[2].to_i]
|
|
73
|
+
else
|
|
74
|
+
raise ArgumentError, "focus line must be a positive integer" if raw.include?(":")
|
|
75
|
+
|
|
76
|
+
[raw, nil]
|
|
77
|
+
end
|
|
78
|
+
raise ArgumentError, "focus line must be a positive integer" if line && line <= 0
|
|
79
|
+
|
|
80
|
+
{ path: normalize_path(path), line: line }.freeze
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def normalize_top(top)
|
|
84
|
+
return nil if top.nil?
|
|
85
|
+
|
|
86
|
+
value = if top.is_a?(Integer)
|
|
87
|
+
top
|
|
88
|
+
elsif top.is_a?(String) && top.match?(/\A[1-9][0-9]*\z/)
|
|
89
|
+
top.to_i
|
|
90
|
+
end
|
|
91
|
+
raise ArgumentError, "top must be a positive integer" unless value&.positive?
|
|
92
|
+
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_path(path)
|
|
97
|
+
text = path.to_s
|
|
98
|
+
raise ArgumentError, "focus path must not be empty" if text.empty? || text.include?("\0")
|
|
99
|
+
|
|
100
|
+
pathname = Pathname.new(text)
|
|
101
|
+
raise ArgumentError, "focus path must be project-relative" if pathname.absolute?
|
|
102
|
+
|
|
103
|
+
normalized = pathname.cleanpath.to_s
|
|
104
|
+
if normalized == "." || normalized == ".." || normalized.start_with?("../")
|
|
105
|
+
raise ArgumentError, "focus path must stay within the project"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
normalized
|
|
109
|
+
rescue ArgumentError
|
|
110
|
+
raise
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
raise ArgumentError, "invalid focus path: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def matches_decision?(decision, sources)
|
|
116
|
+
return false unless @focus
|
|
117
|
+
|
|
118
|
+
source = source_for(decision, sources)
|
|
119
|
+
return false unless normalize_source_path(fetch(source, :relative_path)) == @focus[:path]
|
|
120
|
+
return true unless @focus[:line]
|
|
121
|
+
|
|
122
|
+
start_line = fetch(decision, :line).to_i
|
|
123
|
+
return false unless start_line.positive?
|
|
124
|
+
|
|
125
|
+
end_line = start_line + fetch(decision, :expression).to_s.count("\n")
|
|
126
|
+
@focus[:line].between?(start_line, end_line)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def source_for(decision, sources)
|
|
130
|
+
sources[fetch(decision, :source_id).to_s] || decision
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def source_map(inventory)
|
|
134
|
+
Array(fetch(inventory, :source_units)).to_h do |source|
|
|
135
|
+
[fetch(source, :source_id).to_s, source]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def normalize_source_path(path)
|
|
140
|
+
return nil if path.nil? || path.to_s.empty?
|
|
141
|
+
return nil if Pathname.new(path.to_s).absolute?
|
|
142
|
+
|
|
143
|
+
Pathname.new(path.to_s).cleanpath.to_s
|
|
144
|
+
rescue ArgumentError
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def fetch(hash, key)
|
|
149
|
+
return nil unless hash.respond_to?(:key?)
|
|
150
|
+
return hash[key] if hash.key?(key)
|
|
151
|
+
return hash[key.to_s] if hash.key?(key.to_s)
|
|
152
|
+
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
require "json"
|
|
8
8
|
# rubocop:disable-next Lint/RedundantRequireStatement -- supports standalone core entry
|
|
9
9
|
require "set"
|
|
10
|
+
require_relative "coverage_policy"
|
|
10
11
|
require_relative "decision_table"
|
|
11
12
|
|
|
12
13
|
module Branchproof
|
|
13
14
|
# Reads and validates a persisted JSON report without loading the project.
|
|
14
15
|
class SavedReport
|
|
15
|
-
SUPPORTED_SCHEMAS = %w[1.0 1.1 1.2 1.3].freeze
|
|
16
|
-
STRICT_FLOW_SCHEMAS = %w[1.2 1.3].freeze
|
|
16
|
+
SUPPORTED_SCHEMAS = %w[1.0 1.1 1.2 1.3 1.4].freeze
|
|
17
|
+
STRICT_FLOW_SCHEMAS = %w[1.2 1.3 1.4].freeze
|
|
17
18
|
DECISION_TABLE_STATUSES = %w[calculated not_calculated].freeze
|
|
18
19
|
RULE_CONDITION_VALUES = DecisionTable::CONDITION_VALUES
|
|
19
20
|
RULE_COVERAGE_STATUSES = DecisionTable::COVERAGE_STATUSES
|
|
@@ -60,6 +61,7 @@ module Branchproof
|
|
|
60
61
|
validate_completeness(@document["completeness"])
|
|
61
62
|
validate_completeness(@document.dig("observations", "completeness"))
|
|
62
63
|
validate_completeness(@document.dig("analysis", "completeness")) if @document["analysis"]
|
|
64
|
+
validate_coverage_policy
|
|
63
65
|
validate_optional_sections
|
|
64
66
|
validate_analysis_coverage
|
|
65
67
|
@document
|
|
@@ -218,7 +220,9 @@ module Branchproof
|
|
|
218
220
|
fail_with("nonboolean condition results must be empty") unless results.empty?
|
|
219
221
|
validate_nonboolean_analysis(decision, inventory_decision)
|
|
220
222
|
elsif decision.key?("decision_table")
|
|
221
|
-
|
|
223
|
+
unless %w[1.3 1.4].include?(@schema_version)
|
|
224
|
+
fail_with("decision tables are unsupported in legacy report schemas")
|
|
225
|
+
end
|
|
222
226
|
validate_decision_table(decision["decision_table"], inventory_decision)
|
|
223
227
|
end
|
|
224
228
|
seen_conditions = {}
|
|
@@ -626,6 +630,28 @@ module Branchproof
|
|
|
626
630
|
fail_with("document contains non-string object keys") unless all_hashes_have_string_keys?(@document)
|
|
627
631
|
end
|
|
628
632
|
|
|
633
|
+
def validate_coverage_policy
|
|
634
|
+
policy = @document["coverage_policy"]
|
|
635
|
+
fail_with("missing field: coverage_policy") if @schema_version == "1.4" && !policy
|
|
636
|
+
return if policy.nil?
|
|
637
|
+
|
|
638
|
+
fail_with("coverage_policy must be an object") unless hash_with_string_keys?(policy)
|
|
639
|
+
minimum = policy["minimum"]
|
|
640
|
+
fail_with("coverage_policy minimum must be an object") unless hash_with_string_keys?(minimum)
|
|
641
|
+
normalized = CoveragePolicy.normalize(minimum)
|
|
642
|
+
fail_with("coverage_policy status must be a string") unless policy["status"].is_a?(String)
|
|
643
|
+
fail_with("coverage_policy gates must be an array") unless policy["gates"].is_a?(Array)
|
|
644
|
+
expected = CoveragePolicy.new(minimum: normalized).call(
|
|
645
|
+
document: { baseline: @document["baseline"], completeness: @document["completeness"],
|
|
646
|
+
observations: @document["observations"], analysis: @document["analysis"] }
|
|
647
|
+
)
|
|
648
|
+
actual = JSON.parse(JSON.generate(policy))
|
|
649
|
+
expected = JSON.parse(JSON.generate(expected))
|
|
650
|
+
fail_with("coverage_policy does not match report coverage") unless actual == expected
|
|
651
|
+
rescue ArgumentError => e
|
|
652
|
+
fail_with(e.message.sub(/\Ainvalid saved report: /, ""))
|
|
653
|
+
end
|
|
654
|
+
|
|
629
655
|
def validate_phases(vector)
|
|
630
656
|
phases = vector["phases_by_test"]
|
|
631
657
|
return if phases.nil?
|
data/lib/branchproof/version.rb
CHANGED
data/lib/branchproof.rb
CHANGED
|
@@ -9,6 +9,9 @@ require_relative "branchproof/project"
|
|
|
9
9
|
# Public namespace for source inventory and one-run MC/DC reporting.
|
|
10
10
|
module Branchproof
|
|
11
11
|
class Error < StandardError; end
|
|
12
|
+
autoload :Configuration, "branchproof/configuration"
|
|
13
|
+
autoload :CoveragePolicy, "branchproof/coverage_policy"
|
|
14
|
+
autoload :ReportSelection, "branchproof/report_selection"
|
|
12
15
|
autoload :CLI, "branchproof/cli"
|
|
13
16
|
autoload :Instrumenter, "branchproof/instrumenter"
|
|
14
17
|
autoload :Loader, "branchproof/loader"
|
data/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
4
|
| --- | --- |
|
|
5
|
-
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/minimizer.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/value_syntax.rb, lib/branchproof/rails_support.rb, lib/branchproof/rspec_adapter.rb, lib/branchproof/value_runtime.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/default_syntax.rb, lib/branchproof/focused_report.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/default_runtime.rb, lib/branchproof/exception_syntax.rb, lib/branchproof/iteration_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/comparison_report.rb, lib/branchproof/exception_runtime.rb, lib/branchproof/iteration_runtime.rb, lib/branchproof/flow_instrumentation.rb, lib/branchproof/value_instrumentation.rb, lib/branchproof/default_instrumentation.rb, lib/branchproof/exception_instrumentation.rb, lib/branchproof/iteration_instrumentation.rb, lib/branchproof/extended_alternative_runtime.rb |
|
|
5
|
+
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/minimizer.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/value_syntax.rb, lib/branchproof/configuration.rb, lib/branchproof/rails_support.rb, lib/branchproof/rspec_adapter.rb, lib/branchproof/value_runtime.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/default_syntax.rb, lib/branchproof/focused_report.rb, lib/branchproof/coverage_policy.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/default_runtime.rb, lib/branchproof/exception_syntax.rb, lib/branchproof/iteration_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/report_selection.rb, lib/branchproof/comparison_report.rb, lib/branchproof/exception_runtime.rb, lib/branchproof/iteration_runtime.rb, lib/branchproof/flow_instrumentation.rb, lib/branchproof/value_instrumentation.rb, lib/branchproof/default_instrumentation.rb, lib/branchproof/exception_instrumentation.rb, lib/branchproof/iteration_instrumentation.rb, lib/branchproof/extended_alternative_runtime.rb |
|
|
6
6
|
|
|
7
7
|
Keep source-boundary and parameter-binding rules together for auditing.
|
|
8
8
|
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity,
|
|
@@ -18,9 +18,11 @@ Not documented.
|
|
|
18
18
|
- [Branchproof/CLI.md](doc/Branchproof/CLI.md)
|
|
19
19
|
- [Branchproof/Comparison.md](doc/Branchproof/Comparison.md)
|
|
20
20
|
- [Branchproof/ComparisonReport.md](doc/Branchproof/ComparisonReport.md)
|
|
21
|
+
- [Branchproof/Configuration.md](doc/Branchproof/Configuration.md)
|
|
21
22
|
- [Branchproof/Constraints/Solver.md](doc/Branchproof/Constraints/Solver.md)
|
|
22
23
|
- [Branchproof/Constraints.md](doc/Branchproof/Constraints.md)
|
|
23
24
|
- [Branchproof/CoverageIndex.md](doc/Branchproof/CoverageIndex.md)
|
|
25
|
+
- [Branchproof/CoveragePolicy.md](doc/Branchproof/CoveragePolicy.md)
|
|
24
26
|
- [Branchproof/DecisionSyntax.md](doc/Branchproof/DecisionSyntax.md)
|
|
25
27
|
- [Branchproof/DecisionTable.md](doc/Branchproof/DecisionTable.md)
|
|
26
28
|
- [Branchproof/DefaultInstrumentation.md](doc/Branchproof/DefaultInstrumentation.md)
|
|
@@ -54,6 +56,7 @@ Not documented.
|
|
|
54
56
|
- [Branchproof/RailsSupport.md](doc/Branchproof/RailsSupport.md)
|
|
55
57
|
- [Branchproof/Records.md](doc/Branchproof/Records.md)
|
|
56
58
|
- [Branchproof/Report.md](doc/Branchproof/Report.md)
|
|
59
|
+
- [Branchproof/ReportSelection.md](doc/Branchproof/ReportSelection.md)
|
|
57
60
|
- [Branchproof/Runtime.md](doc/Branchproof/Runtime.md)
|
|
58
61
|
- [Branchproof/RuntimeFlow.md](doc/Branchproof/RuntimeFlow.md)
|
|
59
62
|
- [Branchproof/SavedReport.md](doc/Branchproof/SavedReport.md)
|
data/sig/branchproof.rbs
CHANGED
|
@@ -40,6 +40,15 @@ module Branchproof
|
|
|
40
40
|
def to_h: () -> record
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
class Configuration
|
|
44
|
+
SCHEMA_VERSION: Integer
|
|
45
|
+
PROJECTS: Array[String]
|
|
46
|
+
FRAMEWORKS: Array[String]
|
|
47
|
+
MINIMUM_CRITERIA: Array[String]
|
|
48
|
+
FIELDS: Array[String]
|
|
49
|
+
def self.load: (path: String, root: String, ?explicit: bool, ?disabled: bool) -> record?
|
|
50
|
+
end
|
|
51
|
+
|
|
43
52
|
module RailsSupport
|
|
44
53
|
def self.boot: (project: record) -> record
|
|
45
54
|
def self.validate_rspec!: (project: record) -> record?
|
|
@@ -117,6 +126,14 @@ module Branchproof
|
|
|
117
126
|
def missing: (decision_id: String, condition_index: Integer) -> record?
|
|
118
127
|
end
|
|
119
128
|
|
|
129
|
+
class CoveragePolicy
|
|
130
|
+
CRITERIA: Hash[String, Hash[Symbol, String]]
|
|
131
|
+
COMPLETENESS_FIELDS: Array[String]
|
|
132
|
+
def self.normalize: (Hash[String | Symbol, untyped] minimum) -> Hash[String, Integer | Float]
|
|
133
|
+
def initialize: (minimum: Hash[String | Symbol, untyped]) -> void
|
|
134
|
+
def call: (document: document?) -> record
|
|
135
|
+
end
|
|
136
|
+
|
|
120
137
|
module Constraints
|
|
121
138
|
VERSION: Integer
|
|
122
139
|
NUMERIC_OPERATORS: Array[String]
|
|
@@ -194,13 +211,15 @@ module Branchproof
|
|
|
194
211
|
SCHEMA_VERSION: String
|
|
195
212
|
CRITERION_VERSION: String
|
|
196
213
|
VIEWS: Array[Symbol]
|
|
197
|
-
def initialize: (inventory: document, evidence: document, analysis: document?, minima: Array[document], baseline: document, diagnostics: Array[document], ?level: Integer, ?missing_only: bool, ?view: Symbol, ?run_metadata: document, ?saved_document: document?) -> void
|
|
198
|
-
def self.from_document: (document: document, ?level: Integer?, ?view: Symbol, ?missing_only: bool) -> Report
|
|
214
|
+
def initialize: (inventory: document, evidence: document, analysis: document?, minima: Array[document], baseline: document, diagnostics: Array[document], ?level: Integer, ?missing_only: bool, ?view: Symbol, ?run_metadata: document, ?saved_document: document?, ?focus: String?, ?top: (Integer | String)?, ?minimum: document?) -> void
|
|
215
|
+
def self.from_document: (document: document, ?level: Integer?, ?view: Symbol, ?missing_only: bool, ?focus: String?, ?top: (Integer | String)?, ?minimum: document?) -> Report
|
|
199
216
|
def diagnostic_message: (Hash[Symbol | String, untyped] diagnostic) -> String
|
|
200
217
|
def condition_explanation: (decision_id: String, condition_id: String) -> String
|
|
201
218
|
def decision_table_requirement: (untyped value) -> String?
|
|
202
219
|
def decision_table_reachability: (document rule) -> String
|
|
203
220
|
def decision_table_expected_heading: (document decision) -> String
|
|
221
|
+
def coverage_policy: () -> record
|
|
222
|
+
def coverage_policy_lines: () -> Array[String]
|
|
204
223
|
def rerun_command: (String test_id) -> String?
|
|
205
224
|
def write: (io: _WriteIO, format: Symbol | String) -> nil
|
|
206
225
|
def exit_code: () -> Integer
|
|
@@ -215,10 +234,23 @@ module Branchproof
|
|
|
215
234
|
end
|
|
216
235
|
|
|
217
236
|
class FocusedReport
|
|
218
|
-
def initialize: (document: document, view: Symbol, level: Integer, ?missing_only: bool) -> void
|
|
237
|
+
def initialize: (document: document, view: Symbol, level: Integer, ?missing_only: bool, ?coordinator: Report?, ?focus: String?, ?top: (Integer | String)?, ?selection: ReportSelection?) -> void
|
|
219
238
|
def render: () -> String
|
|
220
239
|
end
|
|
221
240
|
|
|
241
|
+
class ReportSelection
|
|
242
|
+
attr_reader focus: record?
|
|
243
|
+
attr_reader top: Integer?
|
|
244
|
+
def initialize: (?focus: String?, ?top: (Integer | String)?) -> void
|
|
245
|
+
def active?: () -> bool
|
|
246
|
+
def focus_active?: () -> bool
|
|
247
|
+
def focus_label: () -> String?
|
|
248
|
+
def matching_decision_ids: (document: document) -> Array[String]
|
|
249
|
+
def filter_decisions: (Array[record] decisions, ?inventory: document) -> Array[record]
|
|
250
|
+
def limit: (Array[record] items) -> [Array[record], Integer]
|
|
251
|
+
def sort_key: (record decision, ?inventory: document) -> [String, Integer, Integer, String]
|
|
252
|
+
end
|
|
253
|
+
|
|
222
254
|
class SavedReport
|
|
223
255
|
def self.read: (String path) -> document
|
|
224
256
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: branchproof
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -70,9 +70,11 @@ files:
|
|
|
70
70
|
- doc/Branchproof/CLI.md
|
|
71
71
|
- doc/Branchproof/Comparison.md
|
|
72
72
|
- doc/Branchproof/ComparisonReport.md
|
|
73
|
+
- doc/Branchproof/Configuration.md
|
|
73
74
|
- doc/Branchproof/Constraints.md
|
|
74
75
|
- doc/Branchproof/Constraints/Solver.md
|
|
75
76
|
- doc/Branchproof/CoverageIndex.md
|
|
77
|
+
- doc/Branchproof/CoveragePolicy.md
|
|
76
78
|
- doc/Branchproof/DecisionSyntax.md
|
|
77
79
|
- doc/Branchproof/DecisionTable.md
|
|
78
80
|
- doc/Branchproof/DefaultInstrumentation.md
|
|
@@ -106,6 +108,7 @@ files:
|
|
|
106
108
|
- doc/Branchproof/RailsSupport/Error.md
|
|
107
109
|
- doc/Branchproof/Records.md
|
|
108
110
|
- doc/Branchproof/Report.md
|
|
111
|
+
- doc/Branchproof/ReportSelection.md
|
|
109
112
|
- doc/Branchproof/Runtime.md
|
|
110
113
|
- doc/Branchproof/RuntimeFlow.md
|
|
111
114
|
- doc/Branchproof/SavedReport.md
|
|
@@ -123,8 +126,10 @@ files:
|
|
|
123
126
|
- lib/branchproof/cli.rb
|
|
124
127
|
- lib/branchproof/comparison.rb
|
|
125
128
|
- lib/branchproof/comparison_report.rb
|
|
129
|
+
- lib/branchproof/configuration.rb
|
|
126
130
|
- lib/branchproof/constraints.rb
|
|
127
131
|
- lib/branchproof/coverage_index.rb
|
|
132
|
+
- lib/branchproof/coverage_policy.rb
|
|
128
133
|
- lib/branchproof/decision_syntax.rb
|
|
129
134
|
- lib/branchproof/decision_table.rb
|
|
130
135
|
- lib/branchproof/default_instrumentation.rb
|
|
@@ -149,6 +154,7 @@ files:
|
|
|
149
154
|
- lib/branchproof/rails_support.rb
|
|
150
155
|
- lib/branchproof/records.rb
|
|
151
156
|
- lib/branchproof/report.rb
|
|
157
|
+
- lib/branchproof/report_selection.rb
|
|
152
158
|
- lib/branchproof/rspec_adapter.rb
|
|
153
159
|
- lib/branchproof/runtime.rb
|
|
154
160
|
- lib/branchproof/runtime_flow.rb
|