speculation 0.1.0 → 0.2.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 +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
|