speculation 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -0
- data/.travis.yml +2 -4
- data/README.md +22 -12
- data/Rakefile +5 -9
- data/bin/console +54 -7
- data/examples/codebreaker.rb +246 -0
- data/examples/spec_guide.rb +1288 -0
- data/lib/speculation.rb +145 -146
- data/lib/speculation/error.rb +4 -3
- data/lib/speculation/gen.rb +51 -47
- data/lib/speculation/identifier.rb +7 -7
- data/lib/speculation/namespaced_symbols.rb +26 -19
- data/lib/speculation/pmap.rb +9 -10
- data/lib/speculation/spec_impl/and_spec.rb +3 -4
- data/lib/speculation/spec_impl/every_spec.rb +24 -24
- data/lib/speculation/spec_impl/f_spec.rb +32 -35
- data/lib/speculation/spec_impl/hash_spec.rb +33 -41
- data/lib/speculation/spec_impl/merge_spec.rb +2 -3
- data/lib/speculation/spec_impl/nilable_spec.rb +8 -9
- data/lib/speculation/spec_impl/or_spec.rb +5 -7
- data/lib/speculation/spec_impl/regex_spec.rb +2 -3
- data/lib/speculation/spec_impl/spec.rb +3 -5
- data/lib/speculation/spec_impl/tuple_spec.rb +8 -10
- data/lib/speculation/test.rb +126 -101
- data/lib/speculation/utils.rb +31 -5
- data/lib/speculation/version.rb +1 -1
- data/speculation.gemspec +0 -1
- metadata +30 -44
- data/lib/speculation/conj.rb +0 -32
- data/lib/speculation/utils_specs.rb +0 -57
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Speculation
|
3
|
-
using Speculation::NamespacedSymbols.refine(self)
|
4
|
-
using Conj
|
5
|
-
|
6
3
|
# @private
|
7
4
|
class HashSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
8
6
|
S = Speculation
|
9
7
|
|
10
8
|
attr_reader :id
|
@@ -19,7 +17,8 @@ module Speculation
|
|
19
17
|
req_keys = req.flat_map(&method(:extract_keys))
|
20
18
|
req_un_specs = req_un.flat_map(&method(:extract_keys))
|
21
19
|
|
22
|
-
|
20
|
+
all_keys = req_keys + req_un_specs + opt + opt_un
|
21
|
+
unless all_keys.all? { |s| s.is_a?(Symbol) && NamespacedSymbols.namespace(s) }
|
23
22
|
raise "all keys must be namespaced Symbols"
|
24
23
|
end
|
25
24
|
|
@@ -27,8 +26,8 @@ module Speculation
|
|
27
26
|
req_keys += req_un_specs.map(&method(:unqualify_key))
|
28
27
|
|
29
28
|
pred_exprs = [Utils.method(:hash?)]
|
30
|
-
pred_exprs.push(->(v) { parse_req(req, v, :itself.
|
31
|
-
pred_exprs.push(->(v) { parse_req(req_un, v, method(:unqualify_key))
|
29
|
+
pred_exprs.push(->(v) { parse_req(req, v, Utils.method(:itself)).empty? }) if req.any?
|
30
|
+
pred_exprs.push(->(v) { parse_req(req_un, v, method(:unqualify_key)).empty? }) if req_un.any?
|
32
31
|
|
33
32
|
@req_keys = req_keys
|
34
33
|
@req_specs = req_specs
|
@@ -39,7 +38,7 @@ module Speculation
|
|
39
38
|
end
|
40
39
|
|
41
40
|
def conform(value)
|
42
|
-
return :invalid
|
41
|
+
return ns(S, :invalid) unless @keys_pred.call(value)
|
43
42
|
|
44
43
|
reg = S.registry
|
45
44
|
ret = value
|
@@ -53,7 +52,7 @@ module Speculation
|
|
53
52
|
conformed_value = S.conform(spec, v)
|
54
53
|
|
55
54
|
if S.invalid?(conformed_value)
|
56
|
-
return :invalid
|
55
|
+
return ns(S, :invalid)
|
57
56
|
else
|
58
57
|
unless conformed_value.equal?(v)
|
59
58
|
ret = ret.merge(key => conformed_value)
|
@@ -66,30 +65,27 @@ module Speculation
|
|
66
65
|
|
67
66
|
def explain(path, via, inn, value)
|
68
67
|
unless Utils.hash?(value)
|
69
|
-
return [{ :path => path, :pred => :hash
|
68
|
+
return [{ :path => path, :pred => [Utils.method(:hash?), [value]], :val => value, :via => via, :in => inn }]
|
70
69
|
end
|
71
70
|
|
72
71
|
problems = []
|
73
72
|
|
74
73
|
if @req.any?
|
75
|
-
|
74
|
+
failures = parse_req(@req, value, Utils.method(:itself))
|
76
75
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
end
|
76
|
+
failures.each do |failure_sexp|
|
77
|
+
# eww
|
78
|
+
pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]]
|
79
|
+
problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
|
82
80
|
end
|
83
81
|
end
|
84
82
|
|
85
83
|
if @req_un.any?
|
86
|
-
|
84
|
+
failures = parse_req(@req_un, value, method(:unqualify_key))
|
87
85
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
|
92
|
-
end
|
86
|
+
failures.each do |failure_sexp|
|
87
|
+
pred = [Utils.method(:key?), [sexp_to_rb(failure_sexp)]]
|
88
|
+
problems << { :path => path, :pred => pred, :val => value, :via => via, :in => inn }
|
93
89
|
end
|
94
90
|
end
|
95
91
|
|
@@ -97,7 +93,7 @@ module Speculation
|
|
97
93
|
next unless S.registry.key?(@key_to_spec_map[k])
|
98
94
|
|
99
95
|
unless S.pvalid?(@key_to_spec_map.fetch(k), v)
|
100
|
-
S.explain1(@key_to_spec_map.fetch(k),
|
96
|
+
S.explain1(@key_to_spec_map.fetch(k), Utils.conj(path, k), via, Utils.conj(inn, k), v)
|
101
97
|
end
|
102
98
|
}
|
103
99
|
|
@@ -115,7 +111,7 @@ module Speculation
|
|
115
111
|
|
116
112
|
reqs = @req_keys.zip(@req_specs).
|
117
113
|
reduce({}) { |m, (k, s)|
|
118
|
-
m.merge(k => S.gensub(s, overrides,
|
114
|
+
m.merge(k => S.gensub(s, overrides, Utils.conj(path, k), rmap))
|
119
115
|
}
|
120
116
|
|
121
117
|
opts = @opt_keys.zip(@opt_specs).
|
@@ -123,13 +119,13 @@ module Speculation
|
|
123
119
|
if S.recur_limit?(rmap, @id, path, k)
|
124
120
|
m
|
125
121
|
else
|
126
|
-
m.merge(k => Gen.delay { S.gensub(s, overrides,
|
122
|
+
m.merge(k => Gen.delay { S.gensub(s, overrides, Utils.conj(path, k), rmap) })
|
127
123
|
end
|
128
124
|
}
|
129
125
|
|
130
126
|
->(rantly) do
|
131
127
|
count = rantly.range(0, opts.count)
|
132
|
-
opts = opts.to_a.shuffle.take(count)
|
128
|
+
opts = Hash[opts.to_a.shuffle.take(count)]
|
133
129
|
|
134
130
|
reqs.merge(opts).each_with_object({}) { |(k, spec_gen), h|
|
135
131
|
h[k] = spec_gen.call(rantly)
|
@@ -148,17 +144,17 @@ module Speculation
|
|
148
144
|
|
149
145
|
keys.each_with_index do |key, i|
|
150
146
|
unless i.zero?
|
151
|
-
rb_string << " #{
|
147
|
+
rb_string << " #{NamespacedSymbols.name(op)} "
|
152
148
|
end
|
153
149
|
|
154
|
-
rb_string << sexp_to_rb(key, level + 1)
|
150
|
+
rb_string << sexp_to_rb(key, level + 1).to_s
|
155
151
|
end
|
156
152
|
|
157
153
|
rb_string << ")" unless level.zero?
|
158
154
|
|
159
155
|
rb_string
|
160
156
|
else
|
161
|
-
|
157
|
+
sexp
|
162
158
|
end
|
163
159
|
end
|
164
160
|
|
@@ -171,7 +167,7 @@ module Speculation
|
|
171
167
|
end
|
172
168
|
|
173
169
|
def unqualify_key(x)
|
174
|
-
|
170
|
+
NamespacedSymbols.name(x).to_sym
|
175
171
|
end
|
176
172
|
|
177
173
|
def parse_req(ks, v, f)
|
@@ -180,15 +176,15 @@ module Speculation
|
|
180
176
|
ret = if key.is_a?(Array)
|
181
177
|
op, *kks = key
|
182
178
|
case op
|
183
|
-
when :or
|
184
|
-
if kks.one? { |k| parse_req([k], v, f)
|
185
|
-
|
179
|
+
when ns(S, :or)
|
180
|
+
if kks.one? { |k| parse_req([k], v, f).empty? }
|
181
|
+
[]
|
186
182
|
else
|
187
183
|
[key]
|
188
184
|
end
|
189
|
-
when :and
|
190
|
-
if kks.all? { |k| parse_req([k], v, f)
|
191
|
-
|
185
|
+
when ns(S, :and)
|
186
|
+
if kks.all? { |k| parse_req([k], v, f).empty? }
|
187
|
+
[]
|
192
188
|
else
|
193
189
|
[key]
|
194
190
|
end
|
@@ -196,17 +192,13 @@ module Speculation
|
|
196
192
|
raise "Expected or, and, got #{op}"
|
197
193
|
end
|
198
194
|
elsif v.key?(f.call(key))
|
199
|
-
|
195
|
+
[]
|
200
196
|
else
|
201
197
|
[key]
|
202
198
|
end
|
203
199
|
|
204
200
|
if ks.any?
|
205
|
-
|
206
|
-
parse_req(ks, v, f)
|
207
|
-
else
|
208
|
-
ret + parse_req(ks, v, f)
|
209
|
-
end
|
201
|
+
ret + parse_req(ks, v, f)
|
210
202
|
else
|
211
203
|
ret
|
212
204
|
end
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Speculation
|
3
|
-
using NamespacedSymbols.refine(self)
|
4
|
-
|
5
3
|
# @private
|
6
4
|
class MergeSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
7
6
|
S = Speculation
|
8
7
|
|
9
8
|
def initialize(preds)
|
@@ -14,7 +13,7 @@ module Speculation
|
|
14
13
|
ms = @preds.map { |pred| S.dt(pred, x) }
|
15
14
|
|
16
15
|
if ms.any?(&S.method(:invalid?))
|
17
|
-
|
16
|
+
ns(S, :invalid)
|
18
17
|
else
|
19
18
|
ms.reduce(&:merge)
|
20
19
|
end
|
@@ -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 NilableSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
8
6
|
S = Speculation
|
9
7
|
|
10
8
|
def initialize(pred)
|
@@ -13,15 +11,16 @@ module Speculation
|
|
13
11
|
end
|
14
12
|
|
15
13
|
def conform(value)
|
16
|
-
value.nil? ? value : @delayed_spec.value
|
14
|
+
value.nil? ? value : @delayed_spec.value!.conform(value)
|
17
15
|
end
|
18
16
|
|
19
17
|
def explain(path, via, inn, value)
|
20
|
-
return if S.pvalid?(@delayed_spec.value
|
18
|
+
return if S.pvalid?(@delayed_spec.value!, value) || value.nil?
|
21
19
|
|
22
|
-
|
23
|
-
explain1(@pred,
|
24
|
-
|
20
|
+
Utils.conj(
|
21
|
+
S.explain1(@pred, Utils.conj(path, ns(S, :pred)), via, inn, value),
|
22
|
+
:path => Utils.conj(path, ns(S, :nil)), :pred => [NilClass, [value]], :val => value, :via => via, :in => inn
|
23
|
+
)
|
25
24
|
end
|
26
25
|
|
27
26
|
def gen(overrides, path, rmap)
|
@@ -29,7 +28,7 @@ module Speculation
|
|
29
28
|
|
30
29
|
->(rantly) do
|
31
30
|
rantly.freq([1, Gen.delay { Utils.constantly(nil) }],
|
32
|
-
[9, Gen.delay { S.gensub(@pred, overrides,
|
31
|
+
[9, Gen.delay { S.gensub(@pred, overrides, Utils.conj(path, ns(S, :pred)), rmap) }])
|
33
32
|
end
|
34
33
|
end
|
35
34
|
end
|
@@ -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 OrSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
8
6
|
S = Speculation
|
9
7
|
|
10
8
|
attr_reader :id
|
@@ -21,7 +19,7 @@ module Speculation
|
|
21
19
|
end
|
22
20
|
|
23
21
|
def conform(value)
|
24
|
-
@delayed_specs.value
|
22
|
+
@delayed_specs.value!.each_with_index do |spec, index|
|
25
23
|
conformed = spec.conform(value)
|
26
24
|
|
27
25
|
unless S.invalid?(conformed)
|
@@ -29,7 +27,7 @@ module Speculation
|
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
32
|
-
:invalid
|
30
|
+
ns(S, :invalid)
|
33
31
|
end
|
34
32
|
|
35
33
|
def explain(path, via, inn, value)
|
@@ -37,7 +35,7 @@ module Speculation
|
|
37
35
|
|
38
36
|
@keys.zip(@preds).flat_map do |(key, pred)|
|
39
37
|
next if S.pvalid?(pred, value)
|
40
|
-
S.explain1(pred,
|
38
|
+
S.explain1(pred, Utils.conj(path, key), via, inn, value)
|
41
39
|
end
|
42
40
|
end
|
43
41
|
|
@@ -49,7 +47,7 @@ module Speculation
|
|
49
47
|
rmap = S.inck(rmap, @id)
|
50
48
|
|
51
49
|
unless S.recur_limit?(rmap, @id, path, k)
|
52
|
-
Gen.delay { S.gensub(p, overrides,
|
50
|
+
Gen.delay { S.gensub(p, overrides, Utils.conj(path, k), rmap) }
|
53
51
|
end
|
54
52
|
}.
|
55
53
|
compact
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Speculation
|
3
|
-
using NamespacedSymbols.refine(self)
|
4
|
-
|
5
3
|
# @private
|
6
4
|
class RegexSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
7
6
|
S = Speculation
|
8
7
|
|
9
8
|
def initialize(regex)
|
@@ -14,7 +13,7 @@ module Speculation
|
|
14
13
|
if value.nil? || Utils.collection?(value)
|
15
14
|
S.re_conform(@regex, value)
|
16
15
|
else
|
17
|
-
:invalid
|
16
|
+
ns(S, :invalid)
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Speculation
|
3
|
-
using Speculation::NamespacedSymbols.refine(self)
|
4
|
-
using Conj
|
5
|
-
|
6
3
|
# @private
|
7
4
|
class Spec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
8
6
|
S = Speculation
|
9
7
|
|
10
8
|
def initialize(predicate, should_conform)
|
@@ -22,13 +20,13 @@ module Speculation
|
|
22
20
|
if @should_conform
|
23
21
|
ret
|
24
22
|
else
|
25
|
-
ret ? value : :invalid
|
23
|
+
ret ? value : ns(S, :invalid)
|
26
24
|
end
|
27
25
|
end
|
28
26
|
|
29
27
|
def explain(path, via, inn, value)
|
30
28
|
if S.invalid?(S.dt(@predicate, value))
|
31
|
-
[{ :path => path, :val => value, :via => via, :in => inn, :pred => @predicate }]
|
29
|
+
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [@predicate, [value]] }]
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Speculation
|
3
|
-
using Speculation::NamespacedSymbols.refine(self)
|
4
|
-
using Conj
|
5
|
-
|
6
3
|
# @private
|
7
4
|
class TupleSpec < SpecImpl
|
5
|
+
include NamespacedSymbols
|
8
6
|
S = Speculation
|
9
7
|
|
10
8
|
def initialize(preds)
|
@@ -16,10 +14,10 @@ module Speculation
|
|
16
14
|
end
|
17
15
|
|
18
16
|
def conform(collection)
|
19
|
-
specs = @delayed_specs.value
|
17
|
+
specs = @delayed_specs.value!
|
20
18
|
|
21
19
|
unless Utils.array?(collection) && collection.count == specs.count
|
22
|
-
return :invalid
|
20
|
+
return ns(S, :invalid)
|
23
21
|
end
|
24
22
|
|
25
23
|
return_value = collection.class.new
|
@@ -28,7 +26,7 @@ module Speculation
|
|
28
26
|
conformed_value = spec.conform(value)
|
29
27
|
|
30
28
|
if S.invalid?(conformed_value)
|
31
|
-
return :invalid
|
29
|
+
return ns(S, :invalid)
|
32
30
|
else
|
33
31
|
return_value += [conformed_value]
|
34
32
|
end
|
@@ -39,13 +37,13 @@ module Speculation
|
|
39
37
|
|
40
38
|
def explain(path, via, inn, value)
|
41
39
|
if !Utils.array?(value)
|
42
|
-
[{ :path => path, :val => value, :via => via, :in => inn, :pred =>
|
40
|
+
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [Utils.method(:array?), [value]] }]
|
43
41
|
elsif @preds.count != value.count
|
44
|
-
[{ :path => path, :val => value, :via => via, :in => inn, :pred =>
|
42
|
+
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [Utils.method(:count_eq), [@preds, value.count]] }]
|
45
43
|
else
|
46
44
|
probs = @preds.zip(value).each_with_index.flat_map { |(pred, x), index|
|
47
45
|
unless S.pvalid?(pred, x)
|
48
|
-
S.explain1(pred,
|
46
|
+
S.explain1(pred, Utils.conj(path, index), via, Utils.conj(inn, index), x)
|
49
47
|
end
|
50
48
|
}
|
51
49
|
|
@@ -57,7 +55,7 @@ module Speculation
|
|
57
55
|
return @gen if @gen
|
58
56
|
|
59
57
|
gens = @preds.each_with_index.
|
60
|
-
map { |p, i| S.gensub(p, overrides,
|
58
|
+
map { |p, i| S.gensub(p, overrides, Utils.conj(path, i), rmap) }
|
61
59
|
|
62
60
|
->(rantly) do
|
63
61
|
gens.map { |g| g.call(rantly) }
|
data/lib/speculation/test.rb
CHANGED
@@ -2,11 +2,12 @@
|
|
2
2
|
require "concurrent"
|
3
3
|
require "pp"
|
4
4
|
require "speculation/pmap"
|
5
|
+
require "speculation/gen"
|
5
6
|
|
6
7
|
module Speculation
|
7
8
|
module Test
|
8
|
-
|
9
|
-
|
9
|
+
extend NamespacedSymbols
|
10
|
+
extend Pmap
|
10
11
|
|
11
12
|
# @private
|
12
13
|
S = Speculation
|
@@ -27,8 +28,8 @@ module Speculation
|
|
27
28
|
instrument_enabled.value = true
|
28
29
|
end
|
29
30
|
|
30
|
-
# Given an opts hash as per instrument, returns the set of
|
31
|
-
#
|
31
|
+
# Given an opts hash as per instrument, returns the set of methods that can
|
32
|
+
# be instrumented.
|
32
33
|
# @param opts [Hash]
|
33
34
|
# @return [Array<Identifier>]
|
34
35
|
def self.instrumentable_methods(opts = {})
|
@@ -38,16 +39,16 @@ module Speculation
|
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
S.registry.keys.select(&method(:fn_spec_name?)).to_set.tap
|
42
|
+
S.registry.keys.select(&method(:fn_spec_name?)).to_set.tap { |set|
|
42
43
|
set.merge(opts[:spec].keys) if opts[:spec]
|
43
44
|
set.merge(opts[:stub]) if opts[:stub]
|
44
45
|
set.merge(opts[:replace].keys) if opts[:replace]
|
45
|
-
|
46
|
+
}.map(&method(:Method))
|
46
47
|
end
|
47
48
|
|
48
|
-
# @param method_or_methods [Method,
|
49
|
+
# @param method_or_methods [Method, Array<Method>]
|
49
50
|
# Instruments the methods named by method-or-methods, a method or collection
|
50
|
-
# of methods, or all instrumentable methods if
|
51
|
+
# of methods, or all instrumentable methods if method_or_methods is not
|
51
52
|
# specified.
|
52
53
|
# If a method has an :args fn-spec, replaces the method with a method that
|
53
54
|
# checks arg conformance (throwing an exception on failure) before
|
@@ -74,7 +75,7 @@ module Speculation
|
|
74
75
|
# then invokes the method/proc you provide, enabling arbitrary stubbing and
|
75
76
|
# mocking.
|
76
77
|
#
|
77
|
-
# @return [Array<
|
78
|
+
# @return [Array<Method>] a collection of methods instrumented.
|
78
79
|
def self.instrument(method_or_methods = instrumentable_methods, opts = {})
|
79
80
|
if opts[:gen]
|
80
81
|
gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
|
@@ -85,24 +86,26 @@ module Speculation
|
|
85
86
|
map { |method| S.Identifier(method) }.
|
86
87
|
uniq.
|
87
88
|
map { |ident| instrument1(ident, opts) }.
|
88
|
-
compact
|
89
|
+
compact.
|
90
|
+
map(&method(:Method))
|
89
91
|
end
|
90
92
|
|
91
93
|
# Undoes instrument on the method_or_methods, specified as in instrument.
|
92
94
|
# With no args, unstruments all instrumented methods.
|
93
|
-
# @param method_or_methods [Method,
|
94
|
-
# @return [Array<
|
95
|
+
# @param method_or_methods [Method, Array<Method>]
|
96
|
+
# @return [Array<Method>] a collection of methods unstrumented
|
95
97
|
def self.unstrument(method_or_methods = nil)
|
96
98
|
method_or_methods ||= @instrumented_methods.value.keys
|
97
99
|
|
98
100
|
Array(method_or_methods).
|
99
101
|
map { |method| S.Identifier(method) }.
|
100
102
|
map { |ident| unstrument1(ident) }.
|
101
|
-
compact
|
103
|
+
compact.
|
104
|
+
map(&method(:Method))
|
102
105
|
end
|
103
106
|
|
104
107
|
# Runs generative tests for method using spec and opts.
|
105
|
-
# @param method [Method
|
108
|
+
# @param method [Method]
|
106
109
|
# @param spec [Spec]
|
107
110
|
# @param opts [Hash]
|
108
111
|
# @return [Hash]
|
@@ -113,17 +116,16 @@ module Speculation
|
|
113
116
|
end
|
114
117
|
|
115
118
|
# @param opts [Hash] an opts hash as per `check`
|
116
|
-
# @return [Array<
|
119
|
+
# @return [Array<Method>] the array of methods that can be checked.
|
117
120
|
def self.checkable_methods(opts = {})
|
118
121
|
validate_check_opts(opts)
|
119
122
|
|
120
123
|
S.
|
121
124
|
registry.
|
122
125
|
keys.
|
123
|
-
select
|
124
|
-
|
125
|
-
|
126
|
-
tap { |set| set.merge(opts[:spec].keys) if opts[:spec] }
|
126
|
+
select { |k| fn_spec_name?(k) && !k.instance_method? }.
|
127
|
+
concat(Hash(opts[:spec]).keys).
|
128
|
+
map(&method(:Method))
|
127
129
|
end
|
128
130
|
|
129
131
|
# Run generative tests for spec conformance on method_or_methods. If
|
@@ -151,10 +153,15 @@ module Speculation
|
|
151
153
|
def self.check(method_or_methods = nil, opts = {})
|
152
154
|
method_or_methods ||= checkable_methods
|
153
155
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
156
|
+
checkable = Set(checkable_methods(opts))
|
157
|
+
checkable.map!(&S.method(:Identifier))
|
158
|
+
|
159
|
+
methods = Set(method_or_methods)
|
160
|
+
methods.map!(&S.method(:Identifier))
|
161
|
+
|
162
|
+
pmap(methods.intersection(checkable)) { |ident|
|
163
|
+
check1(ident, S.get_spec(ident), opts)
|
164
|
+
}
|
158
165
|
end
|
159
166
|
|
160
167
|
# Given a check result, returns an abbreviated version suitable for summary use.
|
@@ -162,11 +169,11 @@ module Speculation
|
|
162
169
|
# @return [Hash]
|
163
170
|
def self.abbrev_result(x)
|
164
171
|
if x[:failure]
|
165
|
-
x.reject { |k, _| k == :ret
|
172
|
+
x.reject { |k, _| k == ns(:ret) }.
|
166
173
|
merge(:spec => x[:spec].inspect,
|
167
174
|
:failure => unwrap_failure(x[:failure]))
|
168
175
|
else
|
169
|
-
x.reject { |k, _| [:spec, :ret
|
176
|
+
x.reject { |k, _| [:spec, ns(:ret)].include?(k) }
|
170
177
|
end
|
171
178
|
end
|
172
179
|
|
@@ -194,6 +201,13 @@ module Speculation
|
|
194
201
|
}
|
195
202
|
end
|
196
203
|
|
204
|
+
# @param modules [Module, Class]
|
205
|
+
# @return [Array<Method>] an array of public and protected singleton
|
206
|
+
# methods belonging to modules
|
207
|
+
def self.enumerate_methods(*modules)
|
208
|
+
modules.flat_map { |mod| mod.methods(false).map(&mod.method(:method)) } # method
|
209
|
+
end
|
210
|
+
|
197
211
|
class << self
|
198
212
|
private
|
199
213
|
|
@@ -201,27 +215,27 @@ module Speculation
|
|
201
215
|
fspec = S.send(:maybe_spec, fspec)
|
202
216
|
|
203
217
|
conform = ->(args, block) do
|
204
|
-
conformed_args = S.conform(fspec.
|
205
|
-
conformed_block = S.conform(fspec.
|
218
|
+
conformed_args = S.conform(fspec.args, args)
|
219
|
+
conformed_block = S.conform(fspec.block, block) if fspec.block
|
206
220
|
|
207
|
-
if conformed_args ==
|
221
|
+
if conformed_args == ns(S, :invalid)
|
208
222
|
backtrace = backtrace_relevant_to_instrument(caller)
|
209
223
|
|
210
224
|
ed = S.
|
211
|
-
_explain_data(fspec.
|
212
|
-
merge(
|
225
|
+
_explain_data(fspec.args, [:args], [], [], args).
|
226
|
+
merge(ns(S, :args) => args, ns(S, :failure) => :instrument, ns(:caller) => backtrace.first)
|
213
227
|
|
214
228
|
io = StringIO.new
|
215
229
|
S.explain_out(ed, io)
|
216
230
|
msg = io.string
|
217
231
|
|
218
232
|
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
|
219
|
-
elsif conformed_block ==
|
233
|
+
elsif conformed_block == ns(S, :invalid)
|
220
234
|
backtrace = backtrace_relevant_to_instrument(caller)
|
221
235
|
|
222
236
|
ed = S.
|
223
|
-
_explain_data(fspec.
|
224
|
-
merge(
|
237
|
+
_explain_data(fspec.block, [:block], [], [], block).
|
238
|
+
merge(ns(S, :block) => block, ns(S, :failure) => :instrument, ns(:caller) => backtrace.first)
|
225
239
|
|
226
240
|
io = StringIO.new
|
227
241
|
S.explain_out(ed, io)
|
@@ -236,7 +250,7 @@ module Speculation
|
|
236
250
|
|
237
251
|
if Test.instrument_enabled.value
|
238
252
|
Test.with_instrument_disabled do
|
239
|
-
conform.call(args, block) if fspec.
|
253
|
+
conform.call(args, block) if fspec.args
|
240
254
|
|
241
255
|
begin
|
242
256
|
Test.instrument_enabled.value = true
|
@@ -252,7 +266,7 @@ module Speculation
|
|
252
266
|
end
|
253
267
|
|
254
268
|
def no_fspec(ident, spec)
|
255
|
-
S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec,
|
269
|
+
S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec, ns(S, :failure) => :no_fspec)
|
256
270
|
end
|
257
271
|
|
258
272
|
def instrument1(ident, opts)
|
@@ -324,9 +338,9 @@ module Speculation
|
|
324
338
|
def explain_check(args, spec, v, role)
|
325
339
|
data = unless S.valid?(spec, v)
|
326
340
|
S._explain_data(spec, [role], [], [], v).
|
327
|
-
merge(:args
|
328
|
-
:val
|
329
|
-
|
341
|
+
merge(ns(:args) => args,
|
342
|
+
ns(:val) => v,
|
343
|
+
ns(S, :failure) => :check_failed)
|
330
344
|
end
|
331
345
|
|
332
346
|
S::Error.new("Specification-based check failed", data).tap do |e|
|
@@ -338,32 +352,32 @@ module Speculation
|
|
338
352
|
# :backtrace, :cause and :data keys. :data will have a
|
339
353
|
# :"Speculation/failure" key.
|
340
354
|
def check_call(method, spec, args, block)
|
341
|
-
conformed_args = S.conform(spec.
|
355
|
+
conformed_args = S.conform(spec.args, args) if spec.args
|
342
356
|
|
343
|
-
if conformed_args ==
|
344
|
-
return explain_check(args, spec.
|
357
|
+
if conformed_args == ns(S, :invalid)
|
358
|
+
return explain_check(args, spec.args, args, :args)
|
345
359
|
end
|
346
360
|
|
347
|
-
conformed_block = S.conform(spec.
|
361
|
+
conformed_block = S.conform(spec.block, block) if spec.block
|
348
362
|
|
349
|
-
if conformed_block ==
|
363
|
+
if conformed_block == ns(S, :invalid)
|
350
364
|
return explain_check(block, spec.block, block, :block)
|
351
365
|
end
|
352
366
|
|
353
367
|
ret = method.call(*args, &block)
|
354
368
|
|
355
|
-
conformed_ret = S.conform(spec.
|
369
|
+
conformed_ret = S.conform(spec.ret, ret) if spec.ret
|
356
370
|
|
357
|
-
if conformed_ret ==
|
358
|
-
return explain_check(args, spec.
|
371
|
+
if conformed_ret == ns(S, :invalid)
|
372
|
+
return explain_check(args, spec.ret, ret, :ret)
|
359
373
|
end
|
360
374
|
|
361
|
-
return true unless spec.
|
375
|
+
return true unless spec.args && spec.ret && spec.fn
|
362
376
|
|
363
|
-
if S.valid?(spec.
|
377
|
+
if S.valid?(spec.fn, :args => conformed_args, :block => conformed_block, :ret => conformed_ret)
|
364
378
|
true
|
365
379
|
else
|
366
|
-
explain_check(args, spec.
|
380
|
+
explain_check(args, spec.fn, { :args => conformed_args, :block => conformed_block, :ret => conformed_ret }, :fn)
|
367
381
|
end
|
368
382
|
end
|
369
383
|
|
@@ -372,14 +386,14 @@ module Speculation
|
|
372
386
|
num_tests = opts.fetch(:num_tests, 1000)
|
373
387
|
|
374
388
|
args_gen = begin
|
375
|
-
S.gen(spec.
|
389
|
+
S.gen(spec.args, gen)
|
376
390
|
rescue => e
|
377
391
|
return { :result => e }
|
378
392
|
end
|
379
393
|
|
380
|
-
block_gen = if spec.
|
394
|
+
block_gen = if spec.block
|
381
395
|
begin
|
382
|
-
S.gen(spec.
|
396
|
+
S.gen(spec.block, gen)
|
383
397
|
rescue => e
|
384
398
|
return { :result => e }
|
385
399
|
end
|
@@ -387,15 +401,18 @@ module Speculation
|
|
387
401
|
Utils.constantly(nil)
|
388
402
|
end
|
389
403
|
|
390
|
-
|
404
|
+
arg_block_gen = ->(r) { [args_gen.call(r), block_gen.call(r)] }
|
391
405
|
|
392
|
-
|
406
|
+
generator_guard = ->(genned_val) { S.valid?(spec.args, genned_val) }
|
407
|
+
rantly_quick_check(arg_block_gen, num_tests, generator_guard) do |(args, block)|
|
408
|
+
check_call(method, spec, args, block)
|
409
|
+
end
|
393
410
|
end
|
394
411
|
|
395
412
|
def make_check_result(method, spec, check_result)
|
396
|
-
result = { :spec
|
397
|
-
:ret
|
398
|
-
:method
|
413
|
+
result = { :spec => spec,
|
414
|
+
ns(:ret) => check_result,
|
415
|
+
:method => method }
|
399
416
|
|
400
417
|
if check_result[:result] && check_result[:result] != true
|
401
418
|
result[:failure] = check_result[:result]
|
@@ -414,12 +431,12 @@ module Speculation
|
|
414
431
|
reinstrument = unstrument(ident).any?
|
415
432
|
method = ident.get_method
|
416
433
|
|
417
|
-
if specd.
|
434
|
+
if specd.args
|
418
435
|
check_result = quick_check(method, spec, opts)
|
419
436
|
make_check_result(method, spec, check_result)
|
420
437
|
else
|
421
|
-
failure = { :info
|
422
|
-
|
438
|
+
failure = { :info => "No :args spec",
|
439
|
+
ns(:failure) => :no_args_spec }
|
423
440
|
|
424
441
|
{ :failure => failure,
|
425
442
|
:method => method,
|
@@ -447,7 +464,7 @@ module Speculation
|
|
447
464
|
|
448
465
|
# Reimplementation of Rantly's `check` since it does not provide direct access to results
|
449
466
|
# (shrunk data etc.), instead printing them to STDOUT.
|
450
|
-
def rantly_quick_check(gen, num_tests)
|
467
|
+
def rantly_quick_check(gen, num_tests, generator_guard, &invariant)
|
451
468
|
i = 0
|
452
469
|
limit = 100
|
453
470
|
|
@@ -455,32 +472,19 @@ module Speculation
|
|
455
472
|
args, blk = val
|
456
473
|
i += 1
|
457
474
|
|
458
|
-
result =
|
459
|
-
yield([args, blk])
|
460
|
-
rescue => e
|
461
|
-
e
|
462
|
-
end
|
475
|
+
result = yield([args, blk]) rescue $!
|
463
476
|
|
464
477
|
unless result == true
|
465
|
-
# This is a Rantly Tuple.
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
:num_tests => i,
|
476
|
-
:result => result,
|
477
|
-
:shrunk => shrunk }
|
478
|
-
else
|
479
|
-
return { :fail => args.array,
|
480
|
-
:block => blk,
|
481
|
-
:num_tests => i,
|
482
|
-
:result => result }
|
483
|
-
end
|
478
|
+
args = ::Tuple.new(args) # This is a Rantly Tuple.
|
479
|
+
|
480
|
+
shrunk = shrink(generator_guard, args, result, ->(v) { invariant.call([v, blk]) })
|
481
|
+
|
482
|
+
shrunk[:smallest] = { :args => shrunk[:smallest].array, :block => blk }
|
483
|
+
|
484
|
+
return { :fail => { :args => args.array, :block => blk },
|
485
|
+
:num_tests => i,
|
486
|
+
:result => result,
|
487
|
+
:shrunk => shrunk }
|
484
488
|
end
|
485
489
|
end
|
486
490
|
|
@@ -489,32 +493,36 @@ module Speculation
|
|
489
493
|
end
|
490
494
|
|
491
495
|
# reimplementation of Rantly's shrinking.
|
492
|
-
def shrink(
|
493
|
-
smallest =
|
496
|
+
def shrink(generator_guard, value, result, invariant, depth = 0, iteration = 0)
|
497
|
+
smallest = value
|
494
498
|
max_depth = depth
|
495
499
|
|
496
|
-
if
|
500
|
+
if value.shrinkable?
|
497
501
|
while iteration < 1024
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
502
|
+
shrunk_value = value.shrink
|
503
|
+
|
504
|
+
unless generator_guard.call(shrunk_value.array)
|
505
|
+
iteration += 1
|
506
|
+
value = shrunk_value
|
507
|
+
value.shrinkable? ? next : break
|
508
|
+
end
|
504
509
|
|
505
|
-
|
506
|
-
shrunk = shrink(shrunk_data, result, block, depth + 1, iteration + 1)
|
510
|
+
res = invariant.call(shrunk_value.array) rescue $!
|
507
511
|
|
508
|
-
|
509
|
-
|
512
|
+
unless res == true
|
513
|
+
shrunk = shrink(generator_guard, shrunk_value, res, invariant, depth + 1, iteration + 1)
|
514
|
+
|
515
|
+
branch_smallest, branch_depth, iteration, branch_result =
|
516
|
+
shrunk.values_at(:smallest, :depth, :iteration, :result)
|
510
517
|
|
511
518
|
if branch_depth > max_depth
|
512
|
-
smallest = branch_smallest
|
513
519
|
max_depth = branch_depth
|
520
|
+
smallest = branch_smallest
|
521
|
+
result = branch_result
|
514
522
|
end
|
515
523
|
end
|
516
524
|
|
517
|
-
break unless
|
525
|
+
break unless value.retry?
|
518
526
|
end
|
519
527
|
end
|
520
528
|
|
@@ -527,7 +535,7 @@ module Speculation
|
|
527
535
|
### check reporting ###
|
528
536
|
|
529
537
|
def failure_type(x)
|
530
|
-
x.data[
|
538
|
+
x.data[ns(S, :failure)] if x.is_a?(S::Error)
|
531
539
|
end
|
532
540
|
|
533
541
|
def unwrap_failure(x)
|
@@ -548,6 +556,23 @@ module Speculation
|
|
548
556
|
failure_type(failure) || :check_raised
|
549
557
|
end
|
550
558
|
end
|
559
|
+
|
560
|
+
# if x is an Identifier, return its method
|
561
|
+
def Method(x)
|
562
|
+
case x
|
563
|
+
when Identifier then x.get_method
|
564
|
+
when Method, UnboundMethod then x
|
565
|
+
else raise ArgumentError, "unexpected method-like object #{x}"
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
def Set(x)
|
570
|
+
case x
|
571
|
+
when Set then x
|
572
|
+
when Enumerable then Set.new(x)
|
573
|
+
else Set[x]
|
574
|
+
end
|
575
|
+
end
|
551
576
|
end
|
552
577
|
end
|
553
578
|
end
|