branchproof 0.7.0 → 0.9.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 +53 -0
- data/README.md +255 -21
- 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/DefaultInstrumentation.md +13 -0
- data/doc/Branchproof/DefaultRuntime.md +11 -0
- data/doc/Branchproof/DefaultSyntax.md +7 -0
- data/doc/Branchproof/ExceptionInstrumentation.md +9 -0
- data/doc/Branchproof/ExceptionRuntime.md +27 -0
- data/doc/Branchproof/ExceptionSyntax.md +9 -0
- data/doc/Branchproof/ExtendedAlternativeRuntime.md +19 -0
- data/doc/Branchproof/FocusedReport.md +5 -2
- data/doc/Branchproof/Instrumenter.md +1 -1
- data/doc/Branchproof/IterationInstrumentation.md +7 -0
- data/doc/Branchproof/IterationRuntime.md +21 -0
- data/doc/Branchproof/IterationSyntax.md +11 -0
- 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 +60 -1
- data/doc/Branchproof/SavedReport.md +15 -0
- data/doc/Branchproof/Source.md +7 -1
- data/doc/Branchproof/ValueInstrumentation.md +7 -0
- data/doc/Branchproof/ValueRuntime.md +26 -0
- data/doc/Branchproof/ValueSyntax.md +26 -0
- data/doc/Branchproof.md +20 -3
- data/doc/CHANGELOG.md +53 -0
- data/doc/README.md +255 -21
- 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 +50 -24
- data/lib/branchproof/decision_table.rb +377 -0
- data/lib/branchproof/default_instrumentation.rb +140 -0
- data/lib/branchproof/default_runtime.rb +16 -0
- data/lib/branchproof/default_syntax.rb +94 -0
- data/lib/branchproof/evidence.rb +133 -31
- data/lib/branchproof/exception_instrumentation.rb +96 -0
- data/lib/branchproof/exception_runtime.rb +41 -0
- data/lib/branchproof/exception_syntax.rb +157 -0
- data/lib/branchproof/extended_alternative_runtime.rb +23 -0
- data/lib/branchproof/flow_instrumentation.rb +52 -23
- data/lib/branchproof/focused_report.rb +77 -9
- data/lib/branchproof/instrumenter.rb +93 -37
- data/lib/branchproof/iteration_instrumentation.rb +66 -0
- data/lib/branchproof/iteration_runtime.rb +79 -0
- data/lib/branchproof/iteration_syntax.rb +75 -0
- data/lib/branchproof/limits.rb +4 -1
- data/lib/branchproof/loader.rb +24 -7
- 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 +32 -30
- data/lib/branchproof/saved_report.rb +200 -14
- data/lib/branchproof/source.rb +140 -71
- data/lib/branchproof/value_instrumentation.rb +35 -0
- data/lib/branchproof/value_runtime.rb +73 -0
- data/lib/branchproof/value_syntax.rb +116 -0
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +1 -4
- data/lib/branchproof.rb +2 -0
- data/llms.txt +20 -3
- data/sig/branchproof.rbs +64 -1
- metadata +32 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Branchproof
|
|
4
|
+
# Observe entry into source-visible callback bodies, not library internals.
|
|
5
|
+
module IterationSyntax
|
|
6
|
+
ITERATORS = %i[each each_with_index times upto downto step map collect select filter reject filter_map
|
|
7
|
+
find detect find_index take_while drop_while partition count all? any? none? one? fetch].freeze
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def flow_decision_node?(node)
|
|
12
|
+
iteration_node?(node) || node.is_a?(Prism::MatchRequiredNode) || super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def iteration_node?(node)
|
|
16
|
+
node.is_a?(Prism::ForNode) ||
|
|
17
|
+
(node.is_a?(Prism::CallNode) && ITERATORS.include?(node.name) &&
|
|
18
|
+
node.block.is_a?(Prism::BlockNode) && !node.safe_navigation?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
22
|
+
def flow_details(node, bytes, encoding)
|
|
23
|
+
if node.is_a?(Prism::MatchRequiredNode)
|
|
24
|
+
alternatives = %w[matched mismatch].map { |label| iteration_alternative(node, label) }
|
|
25
|
+
return ["pattern", "required_pattern", alternatives,
|
|
26
|
+
{ type: "required_pattern", range: byte_range(node.location) }, unsupported_reasons(node, bytes)]
|
|
27
|
+
end
|
|
28
|
+
return super unless iteration_node?(node)
|
|
29
|
+
|
|
30
|
+
body = node.is_a?(Prism::ForNode) ? node.statements : node.block.body
|
|
31
|
+
closing = node.is_a?(Prism::ForNode) ? node.end_keyword_loc : node.block.closing_loc
|
|
32
|
+
lazy = node.is_a?(Prism::CallNode) && lazy_receiver?(node.receiver)
|
|
33
|
+
fallback = node.is_a?(Prism::CallNode) && node.name == :fetch
|
|
34
|
+
context = if lazy
|
|
35
|
+
"lazy_callback"
|
|
36
|
+
else
|
|
37
|
+
(fallback ? "fetch_fallback" : "iteration")
|
|
38
|
+
end
|
|
39
|
+
labels = if lazy
|
|
40
|
+
["callback entered"]
|
|
41
|
+
else
|
|
42
|
+
(fallback ? ["value present", "fallback entered"] : %w[empty entered])
|
|
43
|
+
end
|
|
44
|
+
metadata = { type: "iteration", range: byte_range(node.location), lazy: lazy,
|
|
45
|
+
insert_at: iteration_insert_at(body, closing),
|
|
46
|
+
empty: body.nil? }
|
|
47
|
+
metadata[:receiver] = byte_range(node.receiver.location) if node.is_a?(Prism::CallNode) && node.receiver
|
|
48
|
+
[lazy ? "multiway" : "implicit", context, labels.map { |label| iteration_alternative(node, label) },
|
|
49
|
+
metadata, unsupported_reasons(node, bytes)]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
53
|
+
|
|
54
|
+
def lazy_receiver?(node)
|
|
55
|
+
node.is_a?(Prism::CallNode) && (node.name == :lazy || lazy_receiver?(node.receiver))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Prism gives a block-level rescue/ensure BeginNode a location beginning at
|
|
59
|
+
# the `do` keyword. Inserting there would produce `call; callback; do`, so
|
|
60
|
+
# anchor the callback after the block parameters and before its statements.
|
|
61
|
+
def iteration_insert_at(body, closing)
|
|
62
|
+
return closing.start_offset unless body
|
|
63
|
+
|
|
64
|
+
if body.is_a?(Prism::BeginNode)
|
|
65
|
+
statements = body.statements
|
|
66
|
+
return statements.location.start_offset if statements&.location
|
|
67
|
+
end
|
|
68
|
+
body.location.start_offset
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def iteration_alternative(node, label)
|
|
72
|
+
{ expression: label, byte_start: node.location.start_offset, byte_length: node.location.length }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/branchproof/limits.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Branchproof
|
|
|
6
6
|
KEYS = %i[
|
|
7
7
|
conditions_per_decision vectors_per_decision owner_associations_per_run
|
|
8
8
|
tests_per_run exact_candidates exact_search_nodes constraint_search_states
|
|
9
|
+
max_conditions_for_decision_table decision_table_rules_per_decision
|
|
9
10
|
].freeze
|
|
10
11
|
DEFAULTS = {
|
|
11
12
|
conditions_per_decision: 64,
|
|
@@ -14,7 +15,9 @@ module Branchproof
|
|
|
14
15
|
tests_per_run: 50_000,
|
|
15
16
|
exact_candidates: 32,
|
|
16
17
|
exact_search_nodes: 100_000,
|
|
17
|
-
constraint_search_states: 10_000
|
|
18
|
+
constraint_search_states: 10_000,
|
|
19
|
+
max_conditions_for_decision_table: 12,
|
|
20
|
+
decision_table_rules_per_decision: 4096
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
20
23
|
module_function
|
data/lib/branchproof/loader.rb
CHANGED
|
@@ -5,8 +5,6 @@ require "digest"
|
|
|
5
5
|
module Branchproof
|
|
6
6
|
# Owns the process-local CRuby compilation hook when the VM exposes it.
|
|
7
7
|
class Loader
|
|
8
|
-
STATUS_KEYS = %i[status reason].freeze
|
|
9
|
-
|
|
10
8
|
def initialize(inventory:, instrumenter:)
|
|
11
9
|
@inventory = inventory
|
|
12
10
|
@instrumenter = instrumenter
|
|
@@ -41,7 +39,7 @@ module Branchproof
|
|
|
41
39
|
def load_iseq(path)
|
|
42
40
|
return nil unless @installed
|
|
43
41
|
|
|
44
|
-
unit = @units[canonical(path)]
|
|
42
|
+
unit = @units[path] || @units[canonical(path)]
|
|
45
43
|
return nil unless unit
|
|
46
44
|
|
|
47
45
|
bytes = File.binread(path)
|
|
@@ -49,7 +47,12 @@ module Branchproof
|
|
|
49
47
|
add_diagnostic("source_drift", "selected source changed after inventory", unit[:source_id])
|
|
50
48
|
return nil
|
|
51
49
|
end
|
|
52
|
-
|
|
50
|
+
begin
|
|
51
|
+
rewritten = @instrumenter.rewrite(unit: unit.merge(original_bytes: bytes))
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
add_diagnostic("rewrite_failure", "#{e.class}: #{e.message}", unit[:source_id], severity: "error")
|
|
54
|
+
return nil
|
|
55
|
+
end
|
|
53
56
|
Array(rewritten[:diagnostics]).each do |diagnostic|
|
|
54
57
|
add_diagnostic(diagnostic[:code], diagnostic[:message], unit[:source_id], severity: diagnostic[:severity])
|
|
55
58
|
end
|
|
@@ -61,6 +64,8 @@ module Branchproof
|
|
|
61
64
|
return nil
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
return rewritten[:iseq] if reusable_iseq?(unit, rewritten)
|
|
68
|
+
|
|
64
69
|
RubyVM::InstructionSequence.compile(
|
|
65
70
|
rewritten[:bytes], unit[:real_path] || canonical(path), unit[:real_path] || canonical(path), 1,
|
|
66
71
|
compile_options(unit)
|
|
@@ -108,13 +113,14 @@ module Branchproof
|
|
|
108
113
|
end
|
|
109
114
|
|
|
110
115
|
def preloaded_target?
|
|
111
|
-
|
|
116
|
+
loaded = $LOADED_FEATURES.each_with_object(Set.new) { |feature, set| set << canonical(feature) }
|
|
117
|
+
@units.keys.any? { |path| loaded.include?(canonical(path)) }
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
def index_units(inventory)
|
|
115
121
|
Array(inventory[:source_units]).each_with_object({}) do |unit, index|
|
|
116
|
-
|
|
117
|
-
index[
|
|
122
|
+
index[unit[:absolute_path]] = unit if unit[:absolute_path]
|
|
123
|
+
index[unit[:real_path]] = unit if unit[:real_path]
|
|
118
124
|
end
|
|
119
125
|
end
|
|
120
126
|
|
|
@@ -136,6 +142,17 @@ module Branchproof
|
|
|
136
142
|
unit[:compile_options] || {}
|
|
137
143
|
end
|
|
138
144
|
|
|
145
|
+
# The instrumenter already compiled the rewritten source once to validate it.
|
|
146
|
+
# Reuse that instruction sequence instead of compiling again, but only when
|
|
147
|
+
# it was built with the exact file/path/options this loader would use:
|
|
148
|
+
# real_path present and equal to absolute_path (so the instrumenter's file
|
|
149
|
+
# and path arguments match what we pass below), and no custom compile
|
|
150
|
+
# options (the instrumenter compiles without any).
|
|
151
|
+
def reusable_iseq?(unit, rewritten)
|
|
152
|
+
rewritten[:iseq] && compile_options(unit).empty? &&
|
|
153
|
+
unit[:real_path] && unit[:absolute_path] == unit[:real_path]
|
|
154
|
+
end
|
|
155
|
+
|
|
139
156
|
def status(state, reason)
|
|
140
157
|
{ status: state.to_s, reason: reason&.to_s }.freeze
|
|
141
158
|
end
|
|
@@ -140,15 +140,18 @@ module Branchproof
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def greedy(ids, candidates, target)
|
|
143
|
+
positions = ids.each_with_index.to_h
|
|
143
144
|
chosen = []
|
|
145
|
+
chosen_set = Set.new
|
|
144
146
|
covered = Set.new
|
|
145
147
|
until covered >= target
|
|
146
|
-
available = ids.reject { |candidate|
|
|
147
|
-
id = available.max_by { |candidate| [(candidates[candidate] - covered).length, -
|
|
148
|
+
available = ids.reject { |candidate| chosen_set.include?(candidate) }
|
|
149
|
+
id = available.max_by { |candidate| [(candidates[candidate] - covered).length, -positions[candidate]] }
|
|
148
150
|
break unless id
|
|
149
151
|
break if (candidates[id] - covered).empty?
|
|
150
152
|
|
|
151
153
|
chosen << id
|
|
154
|
+
chosen_set << id
|
|
152
155
|
covered |= candidates[id]
|
|
153
156
|
end
|
|
154
157
|
chosen.sort.reverse_each do |candidate|
|
|
@@ -159,16 +162,20 @@ module Branchproof
|
|
|
159
162
|
chosen.sort
|
|
160
163
|
end
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
interchangeable = candidates.keys.select do |candidate|
|
|
168
|
-
candidates.fetch(candidate).intersect?(target) && candidates.reject do |key, _|
|
|
169
|
-
key == candidate
|
|
170
|
-
end.values.reduce(Set.new, :|) >= target
|
|
165
|
+
# For each target obligation, how many candidates cover it. An obligation
|
|
166
|
+
# covered by exactly one candidate makes that candidate irreplaceable.
|
|
167
|
+
def coverage_counts(candidates)
|
|
168
|
+
candidates.each_value.with_object(Hash.new(0)) do |signs, counts|
|
|
169
|
+
signs.each { |obligation| counts[obligation] += 1 }
|
|
171
170
|
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def result(objective, scope, target, selected, status, lower, visited, reasons, candidates = {})
|
|
174
|
+
counts = coverage_counts(candidates)
|
|
175
|
+
necessary = candidates.select { |_, signs| signs.any? { |obligation| counts[obligation] == 1 } }.keys
|
|
176
|
+
interchangeable = candidates.select do |_, signs|
|
|
177
|
+
!signs.empty? && signs.none? { |obligation| counts[obligation] == 1 }
|
|
178
|
+
end.keys
|
|
172
179
|
{ objective: objective, scope_decision_ids: scope,
|
|
173
180
|
target_obligations: target.to_a.sort_by do |decision, index, sign|
|
|
174
181
|
[decision.to_s, index.to_i, sign ? 1 : 0]
|
|
@@ -189,8 +196,6 @@ module Branchproof
|
|
|
189
196
|
value&.to_i&.positive? ? value.to_i : default
|
|
190
197
|
end
|
|
191
198
|
|
|
192
|
-
def covers?(set, target) = set >= target
|
|
193
|
-
|
|
194
199
|
def value(vector, index) = (vector[:values] || vector["values"])[index]
|
|
195
200
|
def id(record, key) = record[key].nil? ? record[key.to_s] : record[key]
|
|
196
201
|
def records(record, key) = Array(id(record, key))
|
|
@@ -13,10 +13,6 @@ module Branchproof
|
|
|
13
13
|
@tests = {}
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def capabilities
|
|
17
|
-
{ serial: true, phases: true }.freeze
|
|
18
|
-
end
|
|
19
|
-
|
|
20
16
|
def run(test_files:, runner_args:, on_complete:, before_load: nil)
|
|
21
17
|
raise ArgumentError, "test_files must be an Array" unless test_files.is_a?(Array)
|
|
22
18
|
raise ArgumentError, "on_complete must respond to call" unless on_complete.respond_to?(:call)
|
|
@@ -158,12 +154,7 @@ module Branchproof
|
|
|
158
154
|
record = @tests[test_id]
|
|
159
155
|
return unless record
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
(failure.respond_to?(:skipped?) && failure.skipped?) ||
|
|
163
|
-
(defined?(Minitest::Skip) && failure.respond_to?(:error) && failure.error.is_a?(Minitest::Skip)) ||
|
|
164
|
-
failure.class.name.to_s.include?("Skip")
|
|
165
|
-
end
|
|
166
|
-
record[:status] = if skipped
|
|
157
|
+
record[:status] = if test.skipped?
|
|
167
158
|
"skipped"
|
|
168
159
|
else
|
|
169
160
|
(test.failures.empty? ? "passed" : "failed")
|
|
@@ -182,17 +173,21 @@ module Branchproof
|
|
|
182
173
|
end
|
|
183
174
|
|
|
184
175
|
def test_id_for(test)
|
|
176
|
+
return test.instance_variable_get(:@branchproof_test_id) if test.instance_variable_defined?(:@branchproof_test_id)
|
|
177
|
+
|
|
185
178
|
source = begin
|
|
186
179
|
test.method(test.name).source_location
|
|
187
180
|
rescue NameError
|
|
188
181
|
nil
|
|
189
182
|
end
|
|
190
|
-
if defined?(Branchproof::Records)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
183
|
+
id = if defined?(Branchproof::Records)
|
|
184
|
+
Branchproof::Records.id(adapter: "minitest", class_name: test.class.name, method_name: test.name,
|
|
185
|
+
source: source)
|
|
186
|
+
else
|
|
187
|
+
"minitest:#{test.class}:#{test.name}:#{source}"
|
|
188
|
+
end
|
|
189
|
+
test.instance_variable_set(:@branchproof_test_id, id)
|
|
190
|
+
id
|
|
196
191
|
end
|
|
197
192
|
|
|
198
193
|
def register_test(test)
|