speculation 0.1.0 → 0.2.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.
@@ -3,15 +3,16 @@ require "pp"
3
3
 
4
4
  module Speculation
5
5
  class Error < StandardError
6
- attr_reader :data
6
+ attr_reader :data, :message
7
7
 
8
8
  def initialize(message, data)
9
9
  super(message)
10
- @data = data.merge(:cause => message)
10
+ @data = data
11
+ @message = message
11
12
  end
12
13
 
13
14
  def to_s
14
- PP.pp(@data, String.new)
15
+ "#{@message} #{PP.pp(@data, String.new)}"
15
16
  end
16
17
  end
17
18
  end
@@ -5,54 +5,13 @@ require "rantly/property"
5
5
  require "rantly/shrinks"
6
6
  require "concurrent/delay"
7
7
  require "date"
8
+ require "securerandom"
9
+ require "uri"
10
+ require "date"
11
+ require "time"
8
12
 
9
13
  module Speculation
10
- using NamespacedSymbols.refine(self)
11
-
12
14
  module Gen
13
- # @private
14
- GEN_BUILTINS = {
15
- Integer => ->(r) { r.integer },
16
- String => ->(r) { r.sized(r.range(0, 100)) { string(:alpha) } },
17
- Float => ->(_r) { rand(Float::MIN..Float::MAX) },
18
- Numeric => ->(r) { r.branch(Gen.gen_for_pred(Integer), Gen.gen_for_pred(Float)) },
19
- Symbol => ->(r) { r.sized(r.range(0, 100)) { string(:alpha).to_sym } },
20
- TrueClass => ->(_r) { true },
21
- FalseClass => ->(_r) { false },
22
- Date => ->(r) { Gen.gen_for_pred(Time).call(r).to_date },
23
- Time => ->(r) { Time.at(r.range(-569001744000, 569001744000)) }, # 20k BC => 20k AD
24
- Array => ->(r) do
25
- size = r.range(0, 20)
26
-
27
- r.array(size) do
28
- gen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time, Set[true, false]))
29
- gen.call(r)
30
- end
31
- end,
32
- Set => ->(r) do
33
- gen = Gen.gen_for_pred(Array)
34
- Set.new(gen.call(r))
35
- end,
36
- Hash => ->(r) do
37
- kgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time))
38
- vgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time, Set[true, false]))
39
- size = r.range(0, 20)
40
-
41
- h = {}
42
- r.each(size) do
43
- k = kgen.call(r)
44
- r.guard(!h.key?(k))
45
- h[k] = vgen.call(r)
46
- end
47
- h
48
- end,
49
- Enumerable => ->(r) do
50
- klass = r.choose(Array, Hash, Set)
51
- gen = Gen.gen_for_pred(klass)
52
- gen.call(r)
53
- end
54
- }.freeze
55
-
56
15
  # Adds `pred` as a Rantly `guard` to generator `gen`.
57
16
  # @param pred
58
17
  # @param gen [Proc]
@@ -77,7 +36,7 @@ module Speculation
77
36
  # @param limit [Integer] specifies how many times `gen` can fail to produce a valid value.
78
37
  # @return [Array] array of generated values using gne
79
38
  # @see https://github.com/abargnesi/rantly Rantly
80
- def self.sample(gen, n, limit = 100)
39
+ def self.sample(gen, n = 10, limit = 100)
81
40
  Rantly.map(n, limit, &gen)
82
41
  end
83
42
 
@@ -99,8 +58,53 @@ module Speculation
99
58
  delayed = Concurrent::Delay.new(&block)
100
59
 
101
60
  ->(rantly) do
102
- delayed.value.call(rantly)
61
+ delayed.value!.call(rantly)
103
62
  end
104
63
  end
64
+
65
+ # @private
66
+ GEN_BUILTINS = {
67
+ Integer => ->(r) { r.integer },
68
+ String => ->(r) { r.sized(r.range(0, 20)) { string(:alpha) } },
69
+ Float => ->(_r) { rand(Float::MIN..Float::MAX) },
70
+ Numeric => ->(r) { r.branch(Gen.gen_for_pred(Integer), Gen.gen_for_pred(Float)) },
71
+ Symbol => ->(r) { r.sized(r.range(0, 20)) { string(:alpha).to_sym } },
72
+ TrueClass => ->(_r) { true },
73
+ FalseClass => ->(_r) { false },
74
+ NilClass => ->(_r) { nil },
75
+ Date => Speculation.gen(Speculation.date_in(Date.new(1970, 1, 1)..Date.new(3000, 1, 1))),
76
+ Time => Speculation.gen(Speculation.time_in(Time.new(1970, 1, 1)..Time.new(3000, 1, 1))),
77
+ URI => ->(_r) { URI("http://#{SecureRandom.uuid}.com") },
78
+ Array => ->(r) do
79
+ size = r.range(0, 20)
80
+
81
+ r.array(size) do
82
+ gen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time, Set[true, false]))
83
+ gen.call(r)
84
+ end
85
+ end,
86
+ Set => ->(r) do
87
+ gen = Gen.gen_for_pred(Array)
88
+ Set.new(gen.call(r))
89
+ end,
90
+ Hash => ->(r) do
91
+ kgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time))
92
+ vgen = Gen.gen_for_pred(r.choose(Integer, String, Float, Symbol, Date, Time, Set[true, false]))
93
+ size = r.range(0, 20)
94
+
95
+ h = {}
96
+ r.each(size) do
97
+ k = kgen.call(r)
98
+ r.guard(!h.key?(k))
99
+ h[k] = vgen.call(r)
100
+ end
101
+ h
102
+ end,
103
+ Enumerable => ->(r) do
104
+ klass = r.choose(Array, Hash, Set)
105
+ gen = Gen.gen_for_pred(klass)
106
+ gen.call(r)
107
+ end
108
+ }.freeze
105
109
  end
106
110
  end
@@ -5,22 +5,22 @@ module Speculation
5
5
  class Identifier
6
6
  attr_reader :namespace, :name
7
7
 
8
- def initialize(namespace, name, instance_method)
8
+ def initialize(namespace, name, is_instance_method)
9
9
  @namespace = namespace
10
10
  @name = name
11
- @instance_method = instance_method
11
+ @is_instance_method = is_instance_method
12
12
  end
13
13
 
14
14
  def instance_method?
15
- @instance_method
15
+ @is_instance_method
16
16
  end
17
17
 
18
18
  def get_method
19
- @instance_method ? @namespace.instance_method(@name) : @namespace.method(@name)
19
+ @is_instance_method ? @namespace.instance_method(@name) : @namespace.method(@name)
20
20
  end
21
21
 
22
22
  def redefine_method!(new_method)
23
- if @instance_method
23
+ if @is_instance_method
24
24
  name = @name
25
25
  @namespace.class_eval { define_method(name, new_method) }
26
26
  else
@@ -29,7 +29,7 @@ module Speculation
29
29
  end
30
30
 
31
31
  def hash
32
- [@namespace, @name, @instance_method].hash
32
+ [@namespace, @name, @is_instance_method].hash
33
33
  end
34
34
 
35
35
  def ==(other)
@@ -39,7 +39,7 @@ module Speculation
39
39
  alias eql? ==
40
40
 
41
41
  def to_s
42
- sep = @instance_method ? "#" : "."
42
+ sep = @is_instance_method ? "#" : "."
43
43
  "#{@namespace}#{sep}#{@name}"
44
44
  end
45
45
  alias inspect to_s
@@ -2,27 +2,34 @@
2
2
 
3
3
  module Speculation
4
4
  module NamespacedSymbols
5
- def self.refine(namespace)
6
- Module.new do
7
- refine Symbol do
8
- define_method(:ns) do |mod = nil|
9
- if mod
10
- :"#{mod}/#{self}"
11
- else
12
- :"#{namespace}/#{self}"
13
- end
14
- end
5
+ # @param [#to_s] namespace
6
+ # @param [#to_s] name
7
+ # @return [Symbol] concatenation of `namespace` and `name`
8
+ # @example
9
+ # ns(Foo::Bar, :foo)
10
+ # # => :"Foo::Bar/baz"
11
+ def ns(name_or_namespace, name = nil)
12
+ if name
13
+ namespace = name_or_namespace
14
+ else
15
+ name = name_or_namespace
16
+ namespace = is_a?(Module) ? self.name : self.class.name
17
+ end
15
18
 
16
- def name
17
- to_s.split("/").last
18
- end
19
+ NamespacedSymbols.symbol(namespace, name)
20
+ end
19
21
 
20
- def namespace
21
- parts = to_s.split("/")
22
- parts.first if parts.count == 2
23
- end
24
- end
25
- end
22
+ def self.symbol(ns, name)
23
+ :"#{ns}/#{name}"
24
+ end
25
+
26
+ def self.name(sym)
27
+ sym.to_s.split("/").last
28
+ end
29
+
30
+ def self.namespace(sym)
31
+ parts = sym.to_s.split("/")
32
+ parts.first if parts.count == 2
26
33
  end
27
34
  end
28
35
  end
@@ -1,26 +1,25 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "concurrent"
4
3
 
5
4
  module Speculation
6
5
  # @private
7
6
  module Pmap
8
- refine Array do
9
- if RUBY_PLATFORM == "java"
10
- def pmap(&block)
11
- Pmap.pmap_jruby(self, &block)
12
- end
13
- else
14
- alias_method :pmap, :map
7
+ if RUBY_PLATFORM == "java"
8
+ def pmap(coll, &block)
9
+ Pmap.pmap_jruby(coll, &block)
10
+ end
11
+ else
12
+ def pmap(coll, &block)
13
+ coll.map(&block)
15
14
  end
16
15
  end
17
16
 
18
- def self.pmap_jruby(array, &block)
17
+ def self.pmap_jruby(coll, &block)
19
18
  thread_count = [1, Concurrent.processor_count - 1].max
20
19
  pool = Concurrent::FixedThreadPool.new(thread_count, :auto_terminate => true,
21
20
  :fallback_policy => :abort)
22
21
 
23
- array.
22
+ coll.
24
23
  map { |x| Concurrent::Future.execute(:executor => pool) { block.call(x) } }.
25
24
  map { |f| f.value || f.reason }
26
25
  ensure
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Speculation
4
- using NamespacedSymbols.refine(self)
5
-
6
4
  # @private
7
5
  class AndSpec < SpecImpl
6
+ include NamespacedSymbols
8
7
  S = Speculation
9
8
 
10
9
  def initialize(preds)
@@ -15,10 +14,10 @@ module Speculation
15
14
  end
16
15
 
17
16
  def conform(value)
18
- @specs.value.each do |spec|
17
+ @specs.value!.each do |spec|
19
18
  value = spec.conform(value)
20
19
 
21
- return :invalid.ns if S.invalid?(value)
20
+ return ns(S, :invalid) if S.invalid?(value)
22
21
  end
23
22
 
24
23
  value
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  module Speculation
3
- using NamespacedSymbols.refine(self)
4
- using Conj
5
-
6
3
  # @private
7
4
  class EverySpec < SpecImpl
5
+ include NamespacedSymbols
8
6
  S = Speculation
9
7
 
10
8
  def initialize(predicate, options)
@@ -24,22 +22,22 @@ module Speculation
24
22
  end)
25
23
  end
26
24
 
27
- @collection_predicate = ->(coll) { collection_predicates.all? { |f| f === coll } }
25
+ @collection_predicate = ->(coll) { collection_predicates.all? { |f| f.respond_to?(:call) ? f.call(coll) : f === coll } }
28
26
  @delayed_spec = Concurrent::Delay.new { S.send(:specize, predicate) }
29
- @kfn = options.fetch(:kfn.ns, ->(i, _v) { i })
27
+ @kfn = options.fetch(ns(S, :kfn), ->(i, _v) { i })
30
28
  @conform_keys, @conform_all, @kind, @gen_into, @gen_max, @distinct, @count, @min_count, @max_count =
31
- options.values_at(:conform_keys, :conform_all.ns, :kind, :into, :gen_max, :distinct, :count, :min_count, :max_count)
29
+ options.values_at(:conform_keys, ns(S, :conform_all), :kind, :into, :gen_max, :distinct, :count, :min_count, :max_count)
32
30
  @gen_max ||= 20
33
31
  @conform_into = @gen_into
34
32
 
35
33
  # returns a tuple of [init add complete] fns
36
34
  @cfns = ->(x) do
37
35
  if Utils.array?(x) && (!@conform_into || Utils.array?(@conform_into))
38
- [:itself.to_proc,
36
+ [Utils.method(:itself),
39
37
  ->(ret, i, v, cv) { v.equal?(cv) ? ret : ret.tap { |r| r[i] = cv } },
40
- :itself.to_proc]
38
+ Utils.method(:itself)]
41
39
  elsif Utils.hash?(x) && ((@kind && !@conform_into) || Utils.hash?(@conform_into))
42
- [@conform_keys ? Utils.method(:empty) : :itself.to_proc,
40
+ [@conform_keys ? Utils.method(:empty) : Utils.method(:itself),
43
41
  ->(ret, _i, v, cv) {
44
42
  if v.equal?(cv) && !@conform_keys
45
43
  ret
@@ -47,19 +45,19 @@ module Speculation
47
45
  ret.merge((@conform_keys ? cv : v).first => cv.last)
48
46
  end
49
47
  },
50
- :itself.to_proc]
48
+ Utils.method(:itself)]
51
49
  else
52
50
  [->(init) { Utils.empty(@conform_into || init) },
53
- ->(ret, _i, _v, cv) { ret.conj(cv) },
54
- :itself.to_proc]
51
+ ->(ret, _i, _v, cv) { Utils.conj(ret, cv) },
52
+ Utils.method(:itself)]
55
53
  end
56
54
  end
57
55
  end
58
56
 
59
57
  def conform(value)
60
- return :invalid.ns unless @collection_predicate.call(value)
58
+ return ns(S, :invalid) unless @collection_predicate.call(value)
61
59
 
62
- spec = @delayed_spec.value
60
+ spec = @delayed_spec.value!
63
61
 
64
62
  if @conform_all
65
63
  init, add, complete = @cfns.call(value)
@@ -70,7 +68,7 @@ module Speculation
70
68
  conformed_value = spec.conform(val)
71
69
 
72
70
  if S.invalid?(conformed_value)
73
- return :invalid.ns
71
+ return ns(S, :invalid)
74
72
  else
75
73
  return_value = add.call(return_value, index, val, conformed_value)
76
74
  end
@@ -83,7 +81,7 @@ module Speculation
83
81
 
84
82
  value.each_with_index do |item, index|
85
83
  return value if index == limit
86
- return :invalid.ns unless S.valid?(spec, item)
84
+ return ns(S, :invalid) unless S.valid?(spec, item)
87
85
  end
88
86
 
89
87
  value
@@ -94,13 +92,13 @@ module Speculation
94
92
  probs = collection_problems(value, @kind, @distinct, @count, @min_count, @max_count, path, via, inn)
95
93
  return probs if probs
96
94
 
97
- spec = @delayed_spec.value
95
+ spec = @delayed_spec.value!
98
96
 
99
97
  probs = value.lazy.each_with_index.flat_map { |v, i|
100
98
  k = @kfn.call(i, v)
101
99
 
102
100
  unless S.valid?(spec, v)
103
- S.explain1(@predicate, path, via, inn.conj(k), v)
101
+ S.explain1(@predicate, path, via, Utils.conj(inn, k), v)
104
102
  end
105
103
  }
106
104
 
@@ -158,18 +156,20 @@ module Speculation
158
156
  return S.explain1(pred, path, via, inn, x)
159
157
  end
160
158
 
161
- if count && count != x.count
162
- return [{ :path => path, :pred => "count == x.count", :val => x, :via => via, :in => inn }]
159
+ if count && !Utils.count_eq?(x, count)
160
+ return [{ :path => path, :pred => [Utils.method(:count_eq?), [x, count]], :val => x, :via => via, :in => inn }]
163
161
  end
164
162
 
165
163
  if min_count || max_count
166
- if x.count.between?(min_count || 0, max_count || Float::Infinity)
167
- return [{ :path => path, :pred => "count.between?(min_count || 0, max_count || Float::Infinity)", :val => x, :via => via, :in => inn }]
164
+ min_count ||= 0
165
+ max_count ||= Float::INFINITY
166
+ unless Utils.count_between?(x, min_count, max_count)
167
+ return [{ :path => path, :pred => [Utils.method(:count_between?), [x, min_count, max_count]], :val => x, :via => via, :in => inn }]
168
168
  end
169
169
  end
170
170
 
171
- if distinct && !x.empty? && Utils.distinct?(x)
172
- [{ :path => path, :pred => "distinct?", :val => x, :via => via, :in => inn }]
171
+ if distinct && !x.empty? && !Utils.distinct?(x)
172
+ [{ :path => path, :pred => [Utils.method(:distinct?), [x]], :val => x, :via => via, :in => inn }]
173
173
  end
174
174
  end
175
175
  end
@@ -1,62 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Speculation
4
- using NamespacedSymbols.refine(self)
5
- using Conj
6
-
7
4
  # @private
8
5
  class FSpec < SpecImpl
6
+ include NamespacedSymbols
9
7
  S = Speculation
10
8
 
11
- attr_reader :argspec, :retspec, :fnspec, :blockspec
9
+ attr_reader :args, :ret, :fn, :block
12
10
 
13
- def initialize(argspec: nil, retspec: nil, fnspec: nil, blockspec: nil)
14
- @argspec = argspec
15
- @retspec = retspec
16
- @fnspec = fnspec
17
- @blockspec = blockspec
11
+ def initialize(args: nil, ret: nil, fn: nil, block: nil)
12
+ @args = args
13
+ @ret = ret
14
+ @fn = fn
15
+ @block = block
18
16
  end
19
17
 
20
18
  def conform(f)
21
- raise "Can't conform fspec without args spec: #{inspect}" unless @argspec
19
+ raise "Can't conform fspec without args spec: #{inspect}" unless @args
22
20
 
23
- return :invalid.ns unless f.is_a?(Proc) || f.is_a?(Method)
21
+ return ns(S, :invalid) unless f.is_a?(Proc) || f.is_a?(Method)
24
22
 
25
- specs = { :args => @argspec, :ret => @retspec, :fn => @fnspec, :block => @blockspec }
23
+ specs = { :args => @args, :ret => @ret, :fn => @fn, :block => @block }
26
24
 
27
25
  if f.equal?(FSpec.validate_fn(f, specs, S.fspec_iterations))
28
26
  f
29
27
  else
30
- :invalid.ns
28
+ ns(S, :invalid)
31
29
  end
32
30
  end
33
31
 
34
32
  def explain(path, via, inn, f)
35
33
  unless f.respond_to?(:call)
36
- return [{ :path => path, :pred => "respond_to?(:call)", :val => f, :via => via, :in => inn }]
34
+ return [{ :path => path, :pred => [f.method(:respond_to?), [:call]], :val => f, :via => via, :in => inn }]
37
35
  end
38
36
 
39
- specs = { :args => @argspec, :ret => @retspec, :fn => @fnspec, :block => @blockspec }
40
- args, block = FSpec.validate_fn(f, specs, 100)
41
- return if f.equal?(args)
37
+ specs = { :args => @args, :ret => @ret, :fn => @fn, :block => @block }
38
+ validate_fn_result = FSpec.validate_fn(f, specs, 100)
39
+ return if f.equal?(validate_fn_result)
42
40
 
43
- ret = begin
44
- f.call(*args, &block)
45
- rescue => e
46
- e
47
- end
41
+ ret = f.call(*validate_fn_result[:args], &validate_fn_result[:block]) rescue $!
48
42
 
49
43
  if ret.is_a?(Exception)
50
- val = block ? [args, block] : args
51
- return [{ :path => path, :pred => "f.call(*args)", :val => val, :reason => ret.message.chomp, :via => via, :in => inn }]
44
+ # no args available for pred
45
+ pred = [f, validate_fn_result[:args]]
46
+ pred << validate_fn_result[:block] if validate_fn_result[:block]
47
+ return [{ :path => path, :pred => pred, :val => validate_fn_result, :reason => ret.message.chomp, :via => via, :in => inn }]
52
48
  end
53
49
 
54
- cret = S.dt(@retspec, ret)
55
- return S.explain1(@retspec, path.conj(:ret), via, inn, ret) if S.invalid?(cret)
50
+ cret = S.dt(@ret, ret)
51
+ return S.explain1(@ret, Utils.conj(path, :ret), via, inn, ret) if S.invalid?(cret)
56
52
 
57
- if @fnspec
58
- cargs = S.conform(@argspec, args)
59
- S.explain1(@fnspec, path.conj(:fn), via, inn, :args => cargs, :ret => cret)
53
+ if @fn
54
+ cargs = S.conform(@args, args)
55
+ S.explain1(@fn, Utils.conj(path, :fn), via, inn, :args => cargs, :ret => cret)
60
56
  end
61
57
  end
62
58
 
@@ -65,15 +61,15 @@ module Speculation
65
61
 
66
62
  ->(_rantly) do
67
63
  ->(*args, &block) do
68
- unless S.pvalid?(@argspec, args)
69
- raise S.explain_str(@argspec, args)
64
+ unless S.pvalid?(@args, args)
65
+ raise S.explain_str(@args, args)
70
66
  end
71
67
 
72
- if @blockspec && !S.pvalid?(@blockspec, block)
73
- raise S.explain_str(@blockspec, block)
68
+ if @block && !S.pvalid?(@block, block)
69
+ raise S.explain_str(@block, block)
74
70
  end
75
71
 
76
- S::Gen.generate(S.gen(@retspec, overrides))
72
+ S::Gen.generate(S.gen(@ret, overrides))
77
73
  end
78
74
  end
79
75
  end
@@ -91,7 +87,8 @@ module Speculation
91
87
 
92
88
  combined = ->(r) { [args_gen.call(r), block_gen.call(r)] }
93
89
 
94
- ret = S::Test.send(:rantly_quick_check, combined, iterations) { |(args, block)|
90
+ generator_guard = ->(genned_val) { S.valid?(specs[:args], genned_val) }
91
+ ret = S::Test.send(:rantly_quick_check, combined, iterations, generator_guard) { |(args, block)|
95
92
  call_valid?(f, specs, args, block)
96
93
  }
97
94