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