speculation 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -20
- data/bin/console +1 -1
- data/examples/codebreaker.rb +38 -35
- data/examples/json_parser.rb +4 -8
- data/examples/spec_guide.rb +14 -15
- data/lib/speculation/gen.rb +17 -4
- data/lib/speculation/{identifier.rb → method_identifier.rb} +1 -1
- data/lib/speculation/spec/and_spec.rb +10 -1
- data/lib/speculation/spec/every_spec.rb +10 -1
- data/lib/speculation/spec/f_spec.rb +18 -14
- data/lib/speculation/spec/hash_spec.rb +8 -3
- data/lib/speculation/spec/merge_spec.rb +10 -1
- data/lib/speculation/spec/nilable_spec.rb +10 -1
- data/lib/speculation/spec/nonconforming_spec.rb +37 -0
- data/lib/speculation/spec/or_spec.rb +11 -2
- data/lib/speculation/spec/predicate_spec.rb +10 -1
- data/lib/speculation/spec/regex_spec.rb +10 -1
- data/lib/speculation/spec/tuple_spec.rb +10 -1
- data/lib/speculation/spec.rb +2 -1
- data/lib/speculation/test.rb +19 -15
- data/lib/speculation/utils.rb +1 -1
- data/lib/speculation/version.rb +1 -1
- data/lib/speculation.rb +49 -43
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7aa989a5796d79c562dc7ade44fe3b9b146b208e
|
4
|
+
data.tar.gz: 4e3249fae769e64f274f040ad59ef012ed4e50f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf7cb77e9f081fa6605a7676fd733c1ea5a2d3d8da9634bf529bdfd523e37cf54795e96dabdc84e039c920efbd2e1a620f7fcd3d7abbff16c5d8eed90ca0a28d
|
7
|
+
data.tar.gz: b997fa5b070e09ac6c146bb0587278351e03da7bb27ef12d650765484c66045e7f05576bbdea13ccfaf8f4b72e44d53e1ab54e52149eafff7e3b10f48ee78d54
|
data/README.md
CHANGED
@@ -1,17 +1,24 @@
|
|
1
1
|
# Speculation [![Build Status](https://travis-ci.org/english/speculation.svg?branch=master)](https://travis-ci.org/english/speculation)
|
2
2
|
|
3
|
-
A Ruby port of Clojure's `clojure.spec`.
|
3
|
+
A Ruby port of Clojure's `clojure.spec`. This library is largely a copy-and-paste from clojure.spec so all credit goes to Rich Hickey and contributors for their original work.
|
4
|
+
|
5
|
+
See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to write predicate specs (nothing to do with RSpec!) which enable:
|
6
|
+
|
7
|
+
* declarative data validation and destructuring
|
8
|
+
* error reporting
|
9
|
+
* runtime method instrumentation, argument checking and stubbing
|
10
|
+
* generative testing
|
4
11
|
|
5
12
|
## Project Goals
|
6
13
|
|
7
|
-
The goal of this project is to match clojure.spec as closely as possible, from design to features to API.
|
14
|
+
The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be any, significant departures from clojure.spec.
|
8
15
|
|
9
16
|
## Examples
|
10
17
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
18
|
+
* [sinatra-web-app](examples/sinatra-web-app): A small Sinatra web application demonstrating parameter validation and API error message generation.
|
19
|
+
* [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
|
20
|
+
* [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
|
21
|
+
* [json_parser.rb](examples/json_parser.rb): JSON parser using Speculation.
|
15
22
|
|
16
23
|
## Usage
|
17
24
|
|
@@ -21,7 +28,7 @@ Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/s
|
|
21
28
|
|
22
29
|
### Built in predicates
|
23
30
|
|
24
|
-
clojure.spec
|
31
|
+
clojure.spec utilises its rich standard library of predicate functions and data structures when writing specs. Ruby has neither of those, so we must be creative with what we define as a 'predicate' in Speculation. Each of the following are valid Speculation predicates:
|
25
32
|
|
26
33
|
```rb
|
27
34
|
S.valid?(->(x) { x > 0 }, 2)
|
@@ -80,30 +87,24 @@ S.fdef(method(:hello), :args => S.cat(:name => String),
|
|
80
87
|
|
81
88
|
#### Generators and quick check
|
82
89
|
|
83
|
-
Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While
|
90
|
+
Speculation uses [`Rantly`](https://github.com/abargnesi/rantly) for random data generation. Generator functions in Speculation are Procs that take one argument (Rantly instance) and return a random value. While Clojure's test.check generators generate values that start small and continue to grow and get more complex as a property holds true, Rantly always generates random values.
|
84
91
|
|
85
92
|
Rantly gives Speculation the ability to shrink a failing test case down to its smallest failing case, however in Speculation we limit this to Integers and Strings. This is an area where Speculation may currently be significantly weaker than clojure.spec.
|
86
93
|
|
87
|
-
##
|
88
|
-
|
89
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
90
|
-
|
91
|
-
## Contributing
|
92
|
-
|
93
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/english/speculation.
|
94
|
-
|
95
|
-
## TODO
|
94
|
+
## Project status
|
96
95
|
|
97
|
-
|
96
|
+
Speculation will mirror any changes made to clojure.spec. clojure.spec is still in alpha so breaking changes should be expected.
|
98
97
|
|
99
|
-
|
98
|
+
While most of features of clojure.spec are implemented in Speculation, a few remain:
|
100
99
|
|
101
100
|
- [`unform`](https://clojuredocs.org/clojure.spec/unform)
|
102
101
|
- [`form`](https://clojuredocs.org/clojure.spec/form)
|
103
102
|
- [`abbrev`](https://clojuredocs.org/clojure.spec/abbrev)
|
104
103
|
- [`describe`](https://clojuredocs.org/clojure.spec/describe)
|
105
104
|
|
106
|
-
|
105
|
+
## Improvements
|
106
|
+
|
107
|
+
Some things I hope to focus on in the near future:
|
107
108
|
|
108
109
|
- Explore alternative generator library
|
109
110
|
- Build up a library of generators around Rantly in the meantime?
|
@@ -111,6 +112,14 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
|
|
111
112
|
- perhaps integrating with [Pry's documentation browsing](https://github.com/pry/pry/wiki/Documentation-browsing)?
|
112
113
|
- Profile and optimise
|
113
114
|
|
115
|
+
## Development
|
116
|
+
|
117
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run rubocop and the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
118
|
+
|
119
|
+
## Contributing
|
120
|
+
|
121
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/english/speculation.
|
122
|
+
|
114
123
|
## License
|
115
124
|
|
116
125
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/bin/console
CHANGED
@@ -19,7 +19,7 @@ def reload!
|
|
19
19
|
|
20
20
|
load "./lib/speculation/namespaced_symbols.rb"
|
21
21
|
load "./lib/speculation/pmap.rb"
|
22
|
-
load "./lib/speculation/
|
22
|
+
load "./lib/speculation/method_identifier.rb"
|
23
23
|
load "./lib/speculation/utils.rb"
|
24
24
|
load "./lib/speculation/error.rb"
|
25
25
|
|
data/examples/codebreaker.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# See http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
# Speculation version of
|
4
|
+
# http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec by David
|
5
|
+
# Chelimsky.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
7
|
+
require "bundler/setup"
|
8
|
+
require "speculation"
|
9
|
+
require "speculation/gen"
|
10
|
+
require "speculation/test"
|
12
11
|
|
13
12
|
S = Speculation
|
14
13
|
Gen = S::Gen
|
@@ -103,10 +102,10 @@ end
|
|
103
102
|
|
104
103
|
S.exercise_fn method(:score)
|
105
104
|
|
106
|
-
# [[[[:
|
107
|
-
# [[[:
|
108
|
-
# [[[:
|
109
|
-
# [[[:g, :
|
105
|
+
# [[[[:y, :b, :b, :b, :b, :c], [:y, :y, :g, :r, :y, :b]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
106
|
+
# [[[:b, :c, :w, :w, :w, :g], [:r, :w, :c, :c, :b, :w]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
|
107
|
+
# [[[:c, :c, :g, :w, :r], [:g, :g, :g, :c, :w]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
108
|
+
# [[[:b, :y, :g, :y, :b, :y], [:g, :w, :b, :y, :r, :c]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
110
109
|
|
111
110
|
STest.check method(:score)
|
112
111
|
|
@@ -139,10 +138,10 @@ S.fdef method(:exact_matches),
|
|
139
138
|
|
140
139
|
S.exercise_fn method(:exact_matches)
|
141
140
|
|
142
|
-
# [[[[:
|
143
|
-
# [[[:c, :w, :
|
144
|
-
# [[[:
|
145
|
-
# [[[:
|
141
|
+
# [[[[:y, :y, :c, :g, :b], [:c, :w, :g, :b, :y]], nil, 0],
|
142
|
+
# [[[:y, :c, :w, :y, :b], [:y, :w, :c, :c, :r]], nil, 1],
|
143
|
+
# [[[:r, :g, :r, :y, :y], [:c, :r, :c, :c, :w]], nil, 0],
|
144
|
+
# [[[:r, :b, :r, :c], [:y, :r, :g, :b]], nil, 0],
|
146
145
|
|
147
146
|
STest.check method(:exact_matches)
|
148
147
|
# [{:spec=>Speculation::FSpec(main.exact_matches), :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
|
@@ -150,10 +149,11 @@ STest.check method(:exact_matches)
|
|
150
149
|
STest.instrument method(:exact_matches)
|
151
150
|
S.exercise_fn method(:score)
|
152
151
|
|
153
|
-
# [[[[:
|
154
|
-
# [[[:c, :
|
155
|
-
# [[[:
|
156
|
-
# [[[:
|
152
|
+
# [[[[:b, :g, :w, :b, :r], [:b, :g, :y, :g, :g]], nil, {:"Object/exact_matches"=>2, :"Object/loose_matches"=>0}],
|
153
|
+
# [[[:c, :w, :g, :b, :y], [:g, :r, :g, :y, :c]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
154
|
+
# [[[:c, :b, :c, :y, :r], [:r, :c, :y, :r, :b]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
|
155
|
+
# [[[:y, :c, :y, :g, :c, :c], [:w, :r, :b, :w, :b, :w]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
|
156
|
+
# [[[:b, :b, :w, :c, :g, :c], [:r, :b, :y, :r, :c, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
157
157
|
|
158
158
|
def self.score(secret, guess)
|
159
159
|
{ ns(:exact_matches) => exact_matches(secret, guess.take(3)),
|
@@ -180,11 +180,12 @@ S.fdef method(:match_count),
|
|
180
180
|
:ret => ns(S, :natural_integer),
|
181
181
|
:fn => ->(fn) { fn[:ret].between?(0, fn[:args][:secret].count) }
|
182
182
|
|
183
|
-
S.exercise_fn method(:exact_matches),
|
183
|
+
S.exercise_fn method(:exact_matches), 10, S.get_spec(method(:match_count))
|
184
184
|
|
185
|
-
# [[[[:
|
186
|
-
# [[[:
|
187
|
-
# [[[:
|
185
|
+
# [[[[:w, :c, :w, :b, :b], [:r, :r, :c, :c, :r]], nil, 0],
|
186
|
+
# [[[:y, :b, :g, :y, :y], [:w, :b, :g, :y, :w]], nil, 3],
|
187
|
+
# [[[:y, :r, :r, :r, :b], [:b, :w, :b, :c, :r]], nil, 0],
|
188
|
+
# [[[:w, :g, :r, :b, :b, :b], [:y, :c, :g, :b, :c, :b]], nil, 2],
|
188
189
|
|
189
190
|
STest.check_method method(:exact_matches), S.get_spec(method(:match_count))
|
190
191
|
|
@@ -194,9 +195,10 @@ STest.instrument method(:exact_matches), :spec => { method(:exact_matches) => S.
|
|
194
195
|
|
195
196
|
S.exercise_fn method(:score)
|
196
197
|
|
197
|
-
# [[[[:y, :r, :
|
198
|
-
# [[[:r, :b, :
|
199
|
-
# [[[:c, :
|
198
|
+
# [[[[:y, :y, :r, :b, :c, :c], [:b, :b, :b, :w, :w, :r]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
|
199
|
+
# [[[:r, :b, :r, :b, :b], [:r, :y, :c, :y, :y]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
200
|
+
# [[[:c, :y, :y, :w], [:y, :c, :y, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
201
|
+
# [[[:w, :g, :w, :g, :b, :g], [:r, :y, :r, :y, :c, :b]], nil, {:"Object/exact_matches"=>0, :"Object/loose_matches"=>0}],
|
200
202
|
|
201
203
|
STest.check method(:score)
|
202
204
|
|
@@ -212,11 +214,12 @@ def self.all_matches(secret, guess)
|
|
212
214
|
reduce(0, &:+)
|
213
215
|
end
|
214
216
|
|
215
|
-
S.exercise_fn method(:all_matches),
|
217
|
+
S.exercise_fn method(:all_matches), 10, S.get_spec(method(:match_count))
|
216
218
|
|
217
|
-
# [[[[:c, :
|
218
|
-
# [[[:
|
219
|
-
# [[[:
|
219
|
+
# [[[[:b, :r, :c, :r, :y, :w], [:r, :r, :c, :w, :y, :w]], nil, 5],
|
220
|
+
# [[[:c, :y, :g, :c, :r, :c], [:b, :w, :b, :b, :w, :b]], nil, 0],
|
221
|
+
# [[[:w, :g, :y, :r, :y], [:g, :y, :w, :r, :w]], nil, 4],
|
222
|
+
# [[[:g, :c, :c, :g], [:w, :g, :y, :w]], nil, 1],
|
220
223
|
|
221
224
|
def self.score(secret, guess)
|
222
225
|
exact = exact_matches(secret, guess)
|
@@ -231,10 +234,10 @@ STest.instrument [method(:exact_matches), method(:all_matches)],
|
|
231
234
|
method(:all_matches) => S.get_spec(method(:exact_matches)) }
|
232
235
|
|
233
236
|
S.exercise_fn method(:score)
|
234
|
-
# [[[[:w, :
|
235
|
-
# [[[:
|
236
|
-
# [[[:
|
237
|
-
# [[[:
|
237
|
+
# [[[[:w, :g, :y, :y], [:w, :c, :c, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
238
|
+
# [[[:y, :w, :g, :b, :b], [:y, :w, :g, :y, :b]], nil, {:"Object/exact_matches"=>4, :"Object/loose_matches"=>0}],
|
239
|
+
# [[[:b, :b, :c, :b], [:b, :y, :w, :y]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>0}],
|
240
|
+
# [[[:y, :w, :c, :y, :c], [:y, :g, :w, :w, :r]], nil, {:"Object/exact_matches"=>1, :"Object/loose_matches"=>1}],
|
238
241
|
|
239
242
|
STest.summarize_results STest.check method(:score)
|
240
243
|
|
data/examples/json_parser.rb
CHANGED
@@ -14,19 +14,15 @@ STest = S::Test
|
|
14
14
|
module JSONParser
|
15
15
|
extend S::NamespacedSymbols
|
16
16
|
|
17
|
-
def self.re_literal(string
|
17
|
+
def self.re_literal(string)
|
18
18
|
kvs = string.each_char.each_with_index.reduce({}) { |h, (v, i)| h.merge(i => Set[v]) }
|
19
19
|
S.cat(kvs)
|
20
20
|
end
|
21
21
|
|
22
22
|
S.def ns(:maybe_spaces), S.zero_or_more(Set[" "])
|
23
23
|
|
24
|
-
S.def ns(:
|
25
|
-
|
26
|
-
S.def ns(:false), re_literal("false", false)
|
27
|
-
|
28
|
-
S.def ns(:boolean), S.alt(:true => ns(:true),
|
29
|
-
:false => ns(:false))
|
24
|
+
S.def ns(:boolean), S.alt(:true => re_literal("true"),
|
25
|
+
:false => re_literal("false"))
|
30
26
|
|
31
27
|
S.def ns(:number), S.alt(:integer => ns(:integer),
|
32
28
|
:float => ns(:float))
|
@@ -134,7 +130,7 @@ module JSONParser
|
|
134
130
|
:tail => ns(:object_contents)))
|
135
131
|
|
136
132
|
S.def ns(:json), S.cat(:pre => ns(:maybe_spaces),
|
137
|
-
:val => S.alt(:null =>
|
133
|
+
:val => S.alt(:null => re_literal("null"),
|
138
134
|
:boolean => ns(:boolean),
|
139
135
|
:number => ns(:number),
|
140
136
|
:string => ns(:string),
|
data/examples/spec_guide.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
|
+
# Speculation version of the clojure.spec guide by Alex Miller: https://clojure.org/guides/spec
|
3
4
|
# Output generated by https://github.com/JoshCheek/seeing_is_believing
|
4
5
|
|
6
|
+
require "bundler/setup"
|
5
7
|
require "set"
|
6
8
|
require "date"
|
7
9
|
require "speculation"
|
@@ -249,7 +251,7 @@ S.explain :"unq/person", :first_name => "Elon"
|
|
249
251
|
# >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
|
250
252
|
# >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
|
251
253
|
|
252
|
-
# Unqualified keys can also be used to validate record attributes
|
254
|
+
# Unqualified keys can also be used to validate record attributes - don't support
|
253
255
|
# Keyword args keys* - don't support
|
254
256
|
|
255
257
|
# Sometimes it will be convenient to declare entity maps in parts, either
|
@@ -905,16 +907,16 @@ S.exercise S.or(:k => Symbol, :s => String, :n => Numeric), :n => 5
|
|
905
907
|
# invokes the spec’ed function and returns the args and the return value.
|
906
908
|
|
907
909
|
S.exercise_fn(method(:ranged_rand))
|
908
|
-
# => [[[-
|
909
|
-
# [[
|
910
|
-
# [[-
|
911
|
-
# [[-
|
912
|
-
# [[
|
913
|
-
# [[-
|
914
|
-
# [[
|
915
|
-
# [[
|
916
|
-
# [[
|
917
|
-
# [[
|
910
|
+
# => [[[-700291252660959460, 1315380256022004247], nil, -169059138562218507],
|
911
|
+
# [[-574949810996775321, -378316617969347527], nil, -540044395410145946],
|
912
|
+
# [[-760338081857905380, 484821106293575090], nil, 34570709564512062],
|
913
|
+
# [[-992673786379804322, 1487953925990349054], nil, -422423278764959748],
|
914
|
+
# [[-247390476258570074, 772122472766305147], nil, 397399655705976787],
|
915
|
+
# [[-1690412389556301777, -759316596276798578], nil, -1369751846456512938],
|
916
|
+
# [[1784449723230598375, 2047557531834071249], nil, 1819590014631499324],
|
917
|
+
# [[1116688093572002660, 1455424852058678548], nil, 1197557588479387983],
|
918
|
+
# [[-355113538297959975, 665164320042412170], nil, 195897461527121732],
|
919
|
+
# [[-631825347800361288, 2171238312189431384], nil, 885825441795070534]]
|
918
920
|
|
919
921
|
## Using S.and Generators
|
920
922
|
|
@@ -1022,7 +1024,6 @@ Gen.sample S.gen(ns(:syms)), 5
|
|
1022
1024
|
# :"my.domain/occupation",
|
1023
1025
|
# :"my.domain/id"]
|
1024
1026
|
|
1025
|
-
# TODO: make gens no-arg functions???
|
1026
1027
|
# Note that with_gen (and other places that take a custom generator) take a
|
1027
1028
|
# one-arg function that returns the generator, allowing it to be lazily
|
1028
1029
|
# realized.
|
@@ -1284,5 +1285,3 @@ STest.summarize_results STest.check(method(:run_query))
|
|
1284
1285
|
# In this guide we have covered most of the features for designing and using
|
1285
1286
|
# specs and generators. We expect to add some more advanced generator
|
1286
1287
|
# techniques and help on testing in a future update.
|
1287
|
-
|
1288
|
-
# Original author of clojure.spec guide: Alex Miller
|
data/lib/speculation/gen.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec.gen:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec/gen.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
require "set"
|
4
8
|
require "rantly"
|
5
9
|
require "rantly/property"
|
@@ -13,14 +17,16 @@ require "time"
|
|
13
17
|
|
14
18
|
module Speculation
|
15
19
|
module Gen
|
16
|
-
# Adds
|
17
|
-
# @param pred
|
20
|
+
# Adds pred block as a Rantly `guard` to generator `gen`.
|
18
21
|
# @param gen [Proc]
|
22
|
+
# @yield generated value
|
19
23
|
# @return [Proc]
|
20
24
|
# @see https://github.com/abargnesi/rantly Rantly
|
21
|
-
def self.such_that(
|
25
|
+
def self.such_that(gen)
|
26
|
+
raise ArgumentError, "block required" unless block_given?
|
27
|
+
|
22
28
|
->(rantly) do
|
23
|
-
gen.call(rantly).tap { |val| rantly.guard(
|
29
|
+
gen.call(rantly).tap { |val| rantly.guard(yield(val)) }
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
@@ -63,6 +69,13 @@ module Speculation
|
|
63
69
|
end
|
64
70
|
end
|
65
71
|
|
72
|
+
# @private
|
73
|
+
def self.tuple(*generators)
|
74
|
+
->(r) do
|
75
|
+
generators.map { |g| g.call(r) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
66
79
|
# @private
|
67
80
|
GEN_BUILTINS = {
|
68
81
|
Integer => ->(r) { r.integer },
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class AndSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(preds)
|
13
|
+
def initialize(preds, gen = nil)
|
10
14
|
@preds = preds
|
15
|
+
@gen = gen
|
11
16
|
@specs = Concurrent::Delay.new do
|
12
17
|
preds.map { |pred| S.send(:specize, pred) }
|
13
18
|
end
|
@@ -27,6 +32,10 @@ module Speculation
|
|
27
32
|
S.explain_pred_list(@preds, path, via, inn, value)
|
28
33
|
end
|
29
34
|
|
35
|
+
def with_gen(gen)
|
36
|
+
self.class.new(@preds, gen)
|
37
|
+
end
|
38
|
+
|
30
39
|
def gen(overrides, path, rmap)
|
31
40
|
if @gen
|
32
41
|
@gen
|
@@ -1,14 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class EverySpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(predicate, options)
|
13
|
+
def initialize(predicate, options, gen = nil)
|
10
14
|
@predicate = predicate
|
11
15
|
@options = options
|
16
|
+
@gen = gen
|
12
17
|
|
13
18
|
collection_predicates = [options.fetch(:kind, Enumerable)]
|
14
19
|
|
@@ -107,6 +112,10 @@ module Speculation
|
|
107
112
|
probs.compact
|
108
113
|
end
|
109
114
|
|
115
|
+
def with_gen(gen)
|
116
|
+
self.class.new(@predicate, @options, gen)
|
117
|
+
end
|
118
|
+
|
110
119
|
def gen(overrides, path, rmap)
|
111
120
|
return @gen if @gen
|
112
121
|
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class FSpec < Spec
|
@@ -8,11 +12,12 @@ module Speculation
|
|
8
12
|
|
9
13
|
attr_reader :args, :ret, :fn, :block
|
10
14
|
|
11
|
-
def initialize(args: nil, ret: nil, fn: nil, block: nil)
|
12
|
-
@args
|
13
|
-
@ret
|
14
|
-
@fn
|
15
|
+
def initialize(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
|
16
|
+
@args = args
|
17
|
+
@ret = ret
|
18
|
+
@fn = fn
|
15
19
|
@block = block
|
20
|
+
@gen = gen
|
16
21
|
end
|
17
22
|
|
18
23
|
def conform(f)
|
@@ -56,6 +61,10 @@ module Speculation
|
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
64
|
+
def with_gen(gen)
|
65
|
+
self.class.new(:args => @args, :ret => @ret, :fn => @fn, :block => @block, :gen => gen)
|
66
|
+
end
|
67
|
+
|
59
68
|
def gen(overrides, _path, _rmap)
|
60
69
|
return @gen if @gen
|
61
70
|
|
@@ -77,18 +86,13 @@ module Speculation
|
|
77
86
|
# @private
|
78
87
|
# returns f if valid, else smallest
|
79
88
|
def self.validate_fn(f, specs, iterations)
|
80
|
-
args_gen
|
81
|
-
|
82
|
-
|
83
|
-
S.gen(specs[:block])
|
84
|
-
else
|
85
|
-
Utils.constantly(nil)
|
86
|
-
end
|
87
|
-
|
88
|
-
combined = ->(r) { [args_gen.call(r), block_gen.call(r)] }
|
89
|
+
args_gen = S.gen(specs[:args])
|
90
|
+
block_gen = specs[:block] ? S.gen(specs[:block]) : Utils.constantly(nil)
|
91
|
+
arg_block_gen = Gen.tuple(args_gen, block_gen)
|
89
92
|
|
90
93
|
generator_guard = ->(genned_val) { S.valid?(specs[:args], genned_val) }
|
91
|
-
|
94
|
+
|
95
|
+
ret = S::Test.send(:rantly_quick_check, arg_block_gen, iterations, generator_guard) { |(args, block)|
|
92
96
|
call_valid?(f, specs, args, block)
|
93
97
|
}
|
94
98
|
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class HashSpec < Spec
|
@@ -8,12 +12,13 @@ module Speculation
|
|
8
12
|
|
9
13
|
attr_reader :id
|
10
14
|
|
11
|
-
def initialize(req, opt, req_un, opt_un)
|
15
|
+
def initialize(req, opt, req_un, opt_un, gen = nil)
|
12
16
|
@id = SecureRandom.uuid
|
13
17
|
@req = req
|
14
18
|
@opt = opt
|
15
19
|
@req_un = req_un
|
16
20
|
@opt_un = opt_un
|
21
|
+
@gen = gen
|
17
22
|
|
18
23
|
req_keys = req.flat_map(&method(:extract_keys))
|
19
24
|
req_un_specs = req_un.flat_map(&method(:extract_keys))
|
@@ -100,8 +105,8 @@ module Speculation
|
|
100
105
|
problems.compact
|
101
106
|
end
|
102
107
|
|
103
|
-
def
|
104
|
-
self
|
108
|
+
def with_gen(gen)
|
109
|
+
self.class.new(@req, @opt, @req_un, @opt_un, gen)
|
105
110
|
end
|
106
111
|
|
107
112
|
def gen(overrides, path, rmap)
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class MergeSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(preds)
|
13
|
+
def initialize(preds, gen = nil)
|
10
14
|
@preds = preds
|
15
|
+
@gen = gen
|
11
16
|
end
|
12
17
|
|
13
18
|
def conform(x)
|
@@ -26,6 +31,10 @@ module Speculation
|
|
26
31
|
compact
|
27
32
|
end
|
28
33
|
|
34
|
+
def with_gen(gen)
|
35
|
+
self.class.new(@preds, gen)
|
36
|
+
end
|
37
|
+
|
29
38
|
def gen(overrides, path, rmap)
|
30
39
|
return @gen if @gen
|
31
40
|
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class NilableSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(pred)
|
13
|
+
def initialize(pred, gen = nil)
|
10
14
|
@pred = pred
|
15
|
+
@gen = gen
|
11
16
|
@delayed_spec = Concurrent::Delay.new { S.send(:specize, pred) }
|
12
17
|
end
|
13
18
|
|
@@ -24,6 +29,10 @@ module Speculation
|
|
24
29
|
)
|
25
30
|
end
|
26
31
|
|
32
|
+
def with_gen(gen)
|
33
|
+
self.class.new(@pred, gen)
|
34
|
+
end
|
35
|
+
|
27
36
|
def gen(overrides, path, rmap)
|
28
37
|
return @gen if @gen
|
29
38
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
7
|
+
module Speculation
|
8
|
+
# @private
|
9
|
+
class NonconformingSpec < Spec
|
10
|
+
include NamespacedSymbols
|
11
|
+
S = Speculation
|
12
|
+
|
13
|
+
def initialize(spec, gen = nil)
|
14
|
+
@spec = spec
|
15
|
+
@gen = gen
|
16
|
+
@delayed_spec = Concurrent::Delay.new { S.send(:specize, spec) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def conform(value)
|
20
|
+
ret = @delayed_spec.value!.conform(value)
|
21
|
+
|
22
|
+
S.invalid?(ret) ? S::INVALID : value
|
23
|
+
end
|
24
|
+
|
25
|
+
def explain(path, via, inn, value)
|
26
|
+
@delayed_spec.value!.explain(path, via, inn, value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_gen(gen)
|
30
|
+
self.class.new(@spec, gen)
|
31
|
+
end
|
32
|
+
|
33
|
+
def gen(overrides, path, rmap)
|
34
|
+
@delayed_spec.value!.gen(overrides, path, rmap)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class OrSpec < Spec
|
@@ -8,11 +12,12 @@ module Speculation
|
|
8
12
|
|
9
13
|
attr_reader :id
|
10
14
|
|
11
|
-
def initialize(named_specs)
|
15
|
+
def initialize(named_specs, gen = nil)
|
12
16
|
@id = SecureRandom.uuid
|
13
17
|
@named_specs = named_specs
|
14
18
|
@keys = named_specs.keys
|
15
19
|
@preds = preds = named_specs.values
|
20
|
+
@gen = gen
|
16
21
|
|
17
22
|
@delayed_specs = Concurrent::Delay.new do
|
18
23
|
preds.map { |spec| S.send(:specize, spec) }
|
@@ -40,8 +45,12 @@ module Speculation
|
|
40
45
|
end
|
41
46
|
end
|
42
47
|
|
48
|
+
def with_gen(gen)
|
49
|
+
self.class.new(@named_specs, gen)
|
50
|
+
end
|
51
|
+
|
43
52
|
def gen(overrides, path, rmap)
|
44
|
-
return gen if @gen
|
53
|
+
return @gen if @gen
|
45
54
|
|
46
55
|
gs = @keys.zip(@preds).
|
47
56
|
map { |(k, p)|
|
@@ -1,14 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class PredicateSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(predicate, should_conform)
|
13
|
+
def initialize(predicate, should_conform, gen = nil)
|
10
14
|
@predicate = predicate
|
11
15
|
@should_conform = should_conform
|
16
|
+
@gen = gen
|
12
17
|
end
|
13
18
|
|
14
19
|
def conform(value)
|
@@ -31,6 +36,10 @@ module Speculation
|
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
39
|
+
def with_gen(gen)
|
40
|
+
self.class.new(@predicate, @should_conform, gen)
|
41
|
+
end
|
42
|
+
|
34
43
|
def gen(_, _, _)
|
35
44
|
if @gen
|
36
45
|
@gen
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class RegexSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(regex)
|
13
|
+
def initialize(regex, gen = nil)
|
10
14
|
@regex = regex
|
15
|
+
@gen = gen
|
11
16
|
end
|
12
17
|
|
13
18
|
def conform(value)
|
@@ -26,6 +31,10 @@ module Speculation
|
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
34
|
+
def with_gen(gen)
|
35
|
+
self.class.new(@regex, gen)
|
36
|
+
end
|
37
|
+
|
29
38
|
def gen(overrides, path, rmap)
|
30
39
|
return @gen if @gen
|
31
40
|
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
module Speculation
|
4
8
|
# @private
|
5
9
|
class TupleSpec < Spec
|
6
10
|
include NamespacedSymbols
|
7
11
|
S = Speculation
|
8
12
|
|
9
|
-
def initialize(preds)
|
13
|
+
def initialize(preds, gen = nil)
|
10
14
|
@preds = preds
|
15
|
+
@gen = gen
|
11
16
|
|
12
17
|
@delayed_specs = Concurrent::Delay.new do
|
13
18
|
preds.map { |pred| S.send(:specize, pred) }
|
@@ -52,6 +57,10 @@ module Speculation
|
|
52
57
|
end
|
53
58
|
end
|
54
59
|
|
60
|
+
def with_gen(gen)
|
61
|
+
self.class.new(@preds, gen)
|
62
|
+
end
|
63
|
+
|
55
64
|
def gen(overrides, path, rmap)
|
56
65
|
return @gen if @gen
|
57
66
|
|
data/lib/speculation/spec.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Speculation
|
4
4
|
# @private
|
5
5
|
class Spec
|
6
|
-
attr_accessor :name
|
6
|
+
attr_accessor :name
|
7
7
|
attr_reader :id
|
8
8
|
|
9
9
|
def conform(_x)
|
@@ -34,3 +34,4 @@ require_relative "spec/every_spec"
|
|
34
34
|
require_relative "spec/regex_spec"
|
35
35
|
require_relative "spec/f_spec"
|
36
36
|
require_relative "spec/nilable_spec"
|
37
|
+
require_relative "spec/nonconforming_spec"
|
data/lib/speculation/test.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec.test:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec/test.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
require "concurrent"
|
4
8
|
require "pp"
|
5
9
|
require "speculation/pmap"
|
@@ -32,7 +36,7 @@ module Speculation
|
|
32
36
|
# Given an opts hash as per instrument, returns the set of methods that can
|
33
37
|
# be instrumented.
|
34
38
|
# @param opts [Hash]
|
35
|
-
# @return [Array<
|
39
|
+
# @return [Array<Method>]
|
36
40
|
def self.instrumentable_methods(opts = {})
|
37
41
|
if opts[:gen]
|
38
42
|
unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
|
@@ -79,12 +83,12 @@ module Speculation
|
|
79
83
|
# @return [Array<Method>] a collection of methods instrumented.
|
80
84
|
def self.instrument(method_or_methods = instrumentable_methods, opts = {})
|
81
85
|
if opts[:gen]
|
82
|
-
gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.
|
86
|
+
gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }
|
83
87
|
opts = opts.merge(:gen => gens)
|
84
88
|
end
|
85
89
|
|
86
90
|
Array(method_or_methods).
|
87
|
-
map { |method| S.
|
91
|
+
map { |method| S.MethodIdentifier(method) }.
|
88
92
|
uniq.
|
89
93
|
map { |ident| instrument1(ident, opts) }.
|
90
94
|
compact.
|
@@ -99,7 +103,7 @@ module Speculation
|
|
99
103
|
method_or_methods ||= @instrumented_methods.value.keys
|
100
104
|
|
101
105
|
Array(method_or_methods).
|
102
|
-
map { |method| S.
|
106
|
+
map { |method| S.MethodIdentifier(method) }.
|
103
107
|
map { |ident| unstrument1(ident) }.
|
104
108
|
compact.
|
105
109
|
map(&method(:Method))
|
@@ -113,7 +117,7 @@ module Speculation
|
|
113
117
|
# @see check see check for options and return
|
114
118
|
def self.check_method(method, spec, opts = {})
|
115
119
|
validate_check_opts(opts)
|
116
|
-
check1(S.
|
120
|
+
check1(S.MethodIdentifier(method), spec, opts)
|
117
121
|
end
|
118
122
|
|
119
123
|
# @param opts [Hash] an opts hash as per `check`
|
@@ -137,7 +141,7 @@ module Speculation
|
|
137
141
|
# @option opts :num_tests [Integer] (1000) number of times to generatively test each method
|
138
142
|
# @option opts :gen [Hash] map from spec names to generator overrides.
|
139
143
|
# Generator overrides are passed to Speculation.gen when generating method args.
|
140
|
-
# @return [Array<
|
144
|
+
# @return [Array<Hash>] an array of check result hashes with the following keys:
|
141
145
|
# * :spec the spec tested
|
142
146
|
# * :method optional method tested
|
143
147
|
# * :failure optional test failure
|
@@ -155,10 +159,10 @@ module Speculation
|
|
155
159
|
method_or_methods ||= checkable_methods
|
156
160
|
|
157
161
|
checkable = Set(checkable_methods(opts))
|
158
|
-
checkable.map!(&S.method(:
|
162
|
+
checkable.map!(&S.method(:MethodIdentifier))
|
159
163
|
|
160
164
|
methods = Set(method_or_methods)
|
161
|
-
methods.map!(&S.method(:
|
165
|
+
methods.map!(&S.method(:MethodIdentifier))
|
162
166
|
|
163
167
|
pmap(methods.intersection(checkable)) { |ident|
|
164
168
|
check1(ident, S.get_spec(ident), opts)
|
@@ -300,9 +304,9 @@ module Speculation
|
|
300
304
|
end
|
301
305
|
|
302
306
|
def instrument_choose_fn(f, spec, ident, opts)
|
303
|
-
stubs = (opts[:stub] || []).map(&S.method(:
|
307
|
+
stubs = (opts[:stub] || []).map(&S.method(:MethodIdentifier))
|
304
308
|
over = opts[:gen] || {}
|
305
|
-
replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.
|
309
|
+
replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }
|
306
310
|
|
307
311
|
if stubs.include?(ident)
|
308
312
|
Gen.generate(S.gen(spec, over))
|
@@ -313,7 +317,7 @@ module Speculation
|
|
313
317
|
|
314
318
|
def instrument_choose_spec(spec, ident, overrides)
|
315
319
|
(overrides || {}).
|
316
|
-
reduce({}) { |h, (k, v)| h.merge(S.
|
320
|
+
reduce({}) { |h, (k, v)| h.merge(S.MethodIdentifier(k) => v) }.
|
317
321
|
fetch(ident, spec)
|
318
322
|
end
|
319
323
|
|
@@ -402,7 +406,7 @@ module Speculation
|
|
402
406
|
Utils.constantly(nil)
|
403
407
|
end
|
404
408
|
|
405
|
-
arg_block_gen =
|
409
|
+
arg_block_gen = Gen.tuple(args_gen, block_gen)
|
406
410
|
|
407
411
|
generator_guard = ->(genned_val) { S.valid?(spec.args, genned_val) }
|
408
412
|
rantly_quick_check(arg_block_gen, num_tests, generator_guard) do |(args, block)|
|
@@ -460,7 +464,7 @@ module Speculation
|
|
460
464
|
end
|
461
465
|
|
462
466
|
def fn_spec_name?(spec_name)
|
463
|
-
spec_name.is_a?(S::
|
467
|
+
spec_name.is_a?(S::MethodIdentifier)
|
464
468
|
end
|
465
469
|
|
466
470
|
# Reimplementation of Rantly's `check` since it does not provide direct access to results
|
@@ -558,10 +562,10 @@ module Speculation
|
|
558
562
|
end
|
559
563
|
end
|
560
564
|
|
561
|
-
# if x is an
|
565
|
+
# if x is an MethodIdentifier, return its method
|
562
566
|
def Method(x)
|
563
567
|
case x
|
564
|
-
when
|
568
|
+
when MethodIdentifier then x.get_method
|
565
569
|
when Method, UnboundMethod then x
|
566
570
|
else raise ArgumentError, "unexpected method-like object #{x}"
|
567
571
|
end
|
data/lib/speculation/utils.rb
CHANGED
data/lib/speculation/version.rb
CHANGED
data/lib/speculation.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# This is a Ruby translation of clojure.spec:
|
4
|
+
# https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj
|
5
|
+
# All credit belongs with Rich Hickey and contributors for their original work.
|
6
|
+
|
3
7
|
require "concurrent"
|
4
8
|
require "set"
|
5
9
|
require "securerandom"
|
6
10
|
|
7
11
|
require "speculation/version"
|
8
12
|
require "speculation/namespaced_symbols"
|
9
|
-
require "speculation/
|
13
|
+
require "speculation/method_identifier"
|
10
14
|
require "speculation/utils"
|
11
15
|
require "speculation/spec"
|
12
16
|
require "speculation/error"
|
@@ -152,7 +156,7 @@ module Speculation
|
|
152
156
|
# @param value value to conform
|
153
157
|
# @return [Symbol, Object] :Speculation/invalid if value does not match spec, else the (possibly destructured) value
|
154
158
|
def self.conform(spec, value)
|
155
|
-
spec =
|
159
|
+
spec = MethodIdentifier(spec)
|
156
160
|
specize(spec).conform(value)
|
157
161
|
end
|
158
162
|
|
@@ -164,7 +168,7 @@ module Speculation
|
|
164
168
|
if regex?(spec)
|
165
169
|
spec.merge(ns(:gfn) => gen)
|
166
170
|
else
|
167
|
-
specize(spec).
|
171
|
+
specize(spec).with_gen(gen)
|
168
172
|
end
|
169
173
|
end
|
170
174
|
|
@@ -189,7 +193,7 @@ module Speculation
|
|
189
193
|
# where problem-hash has at least :path :pred and :val keys describing the
|
190
194
|
# predicate and the value that failed at that path.
|
191
195
|
def self.explain_data(spec, x)
|
192
|
-
spec =
|
196
|
+
spec = MethodIdentifier(spec)
|
193
197
|
name = spec_name(spec)
|
194
198
|
_explain_data(spec, [], Array(name), [], x)
|
195
199
|
end
|
@@ -250,7 +254,7 @@ module Speculation
|
|
250
254
|
g = gfn || spec.gen(overrides, path, rmap)
|
251
255
|
|
252
256
|
if g
|
253
|
-
Gen.such_that(
|
257
|
+
Gen.such_that(g) { |x| valid?(spec, x) }
|
254
258
|
else
|
255
259
|
raise Speculation::Error.new("unable to construct gen at: #{path.inspect} for: #{spec.inspect}",
|
256
260
|
ns(:failure) => :no_gen, ns(:path) => path)
|
@@ -272,15 +276,15 @@ module Speculation
|
|
272
276
|
# @param overrides <Hash>
|
273
277
|
# @return [Proc]
|
274
278
|
def self.gen(spec, overrides = nil)
|
275
|
-
spec =
|
279
|
+
spec = MethodIdentifier(spec)
|
276
280
|
gensub(spec, overrides, [], RECURSION_LIMIT => recursion_limit)
|
277
281
|
end
|
278
282
|
|
279
283
|
# @private
|
280
|
-
def self.
|
284
|
+
def self.MethodIdentifier(x)
|
281
285
|
case x
|
282
|
-
when Method then
|
283
|
-
when UnboundMethod then
|
286
|
+
when Method then MethodIdentifier.new(x.receiver, x.name, false)
|
287
|
+
when UnboundMethod then MethodIdentifier.new(x.owner, x.name, true)
|
284
288
|
else x
|
285
289
|
end
|
286
290
|
end
|
@@ -291,7 +295,7 @@ module Speculation
|
|
291
295
|
# @param spec [Spec, Symbol, Proc, Hash] a spec, spec name, predicate or regex-op
|
292
296
|
# @return [Symbol, Method]
|
293
297
|
def self.def(key, spec)
|
294
|
-
key =
|
298
|
+
key = MethodIdentifier(key)
|
295
299
|
|
296
300
|
unless Utils.ident?(key) && (!key.is_a?(Symbol) || NamespacedSymbols.namespace(key))
|
297
301
|
raise ArgumentError, "key must be a namespaced Symbol, e.g. #{ns(:my_spec)}, or a Method"
|
@@ -300,14 +304,14 @@ module Speculation
|
|
300
304
|
spec = if spec?(spec) || regex?(spec) || registry[spec]
|
301
305
|
spec
|
302
306
|
else
|
303
|
-
spec_impl(spec, false)
|
307
|
+
spec_impl(spec, nil, false)
|
304
308
|
end
|
305
309
|
|
306
310
|
@registry_ref.swap do |reg|
|
307
311
|
reg.merge(key => with_name(spec, key)).freeze
|
308
312
|
end
|
309
313
|
|
310
|
-
key.is_a?(
|
314
|
+
key.is_a?(MethodIdentifier) ? key.get_method : key
|
311
315
|
end
|
312
316
|
|
313
317
|
# @return [Hash] the registry hash
|
@@ -319,7 +323,7 @@ module Speculation
|
|
319
323
|
# @param key [Symbol, Method]
|
320
324
|
# @return [Spec, nil] spec registered for key, or nil
|
321
325
|
def self.get_spec(key)
|
322
|
-
registry[
|
326
|
+
registry[MethodIdentifier(key)]
|
323
327
|
end
|
324
328
|
|
325
329
|
# NOTE: it is not generally necessary to wrap predicates in spec when using
|
@@ -346,11 +350,7 @@ module Speculation
|
|
346
350
|
# arg (Rantly instance) that generates a valid value.
|
347
351
|
# @return [Spec]
|
348
352
|
def self.spec(pred, gen: nil)
|
349
|
-
if pred
|
350
|
-
spec_impl(pred, false).tap do |spec|
|
351
|
-
spec.gen = gen if gen
|
352
|
-
end
|
353
|
-
end
|
353
|
+
spec_impl(pred, gen, false) if pred
|
354
354
|
end
|
355
355
|
|
356
356
|
# Creates and returns a hash validating spec. :req and :opt are both arrays of
|
@@ -384,9 +384,7 @@ module Speculation
|
|
384
384
|
# @param gen [Proc] generator function, which must be a proc of one arg
|
385
385
|
# (Rantly instance) that generates a valid value
|
386
386
|
def self.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil)
|
387
|
-
HashSpec.new(req, opt, req_un, opt_un)
|
388
|
-
spec.gen = gen
|
389
|
-
end
|
387
|
+
HashSpec.new(req, opt, req_un, opt_un, gen)
|
390
388
|
end
|
391
389
|
|
392
390
|
# @see keys
|
@@ -450,9 +448,7 @@ module Speculation
|
|
450
448
|
def self.every(pred, opts = {})
|
451
449
|
gen = opts.delete(:gen)
|
452
450
|
|
453
|
-
EverySpec.new(pred, opts)
|
454
|
-
spec.gen = gen
|
455
|
-
end
|
451
|
+
EverySpec.new(pred, opts, gen)
|
456
452
|
end
|
457
453
|
|
458
454
|
# Like 'every' but takes separate key and val preds and works on associative collections.
|
@@ -557,11 +553,11 @@ module Speculation
|
|
557
553
|
{ OP => AMP, :p1 => re, :predicates => preds }
|
558
554
|
end
|
559
555
|
|
560
|
-
# @
|
556
|
+
# @param f predicate function with the semantics of conform i.e. it should
|
561
557
|
# return either a (possibly converted) value or :"Speculation/invalid"
|
562
|
-
# @return [Spec] a spec that uses
|
563
|
-
def self.conformer(
|
564
|
-
spec_impl(
|
558
|
+
# @return [Spec] a spec that uses pred as a predicate/conformer.
|
559
|
+
def self.conformer(f)
|
560
|
+
spec_impl(f, nil, true)
|
565
561
|
end
|
566
562
|
|
567
563
|
# Takes :args :ret and (optional) :block and :fn kwargs whose values are preds and returns a spec
|
@@ -581,9 +577,7 @@ module Speculation
|
|
581
577
|
# @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a
|
582
578
|
# full description of :args, :block, :ret and :fn
|
583
579
|
def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
|
584
|
-
FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block)
|
585
|
-
spec.gen = gen
|
586
|
-
end
|
580
|
+
FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block), :gen => gen)
|
587
581
|
end
|
588
582
|
|
589
583
|
# @param preds [Array] one or more preds
|
@@ -618,7 +612,7 @@ module Speculation
|
|
618
612
|
# @note Note that :fn specs require the presence of :args and :ret specs to conform values, and so :fn
|
619
613
|
# specs will be ignored if :args or :ret are missing.
|
620
614
|
def self.fdef(method, spec)
|
621
|
-
self.def(
|
615
|
+
self.def(MethodIdentifier(method), fspec(spec))
|
622
616
|
method
|
623
617
|
end
|
624
618
|
|
@@ -626,7 +620,7 @@ module Speculation
|
|
626
620
|
# @param x
|
627
621
|
# @return [Boolean] true when x is valid for spec.
|
628
622
|
def self.valid?(spec, x)
|
629
|
-
spec =
|
623
|
+
spec = MethodIdentifier(spec)
|
630
624
|
spec = specize(spec)
|
631
625
|
|
632
626
|
!invalid?(spec.conform(x))
|
@@ -638,6 +632,14 @@ module Speculation
|
|
638
632
|
NilableSpec.new(pred)
|
639
633
|
end
|
640
634
|
|
635
|
+
# @param spec
|
636
|
+
# @return [Spec] a spec that has the same properies as the given spec, except
|
637
|
+
# `conform` will return the original (not the conformed) value. Note, will
|
638
|
+
# specize regex ops.
|
639
|
+
def self.nonconforming(spec)
|
640
|
+
NonconformingSpec.new(spec)
|
641
|
+
end
|
642
|
+
|
641
643
|
# Generates a number (default 10) of values compatible with spec and maps
|
642
644
|
# conform over them, returning a sequence of [val conformed-val] tuples.
|
643
645
|
# @param spec
|
@@ -657,12 +659,15 @@ module Speculation
|
|
657
659
|
# @param method [Method]
|
658
660
|
# @param n [Integer]
|
659
661
|
# @param fspec [Spec]
|
660
|
-
# @return [Array] an
|
661
|
-
def self.exercise_fn(method, n
|
662
|
+
# @return [Array] an array of triples of [args, block, ret].
|
663
|
+
def self.exercise_fn(method, n = 10, fspec = nil)
|
662
664
|
fspec ||= get_spec(method)
|
663
665
|
raise ArgumentError, "No fspec found for #{method}" unless fspec
|
664
666
|
|
665
|
-
|
667
|
+
block_gen = fspec.block ? gen(fspec.block) : Utils.constantly(nil)
|
668
|
+
gen = Gen.tuple(gen(fspec.args), block_gen)
|
669
|
+
|
670
|
+
Gen.sample(gen, n).map { |(args, block)| [args, block, method.call(*args, &block)] }
|
666
671
|
end
|
667
672
|
|
668
673
|
### impl ###
|
@@ -718,15 +723,16 @@ module Speculation
|
|
718
723
|
end
|
719
724
|
|
720
725
|
# @private
|
721
|
-
def self.spec_impl(pred, should_conform)
|
726
|
+
def self.spec_impl(pred, gen, should_conform)
|
722
727
|
if spec?(pred)
|
723
|
-
pred
|
728
|
+
with_gen(pred, gen)
|
724
729
|
elsif regex?(pred)
|
725
|
-
RegexSpec.new(pred)
|
730
|
+
RegexSpec.new(pred, gen)
|
726
731
|
elsif Utils.ident?(pred)
|
727
|
-
the_spec(pred)
|
732
|
+
spec = the_spec(pred)
|
733
|
+
gen ? with_gen(spec, gen) : spec
|
728
734
|
else
|
729
|
-
PredicateSpec.new(pred, should_conform)
|
735
|
+
PredicateSpec.new(pred, should_conform, gen)
|
730
736
|
end
|
731
737
|
end
|
732
738
|
|
@@ -978,10 +984,10 @@ module Speculation
|
|
978
984
|
spec
|
979
985
|
else
|
980
986
|
case spec
|
981
|
-
when Symbol,
|
987
|
+
when Symbol, MethodIdentifier
|
982
988
|
specize(reg_resolve!(spec))
|
983
989
|
else
|
984
|
-
spec_impl(spec, false)
|
990
|
+
spec_impl(spec, nil, false)
|
985
991
|
end
|
986
992
|
end
|
987
993
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: speculation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie English
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -177,7 +177,7 @@ files:
|
|
177
177
|
- lib/speculation.rb
|
178
178
|
- lib/speculation/error.rb
|
179
179
|
- lib/speculation/gen.rb
|
180
|
-
- lib/speculation/
|
180
|
+
- lib/speculation/method_identifier.rb
|
181
181
|
- lib/speculation/namespaced_symbols.rb
|
182
182
|
- lib/speculation/pmap.rb
|
183
183
|
- lib/speculation/spec.rb
|
@@ -187,6 +187,7 @@ files:
|
|
187
187
|
- lib/speculation/spec/hash_spec.rb
|
188
188
|
- lib/speculation/spec/merge_spec.rb
|
189
189
|
- lib/speculation/spec/nilable_spec.rb
|
190
|
+
- lib/speculation/spec/nonconforming_spec.rb
|
190
191
|
- lib/speculation/spec/or_spec.rb
|
191
192
|
- lib/speculation/spec/predicate_spec.rb
|
192
193
|
- lib/speculation/spec/regex_spec.rb
|