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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
4
+ # @private
5
+ module Conj
6
+ refine Array do
7
+ def conj(x)
8
+ self + [x]
9
+ end
10
+ end
11
+
12
+ refine Set do
13
+ def conj(x)
14
+ self + Set[x]
15
+ end
16
+ end
17
+
18
+ refine Hash do
19
+ def conj(x)
20
+ if Utils.array?(x)
21
+ unless x.count == 2
22
+ raise ArgumentError, "Array arg to conj must be a pair"
23
+ end
24
+
25
+ merge(x[0] => x[1])
26
+ else
27
+ merge(x)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ require "pp"
3
+
4
+ module Speculation
5
+ class Error < StandardError
6
+ attr_reader :data
7
+
8
+ def initialize(message, data)
9
+ super(message)
10
+ @data = data.merge(:cause => message)
11
+ end
12
+
13
+ def to_s
14
+ PP.pp(@data, String.new)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+ require "rantly"
4
+ require "rantly/property"
5
+ require "rantly/shrinks"
6
+ require "concurrent/delay"
7
+ require "date"
8
+
9
+ module Speculation
10
+ using NamespacedSymbols.refine(self)
11
+
12
+ 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
+ # Adds `pred` as a Rantly `guard` to generator `gen`.
57
+ # @param pred
58
+ # @param gen [Proc]
59
+ # @return [Proc]
60
+ # @see https://github.com/abargnesi/rantly Rantly
61
+ def self.such_that(pred, gen)
62
+ ->(rantly) do
63
+ gen.call(rantly).tap { |val| rantly.guard(pred.call(val)) }
64
+ end
65
+ end
66
+
67
+ # @param gen [Proc] Rantly generator
68
+ # @param limit [Integer] specifies how many times `gen` can fail to produce a valid value.
69
+ # @return single value using gen
70
+ # @see https://github.com/abargnesi/rantly Rantly
71
+ def self.generate(gen, limit = 100)
72
+ Rantly.value(limit, &gen)
73
+ end
74
+
75
+ # Generate `n` values using `gen`
76
+ # @param gen [Proc] Rantly generator
77
+ # @param limit [Integer] specifies how many times `gen` can fail to produce a valid value.
78
+ # @return [Array] array of generated values using gne
79
+ # @see https://github.com/abargnesi/rantly Rantly
80
+ def self.sample(gen, n, limit = 100)
81
+ Rantly.map(n, limit, &gen)
82
+ end
83
+
84
+ # Given a predicate, returns a built-in generator if one exists.
85
+ # @param pred
86
+ # @return [Proc] built-in generator for pred
87
+ # @return nil if no built-in generator found
88
+ # @see https://github.com/abargnesi/rantly Rantly
89
+ def self.gen_for_pred(pred)
90
+ if pred.is_a?(Set)
91
+ ->(r) { r.choose(*pred) }
92
+ else
93
+ GEN_BUILTINS[pred]
94
+ end
95
+ end
96
+
97
+ # @private
98
+ def self.delay(&block)
99
+ delayed = Concurrent::Delay.new(&block)
100
+
101
+ ->(rantly) do
102
+ delayed.value.call(rantly)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
4
+ # @private
5
+ class Identifier
6
+ attr_reader :namespace, :name
7
+
8
+ def initialize(namespace, name, instance_method)
9
+ @namespace = namespace
10
+ @name = name
11
+ @instance_method = instance_method
12
+ end
13
+
14
+ def instance_method?
15
+ @instance_method
16
+ end
17
+
18
+ def get_method
19
+ @instance_method ? @namespace.instance_method(@name) : @namespace.method(@name)
20
+ end
21
+
22
+ def redefine_method!(new_method)
23
+ if @instance_method
24
+ name = @name
25
+ @namespace.class_eval { define_method(name, new_method) }
26
+ else
27
+ @namespace.define_singleton_method(@name, new_method)
28
+ end
29
+ end
30
+
31
+ def hash
32
+ [@namespace, @name, @instance_method].hash
33
+ end
34
+
35
+ def ==(other)
36
+ self.class === other &&
37
+ other.hash == hash
38
+ end
39
+ alias eql? ==
40
+
41
+ def to_s
42
+ sep = @instance_method ? "#" : "."
43
+ "#{@namespace}#{sep}#{@name}"
44
+ end
45
+ alias inspect to_s
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
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
15
+
16
+ def name
17
+ to_s.split("/").last
18
+ end
19
+
20
+ def namespace
21
+ parts = to_s.split("/")
22
+ parts.first if parts.count == 2
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Speculation
6
+ # @private
7
+ 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
15
+ end
16
+ end
17
+
18
+ def self.pmap_jruby(array, &block)
19
+ thread_count = [1, Concurrent.processor_count - 1].max
20
+ pool = Concurrent::FixedThreadPool.new(thread_count, :auto_terminate => true,
21
+ :fallback_policy => :abort)
22
+
23
+ array.
24
+ map { |x| Concurrent::Future.execute(:executor => pool) { block.call(x) } }.
25
+ map { |f| f.value || f.reason }
26
+ ensure
27
+ pool.shutdown
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
4
+ using NamespacedSymbols.refine(self)
5
+
6
+ # @private
7
+ class AndSpec < SpecImpl
8
+ S = Speculation
9
+
10
+ def initialize(preds)
11
+ @preds = preds
12
+ @specs = Concurrent::Delay.new do
13
+ preds.map { |pred| S.send(:specize, pred) }
14
+ end
15
+ end
16
+
17
+ def conform(value)
18
+ @specs.value.each do |spec|
19
+ value = spec.conform(value)
20
+
21
+ return :invalid.ns if S.invalid?(value)
22
+ end
23
+
24
+ value
25
+ end
26
+
27
+ def explain(path, via, inn, value)
28
+ S.explain_pred_list(@preds, path, via, inn, value)
29
+ end
30
+
31
+ def gen(overrides, path, rmap)
32
+ if @gen
33
+ @gen
34
+ else
35
+ S.gensub(@preds.first, overrides, path, rmap)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+ module Speculation
3
+ using NamespacedSymbols.refine(self)
4
+ using Conj
5
+
6
+ # @private
7
+ class EverySpec < SpecImpl
8
+ S = Speculation
9
+
10
+ def initialize(predicate, options)
11
+ @predicate = predicate
12
+ @options = options
13
+
14
+ collection_predicates = [options.fetch(:kind, Enumerable)]
15
+
16
+ if options.key?(:count)
17
+ collection_predicates.push(->(coll) { coll.count == options[:count] })
18
+ elsif options.key?(:min_count) || options.key?(:max_count)
19
+ collection_predicates.push(->(coll) do
20
+ min = options.fetch(:min_count, 0)
21
+ max = options.fetch(:max_count, Float::INFINITY)
22
+
23
+ coll.count.between?(min, max)
24
+ end)
25
+ end
26
+
27
+ @collection_predicate = ->(coll) { collection_predicates.all? { |f| f === coll } }
28
+ @delayed_spec = Concurrent::Delay.new { S.send(:specize, predicate) }
29
+ @kfn = options.fetch(:kfn.ns, ->(i, _v) { i })
30
+ @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)
32
+ @gen_max ||= 20
33
+ @conform_into = @gen_into
34
+
35
+ # returns a tuple of [init add complete] fns
36
+ @cfns = ->(x) do
37
+ if Utils.array?(x) && (!@conform_into || Utils.array?(@conform_into))
38
+ [:itself.to_proc,
39
+ ->(ret, i, v, cv) { v.equal?(cv) ? ret : ret.tap { |r| r[i] = cv } },
40
+ :itself.to_proc]
41
+ elsif Utils.hash?(x) && ((@kind && !@conform_into) || Utils.hash?(@conform_into))
42
+ [@conform_keys ? Utils.method(:empty) : :itself.to_proc,
43
+ ->(ret, _i, v, cv) {
44
+ if v.equal?(cv) && !@conform_keys
45
+ ret
46
+ else
47
+ ret.merge((@conform_keys ? cv : v).first => cv.last)
48
+ end
49
+ },
50
+ :itself.to_proc]
51
+ else
52
+ [->(init) { Utils.empty(@conform_into || init) },
53
+ ->(ret, _i, _v, cv) { ret.conj(cv) },
54
+ :itself.to_proc]
55
+ end
56
+ end
57
+ end
58
+
59
+ def conform(value)
60
+ return :invalid.ns unless @collection_predicate.call(value)
61
+
62
+ spec = @delayed_spec.value
63
+
64
+ if @conform_all
65
+ init, add, complete = @cfns.call(value)
66
+
67
+ return_value = init.call(value)
68
+
69
+ value.each_with_index do |val, index|
70
+ conformed_value = spec.conform(val)
71
+
72
+ if S.invalid?(conformed_value)
73
+ return :invalid.ns
74
+ else
75
+ return_value = add.call(return_value, index, val, conformed_value)
76
+ end
77
+ end
78
+
79
+ complete.call(return_value)
80
+ else
81
+ # OPTIMIZE: check if value is indexed (array, hash etc.) vs not indexed (list, custom enumerable)
82
+ limit = S.coll_check_limit
83
+
84
+ value.each_with_index do |item, index|
85
+ return value if index == limit
86
+ return :invalid.ns unless S.valid?(spec, item)
87
+ end
88
+
89
+ value
90
+ end
91
+ end
92
+
93
+ def explain(path, via, inn, value)
94
+ probs = collection_problems(value, @kind, @distinct, @count, @min_count, @max_count, path, via, inn)
95
+ return probs if probs
96
+
97
+ spec = @delayed_spec.value
98
+
99
+ probs = value.lazy.each_with_index.flat_map { |v, i|
100
+ k = @kfn.call(i, v)
101
+
102
+ unless S.valid?(spec, v)
103
+ S.explain1(@predicate, path, via, inn.conj(k), v)
104
+ end
105
+ }
106
+
107
+ probs = @conform_all ? probs.to_a : probs.take(S.coll_error_limit)
108
+ probs.compact
109
+ end
110
+
111
+ def gen(overrides, path, rmap)
112
+ return @gen if @gen
113
+
114
+ pgen = S.gensub(@predicate, overrides, path, rmap)
115
+
116
+ ->(rantly) do
117
+ init = if @gen_into
118
+ Utils.empty(@gen_into)
119
+ elsif @kind
120
+ Utils.empty(S.gensub(@kind, overrides, path, rmap).call(rantly))
121
+ else
122
+ []
123
+ end
124
+
125
+ val = if @distinct
126
+ if @count
127
+ rantly.array(@count, &pgen).tap { |arr| rantly.guard(Utils.distinct?(arr)) }
128
+ else
129
+ min = @min_count || 0
130
+ max = @max_count || [@gen_max, 2 * min].max
131
+ count = rantly.range(min, max)
132
+
133
+ rantly.array(count, &pgen).tap { |arr| rantly.guard(Utils.distinct?(arr)) }
134
+ end
135
+ elsif @count
136
+ rantly.array(@count, &pgen)
137
+ elsif @min_count || @max_count
138
+ min = @min_count || 0
139
+ max = @max_count || [@gen_max, 2 * min].max
140
+ count = rantly.range(min, max)
141
+
142
+ rantly.array(count, &pgen)
143
+ else
144
+ count = rantly.range(0, @gen_max)
145
+ rantly.array(count, &pgen)
146
+ end
147
+
148
+ Utils.into(init, val)
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def collection_problems(x, kfn, distinct, count, min_count, max_count, path, via, inn)
155
+ pred = kfn || Utils.method(:collection?)
156
+
157
+ unless S.pvalid?(pred, x)
158
+ return S.explain1(pred, path, via, inn, x)
159
+ end
160
+
161
+ if count && count != x.count
162
+ return [{ :path => path, :pred => "count == x.count", :val => x, :via => via, :in => inn }]
163
+ end
164
+
165
+ 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 }]
168
+ end
169
+ end
170
+
171
+ if distinct && !x.empty? && Utils.distinct?(x)
172
+ [{ :path => path, :pred => "distinct?", :val => x, :via => via, :in => inn }]
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speculation
4
+ using NamespacedSymbols.refine(self)
5
+ using Conj
6
+
7
+ # @private
8
+ class FSpec < SpecImpl
9
+ S = Speculation
10
+
11
+ attr_reader :argspec, :retspec, :fnspec, :blockspec
12
+
13
+ def initialize(argspec: nil, retspec: nil, fnspec: nil, blockspec: nil)
14
+ @argspec = argspec
15
+ @retspec = retspec
16
+ @fnspec = fnspec
17
+ @blockspec = blockspec
18
+ end
19
+
20
+ def conform(f)
21
+ raise "Can't conform fspec without args spec: #{inspect}" unless @argspec
22
+
23
+ return :invalid.ns unless f.is_a?(Proc) || f.is_a?(Method)
24
+
25
+ specs = { :args => @argspec, :ret => @retspec, :fn => @fnspec, :block => @blockspec }
26
+
27
+ if f.equal?(FSpec.validate_fn(f, specs, S.fspec_iterations))
28
+ f
29
+ else
30
+ :invalid.ns
31
+ end
32
+ end
33
+
34
+ def explain(path, via, inn, f)
35
+ unless f.respond_to?(:call)
36
+ return [{ :path => path, :pred => "respond_to?(:call)", :val => f, :via => via, :in => inn }]
37
+ end
38
+
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)
42
+
43
+ ret = begin
44
+ f.call(*args, &block)
45
+ rescue => e
46
+ e
47
+ end
48
+
49
+ 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 }]
52
+ end
53
+
54
+ cret = S.dt(@retspec, ret)
55
+ return S.explain1(@retspec, path.conj(:ret), via, inn, ret) if S.invalid?(cret)
56
+
57
+ if @fnspec
58
+ cargs = S.conform(@argspec, args)
59
+ S.explain1(@fnspec, path.conj(:fn), via, inn, :args => cargs, :ret => cret)
60
+ end
61
+ end
62
+
63
+ def gen(overrides, _path, _rmap)
64
+ return @gen if @gen
65
+
66
+ ->(_rantly) do
67
+ ->(*args, &block) do
68
+ unless S.pvalid?(@argspec, args)
69
+ raise S.explain_str(@argspec, args)
70
+ end
71
+
72
+ if @blockspec && !S.pvalid?(@blockspec, block)
73
+ raise S.explain_str(@blockspec, block)
74
+ end
75
+
76
+ S::Gen.generate(S.gen(@retspec, overrides))
77
+ end
78
+ end
79
+ end
80
+
81
+ # @private
82
+ # returns f if valid, else smallest
83
+ def self.validate_fn(f, specs, iterations)
84
+ args_gen = S.gen(specs[:args])
85
+
86
+ block_gen = if specs[:block]
87
+ S.gen(specs[:block])
88
+ else
89
+ Utils.constantly(nil)
90
+ end
91
+
92
+ combined = ->(r) { [args_gen.call(r), block_gen.call(r)] }
93
+
94
+ ret = S::Test.send(:rantly_quick_check, combined, iterations) { |(args, block)|
95
+ call_valid?(f, specs, args, block)
96
+ }
97
+
98
+ smallest = ret[:shrunk] && ret[:shrunk][:smallest]
99
+ smallest || f
100
+ end
101
+
102
+ def self.call_valid?(f, specs, args, block)
103
+ cargs = S.conform(specs[:args], args)
104
+ return if S.invalid?(cargs)
105
+
106
+ if specs[:block]
107
+ cblock = S.conform(specs[:block], block)
108
+ return if S.invalid?(cblock)
109
+ end
110
+
111
+ ret = f.call(*args, &block)
112
+
113
+ cret = S.conform(specs[:ret], ret)
114
+ return if S.invalid?(cret)
115
+
116
+ return true unless specs[:fn]
117
+
118
+ S.pvalid?(specs[:fn], :args => cargs, :block => block, :ret => cret)
119
+ end
120
+ end
121
+ end