speculation 0.4.0 → 0.4.2
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/README.md +154 -53
- data/examples/codebreaker.rb +8 -8
- data/examples/game_of_life.rb +120 -0
- data/examples/sinatra-web-app/Gemfile +1 -1
- data/examples/sinatra-web-app/Gemfile.lock +3 -5
- data/examples/sinatra-web-app/app.rb +5 -5
- data/examples/spec_guide.rb +33 -28
- data/lib/speculation.rb +175 -145
- data/lib/speculation/gen.rb +11 -0
- data/lib/speculation/namespaced_symbols.rb +4 -2
- data/lib/speculation/predicates.rb +48 -0
- data/lib/speculation/spec.rb +4 -0
- data/lib/speculation/spec/and_spec.rb +6 -2
- data/lib/speculation/spec/every_spec.rb +41 -18
- data/lib/speculation/spec/f_spec.rb +8 -4
- data/lib/speculation/spec/hash_spec.rb +49 -28
- data/lib/speculation/spec/merge_spec.rb +6 -2
- data/lib/speculation/spec/nilable_spec.rb +8 -4
- data/lib/speculation/spec/nonconforming_spec.rb +5 -1
- data/lib/speculation/spec/or_spec.rb +10 -3
- data/lib/speculation/spec/predicate_spec.rb +15 -4
- data/lib/speculation/spec/regex_spec.rb +8 -4
- data/lib/speculation/spec/tuple_spec.rb +14 -6
- data/lib/speculation/test.rb +24 -24
- data/lib/speculation/utils.rb +4 -46
- data/lib/speculation/version.rb +1 -1
- metadata +5 -3
@@ -19,12 +19,16 @@ module Speculation
|
|
19
19
|
ms = @preds.map { |pred| S.dt(pred, x) }
|
20
20
|
|
21
21
|
if ms.any?(&S.method(:invalid?))
|
22
|
-
|
22
|
+
:"Speculation/invalid"
|
23
23
|
else
|
24
24
|
ms.reduce(&:merge)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
def unform(x)
|
29
|
+
@preds.reverse.map { |pred| S.unform(pred, x) }.reduce(&:merge)
|
30
|
+
end
|
31
|
+
|
28
32
|
def explain(path, via, inn, x)
|
29
33
|
@preds.
|
30
34
|
flat_map { |pred| S.explain1(pred, path, via, inn, x) }.
|
@@ -36,7 +40,7 @@ module Speculation
|
|
36
40
|
end
|
37
41
|
|
38
42
|
def gen(overrides, path, rmap)
|
39
|
-
return @gen if @gen
|
43
|
+
return @gen.call if @gen
|
40
44
|
|
41
45
|
gens = @preds.
|
42
46
|
map { |pred| S.gensub(pred, overrides, path, rmap) }
|
@@ -20,12 +20,16 @@ module Speculation
|
|
20
20
|
value.nil? ? value : @delayed_spec.value!.conform(value)
|
21
21
|
end
|
22
22
|
|
23
|
+
def unform(value)
|
24
|
+
value.nil? ? nil : @delayed_spec.value!.unform(value)
|
25
|
+
end
|
26
|
+
|
23
27
|
def explain(path, via, inn, value)
|
24
28
|
return if S.pvalid?(@delayed_spec.value!, value) || value.nil?
|
25
29
|
|
26
30
|
Utils.conj(
|
27
|
-
S.explain1(@pred, Utils.conj(path,
|
28
|
-
:path => Utils.conj(path,
|
31
|
+
S.explain1(@pred, Utils.conj(path, :pred), via, inn, value),
|
32
|
+
:path => Utils.conj(path, :nil), :pred => [NilClass, [value]], :val => value, :via => via, :in => inn
|
29
33
|
)
|
30
34
|
end
|
31
35
|
|
@@ -34,11 +38,11 @@ module Speculation
|
|
34
38
|
end
|
35
39
|
|
36
40
|
def gen(overrides, path, rmap)
|
37
|
-
return @gen if @gen
|
41
|
+
return @gen.call if @gen
|
38
42
|
|
39
43
|
->(rantly) do
|
40
44
|
rantly.freq([1, Gen.delay { Utils.constantly(nil) }],
|
41
|
-
[9, Gen.delay { S.gensub(@pred, overrides, Utils.conj(path,
|
45
|
+
[9, Gen.delay { S.gensub(@pred, overrides, Utils.conj(path, :pred), rmap) }])
|
42
46
|
end
|
43
47
|
end
|
44
48
|
end
|
@@ -19,7 +19,11 @@ module Speculation
|
|
19
19
|
def conform(value)
|
20
20
|
ret = @delayed_spec.value!.conform(value)
|
21
21
|
|
22
|
-
S.invalid?(ret) ?
|
22
|
+
S.invalid?(ret) ? :"Speculation/invalid" : value
|
23
|
+
end
|
24
|
+
|
25
|
+
def unform(value)
|
26
|
+
value
|
23
27
|
end
|
24
28
|
|
25
29
|
def explain(path, via, inn, value)
|
@@ -33,13 +33,20 @@ module Speculation
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
|
36
|
+
:"Speculation/invalid"
|
37
|
+
end
|
38
|
+
|
39
|
+
def unform(value)
|
40
|
+
spec_name, conformed_val = value
|
41
|
+
spec = @named_specs.fetch(spec_name)
|
42
|
+
|
43
|
+
S.unform(spec, conformed_val)
|
37
44
|
end
|
38
45
|
|
39
46
|
def explain(path, via, inn, value)
|
40
47
|
return if S.pvalid?(self, value)
|
41
48
|
|
42
|
-
@
|
49
|
+
@named_specs.flat_map do |(key, pred)|
|
43
50
|
next if S.pvalid?(pred, value)
|
44
51
|
S.explain1(pred, Utils.conj(path, key), via, inn, value)
|
45
52
|
end
|
@@ -50,7 +57,7 @@ module Speculation
|
|
50
57
|
end
|
51
58
|
|
52
59
|
def gen(overrides, path, rmap)
|
53
|
-
return @gen if @gen
|
60
|
+
return @gen.call if @gen
|
54
61
|
|
55
62
|
gs = @keys.zip(@preds).
|
56
63
|
map { |(k, p)|
|
@@ -10,10 +10,11 @@ module Speculation
|
|
10
10
|
include NamespacedSymbols
|
11
11
|
S = Speculation
|
12
12
|
|
13
|
-
def initialize(predicate, should_conform, gen
|
13
|
+
def initialize(predicate, should_conform, gen, unconformer)
|
14
14
|
@predicate = predicate
|
15
15
|
@should_conform = should_conform
|
16
16
|
@gen = gen
|
17
|
+
@unconformer = unconformer
|
17
18
|
end
|
18
19
|
|
19
20
|
def conform(value)
|
@@ -26,7 +27,17 @@ module Speculation
|
|
26
27
|
if @should_conform
|
27
28
|
ret
|
28
29
|
else
|
29
|
-
ret ? value :
|
30
|
+
ret ? value : :"Speculation/invalid"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def unform(value)
|
35
|
+
return value unless @should_conform
|
36
|
+
|
37
|
+
if @unconformer
|
38
|
+
@unconformer.call(value)
|
39
|
+
else
|
40
|
+
raise "no unformer for conformer"
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
@@ -37,12 +48,12 @@ module Speculation
|
|
37
48
|
end
|
38
49
|
|
39
50
|
def with_gen(gen)
|
40
|
-
self.class.new(@predicate, @should_conform, gen)
|
51
|
+
self.class.new(@predicate, @should_conform, gen, @unconformer)
|
41
52
|
end
|
42
53
|
|
43
54
|
def gen(_, _, _)
|
44
55
|
if @gen
|
45
|
-
@gen
|
56
|
+
@gen.call
|
46
57
|
else
|
47
58
|
Gen.gen_for_pred(@predicate)
|
48
59
|
end
|
@@ -16,15 +16,19 @@ module Speculation
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def conform(value)
|
19
|
-
if value.nil? ||
|
19
|
+
if value.nil? || Predicates.collection?(value)
|
20
20
|
S.re_conform(@regex, value)
|
21
21
|
else
|
22
|
-
|
22
|
+
:"Speculation/invalid"
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
def unform(value)
|
27
|
+
S.op_unform(@regex, value)
|
28
|
+
end
|
29
|
+
|
26
30
|
def explain(path, via, inn, value)
|
27
|
-
if value.nil? ||
|
31
|
+
if value.nil? || Predicates.collection?(value)
|
28
32
|
S.re_explain(path, via, inn, @regex, value || [])
|
29
33
|
else
|
30
34
|
[{ :path => path, :val => value, :via => via, :in => inn }]
|
@@ -36,7 +40,7 @@ module Speculation
|
|
36
40
|
end
|
37
41
|
|
38
42
|
def gen(overrides, path, rmap)
|
39
|
-
return @gen if @gen
|
43
|
+
return @gen.call if @gen
|
40
44
|
|
41
45
|
S.re_gen(@regex, overrides, path, rmap)
|
42
46
|
end
|
@@ -22,8 +22,8 @@ module Speculation
|
|
22
22
|
def conform(collection)
|
23
23
|
specs = @delayed_specs.value!
|
24
24
|
|
25
|
-
unless
|
26
|
-
return
|
25
|
+
unless Predicates.array?(collection) && collection.count == specs.count
|
26
|
+
return :"Speculation/invalid"
|
27
27
|
end
|
28
28
|
|
29
29
|
return_value = collection.class.new
|
@@ -32,7 +32,7 @@ module Speculation
|
|
32
32
|
conformed_value = spec.conform(value)
|
33
33
|
|
34
34
|
if S.invalid?(conformed_value)
|
35
|
-
return
|
35
|
+
return :"Speculation/invalid"
|
36
36
|
else
|
37
37
|
return_value += [conformed_value]
|
38
38
|
end
|
@@ -41,9 +41,17 @@ module Speculation
|
|
41
41
|
return_value
|
42
42
|
end
|
43
43
|
|
44
|
+
def unform(value)
|
45
|
+
unless Predicates.array?(value) && value.count == @preds.count
|
46
|
+
raise ArgumentError, "unform value must be an array of length #{@preds.count}"
|
47
|
+
end
|
48
|
+
|
49
|
+
@preds.zip(value).map { |(pred, val)| S.unform(pred, val) }
|
50
|
+
end
|
51
|
+
|
44
52
|
def explain(path, via, inn, value)
|
45
|
-
if !
|
46
|
-
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [
|
53
|
+
if !Predicates.array?(value)
|
54
|
+
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [Predicates.method(:array?), [value]] }]
|
47
55
|
elsif @preds.count != value.count
|
48
56
|
[{ :path => path, :val => value, :via => via, :in => inn, :pred => [Utils.method(:count_eq), [@preds, value.count]] }]
|
49
57
|
else
|
@@ -62,7 +70,7 @@ module Speculation
|
|
62
70
|
end
|
63
71
|
|
64
72
|
def gen(overrides, path, rmap)
|
65
|
-
return @gen if @gen
|
73
|
+
return @gen.call if @gen
|
66
74
|
|
67
75
|
gens = @preds.each_with_index.
|
68
76
|
map { |p, i| S.gensub(p, overrides, Utils.conj(path, i), rmap) }
|
data/lib/speculation/test.rb
CHANGED
@@ -148,7 +148,7 @@ module Speculation
|
|
148
148
|
# * :result optional boolean as to whether all generative tests passed
|
149
149
|
# * :num_tests optional number of generative tests ran
|
150
150
|
#
|
151
|
-
# :failure is a hash that will contain a :
|
151
|
+
# :failure is a hash that will contain a :failure key with possible values:
|
152
152
|
#
|
153
153
|
# * :check_failed at least one checked return did not conform
|
154
154
|
# * :no_args_spec no :args spec provided
|
@@ -174,11 +174,11 @@ module Speculation
|
|
174
174
|
# @return [Hash]
|
175
175
|
def self.abbrev_result(x)
|
176
176
|
if x[:failure]
|
177
|
-
x.reject { |k, _| k ==
|
177
|
+
x.reject { |k, _| k == :ret }.
|
178
178
|
merge(:spec => x[:spec].inspect,
|
179
179
|
:failure => unwrap_failure(x[:failure]))
|
180
180
|
else
|
181
|
-
x.reject { |k, _| [:spec,
|
181
|
+
x.reject { |k, _| [:spec, :ret].include?(k) }
|
182
182
|
end
|
183
183
|
end
|
184
184
|
|
@@ -223,30 +223,30 @@ module Speculation
|
|
223
223
|
conformed_args = S.conform(fspec.args, args)
|
224
224
|
conformed_block = S.conform(fspec.block, block) if fspec.block
|
225
225
|
|
226
|
-
if conformed_args ==
|
226
|
+
if conformed_args == :"Speculation/invalid"
|
227
227
|
backtrace = backtrace_relevant_to_instrument(caller)
|
228
228
|
|
229
229
|
ed = S.
|
230
230
|
_explain_data(fspec.args, [:args], [], [], args).
|
231
|
-
merge(
|
231
|
+
merge(:args => args, :failure => :instrument, :caller => backtrace.first)
|
232
232
|
|
233
233
|
io = StringIO.new
|
234
234
|
S.explain_out(ed, io)
|
235
235
|
msg = io.string
|
236
236
|
|
237
|
-
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n
|
238
|
-
elsif conformed_block ==
|
237
|
+
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n#{msg}", ed)
|
238
|
+
elsif conformed_block == :"Speculation/invalid"
|
239
239
|
backtrace = backtrace_relevant_to_instrument(caller)
|
240
240
|
|
241
241
|
ed = S.
|
242
242
|
_explain_data(fspec.block, [:block], [], [], block).
|
243
|
-
merge(
|
243
|
+
merge(:block => block, :failure => :instrument, :caller => backtrace.first)
|
244
244
|
|
245
245
|
io = StringIO.new
|
246
246
|
S.explain_out(ed, io)
|
247
247
|
msg = io.string
|
248
248
|
|
249
|
-
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n
|
249
|
+
raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n#{msg}", ed)
|
250
250
|
end
|
251
251
|
end
|
252
252
|
|
@@ -271,7 +271,7 @@ module Speculation
|
|
271
271
|
end
|
272
272
|
|
273
273
|
def no_fspec(ident, spec)
|
274
|
-
S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec,
|
274
|
+
S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec, :failure => :no_fspec)
|
275
275
|
end
|
276
276
|
|
277
277
|
def instrument1(ident, opts)
|
@@ -343,9 +343,9 @@ module Speculation
|
|
343
343
|
def explain_check(args, spec, v, role)
|
344
344
|
data = unless S.valid?(spec, v)
|
345
345
|
S._explain_data(spec, [role], [], [], v).
|
346
|
-
merge(
|
347
|
-
|
348
|
-
|
346
|
+
merge(:args => args,
|
347
|
+
:val => v,
|
348
|
+
:failure => :check_failed)
|
349
349
|
end
|
350
350
|
|
351
351
|
S::Error.new("Specification-based check failed", data).tap do |e|
|
@@ -355,17 +355,17 @@ module Speculation
|
|
355
355
|
|
356
356
|
# Returns true if call passes specs, otherwise returns a hash with
|
357
357
|
# :backtrace, :cause and :data keys. :data will have a
|
358
|
-
# :
|
358
|
+
# :failure key.
|
359
359
|
def check_call(method, spec, args, block)
|
360
360
|
conformed_args = S.conform(spec.args, args) if spec.args
|
361
361
|
|
362
|
-
if conformed_args ==
|
362
|
+
if conformed_args == :"Speculation/invalid"
|
363
363
|
return explain_check(args, spec.args, args, :args)
|
364
364
|
end
|
365
365
|
|
366
366
|
conformed_block = S.conform(spec.block, block) if spec.block
|
367
367
|
|
368
|
-
if conformed_block ==
|
368
|
+
if conformed_block == :"Speculation/invalid"
|
369
369
|
return explain_check(block, spec.block, block, :block)
|
370
370
|
end
|
371
371
|
|
@@ -373,7 +373,7 @@ module Speculation
|
|
373
373
|
|
374
374
|
conformed_ret = S.conform(spec.ret, ret) if spec.ret
|
375
375
|
|
376
|
-
if conformed_ret ==
|
376
|
+
if conformed_ret == :"Speculation/invalid"
|
377
377
|
return explain_check(args, spec.ret, ret, :ret)
|
378
378
|
end
|
379
379
|
|
@@ -415,9 +415,9 @@ module Speculation
|
|
415
415
|
end
|
416
416
|
|
417
417
|
def make_check_result(method, spec, check_result)
|
418
|
-
result = { :spec
|
419
|
-
|
420
|
-
:method
|
418
|
+
result = { :spec => spec,
|
419
|
+
:ret => check_result,
|
420
|
+
:method => method }
|
421
421
|
|
422
422
|
if check_result[:result] && check_result[:result] != true
|
423
423
|
result[:failure] = check_result[:result]
|
@@ -440,8 +440,8 @@ module Speculation
|
|
440
440
|
check_result = quick_check(method, spec, opts)
|
441
441
|
make_check_result(method, spec, check_result)
|
442
442
|
else
|
443
|
-
failure = { :info
|
444
|
-
|
443
|
+
failure = { :info => "No :args spec",
|
444
|
+
:failure => :no_args_spec }
|
445
445
|
|
446
446
|
{ :failure => failure,
|
447
447
|
:method => method,
|
@@ -540,7 +540,7 @@ module Speculation
|
|
540
540
|
### check reporting ###
|
541
541
|
|
542
542
|
def failure_type(x)
|
543
|
-
x.data[
|
543
|
+
x.data[:failure] if x.is_a?(S::Error)
|
544
544
|
end
|
545
545
|
|
546
546
|
def unwrap_failure(x)
|
@@ -548,7 +548,7 @@ module Speculation
|
|
548
548
|
end
|
549
549
|
|
550
550
|
# Returns the type of the check result. This can be any of the
|
551
|
-
# :
|
551
|
+
# :failure symbols documented in 'check', or:
|
552
552
|
#
|
553
553
|
# :check_passed all checked method returns conformed
|
554
554
|
# :check_raised checked fn threw an exception
|
data/lib/speculation/utils.rb
CHANGED
@@ -5,18 +5,6 @@ require "set"
|
|
5
5
|
module Speculation
|
6
6
|
# @private
|
7
7
|
module Utils
|
8
|
-
def self.hash?(x)
|
9
|
-
x.respond_to?(:store)
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.array?(x)
|
13
|
-
x.respond_to?(:at)
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.collection?(xs)
|
17
|
-
xs.respond_to?(:each)
|
18
|
-
end
|
19
|
-
|
20
8
|
def self.itself(x)
|
21
9
|
x
|
22
10
|
end
|
@@ -25,24 +13,6 @@ module Speculation
|
|
25
13
|
->(*) { x }
|
26
14
|
end
|
27
15
|
|
28
|
-
def self.complement(&f)
|
29
|
-
->(*args) { !f.call(*args) }
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.distinct?(xs)
|
33
|
-
seen = Set[]
|
34
|
-
|
35
|
-
xs.each do |x|
|
36
|
-
if seen.include?(x)
|
37
|
-
return false
|
38
|
-
else
|
39
|
-
seen << x
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
true
|
44
|
-
end
|
45
|
-
|
46
16
|
def self.ident?(x)
|
47
17
|
x.is_a?(Symbol) || x.is_a?(MethodIdentifier)
|
48
18
|
end
|
@@ -59,22 +29,6 @@ module Speculation
|
|
59
29
|
from.reduce(to) { |memo, obj| conj(memo, obj) }
|
60
30
|
end
|
61
31
|
|
62
|
-
def self.count_eq?(coll, count)
|
63
|
-
coll.count == count
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.count_between?(coll, min_count, max_count)
|
67
|
-
coll.count.between?(min_count, max_count)
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.key?(hash, key)
|
71
|
-
hash.key?(key)
|
72
|
-
end
|
73
|
-
|
74
|
-
def self.empty?(coll)
|
75
|
-
coll.empty?
|
76
|
-
end
|
77
|
-
|
78
32
|
def self.conj(a, b)
|
79
33
|
case a
|
80
34
|
when Array, Set
|
@@ -87,5 +41,9 @@ module Speculation
|
|
87
41
|
else raise ArgumentError, "#{a}: must be an Array, Set or Hash"
|
88
42
|
end
|
89
43
|
end
|
44
|
+
|
45
|
+
def self.sort_descending(coll)
|
46
|
+
coll.sort { |a, b| yield(b) <=> yield(a) }
|
47
|
+
end
|
90
48
|
end
|
91
49
|
end
|