speculation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +87 -0
  4. data/.travis.yml +16 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +116 -0
  9. data/Rakefile +29 -0
  10. data/bin/bundler +17 -0
  11. data/bin/byebug +17 -0
  12. data/bin/coderay +17 -0
  13. data/bin/console +70 -0
  14. data/bin/cucumber-queue +17 -0
  15. data/bin/minitest-queue +17 -0
  16. data/bin/pry +17 -0
  17. data/bin/rake +17 -0
  18. data/bin/rspec-queue +17 -0
  19. data/bin/rubocop +17 -0
  20. data/bin/ruby-parse +17 -0
  21. data/bin/ruby-rewrite +17 -0
  22. data/bin/setup +8 -0
  23. data/bin/testunit-queue +17 -0
  24. data/bin/yard +17 -0
  25. data/bin/yardoc +17 -0
  26. data/bin/yri +17 -0
  27. data/lib/speculation/conj.rb +32 -0
  28. data/lib/speculation/error.rb +17 -0
  29. data/lib/speculation/gen.rb +106 -0
  30. data/lib/speculation/identifier.rb +47 -0
  31. data/lib/speculation/namespaced_symbols.rb +28 -0
  32. data/lib/speculation/pmap.rb +30 -0
  33. data/lib/speculation/spec_impl/and_spec.rb +39 -0
  34. data/lib/speculation/spec_impl/every_spec.rb +176 -0
  35. data/lib/speculation/spec_impl/f_spec.rb +121 -0
  36. data/lib/speculation/spec_impl/hash_spec.rb +215 -0
  37. data/lib/speculation/spec_impl/merge_spec.rb +40 -0
  38. data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
  39. data/lib/speculation/spec_impl/or_spec.rb +62 -0
  40. data/lib/speculation/spec_impl/regex_spec.rb +35 -0
  41. data/lib/speculation/spec_impl/spec.rb +47 -0
  42. data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
  43. data/lib/speculation/spec_impl.rb +36 -0
  44. data/lib/speculation/test.rb +553 -0
  45. data/lib/speculation/utils.rb +64 -0
  46. data/lib/speculation/utils_specs.rb +57 -0
  47. data/lib/speculation/version.rb +4 -0
  48. data/lib/speculation.rb +1308 -0
  49. data/speculation.gemspec +43 -0
  50. 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ VERSION = "0.1.0"
4
+ end