bparity 0.1.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 +7 -0
- data/.rubocop.yml +61 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +36 -0
- data/docs/application_example.md +25 -0
- data/docs/formal_assurance_limits.md +21 -0
- data/exe/bparity +7 -0
- data/fixtures/scenarios/01_pure_function/adapter.rb +13 -0
- data/fixtures/scenarios/01_pure_function/boundary.rb +17 -0
- data/fixtures/scenarios/01_pure_function/legacy/dead_gem.rb +9 -0
- data/fixtures/scenarios/01_pure_function/legacy/slugifier.rb +17 -0
- data/fixtures/scenarios/01_pure_function/replacement/broken.rb +15 -0
- data/fixtures/scenarios/01_pure_function/replacement/good.rb +19 -0
- data/fixtures/scenarios/01_pure_function/spec/slugifier_spec.rb +20 -0
- data/fixtures/scenarios/01_pure_function/test/slugifier_test.rb +10 -0
- data/fixtures/scenarios/02_stateful_client/adapter.rb +19 -0
- data/fixtures/scenarios/02_stateful_client/boundary.rb +10 -0
- data/fixtures/scenarios/02_stateful_client/legacy/client.rb +29 -0
- data/fixtures/scenarios/02_stateful_client/replacement/broken.rb +14 -0
- data/fixtures/scenarios/02_stateful_client/replacement/good.rb +23 -0
- data/fixtures/scenarios/02_stateful_client/spec/client_spec.rb +31 -0
- data/fixtures/scenarios/03_external_boundary/adapter.rb +13 -0
- data/fixtures/scenarios/03_external_boundary/boundary.rb +11 -0
- data/fixtures/scenarios/03_external_boundary/legacy/dead_formatter.rb +7 -0
- data/fixtures/scenarios/03_external_boundary/legacy/receipt.rb +11 -0
- data/fixtures/scenarios/03_external_boundary/replacement/broken.rb +11 -0
- data/fixtures/scenarios/03_external_boundary/replacement/good.rb +11 -0
- data/fixtures/scenarios/03_external_boundary/spec/receipt_spec.rb +10 -0
- data/fixtures/scenarios/04_intentional_divergence/adapter.rb +12 -0
- data/fixtures/scenarios/04_intentional_divergence/adapter_unwaived.rb +10 -0
- data/fixtures/scenarios/04_intentional_divergence/boundary.rb +8 -0
- data/fixtures/scenarios/04_intentional_divergence/legacy/dead_identity.rb +7 -0
- data/fixtures/scenarios/04_intentional_divergence/legacy/identity.rb +9 -0
- data/fixtures/scenarios/04_intentional_divergence/replacement/broken.rb +9 -0
- data/fixtures/scenarios/04_intentional_divergence/replacement/good.rb +9 -0
- data/fixtures/scenarios/04_intentional_divergence/spec/identity_spec.rb +10 -0
- data/fixtures/scenarios/05_formal_negative/adapter.rb +15 -0
- data/fixtures/scenarios/05_formal_negative/boundary.rb +16 -0
- data/fixtures/scenarios/05_formal_negative/legacy/dead_lock.rb +5 -0
- data/fixtures/scenarios/05_formal_negative/legacy/turnstile.rb +24 -0
- data/fixtures/scenarios/05_formal_negative/replacement/broken.rb +18 -0
- data/fixtures/scenarios/05_formal_negative/replacement/formal_broken.rb +24 -0
- data/fixtures/scenarios/05_formal_negative/replacement/good.rb +24 -0
- data/fixtures/scenarios/05_formal_negative/spec/turnstile_spec.rb +21 -0
- data/lib/bparity/adapter.rb +115 -0
- data/lib/bparity/adequacy.rb +78 -0
- data/lib/bparity/boundary.rb +97 -0
- data/lib/bparity/cli/formal_commands.rb +428 -0
- data/lib/bparity/cli/verification_commands.rb +242 -0
- data/lib/bparity/cli.rb +262 -0
- data/lib/bparity/corpus.rb +45 -0
- data/lib/bparity/errors.rb +12 -0
- data/lib/bparity/formal/assumptions.rb +131 -0
- data/lib/bparity/formal/bounded.rb +543 -0
- data/lib/bparity/formal/contract.rb +81 -0
- data/lib/bparity/formal/deductive.rb +481 -0
- data/lib/bparity/formal/lts.rb +344 -0
- data/lib/bparity/formal/result.rb +50 -0
- data/lib/bparity/formal.rb +8 -0
- data/lib/bparity/recording.rb +412 -0
- data/lib/bparity/reporting.rb +128 -0
- data/lib/bparity/spec_bundle.rb +208 -0
- data/lib/bparity/synthesis.rb +487 -0
- data/lib/bparity/verification.rb +310 -0
- data/lib/bparity/version.rb +5 -0
- data/lib/bparity.rb +42 -0
- metadata +125 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bparity
|
|
6
|
+
module Formal
|
|
7
|
+
module LtsEncoding
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def output(outcome)
|
|
11
|
+
if outcome.is_a?(Hash) && outcome["kind"] == "raise"
|
|
12
|
+
return "!#{outcome['class']}:#{JSON.generate(normalize(outcome.except('kind', 'class')))}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
JSON.generate(normalize(outcome))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def normalize(value)
|
|
19
|
+
case value
|
|
20
|
+
when Hash
|
|
21
|
+
value.compact.sort_by { |key, _item| key.to_s }
|
|
22
|
+
.to_h { |key, item| [key.to_s, normalize(item)] }
|
|
23
|
+
when Array then value.map { |item| normalize(item) }
|
|
24
|
+
else value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Transition = Struct.new(:from, :input, :output, :to, keyword_init: true) do
|
|
30
|
+
def to_h = { "from" => from, "input" => input, "output" => output, "to" => to }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class LTS
|
|
34
|
+
attr_reader :initial, :transitions
|
|
35
|
+
|
|
36
|
+
def initialize(initial:, transitions: [])
|
|
37
|
+
@initial = initial
|
|
38
|
+
@transitions = transitions.map do |transition|
|
|
39
|
+
transition.is_a?(Transition) ? transition : Transition.new(**transition.transform_keys(&:to_sym))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def states = ([initial] + transitions.flat_map { |transition| [transition.from, transition.to] }).uniq.sort
|
|
44
|
+
def alphabet = transitions.map(&:input).uniq.sort
|
|
45
|
+
def outgoing(state) = transitions.select { |transition| transition.from == state }
|
|
46
|
+
def step(state, input) = outgoing(state).find { |transition| transition.input == input }
|
|
47
|
+
|
|
48
|
+
def deterministic?
|
|
49
|
+
transitions.group_by { |transition| [transition.from, transition.input] }.values.none? do |items|
|
|
50
|
+
items.map { |transition| [transition.output, transition.to] }.uniq.length > 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{ "initial" => initial, "states" => states, "alphabet" => alphabet,
|
|
56
|
+
"transitions" => transitions.map(&:to_h) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.from_h(value)
|
|
60
|
+
new(initial: value.fetch("initial"), transitions: value.fetch("transitions", []))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class PassiveLearner
|
|
65
|
+
def learn(records)
|
|
66
|
+
usable = records.select { |record| record.key?("pre_state") && record.key?("post_state") }
|
|
67
|
+
if usable.empty?
|
|
68
|
+
raise ConfigurationError,
|
|
69
|
+
"The corpus has no state projections. Add a state block to the boundary."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
labels = usable.flat_map { |record| [state_label(record["pre_state"]), state_label(record["post_state"])] }.uniq
|
|
73
|
+
names = labels.sort.each_with_index.to_h { |label, index| [label, "s#{index}"] }
|
|
74
|
+
transitions = usable.map do |record|
|
|
75
|
+
Transition.new(from: names.fetch(state_label(record["pre_state"])), input: record.fetch("operation"),
|
|
76
|
+
output: output_label(record.fetch("outcome")),
|
|
77
|
+
to: names.fetch(state_label(record["post_state"])))
|
|
78
|
+
end.uniq(&:to_h)
|
|
79
|
+
LTS.new(initial: names.fetch(state_label(usable.first["pre_state"])), transitions:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def state_label(state) = JSON.generate(state)
|
|
85
|
+
def output_label(outcome) = LtsEncoding.output(outcome)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
LearnedModel = Struct.new(:lts, :complete, :query_count, :algorithm, keyword_init: true)
|
|
89
|
+
ObservedOutput = Struct.new(:value)
|
|
90
|
+
|
|
91
|
+
class ActiveLearner
|
|
92
|
+
def initialize(factory:, state_projection:, operations:, state_limit: 500)
|
|
93
|
+
@factory = factory
|
|
94
|
+
@state_projection = state_projection
|
|
95
|
+
@operations = operations
|
|
96
|
+
@state_limit = state_limit
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def learn
|
|
100
|
+
initial_subject = @factory.call
|
|
101
|
+
initial_label = state_label(initial_subject)
|
|
102
|
+
paths = { initial_label => [] }
|
|
103
|
+
queue = [initial_label]
|
|
104
|
+
transitions = []
|
|
105
|
+
queries = 0
|
|
106
|
+
until queue.empty?
|
|
107
|
+
from = queue.shift
|
|
108
|
+
@operations.each do |name, operation|
|
|
109
|
+
subject = replay(paths.fetch(from))
|
|
110
|
+
output = observe(subject, operation)
|
|
111
|
+
queries += 1
|
|
112
|
+
target = state_label(subject)
|
|
113
|
+
transitions << Transition.new(from:, input: name, output:, to: target)
|
|
114
|
+
next if paths.key?(target)
|
|
115
|
+
return learned(initial_label, transitions, false, queries) if paths.length >= @state_limit
|
|
116
|
+
|
|
117
|
+
paths[target] = paths.fetch(from) + [name]
|
|
118
|
+
queue << target
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
learned(initial_label, transitions, true, queries)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def replay(path)
|
|
127
|
+
subject = @factory.call
|
|
128
|
+
path.each { |name| observe(subject, @operations.fetch(name)) }
|
|
129
|
+
subject
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def observe(subject, operation)
|
|
133
|
+
value = operation.call(subject)
|
|
134
|
+
return LtsEncoding.output(value.value) if value.is_a?(ObservedOutput)
|
|
135
|
+
|
|
136
|
+
LtsEncoding.output("kind" => "return", "value" => Recording::Serializer.dump(value))
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
LtsEncoding.output("kind" => "raise", "class" => e.class.name,
|
|
139
|
+
"message" => Bparity.exception_message(e))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def state_label(subject) = JSON.generate(Recording::Serializer.dump(@state_projection.call(subject)))
|
|
143
|
+
|
|
144
|
+
def learned(initial, transitions, complete, queries)
|
|
145
|
+
labels = ([initial] + transitions.flat_map { |transition| [transition.from, transition.to] }).uniq.sort
|
|
146
|
+
names = labels.each_with_index.to_h { |label, index| [label, "s#{index}"] }
|
|
147
|
+
normalized = transitions.map do |transition|
|
|
148
|
+
Transition.new(from: names.fetch(transition.from), input: transition.input,
|
|
149
|
+
output: transition.output, to: names.fetch(transition.to))
|
|
150
|
+
end
|
|
151
|
+
LearnedModel.new(lts: LTS.new(initial: names.fetch(initial), transitions: normalized), complete:,
|
|
152
|
+
query_count: queries, algorithm: "active state exploration")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class LtsEquivalence
|
|
157
|
+
def compare(old_lts, new_lts, relation: :trace, assumptions: %i[h1 h6 h7], exact: true)
|
|
158
|
+
unless %i[trace bisim].include?(relation.to_sym)
|
|
159
|
+
raise ConfigurationError, "Unsupported LTS relation #{relation}. Use trace or bisim."
|
|
160
|
+
end
|
|
161
|
+
unless old_lts.deterministic? && new_lts.deterministic?
|
|
162
|
+
return Result.new(level: :f3, verdict: :inconclusive,
|
|
163
|
+
scope: Scope.new(size: [old_lts.states.length, new_lts.states.length].max,
|
|
164
|
+
depth: nil, cases: 0, exhaustive: false),
|
|
165
|
+
assumptions:, out_of_scope: ["nondeterministic model equivalence"],
|
|
166
|
+
details: details(old_lts, new_lts, relation, false).merge(
|
|
167
|
+
"reason" => "The built-in F3 checker requires deterministic LTS models."
|
|
168
|
+
))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
counterexample = distinguishing_sequence(old_lts, new_lts)
|
|
172
|
+
verdict = if counterexample then :difference_found
|
|
173
|
+
elsif exact then :no_difference_found
|
|
174
|
+
else :inconclusive
|
|
175
|
+
end
|
|
176
|
+
Result.new(level: :f3, verdict:,
|
|
177
|
+
scope: Scope.new(size: [old_lts.states.length, new_lts.states.length].max,
|
|
178
|
+
depth: nil, cases: visited_count, exhaustive: exact),
|
|
179
|
+
assumptions:, out_of_scope: ["operations outside the learned alphabet", "unprojected state"],
|
|
180
|
+
counterexample: counterexample && { "sequence" => counterexample },
|
|
181
|
+
details: details(old_lts, new_lts, relation, exact))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def distinguishing_sequence(old_lts, new_lts)
|
|
187
|
+
@visited = {}
|
|
188
|
+
queue = [[old_lts.initial, new_lts.initial, []]]
|
|
189
|
+
until queue.empty?
|
|
190
|
+
old_state, new_state, path = queue.shift
|
|
191
|
+
next if @visited[[old_state, new_state]]
|
|
192
|
+
|
|
193
|
+
@visited[[old_state, new_state]] = true
|
|
194
|
+
inputs = (old_lts.outgoing(old_state).map(&:input) | new_lts.outgoing(new_state).map(&:input)).sort
|
|
195
|
+
inputs.each do |input|
|
|
196
|
+
old_transition = old_lts.step(old_state, input)
|
|
197
|
+
new_transition = new_lts.step(new_state, input)
|
|
198
|
+
return path + [input] unless old_transition && new_transition
|
|
199
|
+
return path + [input] unless old_transition.output == new_transition.output
|
|
200
|
+
|
|
201
|
+
queue << [old_transition.to, new_transition.to, path + [input]]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def visited_count = @visited.length
|
|
208
|
+
|
|
209
|
+
def details(old_lts, new_lts, relation, exact)
|
|
210
|
+
{ "relation" => relation.to_s,
|
|
211
|
+
"lts_o" => model_details(old_lts), "lts_n" => model_details(new_lts),
|
|
212
|
+
"equivalence_query" => { "method" => "paired-state breadth-first search", "exact" => exact },
|
|
213
|
+
"caveat" => "This result applies to the learned models, not to the complete implementations." }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def model_details(lts)
|
|
217
|
+
{ "states" => lts.states.length, "transitions" => lts.transitions.length, "alphabet" => lts.alphabet }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
module AldebaranExporter
|
|
222
|
+
module_function
|
|
223
|
+
|
|
224
|
+
def call(lts)
|
|
225
|
+
states = lts.states.each_with_index.to_h
|
|
226
|
+
lines = ["des (#{states.fetch(lts.initial)},#{lts.transitions.length},#{states.length})"]
|
|
227
|
+
lts.transitions.each do |transition|
|
|
228
|
+
label = JSON.generate("#{transition.input}/#{transition.output}")
|
|
229
|
+
lines << "(#{states.fetch(transition.from)},#{label},#{states.fetch(transition.to)})"
|
|
230
|
+
end
|
|
231
|
+
lines.join("\n") << "\n"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
module CounterexampleRSpec
|
|
236
|
+
module_function
|
|
237
|
+
|
|
238
|
+
def call(lts:, sequence:, subject_name:)
|
|
239
|
+
expected = outputs(lts, sequence)
|
|
240
|
+
<<~RUBY
|
|
241
|
+
# frozen_string_literal: true
|
|
242
|
+
|
|
243
|
+
require "bparity"
|
|
244
|
+
load ENV.fetch("BPARITY_ADAPTER")
|
|
245
|
+
|
|
246
|
+
RSpec.describe "F3 distinguishing sequence for #{subject_name}" do
|
|
247
|
+
it "matches the learned legacy model for #{sequence.join(' -> ')}" do
|
|
248
|
+
binding = Bparity.adapter_definition.subjects.fetch(#{subject_name.inspect})
|
|
249
|
+
subject = binding.build({})
|
|
250
|
+
sequence = #{sequence.inspect}
|
|
251
|
+
actual = sequence.map do |name|
|
|
252
|
+
operation = binding.operations.fetch(name)
|
|
253
|
+
begin
|
|
254
|
+
value = operation.invoke(subject, [], {})
|
|
255
|
+
Bparity::Formal::LtsEncoding.output({ "kind" => "return", "value" =>
|
|
256
|
+
Bparity::Recording::Serializer.dump(operation.map_return(value)) })
|
|
257
|
+
rescue StandardError => error
|
|
258
|
+
Bparity::Formal::LtsEncoding.output({ "kind" => "raise", **operation.map_error(error) })
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
expect(actual).to eq(#{expected.inspect})
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
RUBY
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def outputs(lts, sequence)
|
|
268
|
+
state = lts.initial
|
|
269
|
+
sequence.each_with_object([]) do |input, outputs|
|
|
270
|
+
transition = lts.step(state, input)
|
|
271
|
+
unless transition
|
|
272
|
+
outputs << LtsEncoding.output("kind" => "missing_transition", "input" => input)
|
|
273
|
+
break
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
state = transition.to
|
|
277
|
+
outputs << transition.output
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
private_class_method :outputs
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class WMethod
|
|
284
|
+
def initialize(lts)
|
|
285
|
+
@lts = lts
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def sequences(additional_states: 0)
|
|
289
|
+
cover = transition_cover
|
|
290
|
+
middle = (0..additional_states).flat_map { |length| @lts.alphabet.repeated_permutation(length).to_a }
|
|
291
|
+
suffixes = characterization_set
|
|
292
|
+
cover.product(middle, suffixes).map(&:flatten).uniq.sort_by { |sequence| [sequence.length, sequence] }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def transition_cover
|
|
298
|
+
paths = state_paths
|
|
299
|
+
(paths.values + @lts.transitions.map { |transition| paths.fetch(transition.from) + [transition.input] }).uniq
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def state_paths
|
|
303
|
+
paths = { @lts.initial => [] }
|
|
304
|
+
queue = [@lts.initial]
|
|
305
|
+
until queue.empty?
|
|
306
|
+
state = queue.shift
|
|
307
|
+
@lts.outgoing(state).each do |transition|
|
|
308
|
+
next if paths.key?(transition.to)
|
|
309
|
+
|
|
310
|
+
paths[transition.to] = paths.fetch(state) + [transition.input]
|
|
311
|
+
queue << transition.to
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
paths
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def characterization_set
|
|
318
|
+
pairs = @lts.states.combination(2)
|
|
319
|
+
sequences = pairs.filter_map { |left, right| distinguish(left, right) }
|
|
320
|
+
sequences.empty? ? [[]] : sequences.uniq
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def distinguish(left, right)
|
|
324
|
+
queue = [[left, right, []]]
|
|
325
|
+
visited = {}
|
|
326
|
+
until queue.empty?
|
|
327
|
+
state_a, state_b, path = queue.shift
|
|
328
|
+
next if visited[[state_a, state_b]]
|
|
329
|
+
|
|
330
|
+
visited[[state_a, state_b]] = true
|
|
331
|
+
@lts.alphabet.each do |input|
|
|
332
|
+
transition_a = @lts.step(state_a, input)
|
|
333
|
+
transition_b = @lts.step(state_b, input)
|
|
334
|
+
return path + [input] unless transition_a && transition_b
|
|
335
|
+
return path + [input] unless transition_a.output == transition_b.output
|
|
336
|
+
|
|
337
|
+
queue << [transition_a.to, transition_b.to, path + [input]]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
nil
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bparity
|
|
4
|
+
module Formal
|
|
5
|
+
class Scope
|
|
6
|
+
attr_reader :size, :depth, :cases, :exhaustive, :timebox
|
|
7
|
+
|
|
8
|
+
def initialize(size:, depth:, cases:, exhaustive:, timebox: nil)
|
|
9
|
+
@size = size
|
|
10
|
+
@depth = depth
|
|
11
|
+
@cases = cases
|
|
12
|
+
@exhaustive = exhaustive
|
|
13
|
+
@timebox = timebox
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{ "size" => size, "depth" => depth, "cases" => cases, "exhaustive" => exhaustive,
|
|
18
|
+
"timebox_seconds" => timebox }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Result
|
|
23
|
+
VERDICTS = %i[no_difference_found difference_found inconclusive].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :level, :verdict, :scope, :assumptions, :out_of_scope, :counterexample, :details
|
|
26
|
+
|
|
27
|
+
def initialize(level:, verdict:, scope:, assumptions:, out_of_scope:, counterexample: nil, details: {})
|
|
28
|
+
raise ConfigurationError, "A formal result requires a scope." unless scope
|
|
29
|
+
raise ConfigurationError, "A formal result requires verification assumptions." if assumptions.nil?
|
|
30
|
+
raise ConfigurationError, "Invalid formal verdict: #{verdict}." unless VERDICTS.include?(verdict)
|
|
31
|
+
|
|
32
|
+
@level = level.to_s.upcase
|
|
33
|
+
@verdict = verdict
|
|
34
|
+
@scope = scope
|
|
35
|
+
@assumptions = assumptions
|
|
36
|
+
@out_of_scope = out_of_scope
|
|
37
|
+
@counterexample = counterexample
|
|
38
|
+
@details = details
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_h
|
|
42
|
+
{ "level" => level, "verdict" => verdict.to_s, "scope" => scope.to_h,
|
|
43
|
+
"assumptions" => assumptions.map(&:to_s), "out_of_scope" => out_of_scope,
|
|
44
|
+
"counterexample" => counterexample, "details" => details }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def success? = verdict == :no_difference_found
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|