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.
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