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
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
# rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
6
6
|
|
|
7
7
|
require "json"
|
|
8
|
+
# rubocop:disable-next Lint/RedundantRequireStatement -- supports standalone core entry
|
|
9
|
+
require "set"
|
|
10
|
+
require_relative "decision_table"
|
|
8
11
|
|
|
9
12
|
module Branchproof
|
|
10
13
|
# Reads and validates a persisted JSON report without loading the project.
|
|
11
14
|
class SavedReport
|
|
12
|
-
SUPPORTED_SCHEMAS = %w[1.0 1.1 1.2].freeze
|
|
15
|
+
SUPPORTED_SCHEMAS = %w[1.0 1.1 1.2 1.3].freeze
|
|
16
|
+
STRICT_FLOW_SCHEMAS = %w[1.2 1.3].freeze
|
|
17
|
+
DECISION_TABLE_STATUSES = %w[calculated not_calculated].freeze
|
|
18
|
+
RULE_CONDITION_VALUES = DecisionTable::CONDITION_VALUES
|
|
19
|
+
RULE_COVERAGE_STATUSES = DecisionTable::COVERAGE_STATUSES
|
|
20
|
+
RULE_REACHABILITY_STATUSES = DecisionTable::REACHABILITY_STATUSES
|
|
13
21
|
DECISION_KINDS = %w[boolean implicit multiway pattern exception].freeze
|
|
14
22
|
NONBOOLEAN_KINDS = (DECISION_KINDS - ["boolean"]).freeze
|
|
15
23
|
CRITERION_VERSION = "masking_occurrence_v1"
|
|
@@ -107,7 +115,7 @@ module Branchproof
|
|
|
107
115
|
end
|
|
108
116
|
fail_with("duplicate condition id") unless @condition_ids.uniq.length == @condition_ids.length
|
|
109
117
|
@conditions_by_decision = decisions.to_h do |decision|
|
|
110
|
-
[decision["id"], Array(decision["conditions"]).
|
|
118
|
+
[decision["id"], Array(decision["conditions"]).to_set { |condition| condition["id"] }]
|
|
111
119
|
end
|
|
112
120
|
sources.each do |source|
|
|
113
121
|
validate_string_field(source, "relative_path", nullable: true)
|
|
@@ -142,11 +150,12 @@ module Branchproof
|
|
|
142
150
|
fail_with("tests must be an array") unless tests.is_a?(Array)
|
|
143
151
|
fail_with("vectors must be an array") unless vectors.is_a?(Array)
|
|
144
152
|
@test_ids = unique_ids(tests, "id", "test")
|
|
153
|
+
@test_id_set = @test_ids.to_set
|
|
145
154
|
tests.each { |test| validate_test(test) }
|
|
146
155
|
vector_ids = unique_ids(vectors, "id", "vector")
|
|
147
156
|
vectors.each do |vector|
|
|
148
157
|
fail_with("invalid vector") unless hash_with_string_keys?(vector)
|
|
149
|
-
fail_with("unknown vector decision") unless @
|
|
158
|
+
fail_with("unknown vector decision") unless @decisions_by_id.key?(vector["decision_id"])
|
|
150
159
|
decision = @decisions_by_id.fetch(vector["decision_id"])
|
|
151
160
|
values = vector["values"]
|
|
152
161
|
valid_values = values.is_a?(Array) && values.all? do |value|
|
|
@@ -162,7 +171,7 @@ module Branchproof
|
|
|
162
171
|
fail_with("vector outcome must be boolean") unless [true, false].include?(vector["outcome"])
|
|
163
172
|
validate_flow_vector(vector, decision) if strict_flow_decision?(decision)
|
|
164
173
|
fail_with("unknown vector test") unless strings?(vector["test_ids"]) && vector["test_ids"].all? do |id|
|
|
165
|
-
@
|
|
174
|
+
@test_id_set.include?(id)
|
|
166
175
|
end
|
|
167
176
|
validate_phases(vector)
|
|
168
177
|
validate_integer_field(vector, "count")
|
|
@@ -171,6 +180,7 @@ module Branchproof
|
|
|
171
180
|
@vector_ids = vector_ids
|
|
172
181
|
@vector_decision_by_id = vectors.to_h { |vector| [vector["id"], vector["decision_id"]] }
|
|
173
182
|
@vectors_by_id = vectors.to_h { |vector| [vector["id"], vector] }
|
|
183
|
+
@vectors_by_decision = vectors.group_by { |vector| vector["decision_id"] }
|
|
174
184
|
aborts = observations["abort_counts"]
|
|
175
185
|
return if aborts.nil? || aborts.is_a?(Integer) || (aborts.is_a?(Hash) && aborts.values.all?(Integer))
|
|
176
186
|
|
|
@@ -199,7 +209,7 @@ module Branchproof
|
|
|
199
209
|
decisions.each do |decision|
|
|
200
210
|
fail_with("invalid analysis decision") unless hash_with_string_keys?(decision)
|
|
201
211
|
id = decision["decision_id"]
|
|
202
|
-
fail_with("unknown analysis decision") unless @
|
|
212
|
+
fail_with("unknown analysis decision") unless @decisions_by_id.key?(id) && !seen.key?(id)
|
|
203
213
|
seen[id] = true
|
|
204
214
|
results = decision["condition_results"]
|
|
205
215
|
fail_with("condition results must be an array") unless results.is_a?(Array)
|
|
@@ -207,6 +217,9 @@ module Branchproof
|
|
|
207
217
|
if nonboolean_kind?(decision_kind(inventory_decision))
|
|
208
218
|
fail_with("nonboolean condition results must be empty") unless results.empty?
|
|
209
219
|
validate_nonboolean_analysis(decision, inventory_decision)
|
|
220
|
+
elsif decision.key?("decision_table")
|
|
221
|
+
fail_with("decision tables are unsupported in legacy report schemas") unless @schema_version == "1.3"
|
|
222
|
+
validate_decision_table(decision["decision_table"], inventory_decision)
|
|
210
223
|
end
|
|
211
224
|
seen_conditions = {}
|
|
212
225
|
results.each do |result|
|
|
@@ -221,13 +234,183 @@ module Branchproof
|
|
|
221
234
|
|
|
222
235
|
pair = result["canonical_pair"]
|
|
223
236
|
valid_pair = pair.is_a?(Array) && pair.length == 2 && pair.uniq.length == 2 && pair.all? do |id|
|
|
224
|
-
@
|
|
237
|
+
@vectors_by_id.key?(id) && @vector_decision_by_id[id] == decision["decision_id"]
|
|
225
238
|
end
|
|
226
239
|
fail_with("canonical pair must reference vectors") unless valid_pair
|
|
227
240
|
end
|
|
228
241
|
end
|
|
229
242
|
end
|
|
230
243
|
|
|
244
|
+
# The persisted decision table must describe the same decision it was
|
|
245
|
+
# derived from, so a saved report can be reported on and compared offline.
|
|
246
|
+
def validate_decision_table(table, decision)
|
|
247
|
+
fail_with("decision_table must be an object") unless hash_with_string_keys?(table)
|
|
248
|
+
fail_with("decision_table status is invalid") unless DECISION_TABLE_STATUSES.include?(table["status"])
|
|
249
|
+
fail_with("decision_table decision id mismatch") unless table["decision_id"] == decision["id"]
|
|
250
|
+
%w[schema_version constraint_analysis_version].each do |field|
|
|
251
|
+
fail_with("decision_table #{field} must be an integer") unless table[field].is_a?(Integer)
|
|
252
|
+
end
|
|
253
|
+
unless table["schema_version"] == DecisionTable::SCHEMA_VERSION
|
|
254
|
+
fail_with("unsupported decision_table schema version")
|
|
255
|
+
end
|
|
256
|
+
solver_version = table["constraint_analysis_version"]
|
|
257
|
+
supported_solver = [1, DecisionTable::CONSTRAINT_ANALYSIS_VERSION].include?(solver_version) &&
|
|
258
|
+
solver_version <= DecisionTable::CONSTRAINT_ANALYSIS_VERSION
|
|
259
|
+
fail_with("unsupported decision_table constraint analysis version") unless supported_solver
|
|
260
|
+
validate_string_field(table, "reason", nullable: true)
|
|
261
|
+
unless DecisionTable::COVERAGE_SUMMARY_STATUSES.include?(table["coverage_status"])
|
|
262
|
+
fail_with("invalid decision_table coverage status")
|
|
263
|
+
end
|
|
264
|
+
%w[generated_rules impossible_rules required_rules covered_rules missing_rules].each do |field|
|
|
265
|
+
unless table[field].is_a?(Integer) && table[field] >= 0
|
|
266
|
+
fail_with("decision_table #{field} must be a nonnegative integer")
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
rules = table["rules"]
|
|
270
|
+
fail_with("decision_table rules must be an array") unless rules.is_a?(Array)
|
|
271
|
+
unique_ids(rules, "id", "decision table rule")
|
|
272
|
+
indexes = rules.map { |rule| rule["index"] }
|
|
273
|
+
fail_with("decision table rule indexes must be consecutive") unless indexes.sort == (0...rules.length).to_a
|
|
274
|
+
condition_count = decision["conditions"].length
|
|
275
|
+
rules.each { |rule| validate_decision_table_rule(rule, decision, condition_count) }
|
|
276
|
+
validate_decision_table_counts(table, rules)
|
|
277
|
+
validate_decision_table_summary(table, rules)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def validate_decision_table_rule(rule, decision, condition_count)
|
|
281
|
+
fail_with("invalid decision table rule") unless hash_with_string_keys?(rule)
|
|
282
|
+
validate_string_field(rule, "label")
|
|
283
|
+
validate_integer_field(rule, "index")
|
|
284
|
+
unless rule["index"].is_a?(Integer) && rule["index"] >= 0
|
|
285
|
+
fail_with("decision table rule index must be nonnegative")
|
|
286
|
+
end
|
|
287
|
+
fail_with("decision table rule label does not match its index") unless rule["label"] == "R#{rule["index"] + 1}"
|
|
288
|
+
conditions = rule["conditions"]
|
|
289
|
+
valid = conditions.is_a?(Array) && conditions.length == condition_count &&
|
|
290
|
+
conditions.all? { |value| RULE_CONDITION_VALUES.include?(value) }
|
|
291
|
+
fail_with("decision table rule conditions do not match the decision") unless valid
|
|
292
|
+
fail_with("decision table rule outcome must be boolean") unless [true, false].include?(rule["outcome"])
|
|
293
|
+
expected_id = DecisionTable.rule_id(decision["id"], conditions, rule["outcome"])
|
|
294
|
+
unless rule["id"] == expected_id
|
|
295
|
+
fail_with("decision table rule id does not match its decision, conditions, and outcome")
|
|
296
|
+
end
|
|
297
|
+
fail_with("invalid decision table rule coverage") unless RULE_COVERAGE_STATUSES.include?(rule["coverage"])
|
|
298
|
+
unless RULE_REACHABILITY_STATUSES.include?(rule["reachability"])
|
|
299
|
+
fail_with("invalid decision table rule reachability")
|
|
300
|
+
end
|
|
301
|
+
validate_string_field(rule, "reachability_reason", nullable: true)
|
|
302
|
+
if rule["reachability"] == "statically_impossible" && !rule["reachability_reason"].is_a?(String)
|
|
303
|
+
fail_with("statically impossible rule requires a reason code")
|
|
304
|
+
end
|
|
305
|
+
if %w[observed unknown].include?(rule["reachability"]) && !rule["reachability_reason"].nil?
|
|
306
|
+
fail_with("observed or unknown rule must not have a reachability reason")
|
|
307
|
+
end
|
|
308
|
+
fail_with("decision table rule tests must be strings") unless strings?(rule["tests"])
|
|
309
|
+
fail_with("unknown decision table rule test") unless rule["tests"].all? { |id| @test_id_set.include?(id) }
|
|
310
|
+
fail_with("decision table rule vector_ids must be strings") unless strings?(rule["vector_ids"])
|
|
311
|
+
unless rule["vector_ids"].uniq.length == rule["vector_ids"].length
|
|
312
|
+
fail_with("duplicate decision table rule vector")
|
|
313
|
+
end
|
|
314
|
+
fail_with("unknown decision table rule vector") unless rule["vector_ids"].all? do |id|
|
|
315
|
+
@vector_decision_by_id[id] == decision["id"]
|
|
316
|
+
end
|
|
317
|
+
validate_decision_table_evidence(rule, decision)
|
|
318
|
+
validate_integer_field(rule, "unattributed_count")
|
|
319
|
+
unless rule["unattributed_count"].is_a?(Integer) && rule["unattributed_count"] >= 0
|
|
320
|
+
fail_with("decision table rule unattributed count must be nonnegative")
|
|
321
|
+
end
|
|
322
|
+
return unless rule["coverage"] == "covered" && rule["vector_ids"].empty?
|
|
323
|
+
|
|
324
|
+
fail_with("covered decision table rule requires evidence")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def validate_decision_table_counts(table, rules)
|
|
328
|
+
if table["status"] == "not_calculated"
|
|
329
|
+
fail_with("not_calculated decision table must not contain rules") unless rules.empty?
|
|
330
|
+
%w[generated_rules impossible_rules required_rules covered_rules missing_rules].each do |field|
|
|
331
|
+
fail_with("not_calculated decision table #{field} must be zero") unless table[field].zero?
|
|
332
|
+
end
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
fail_with("decision_table generated_rules mismatch") unless table["generated_rules"] == rules.length
|
|
337
|
+
impossible = rules.count { |rule| rule["coverage"] == "excluded" }
|
|
338
|
+
covered = rules.count { |rule| rule["coverage"] == "covered" }
|
|
339
|
+
fail_with("decision_table impossible_rules mismatch") unless table["impossible_rules"] == impossible
|
|
340
|
+
fail_with("decision_table required_rules mismatch") unless table["required_rules"] == rules.length - impossible
|
|
341
|
+
fail_with("decision_table covered_rules mismatch") unless table["covered_rules"] == covered
|
|
342
|
+
return if table["missing_rules"] == table["required_rules"] - table["covered_rules"]
|
|
343
|
+
|
|
344
|
+
fail_with("decision_table missing_rules mismatch")
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def validate_decision_table_summary(table, rules)
|
|
348
|
+
if table["status"] == "not_calculated"
|
|
349
|
+
allowed_reason = DecisionTable::NOT_CALCULATED_REASONS.include?(table["reason"])
|
|
350
|
+
fail_with("not_calculated decision table requires a supported reason") unless allowed_reason
|
|
351
|
+
expected_status = table["reason"] == "unsupported_decision" ? "unsupported" : "not_calculated"
|
|
352
|
+
unless table["coverage_status"] == expected_status
|
|
353
|
+
fail_with("not_calculated decision table coverage status mismatch")
|
|
354
|
+
end
|
|
355
|
+
fail_with("not_calculated decision table percentage must be null") unless table["percentage"].nil?
|
|
356
|
+
unless table["reachability_analyzed"] == false
|
|
357
|
+
fail_with("not_calculated decision table reachability must be false")
|
|
358
|
+
end
|
|
359
|
+
return
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
fail_with("calculated decision table reason must be null") unless table["reason"].nil?
|
|
363
|
+
reachability_analyzed = table["reachability_analyzed"]
|
|
364
|
+
unless [true, false].include?(reachability_analyzed)
|
|
365
|
+
fail_with("decision_table reachability_analyzed must be boolean")
|
|
366
|
+
end
|
|
367
|
+
required = table["required_rules"]
|
|
368
|
+
covered = table["covered_rules"]
|
|
369
|
+
generated = table["generated_rules"]
|
|
370
|
+
expected_status = DecisionTable.coverage_status(covered, required, generated)
|
|
371
|
+
fail_with("decision_table coverage status mismatch") unless table["coverage_status"] == expected_status
|
|
372
|
+
expected_percentage = DecisionTable.percentage(covered, required)
|
|
373
|
+
fail_with("decision_table percentage mismatch") unless table["percentage"] == expected_percentage
|
|
374
|
+
return unless table["reachability_analyzed"] == false
|
|
375
|
+
|
|
376
|
+
fail_with("un-analyzed reachability cannot exclude rules") if rules.any? do |rule|
|
|
377
|
+
rule["coverage"] == "excluded"
|
|
378
|
+
end
|
|
379
|
+
fail_with("un-analyzed reachability contains impossible rule") if rules.any? do |rule|
|
|
380
|
+
rule["reachability"] == "statically_impossible"
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def validate_decision_table_evidence(rule, _decision)
|
|
385
|
+
rule["vector_ids"].each do |vector_id|
|
|
386
|
+
vector = @vectors_by_id.fetch(vector_id)
|
|
387
|
+
matches = vector["outcome"] == rule["outcome"] && rule["conditions"].each_with_index.all? do |required, index|
|
|
388
|
+
required == DecisionTable::DONT_CARE || vector["values"][index] == (required == DecisionTable::TRUE_VALUE)
|
|
389
|
+
end
|
|
390
|
+
fail_with("decision table rule evidence does not match its conditions and outcome") unless matches
|
|
391
|
+
end
|
|
392
|
+
expected_tests = rule["vector_ids"].flat_map { |id| @vectors_by_id.fetch(id)["test_ids"] }.uniq.sort
|
|
393
|
+
fail_with("decision table rule tests do not match its evidence") unless rule["tests"].sort == expected_tests
|
|
394
|
+
expected_unattributed = rule["vector_ids"].sum { |id| @vectors_by_id.fetch(id).fetch("unattributed_count", 0) }
|
|
395
|
+
unless rule["unattributed_count"] == expected_unattributed
|
|
396
|
+
fail_with("decision table rule unattributed count does not match its evidence")
|
|
397
|
+
end
|
|
398
|
+
if rule["coverage"] == "covered" && rule["reachability"] != "observed"
|
|
399
|
+
fail_with("covered decision table rule requires observed reachability")
|
|
400
|
+
end
|
|
401
|
+
if rule["coverage"] == "excluded" && rule["reachability"] != "statically_impossible"
|
|
402
|
+
fail_with("excluded decision table rule requires static impossibility")
|
|
403
|
+
end
|
|
404
|
+
if rule["coverage"] == "missing" && rule["reachability"] != "unknown"
|
|
405
|
+
fail_with("missing decision table rule requires unknown reachability")
|
|
406
|
+
end
|
|
407
|
+
return unless %w[missing excluded].include?(rule["coverage"])
|
|
408
|
+
|
|
409
|
+
fail_with("noncovered decision table rule must not contain evidence") unless rule["vector_ids"].empty? &&
|
|
410
|
+
rule["tests"].empty? &&
|
|
411
|
+
rule["unattributed_count"].zero?
|
|
412
|
+
end
|
|
413
|
+
|
|
231
414
|
def decision_kind(decision)
|
|
232
415
|
kind = decision && decision["kind"]
|
|
233
416
|
kind.nil? || kind.empty? ? "boolean" : kind
|
|
@@ -359,12 +542,16 @@ module Branchproof
|
|
|
359
542
|
unless evidence["vector_ids"].uniq.length == evidence["vector_ids"].length
|
|
360
543
|
fail_with("duplicate alternative evidence vector")
|
|
361
544
|
end
|
|
362
|
-
fail_with("unknown alternative evidence vector") unless
|
|
545
|
+
fail_with("unknown alternative evidence vector") unless evidence["vector_ids"].all? do |id|
|
|
546
|
+
@vectors_by_id.key?(id)
|
|
547
|
+
end
|
|
363
548
|
fail_with("alternative evidence test_ids must be strings") unless strings?(evidence["test_ids"])
|
|
364
549
|
unless evidence["test_ids"].uniq.length == evidence["test_ids"].length
|
|
365
550
|
fail_with("duplicate alternative evidence test")
|
|
366
551
|
end
|
|
367
|
-
fail_with("unknown alternative evidence test") unless
|
|
552
|
+
fail_with("unknown alternative evidence test") unless evidence["test_ids"].all? do |id|
|
|
553
|
+
@test_id_set.include?(id)
|
|
554
|
+
end
|
|
368
555
|
validate_integer_field(evidence, "unattributed_count")
|
|
369
556
|
return unless decision
|
|
370
557
|
|
|
@@ -381,9 +568,8 @@ module Branchproof
|
|
|
381
568
|
end
|
|
382
569
|
end
|
|
383
570
|
fail_with("alternative evidence state mismatch") unless vectors.all?(&expected_value)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
end
|
|
571
|
+
decision_vectors = @vectors_by_decision.fetch(decision["id"], [])
|
|
572
|
+
expected_vectors = decision_vectors.select(&expected_value)
|
|
387
573
|
expected_vector_ids = expected_vectors.map { |vector| vector["id"] }.sort
|
|
388
574
|
fail_with("alternative evidence is incomplete") unless evidence["vector_ids"].sort == expected_vector_ids
|
|
389
575
|
expected_tests = vectors.flat_map { |vector| vector["test_ids"] }.uniq.sort
|
|
@@ -412,7 +598,7 @@ module Branchproof
|
|
|
412
598
|
end
|
|
413
599
|
|
|
414
600
|
def strict_flow_decision?(decision)
|
|
415
|
-
@schema_version
|
|
601
|
+
STRICT_FLOW_SCHEMAS.include?(@schema_version) &&
|
|
416
602
|
nonboolean_kind?(decision_kind(decision)) &&
|
|
417
603
|
decision["support_status"].to_s.upcase != "UNSUPPORTED"
|
|
418
604
|
end
|
|
@@ -446,7 +632,7 @@ module Branchproof
|
|
|
446
632
|
|
|
447
633
|
valid = hash_with_string_keys?(phases) && phases.all? do |test_id, values|
|
|
448
634
|
valid_phases = %w[setup body teardown suite unattributed]
|
|
449
|
-
@
|
|
635
|
+
@test_id_set.include?(test_id) && strings?(values) && values.all? { |phase| valid_phases.include?(phase) }
|
|
450
636
|
end
|
|
451
637
|
fail_with("invalid phases_by_test") unless valid
|
|
452
638
|
end
|
|
@@ -556,7 +742,7 @@ module Branchproof
|
|
|
556
742
|
return unless locations
|
|
557
743
|
|
|
558
744
|
valid = hash_with_string_keys?(locations) && locations.all? do |test_id, location|
|
|
559
|
-
@
|
|
745
|
+
@test_id_set.include?(test_id) && hash_with_string_keys?(location) &&
|
|
560
746
|
(!location.key?("relative_path") || location["relative_path"].nil? ||
|
|
561
747
|
location["relative_path"].is_a?(String)) &&
|
|
562
748
|
(!location.key?("line") || location["line"].nil? || location["line"].is_a?(Integer))
|