speculation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +87 -0
- data/.travis.yml +16 -0
- data/.yardopts +3 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +29 -0
- data/bin/bundler +17 -0
- data/bin/byebug +17 -0
- data/bin/coderay +17 -0
- data/bin/console +70 -0
- data/bin/cucumber-queue +17 -0
- data/bin/minitest-queue +17 -0
- data/bin/pry +17 -0
- data/bin/rake +17 -0
- data/bin/rspec-queue +17 -0
- data/bin/rubocop +17 -0
- data/bin/ruby-parse +17 -0
- data/bin/ruby-rewrite +17 -0
- data/bin/setup +8 -0
- data/bin/testunit-queue +17 -0
- data/bin/yard +17 -0
- data/bin/yardoc +17 -0
- data/bin/yri +17 -0
- data/lib/speculation/conj.rb +32 -0
- data/lib/speculation/error.rb +17 -0
- data/lib/speculation/gen.rb +106 -0
- data/lib/speculation/identifier.rb +47 -0
- data/lib/speculation/namespaced_symbols.rb +28 -0
- data/lib/speculation/pmap.rb +30 -0
- data/lib/speculation/spec_impl/and_spec.rb +39 -0
- data/lib/speculation/spec_impl/every_spec.rb +176 -0
- data/lib/speculation/spec_impl/f_spec.rb +121 -0
- data/lib/speculation/spec_impl/hash_spec.rb +215 -0
- data/lib/speculation/spec_impl/merge_spec.rb +40 -0
- data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
- data/lib/speculation/spec_impl/or_spec.rb +62 -0
- data/lib/speculation/spec_impl/regex_spec.rb +35 -0
- data/lib/speculation/spec_impl/spec.rb +47 -0
- data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
- data/lib/speculation/spec_impl.rb +36 -0
- data/lib/speculation/test.rb +553 -0
- data/lib/speculation/utils.rb +64 -0
- data/lib/speculation/utils_specs.rb +57 -0
- data/lib/speculation/version.rb +4 -0
- data/lib/speculation.rb +1308 -0
- data/speculation.gemspec +43 -0
- 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
|