speculation 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/.gitignore +9 -0
- data/.rubocop.yml +87 -0
- data/.travis.yml +16 -0
- data/.yardopts +3 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +29 -0
- data/bin/bundler +17 -0
- data/bin/byebug +17 -0
- data/bin/coderay +17 -0
- data/bin/console +70 -0
- data/bin/cucumber-queue +17 -0
- data/bin/minitest-queue +17 -0
- data/bin/pry +17 -0
- data/bin/rake +17 -0
- data/bin/rspec-queue +17 -0
- data/bin/rubocop +17 -0
- data/bin/ruby-parse +17 -0
- data/bin/ruby-rewrite +17 -0
- data/bin/setup +8 -0
- data/bin/testunit-queue +17 -0
- data/bin/yard +17 -0
- data/bin/yardoc +17 -0
- data/bin/yri +17 -0
- data/lib/speculation/conj.rb +32 -0
- data/lib/speculation/error.rb +17 -0
- data/lib/speculation/gen.rb +106 -0
- data/lib/speculation/identifier.rb +47 -0
- data/lib/speculation/namespaced_symbols.rb +28 -0
- data/lib/speculation/pmap.rb +30 -0
- data/lib/speculation/spec_impl/and_spec.rb +39 -0
- data/lib/speculation/spec_impl/every_spec.rb +176 -0
- data/lib/speculation/spec_impl/f_spec.rb +121 -0
- data/lib/speculation/spec_impl/hash_spec.rb +215 -0
- data/lib/speculation/spec_impl/merge_spec.rb +40 -0
- data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
- data/lib/speculation/spec_impl/or_spec.rb +62 -0
- data/lib/speculation/spec_impl/regex_spec.rb +35 -0
- data/lib/speculation/spec_impl/spec.rb +47 -0
- data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
- data/lib/speculation/spec_impl.rb +36 -0
- data/lib/speculation/test.rb +553 -0
- data/lib/speculation/utils.rb +64 -0
- data/lib/speculation/utils_specs.rb +57 -0
- data/lib/speculation/version.rb +4 -0
- data/lib/speculation.rb +1308 -0
- data/speculation.gemspec +43 -0
- metadata +246 -0
@@ -0,0 +1,553 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "concurrent"
|
3
|
+
require "pp"
|
4
|
+
require "speculation/pmap"
|
5
|
+
|
6
|
+
module Speculation
|
7
|
+
module Test
|
8
|
+
using NamespacedSymbols.refine(self)
|
9
|
+
using Pmap
|
10
|
+
|
11
|
+
# @private
|
12
|
+
S = Speculation
|
13
|
+
|
14
|
+
@instrumented_methods = Concurrent::Atom.new({})
|
15
|
+
@instrument_enabled = Concurrent::ThreadLocalVar.new(true)
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# if false, instrumented methods call straight through
|
19
|
+
attr_accessor :instrument_enabled
|
20
|
+
end
|
21
|
+
|
22
|
+
# Disables instrument's checking of calls within a block
|
23
|
+
def self.with_instrument_disabled
|
24
|
+
instrument_enabled.value = false
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
instrument_enabled.value = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Given an opts hash as per instrument, returns the set of
|
31
|
+
# Speculation::Identifiers for methods that can be instrumented.
|
32
|
+
# @param opts [Hash]
|
33
|
+
# @return [Array<Identifier>]
|
34
|
+
def self.instrumentable_methods(opts = {})
|
35
|
+
if opts[:gen]
|
36
|
+
unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
|
37
|
+
raise ArgumentError, "instrument :gen expects Method or Symbol keys"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
S.registry.keys.select(&method(:fn_spec_name?)).to_set.tap do |set|
|
42
|
+
set.merge(opts[:spec].keys) if opts[:spec]
|
43
|
+
set.merge(opts[:stub]) if opts[:stub]
|
44
|
+
set.merge(opts[:replace].keys) if opts[:replace]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param method_or_methods [Method, Identifier, Array<Method>, Array<Identifier>]
|
49
|
+
# Instruments the methods named by method-or-methods, a method or collection
|
50
|
+
# of methods, or all instrumentable methods if method-or-methods is not
|
51
|
+
# specified.
|
52
|
+
# If a method has an :args fn-spec, replaces the method with a method that
|
53
|
+
# checks arg conformance (throwing an exception on failure) before
|
54
|
+
# delegating to the original method.
|
55
|
+
# @param opts [Hash] opts hash can be used to override registered specs, and/or to replace
|
56
|
+
# method implementations entirely. Opts for methods not included in
|
57
|
+
# method-or-methods are ignored. This facilitates sharing a common options
|
58
|
+
# hash across many different calls to instrument
|
59
|
+
# @option opts :spec [Hash] a map from methods to override specs.
|
60
|
+
# :spec overrides registered method specs with specs you provide. Use :spec
|
61
|
+
# overrides to provide specs for libraries that do not have them, or to
|
62
|
+
# constrain your own use of a fn to a subset of its spec'ed contract.
|
63
|
+
# :spec can be used in combination with :stub or :replace.
|
64
|
+
#
|
65
|
+
# @option opts :stub [Set, Array] a set of methods to be replaced by stubs.
|
66
|
+
# :stub replaces a fn with a stub that checks :args, then uses the :ret spec
|
67
|
+
# to generate a return value.
|
68
|
+
#
|
69
|
+
# @option opts :gen [Hash{Symbol => Proc}] a map from spec names to generator overrides.
|
70
|
+
# :gen overrides are used only for :stub generation.
|
71
|
+
#
|
72
|
+
# @option opts :replace [Hash{Method => Proc}] a map from methods to replacement procs.
|
73
|
+
# :replace replaces a method with a method that checks args conformance,
|
74
|
+
# then invokes the method/proc you provide, enabling arbitrary stubbing and
|
75
|
+
# mocking.
|
76
|
+
#
|
77
|
+
# @return [Array<Identifier>] a collection of Identifiers naming the methods instrumented.
|
78
|
+
def self.instrument(method_or_methods = instrumentable_methods, opts = {})
|
79
|
+
if opts[:gen]
|
80
|
+
gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
|
81
|
+
opts = opts.merge(:gen => gens)
|
82
|
+
end
|
83
|
+
|
84
|
+
Array(method_or_methods).
|
85
|
+
map { |method| S.Identifier(method) }.
|
86
|
+
uniq.
|
87
|
+
map { |ident| instrument1(ident, opts) }.
|
88
|
+
compact
|
89
|
+
end
|
90
|
+
|
91
|
+
# Undoes instrument on the method_or_methods, specified as in instrument.
|
92
|
+
# With no args, unstruments all instrumented methods.
|
93
|
+
# @param method_or_methods [Method, Identifier, Array<Method>, Array<Identifier>]
|
94
|
+
# @return [Array<Identifier>] a collection of Identifiers naming the methods unstrumented
|
95
|
+
def self.unstrument(method_or_methods = nil)
|
96
|
+
method_or_methods ||= @instrumented_methods.value.keys
|
97
|
+
|
98
|
+
Array(method_or_methods).
|
99
|
+
map { |method| S.Identifier(method) }.
|
100
|
+
map { |ident| unstrument1(ident) }.
|
101
|
+
compact
|
102
|
+
end
|
103
|
+
|
104
|
+
# Runs generative tests for method using spec and opts.
|
105
|
+
# @param method [Method, Identifier]
|
106
|
+
# @param spec [Spec]
|
107
|
+
# @param opts [Hash]
|
108
|
+
# @return [Hash]
|
109
|
+
# @see check see check for options and return
|
110
|
+
def self.check_method(method, spec, opts = {})
|
111
|
+
validate_check_opts(opts)
|
112
|
+
check1(S.Identifier(method), spec, opts)
|
113
|
+
end
|
114
|
+
|
115
|
+
# @param opts [Hash] an opts hash as per `check`
|
116
|
+
# @return [Array<Identifier>] the set of Identifiers that can be checked.
|
117
|
+
def self.checkable_methods(opts = {})
|
118
|
+
validate_check_opts(opts)
|
119
|
+
|
120
|
+
S.
|
121
|
+
registry.
|
122
|
+
keys.
|
123
|
+
select(&method(:fn_spec_name?)).
|
124
|
+
reject(&:instance_method?).
|
125
|
+
to_set.
|
126
|
+
tap { |set| set.merge(opts[:spec].keys) if opts[:spec] }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Run generative tests for spec conformance on method_or_methods. If
|
130
|
+
# method_or_methods is not specified, check all checkable methods.
|
131
|
+
#
|
132
|
+
# @param method_or_methods [Array<Method>, Method]
|
133
|
+
# @param opts [Hash]
|
134
|
+
# @option opts :num_tests [Integer] (1000) number of times to generatively test each method
|
135
|
+
# @option opts :gen [Hash] map from spec names to generator overrides.
|
136
|
+
# Generator overrides are passed to Speculation.gen when generating method args.
|
137
|
+
# @return [Array<Identifier>] an array of check result hashes with the following keys:
|
138
|
+
# * :spec the spec tested
|
139
|
+
# * :method optional method tested
|
140
|
+
# * :failure optional test failure
|
141
|
+
# * :result optional boolean as to whether all generative tests passed
|
142
|
+
# * :num_tests optional number of generative tests ran
|
143
|
+
#
|
144
|
+
# :failure is a hash that will contain a :"Speculation/failure" key with possible values:
|
145
|
+
#
|
146
|
+
# * :check_failed at least one checked return did not conform
|
147
|
+
# * :no_args_spec no :args spec provided
|
148
|
+
# * :no_fspec no fspec provided
|
149
|
+
# * :no_gen unable to generate :args
|
150
|
+
# * :instrument invalid args detected by instrument
|
151
|
+
def self.check(method_or_methods = nil, opts = {})
|
152
|
+
method_or_methods ||= checkable_methods
|
153
|
+
|
154
|
+
Array(method_or_methods).
|
155
|
+
map { |method| S.Identifier(method) }.
|
156
|
+
select { |ident| checkable_methods(opts).include?(ident) }.
|
157
|
+
pmap { |ident| check1(ident, S.get_spec(ident), opts) }
|
158
|
+
end
|
159
|
+
|
160
|
+
# Given a check result, returns an abbreviated version suitable for summary use.
|
161
|
+
# @param x [Hash]
|
162
|
+
# @return [Hash]
|
163
|
+
def self.abbrev_result(x)
|
164
|
+
if x[:failure]
|
165
|
+
x.reject { |k, _| k == :ret.ns }.
|
166
|
+
merge(:spec => x[:spec].inspect,
|
167
|
+
:failure => unwrap_failure(x[:failure]))
|
168
|
+
else
|
169
|
+
x.reject { |k, _| [:spec, :ret.ns].include?(k) }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Given a collection of check_results, e.g. from `check`, pretty prints the
|
174
|
+
# summary_result (default abbrev_result) of each.
|
175
|
+
#
|
176
|
+
# @param check_results [Array] a collection of check_results
|
177
|
+
# @yield [Hash]
|
178
|
+
# @return [Hash] a hash with :total, the total number of results, plus a key with a
|
179
|
+
# count for each different :type of result.
|
180
|
+
# @see check see check for check_results
|
181
|
+
# @see abbrev_result
|
182
|
+
def self.summarize_results(check_results, &summary_result)
|
183
|
+
summary_result ||= method(:abbrev_result)
|
184
|
+
|
185
|
+
check_results.reduce(:total => 0) { |summary, result|
|
186
|
+
pp summary_result.call(result)
|
187
|
+
|
188
|
+
result_key = result_type(result)
|
189
|
+
|
190
|
+
summary.merge(
|
191
|
+
:total => summary[:total].next,
|
192
|
+
result_key => summary.fetch(result_key, 0).next
|
193
|
+
)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
class << self
|
198
|
+
private
|
199
|
+
|
200
|
+
def spec_checking_fn(ident, method, fspec)
|
201
|
+
fspec = S.send(:maybe_spec, fspec)
|
202
|
+
|
203
|
+
conform = ->(args, block) do
|
204
|
+
conformed_args = S.conform(fspec.argspec, args)
|
205
|
+
conformed_block = S.conform(fspec.blockspec, block) if fspec.blockspec
|
206
|
+
|
207
|
+
if conformed_args == :invalid.ns(S)
|
208
|
+
backtrace = backtrace_relevant_to_instrument(caller)
|
209
|
+
|
210
|
+
ed = S.
|
211
|
+
_explain_data(fspec.argspec, [:args], [], [], args).
|
212
|
+
merge(:args.ns(S) => args, :failure.ns(S) => :instrument, :caller.ns => backtrace)
|
213
|
+
|
214
|
+
io = StringIO.new
|
215
|
+
S.explain_out(ed, io)
|
216
|
+
msg = io.string
|
217
|
+
|
218
|
+
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
|
219
|
+
elsif conformed_block == :invalid.ns(S)
|
220
|
+
backtrace = backtrace_relevant_to_instrument(caller)
|
221
|
+
|
222
|
+
ed = S.
|
223
|
+
_explain_data(fspec.blockspec, [:block], [], [], block).
|
224
|
+
merge(:block.ns(S) => block, :failure.ns(S) => :instrument, :caller.ns => backtrace)
|
225
|
+
|
226
|
+
io = StringIO.new
|
227
|
+
S.explain_out(ed, io)
|
228
|
+
msg = io.string
|
229
|
+
|
230
|
+
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
->(*args, &block) do
|
235
|
+
method = method.bind(self) if method.is_a?(UnboundMethod)
|
236
|
+
|
237
|
+
if Test.instrument_enabled.value
|
238
|
+
Test.with_instrument_disabled do
|
239
|
+
conform.call(args, block) if fspec.argspec
|
240
|
+
|
241
|
+
begin
|
242
|
+
Test.instrument_enabled.value = true
|
243
|
+
method.call(*args, &block)
|
244
|
+
ensure
|
245
|
+
Test.instrument_enabled.value = false
|
246
|
+
end
|
247
|
+
end
|
248
|
+
else
|
249
|
+
method.call(*args, &block)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def no_fspec(ident, spec)
|
255
|
+
S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec, :failure.ns(S) => :no_fspec)
|
256
|
+
end
|
257
|
+
|
258
|
+
def instrument1(ident, opts)
|
259
|
+
spec = S.get_spec(ident)
|
260
|
+
|
261
|
+
raw, wrapped = @instrumented_methods.
|
262
|
+
value.
|
263
|
+
fetch(ident, {}).
|
264
|
+
values_at(:raw, :wrapped)
|
265
|
+
|
266
|
+
current = ident.get_method
|
267
|
+
to_wrap = wrapped == current ? raw : current
|
268
|
+
|
269
|
+
ospec = instrument_choose_spec(spec, ident, opts[:spec])
|
270
|
+
raise no_fspec(ident, spec) unless ospec
|
271
|
+
|
272
|
+
ofn = instrument_choose_fn(to_wrap, ospec, ident, opts)
|
273
|
+
|
274
|
+
checked = spec_checking_fn(ident, ofn, ospec)
|
275
|
+
|
276
|
+
ident.redefine_method!(checked)
|
277
|
+
|
278
|
+
wrapped = ident.get_method
|
279
|
+
|
280
|
+
@instrumented_methods.swap do |methods|
|
281
|
+
methods.merge(ident => { :raw => to_wrap, :wrapped => wrapped })
|
282
|
+
end
|
283
|
+
|
284
|
+
ident
|
285
|
+
end
|
286
|
+
|
287
|
+
def instrument_choose_fn(f, spec, ident, opts)
|
288
|
+
stubs = (opts[:stub] || []).map(&S.method(:Identifier))
|
289
|
+
over = opts[:gen] || {}
|
290
|
+
replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
|
291
|
+
|
292
|
+
if stubs.include?(ident)
|
293
|
+
Gen.generate(S.gen(spec, over))
|
294
|
+
else
|
295
|
+
replace.fetch(ident, f)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def instrument_choose_spec(spec, ident, overrides)
|
300
|
+
(overrides || {}).
|
301
|
+
reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }.
|
302
|
+
fetch(ident, spec)
|
303
|
+
end
|
304
|
+
|
305
|
+
def unstrument1(ident)
|
306
|
+
instrumented = @instrumented_methods.value[ident]
|
307
|
+
return unless instrumented
|
308
|
+
|
309
|
+
raw, wrapped = instrumented.values_at(:raw, :wrapped)
|
310
|
+
|
311
|
+
@instrumented_methods.swap do |h|
|
312
|
+
h.reject { |k, _v| k == ident }
|
313
|
+
end
|
314
|
+
|
315
|
+
current = ident.get_method
|
316
|
+
|
317
|
+
# Only redefine to original if it has not been modified since it was
|
318
|
+
# instrumented.
|
319
|
+
if wrapped == current
|
320
|
+
ident.tap { |i| i.redefine_method!(raw) }
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def explain_check(args, spec, v, role)
|
325
|
+
data = unless S.valid?(spec, v)
|
326
|
+
S._explain_data(spec, [role], [], [], v).
|
327
|
+
merge(:args.ns => args,
|
328
|
+
:val.ns => v,
|
329
|
+
:failure.ns(S) => :check_failed)
|
330
|
+
end
|
331
|
+
|
332
|
+
S::Error.new("Specification-based check failed", data).tap do |e|
|
333
|
+
e.set_backtrace(caller)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Returns true if call passes specs, otherwise returns a hash with
|
338
|
+
# :backtrace, :cause and :data keys. :data will have a
|
339
|
+
# :"Speculation/failure" key.
|
340
|
+
def check_call(method, spec, args, block)
|
341
|
+
conformed_args = S.conform(spec.argspec, args) if spec.argspec
|
342
|
+
|
343
|
+
if conformed_args == :invalid.ns(S)
|
344
|
+
return explain_check(args, spec.argspec, args, :args)
|
345
|
+
end
|
346
|
+
|
347
|
+
conformed_block = S.conform(spec.blockspec, block) if spec.blockspec
|
348
|
+
|
349
|
+
if conformed_block == :invalid.ns(S)
|
350
|
+
return explain_check(block, spec.block, block, :block)
|
351
|
+
end
|
352
|
+
|
353
|
+
ret = method.call(*args, &block)
|
354
|
+
|
355
|
+
conformed_ret = S.conform(spec.retspec, ret) if spec.retspec
|
356
|
+
|
357
|
+
if conformed_ret == :invalid.ns(S)
|
358
|
+
return explain_check(args, spec.retspec, ret, :ret)
|
359
|
+
end
|
360
|
+
|
361
|
+
return true unless spec.argspec && spec.retspec && spec.fnspec
|
362
|
+
|
363
|
+
if S.valid?(spec.fnspec, :args => conformed_args, :block => conformed_block, :ret => conformed_ret)
|
364
|
+
true
|
365
|
+
else
|
366
|
+
explain_check(args, spec.fnspec, { :args => conformed_args, :block => conformed_block, :ret => conformed_ret }, :fn)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def quick_check(method, spec, opts)
|
371
|
+
gen = opts[:gen]
|
372
|
+
num_tests = opts.fetch(:num_tests, 1000)
|
373
|
+
|
374
|
+
args_gen = begin
|
375
|
+
S.gen(spec.argspec, gen)
|
376
|
+
rescue => e
|
377
|
+
return { :result => e }
|
378
|
+
end
|
379
|
+
|
380
|
+
block_gen = if spec.blockspec
|
381
|
+
begin
|
382
|
+
S.gen(spec.blockspec, gen)
|
383
|
+
rescue => e
|
384
|
+
return { :result => e }
|
385
|
+
end
|
386
|
+
else
|
387
|
+
Utils.constantly(nil)
|
388
|
+
end
|
389
|
+
|
390
|
+
combined_gen = ->(r) { [args_gen.call(r), block_gen.call(r)] }
|
391
|
+
|
392
|
+
rantly_quick_check(combined_gen, num_tests) { |(args, block)| check_call(method, spec, args, block) }
|
393
|
+
end
|
394
|
+
|
395
|
+
def make_check_result(method, spec, check_result)
|
396
|
+
result = { :spec => spec,
|
397
|
+
:ret.ns => check_result,
|
398
|
+
:method => method }
|
399
|
+
|
400
|
+
if check_result[:result] && check_result[:result] != true
|
401
|
+
result[:failure] = check_result[:result]
|
402
|
+
end
|
403
|
+
|
404
|
+
if check_result[:shrunk]
|
405
|
+
result[:failure] = check_result[:shrunk][:result]
|
406
|
+
end
|
407
|
+
|
408
|
+
result
|
409
|
+
end
|
410
|
+
|
411
|
+
def check1(ident, spec, opts)
|
412
|
+
specd = S.spec(spec)
|
413
|
+
|
414
|
+
reinstrument = unstrument(ident).any?
|
415
|
+
method = ident.get_method
|
416
|
+
|
417
|
+
if specd.argspec # or blockspec?
|
418
|
+
check_result = quick_check(method, spec, opts)
|
419
|
+
make_check_result(method, spec, check_result)
|
420
|
+
else
|
421
|
+
failure = { :info => "No :args spec",
|
422
|
+
failure.ns(S) => :no_args_spec }
|
423
|
+
|
424
|
+
{ :failure => failure,
|
425
|
+
:method => method,
|
426
|
+
:spec => spec }
|
427
|
+
end
|
428
|
+
ensure
|
429
|
+
instrument(ident) if reinstrument
|
430
|
+
end
|
431
|
+
|
432
|
+
def validate_check_opts(opts)
|
433
|
+
return unless opts[:gen]
|
434
|
+
|
435
|
+
unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
|
436
|
+
raise ArgumentErorr, "check :gen expects Method or Symbol keys"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def backtrace_relevant_to_instrument(backtrace)
|
441
|
+
backtrace.drop_while { |line| line.include?(__FILE__) }
|
442
|
+
end
|
443
|
+
|
444
|
+
def fn_spec_name?(spec_name)
|
445
|
+
spec_name.is_a?(S::Identifier)
|
446
|
+
end
|
447
|
+
|
448
|
+
# Reimplementation of Rantly's `check` since it does not provide direct access to results
|
449
|
+
# (shrunk data etc.), instead printing them to STDOUT.
|
450
|
+
def rantly_quick_check(gen, num_tests)
|
451
|
+
i = 0
|
452
|
+
limit = 100
|
453
|
+
|
454
|
+
Rantly.singleton.generate(num_tests, limit, gen) do |val|
|
455
|
+
args, blk = val
|
456
|
+
i += 1
|
457
|
+
|
458
|
+
result = begin
|
459
|
+
yield([args, blk])
|
460
|
+
rescue => e
|
461
|
+
e
|
462
|
+
end
|
463
|
+
|
464
|
+
unless result == true
|
465
|
+
# This is a Rantly Tuple.
|
466
|
+
args = ::Tuple.new(args)
|
467
|
+
|
468
|
+
if args.respond_to?(:shrink)
|
469
|
+
shrunk = shrink(args, result, ->(v) { yield([v, blk]) })
|
470
|
+
|
471
|
+
shrunk[:smallest] = [shrunk[:smallest].array, blk]
|
472
|
+
|
473
|
+
return { :fail => args.array,
|
474
|
+
:block => blk,
|
475
|
+
:num_tests => i,
|
476
|
+
:result => result,
|
477
|
+
:shrunk => shrunk }
|
478
|
+
else
|
479
|
+
return { :fail => args.array,
|
480
|
+
:block => blk,
|
481
|
+
:num_tests => i,
|
482
|
+
:result => result }
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
{ :num_tests => i,
|
488
|
+
:result => true }
|
489
|
+
end
|
490
|
+
|
491
|
+
# reimplementation of Rantly's shrinking.
|
492
|
+
def shrink(data, result, block, depth = 0, iteration = 0)
|
493
|
+
smallest = data
|
494
|
+
max_depth = depth
|
495
|
+
|
496
|
+
if data.shrinkable?
|
497
|
+
while iteration < 1024
|
498
|
+
shrunk_data = data.shrink
|
499
|
+
result = begin
|
500
|
+
block.call(shrunk_data.array)
|
501
|
+
rescue => e
|
502
|
+
e
|
503
|
+
end
|
504
|
+
|
505
|
+
unless result == true
|
506
|
+
shrunk = shrink(shrunk_data, result, block, depth + 1, iteration + 1)
|
507
|
+
|
508
|
+
branch_smallest, branch_depth, iteration =
|
509
|
+
shrunk.values_at(:smallest, :depth, :iteration)
|
510
|
+
|
511
|
+
if branch_depth > max_depth
|
512
|
+
smallest = branch_smallest
|
513
|
+
max_depth = branch_depth
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
break unless data.retry?
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
{ :depth => max_depth,
|
522
|
+
:iteration => iteration,
|
523
|
+
:result => result,
|
524
|
+
:smallest => smallest }
|
525
|
+
end
|
526
|
+
|
527
|
+
### check reporting ###
|
528
|
+
|
529
|
+
def failure_type(x)
|
530
|
+
x.data[:failure.ns(S)] if x.is_a?(S::Error)
|
531
|
+
end
|
532
|
+
|
533
|
+
def unwrap_failure(x)
|
534
|
+
failure_type(x) ? x.data : x
|
535
|
+
end
|
536
|
+
|
537
|
+
# Returns the type of the check result. This can be any of the
|
538
|
+
# :"Speculation/failure" symbols documented in 'check', or:
|
539
|
+
#
|
540
|
+
# :check_passed all checked method returns conformed
|
541
|
+
# :check_raised checked fn threw an exception
|
542
|
+
def result_type(ret)
|
543
|
+
failure = ret[:failure]
|
544
|
+
|
545
|
+
if failure.nil?
|
546
|
+
:check_passed
|
547
|
+
else
|
548
|
+
failure_type(failure) || :check_raised
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "set"
|
3
|
+
require "speculation/conj"
|
4
|
+
|
5
|
+
module Speculation
|
6
|
+
using Conj
|
7
|
+
|
8
|
+
# @private
|
9
|
+
module Utils
|
10
|
+
def self.hash?(x)
|
11
|
+
x.respond_to?(:store)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.array?(x)
|
15
|
+
x.respond_to?(:at)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.collection?(xs)
|
19
|
+
xs.respond_to?(:each)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.identity(x)
|
23
|
+
x
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.constantly(x)
|
27
|
+
->(*) { x }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.complement(&f)
|
31
|
+
->(*args) { !f.call(*args) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.distinct?(xs)
|
35
|
+
seen = Set[]
|
36
|
+
|
37
|
+
xs.each do |x|
|
38
|
+
if seen.include?(x)
|
39
|
+
return false
|
40
|
+
else
|
41
|
+
seen << x
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.ident?(x)
|
49
|
+
x.is_a?(Symbol) || x.is_a?(Identifier)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.method?(x)
|
53
|
+
x.is_a?(Method) || x.is_a?(UnboundMethod)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.empty(coll)
|
57
|
+
coll.class.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.into(to, from)
|
61
|
+
from.reduce(to) { |memo, obj| memo.conj(obj) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Speculation
|
4
|
+
# @private
|
5
|
+
module UtilsSpecs
|
6
|
+
using Speculation::NamespacedSymbols.refine(self)
|
7
|
+
|
8
|
+
S = Speculation
|
9
|
+
U = Speculation::Utils
|
10
|
+
|
11
|
+
S.fdef(U.method(:hash?),
|
12
|
+
:args => S.tuple(:any.ns(S)),
|
13
|
+
:ret => :boolean.ns(S))
|
14
|
+
|
15
|
+
S.fdef(U.method(:array?),
|
16
|
+
:args => S.tuple(:any.ns(S)),
|
17
|
+
:ret => :boolean.ns(S))
|
18
|
+
|
19
|
+
S.fdef(U.method(:collection?),
|
20
|
+
:args => S.tuple(:any.ns(S)),
|
21
|
+
:ret => :boolean.ns(S))
|
22
|
+
|
23
|
+
S.fdef(U.method(:identity),
|
24
|
+
:args => S.cat(:x => :any.ns(S)),
|
25
|
+
:ret => :any.ns(S),
|
26
|
+
:fn => ->(x) { x[:args][:x].equal?(x[:ret]) })
|
27
|
+
|
28
|
+
S.fdef(U.method(:complement),
|
29
|
+
:args => :empty.ns(S),
|
30
|
+
:block => S.fspec(:args => S.zero_or_more(:any.ns(S)),
|
31
|
+
:ret => :any.ns(S)),
|
32
|
+
:ret => S.fspec(:args => S.zero_or_more(:any.ns(S)),
|
33
|
+
:ret => :boolean.ns(S)))
|
34
|
+
|
35
|
+
S.fdef(U.method(:constantly),
|
36
|
+
:args => S.cat(:x => :any.ns(S)),
|
37
|
+
:ret => Proc,
|
38
|
+
:fn => ->(x) { x[:args][:x].equal?(x[:ret].call) })
|
39
|
+
|
40
|
+
S.fdef(U.method(:distinct?),
|
41
|
+
:args => S.cat(:coll => Enumerable),
|
42
|
+
:ret => :boolean.ns(S))
|
43
|
+
|
44
|
+
S.fdef(U.method(:ident?),
|
45
|
+
:args => S.cat(:x => :any.ns(S)),
|
46
|
+
:ret => :boolean.ns(S))
|
47
|
+
|
48
|
+
S.fdef(U.method(:method?),
|
49
|
+
:args => S.cat(:x => :any.ns(S)),
|
50
|
+
:ret => :boolean.ns(S))
|
51
|
+
|
52
|
+
S.fdef(U.method(:empty),
|
53
|
+
:args => S.cat(:coll => Enumerable),
|
54
|
+
:ret => S.and(Enumerable, ->(coll) { coll.empty? }),
|
55
|
+
:fn => ->(x) { x[:args][:coll].class == x[:ret].class })
|
56
|
+
end
|
57
|
+
end
|