branchproof 0.6.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 +45 -0
- data/README.md +279 -27
- 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 +6 -0
- data/doc/Branchproof/DecisionSyntax.md +22 -0
- data/doc/Branchproof/DecisionTable.md +153 -0
- data/doc/Branchproof/FlowInstrumentation.md +7 -0
- data/doc/Branchproof/FocusedReport.md +5 -2
- data/doc/Branchproof/Instrumenter.md +1 -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 +23 -0
- data/doc/Branchproof/RuntimeFlow.md +27 -0
- data/doc/Branchproof/SavedReport.md +21 -0
- data/doc/Branchproof/Source.md +1 -0
- data/doc/Branchproof.md +9 -2
- data/doc/CHANGELOG.md +45 -0
- data/doc/README.md +279 -27
- data/lib/branchproof/analyzer.rb +222 -54
- 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 +120 -3
- data/lib/branchproof/decision_syntax.rb +310 -0
- data/lib/branchproof/decision_table.rb +377 -0
- data/lib/branchproof/evidence.rb +101 -36
- data/lib/branchproof/flow_instrumentation.rb +107 -0
- data/lib/branchproof/focused_report.rb +161 -26
- data/lib/branchproof/instrumenter.rb +65 -39
- 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 +470 -66
- data/lib/branchproof/runtime.rb +25 -30
- data/lib/branchproof/runtime_flow.rb +58 -0
- data/lib/branchproof/saved_report.rb +438 -13
- data/lib/branchproof/source.rb +239 -41
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +1 -4
- data/lib/branchproof.rb +2 -0
- data/llms.txt +9 -2
- data/sig/branchproof.rbs +54 -1
- metadata +12 -1
|
@@ -1,27 +1,27 @@
|
|
|
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:)
|
|
11
15
|
bytes = unit.fetch(:original_bytes).dup.force_encoding(Encoding::BINARY)
|
|
12
16
|
reasons = Array(unit[:support_reasons])
|
|
13
|
-
|
|
14
|
-
return result(bytes, diagnostics: [diagnostic("unsupported_source", reasons.join(", "))])
|
|
15
|
-
end
|
|
17
|
+
return result(bytes, diagnostics: [diagnostic("unsupported_source", reasons.join(", "))]) unless supported?(unit)
|
|
16
18
|
|
|
17
19
|
decisions = Array(unit[:decisions]).select { |decision| supported?(decision) }
|
|
20
|
+
encloses, enclosed = build_enclosures(decisions)
|
|
18
21
|
edits = decisions.filter_map do |decision|
|
|
19
|
-
next if
|
|
20
|
-
outer != decision && contains?(outer[:byte_start], outer[:byte_length], decision[:byte_start],
|
|
21
|
-
decision[:byte_length])
|
|
22
|
-
end
|
|
22
|
+
next if enclosed[decision]
|
|
23
23
|
|
|
24
|
-
decision_edit(bytes, decision,
|
|
24
|
+
decision_edit(bytes, decision, encloses)
|
|
25
25
|
end
|
|
26
26
|
if decisions.any? && edits.empty?
|
|
27
27
|
return result(bytes,
|
|
@@ -31,43 +31,54 @@ module Branchproof
|
|
|
31
31
|
|
|
32
32
|
rewritten = apply_edits(bytes, edits)
|
|
33
33
|
begin
|
|
34
|
-
RubyVM::InstructionSequence.compile(rewritten, unit[:absolute_path] || "(branchproof)",
|
|
35
|
-
|
|
34
|
+
iseq = RubyVM::InstructionSequence.compile(rewritten, unit[:absolute_path] || "(branchproof)",
|
|
35
|
+
unit[:real_path] || unit[:absolute_path] || "(branchproof)", 1)
|
|
36
36
|
rescue SyntaxError => e
|
|
37
|
-
return result(
|
|
37
|
+
return result(rewritten, diagnostics: [diagnostic("invalid_rewrite", e.message)])
|
|
38
38
|
end
|
|
39
|
-
result(rewritten.force_encoding(unit[:original_bytes].encoding), changed:
|
|
39
|
+
result(rewritten.force_encoding(unit[:original_bytes].encoding), changed: edits.any?, iseq: iseq)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
status =
|
|
44
|
+
def supported?(record)
|
|
45
|
+
status = record[:support_status]
|
|
46
46
|
status.nil? || status.to_s.casecmp("supported").zero?
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
# Precompute, once, which decisions each decision encloses (and whether it is
|
|
50
|
+
# itself enclosed by any other), so rendering never re-scans the full decision
|
|
51
|
+
# list at every recursion level. Safe because `encloses_decision?` containment
|
|
52
|
+
# is transitive: if the encloses map for `decision` says it contains X, that
|
|
53
|
+
# holds true within any nested subset that already contains `decision`.
|
|
54
|
+
def build_enclosures(decisions)
|
|
55
|
+
encloses = {}.compare_by_identity
|
|
56
|
+
enclosed = {}.compare_by_identity
|
|
57
|
+
decisions.each do |outer|
|
|
58
|
+
contained = decisions.select { |inner| encloses_decision?(outer, inner) }
|
|
59
|
+
encloses[outer] = contained
|
|
60
|
+
contained.each { |inner| enclosed[inner] = true }
|
|
61
|
+
end
|
|
62
|
+
[encloses, enclosed]
|
|
52
63
|
end
|
|
53
64
|
|
|
54
|
-
def decision_edit(bytes, decision,
|
|
65
|
+
def decision_edit(bytes, decision, encloses)
|
|
55
66
|
start = decision[:byte_start]
|
|
56
67
|
length = decision[:byte_length]
|
|
57
68
|
return nil unless valid_range?(bytes, start, length)
|
|
58
69
|
|
|
59
|
-
nested =
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
expression = render_range(bytes, start, length, decision, nested)
|
|
70
|
+
nested = encloses[decision] || []
|
|
71
|
+
expression = render_decision(bytes, decision, nested, encloses)
|
|
63
72
|
{
|
|
64
73
|
start: start,
|
|
65
74
|
finish: start + length,
|
|
66
|
-
text:
|
|
75
|
+
text: expression
|
|
67
76
|
}
|
|
68
77
|
end
|
|
69
78
|
|
|
70
|
-
def render_range(bytes,
|
|
79
|
+
def render_range(bytes, decision, nested, encloses)
|
|
80
|
+
start = decision[:byte_start]
|
|
81
|
+
length = decision[:byte_length]
|
|
71
82
|
conditions = Array(decision[:conditions]).sort_by { |condition| condition[:byte_start] }
|
|
72
83
|
cursor = start
|
|
73
84
|
chunks = []
|
|
@@ -77,37 +88,49 @@ module Branchproof
|
|
|
77
88
|
next unless valid_range?(bytes, cstart, clen) && cstart >= start && cstart + clen <= start + length
|
|
78
89
|
|
|
79
90
|
chunks << bytes.byteslice(cursor, cstart - cursor)
|
|
80
|
-
original = render_children(bytes, cstart, clen, nested)
|
|
91
|
+
original = render_children(bytes, cstart, clen, nested, encloses)
|
|
81
92
|
chunks << condition_wrapper(decision[:id], condition[:index], original)
|
|
82
93
|
cursor = cstart + clen
|
|
83
94
|
end
|
|
84
|
-
chunks << render_children(bytes, cursor, length - (cursor - start), nested)
|
|
95
|
+
chunks << render_children(bytes, cursor, length - (cursor - start), nested, encloses)
|
|
85
96
|
chunks.join
|
|
86
97
|
end
|
|
87
98
|
|
|
88
|
-
def render_children(bytes, start, length, nested)
|
|
99
|
+
def render_children(bytes, start, length, nested, encloses)
|
|
89
100
|
children = nested.select do |child|
|
|
90
101
|
contains?(start, length, child[:byte_start], child[:byte_length])
|
|
91
102
|
end
|
|
92
103
|
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
|
|
104
|
+
children.any? { |candidate| encloses_decision?(candidate, child) }
|
|
97
105
|
end
|
|
98
106
|
children.sort_by! { |child| -child[:byte_start] }
|
|
99
107
|
output = bytes.byteslice(start, length)
|
|
100
108
|
children.each do |child|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end)
|
|
104
|
-
child_text = frame(child[:id], child_text)
|
|
109
|
+
descendants = encloses[child] || []
|
|
110
|
+
child_text = render_decision(bytes, child, descendants, encloses)
|
|
105
111
|
offset = child[:byte_start] - start
|
|
106
112
|
output[offset, child[:byte_length]] = child_text
|
|
107
113
|
end
|
|
108
114
|
output
|
|
109
115
|
end
|
|
110
116
|
|
|
117
|
+
def encloses_decision?(outer, inner)
|
|
118
|
+
return false if outer.equal?(inner) || outer == inner
|
|
119
|
+
return false unless contains?(outer[:byte_start], outer[:byte_length], inner[:byte_start], inner[:byte_length])
|
|
120
|
+
return true unless outer[:byte_start] == inner[:byte_start] && outer[:byte_length] == inner[:byte_length]
|
|
121
|
+
|
|
122
|
+
(outer[:kind] || "boolean") == "boolean" && inner[:kind] != "boolean" && !inner[:kind].nil?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def render_decision(bytes, decision, nested, encloses)
|
|
126
|
+
if decision[:instrumentation]
|
|
127
|
+
render_flow(bytes, decision, nested, encloses)
|
|
128
|
+
else
|
|
129
|
+
expression = render_range(bytes, decision, nested, encloses)
|
|
130
|
+
frame(decision[:id], expression)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
111
134
|
def condition_wrapper(decision_id, index, expression)
|
|
112
135
|
"#{RUNTIME}.condition(#{decision_id.inspect}, #{index}, (#{expression}))"
|
|
113
136
|
end
|
|
@@ -118,10 +141,13 @@ module Branchproof
|
|
|
118
141
|
"#{RUNTIME}.leave(#{decision_id.inspect}); end; end)"
|
|
119
142
|
end
|
|
120
143
|
|
|
144
|
+
# Mutates `bytes` in place: it is already a fresh copy made at the top of
|
|
145
|
+
# rewrite, so there is no need to copy it again before splicing edits in.
|
|
121
146
|
def apply_edits(bytes, edits)
|
|
122
|
-
edits.sort_by { |edit| -edit[:start] }.
|
|
123
|
-
|
|
147
|
+
edits.sort_by { |edit| -edit[:start] }.each do |edit|
|
|
148
|
+
bytes[edit[:start]...edit[:finish]] = edit[:text]
|
|
124
149
|
end
|
|
150
|
+
bytes
|
|
125
151
|
end
|
|
126
152
|
|
|
127
153
|
def valid_range?(bytes, start, length)
|
|
@@ -138,8 +164,8 @@ module Branchproof
|
|
|
138
164
|
start.is_a?(Integer) && length.is_a?(Integer) && start >= 0 && length >= 0
|
|
139
165
|
end
|
|
140
166
|
|
|
141
|
-
def result(bytes, changed: false, diagnostics: [])
|
|
142
|
-
{ bytes: bytes, changed: changed, diagnostics: diagnostics }.freeze
|
|
167
|
+
def result(bytes, changed: false, diagnostics: [], iseq: nil)
|
|
168
|
+
{ bytes: bytes, changed: changed, diagnostics: diagnostics, iseq: iseq }.freeze
|
|
143
169
|
end
|
|
144
170
|
|
|
145
171
|
def diagnostic(code, message)
|
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)
|
|
@@ -61,6 +59,8 @@ module Branchproof
|
|
|
61
59
|
return nil
|
|
62
60
|
end
|
|
63
61
|
|
|
62
|
+
return rewritten[:iseq] if reusable_iseq?(unit, rewritten)
|
|
63
|
+
|
|
64
64
|
RubyVM::InstructionSequence.compile(
|
|
65
65
|
rewritten[:bytes], unit[:real_path] || canonical(path), unit[:real_path] || canonical(path), 1,
|
|
66
66
|
compile_options(unit)
|
|
@@ -108,13 +108,14 @@ module Branchproof
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def preloaded_target?
|
|
111
|
-
|
|
111
|
+
loaded = $LOADED_FEATURES.each_with_object(Set.new) { |feature, set| set << canonical(feature) }
|
|
112
|
+
@units.keys.any? { |path| loaded.include?(canonical(path)) }
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
def index_units(inventory)
|
|
115
116
|
Array(inventory[:source_units]).each_with_object({}) do |unit, index|
|
|
116
|
-
|
|
117
|
-
index[
|
|
117
|
+
index[unit[:absolute_path]] = unit if unit[:absolute_path]
|
|
118
|
+
index[unit[:real_path]] = unit if unit[:real_path]
|
|
118
119
|
end
|
|
119
120
|
end
|
|
120
121
|
|
|
@@ -136,6 +137,17 @@ module Branchproof
|
|
|
136
137
|
unit[:compile_options] || {}
|
|
137
138
|
end
|
|
138
139
|
|
|
140
|
+
# The instrumenter already compiled the rewritten source once to validate it.
|
|
141
|
+
# Reuse that instruction sequence instead of compiling again, but only when
|
|
142
|
+
# it was built with the exact file/path/options this loader would use:
|
|
143
|
+
# real_path present and equal to absolute_path (so the instrumenter's file
|
|
144
|
+
# and path arguments match what we pass below), and no custom compile
|
|
145
|
+
# options (the instrumenter compiles without any).
|
|
146
|
+
def reusable_iseq?(unit, rewritten)
|
|
147
|
+
rewritten[:iseq] && compile_options(unit).empty? &&
|
|
148
|
+
unit[:real_path] && unit[:absolute_path] == unit[:real_path]
|
|
149
|
+
end
|
|
150
|
+
|
|
139
151
|
def status(state, reason)
|
|
140
152
|
{ status: state.to_s, reason: reason&.to_s }.freeze
|
|
141
153
|
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)
|