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