speculation 0.1.0 → 0.2.0

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