speculation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|