speculation 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -0
- data/.travis.yml +2 -4
- data/README.md +22 -12
- data/Rakefile +5 -9
- data/bin/console +54 -7
- data/examples/codebreaker.rb +246 -0
- data/examples/spec_guide.rb +1288 -0
- data/lib/speculation.rb +145 -146
- data/lib/speculation/error.rb +4 -3
- data/lib/speculation/gen.rb +51 -47
- data/lib/speculation/identifier.rb +7 -7
- data/lib/speculation/namespaced_symbols.rb +26 -19
- data/lib/speculation/pmap.rb +9 -10
- data/lib/speculation/spec_impl/and_spec.rb +3 -4
- data/lib/speculation/spec_impl/every_spec.rb +24 -24
- data/lib/speculation/spec_impl/f_spec.rb +32 -35
- data/lib/speculation/spec_impl/hash_spec.rb +33 -41
- data/lib/speculation/spec_impl/merge_spec.rb +2 -3
- data/lib/speculation/spec_impl/nilable_spec.rb +8 -9
- data/lib/speculation/spec_impl/or_spec.rb +5 -7
- data/lib/speculation/spec_impl/regex_spec.rb +2 -3
- data/lib/speculation/spec_impl/spec.rb +3 -5
- data/lib/speculation/spec_impl/tuple_spec.rb +8 -10
- data/lib/speculation/test.rb +126 -101
- data/lib/speculation/utils.rb +31 -5
- data/lib/speculation/version.rb +1 -1
- data/speculation.gemspec +0 -1
- metadata +30 -44
- data/lib/speculation/conj.rb +0 -32
- data/lib/speculation/utils_specs.rb +0 -57
@@ -0,0 +1,1288 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# See https://clojure.org/guides/spec (as of 2017-18-02)
|
3
|
+
# Output generated by https://github.com/JoshCheek/seeing_is_believing
|
4
|
+
|
5
|
+
require "set"
|
6
|
+
require "date"
|
7
|
+
require "speculation"
|
8
|
+
|
9
|
+
S = Speculation
|
10
|
+
extend S::NamespacedSymbols
|
11
|
+
|
12
|
+
## Predicates
|
13
|
+
|
14
|
+
# Each spec describes a set of allowed values. There are several ways to build
|
15
|
+
# specs and all of them can be composed to build more sophisticated specs.
|
16
|
+
|
17
|
+
# A Ruby proc that takes a single argument and returns a truthy value is a
|
18
|
+
# valid predicate spec. We can check whether a particular data value conforms
|
19
|
+
# to a spec using conform:
|
20
|
+
|
21
|
+
S.conform :even?.to_proc, 1000 # => 1000
|
22
|
+
|
23
|
+
# The conform function takes something that can be a spec and a data value.
|
24
|
+
# Here we are passing a predicate which is implicitly converted into a spec.
|
25
|
+
# The return value is "conformed". Here, the conformed value is the same as the
|
26
|
+
# original value - we’ll see later where that starts to deviate. If the value
|
27
|
+
# does not conform to the spec, the special value :"Speculation/invalid" is
|
28
|
+
# returned.
|
29
|
+
|
30
|
+
# If you don’t want to use the conformed value or check for
|
31
|
+
# :"Speculation/invalid", the helper valid? can be used instead to return a
|
32
|
+
# boolean.
|
33
|
+
|
34
|
+
S.valid? :even?.to_proc, 10 # => true
|
35
|
+
|
36
|
+
# Note that again valid? implicitly converts the predicate function into a
|
37
|
+
# spec. The spec library allows you to leverage all of the functions you
|
38
|
+
# already have - there is no special dictionary of predicates. Some more
|
39
|
+
# examples:
|
40
|
+
|
41
|
+
S.valid? :nil?.to_proc, nil # => true
|
42
|
+
S.valid? ->(x) { x.is_a?(String) }, "abc" # => true
|
43
|
+
S.valid? ->(x) { x > 5 }, 10 # => true
|
44
|
+
S.valid? ->(x) { x > 5 }, 0 # => false
|
45
|
+
|
46
|
+
# Regexps, Classes and Modules can be used as predicates.
|
47
|
+
|
48
|
+
S.valid? /^\d+$/, "123" # => true
|
49
|
+
S.valid? String, "abc" # => true
|
50
|
+
S.valid? Enumerable, [1, 2, 3] # => true
|
51
|
+
S.valid? Date, Date.new # => true
|
52
|
+
|
53
|
+
# Sets can also be used as predicates that match one or more literal values:
|
54
|
+
|
55
|
+
S.valid? Set[:club, :diamond, :heart, :spade], :club # => true
|
56
|
+
S.valid? Set[:club, :diamond, :heart, :spade], 42 # => false
|
57
|
+
S.valid? Set[42], 42 # => true
|
58
|
+
|
59
|
+
## Registry
|
60
|
+
|
61
|
+
# Until now, we’ve been using specs directly. However, spec provides a central
|
62
|
+
# registry for globally declaring reusable specs. The registry associates a
|
63
|
+
# namespaced symbol with a specification. The use of namespaces ensures that
|
64
|
+
# we can define reusable non-conflicting specs across libraries or
|
65
|
+
# applications.
|
66
|
+
|
67
|
+
# Specs are registered using def. It’s up to you to register the specification
|
68
|
+
# in a namespace that makes sense (typically a namespace you control).
|
69
|
+
|
70
|
+
S.def ns(:date), Date # => :"Object/date"
|
71
|
+
S.def ns(:suit), Set[:club, :diamond, :heart, :spade] # => :"Object/suit"
|
72
|
+
|
73
|
+
# A registered spec identifier can be used in place of a spec definition in the
|
74
|
+
# operations we’ve seen so far - conform and valid?.
|
75
|
+
|
76
|
+
S.valid? ns(:date), Date.new # => true
|
77
|
+
S.conform ns(:suit), :club # => :club
|
78
|
+
|
79
|
+
# You will see later that registered specs can (and should) be used anywhere we
|
80
|
+
# compose specs.
|
81
|
+
|
82
|
+
## Composing predicates
|
83
|
+
|
84
|
+
# The simplest way to compose specs is with and and or. Let’s create a spec
|
85
|
+
# that combines several predicates into a composite spec with S.and:
|
86
|
+
|
87
|
+
S.def ns(:big_even), S.and(Integer, :even?.to_proc, ->(x) { x > 1000 })
|
88
|
+
S.valid? ns(:big_even), :foo # => false
|
89
|
+
S.valid? ns(:big_even), 10 # => false
|
90
|
+
S.valid? ns(:big_even), 100000 # => true
|
91
|
+
|
92
|
+
# We can also use S.or to specify two alternatives:
|
93
|
+
|
94
|
+
S.def ns(:name_or_id), S.or(:name => String, :id => Integer)
|
95
|
+
S.valid? ns(:name_or_id), "abc" # => true
|
96
|
+
S.valid? ns(:name_or_id), 100 # => true
|
97
|
+
S.valid? ns(:name_or_id), :foo # => false
|
98
|
+
|
99
|
+
# This or spec is the first case we’ve seen that involves a choice during
|
100
|
+
# validity checking. Each choice is annotated with a tag (here, between :name
|
101
|
+
# and :id) and those tags give the branches names that can be used to
|
102
|
+
# understand or enrich the data returned from conform and other spec functions.
|
103
|
+
|
104
|
+
# When an or is conformed, it returns an array with the tag name and conformed
|
105
|
+
# value:
|
106
|
+
|
107
|
+
S.conform ns(:name_or_id), "abc" # => [:name, "abc"]
|
108
|
+
S.conform ns(:name_or_id), 100 # => [:id, 100]
|
109
|
+
|
110
|
+
# Many predicates that check an instance’s type do not allow nil as a valid
|
111
|
+
# value (String, ->(x) { x.even? }, /foo/, etc). To include nil as a valid
|
112
|
+
# value, use the provided function nilable to make a spec:
|
113
|
+
|
114
|
+
S.valid? String, nil # => false
|
115
|
+
S.valid? S.nilable(String), nil # => true
|
116
|
+
|
117
|
+
## Explain
|
118
|
+
|
119
|
+
# explain is another high-level operation in spec that can be used to report
|
120
|
+
# (to STDOUT) why a value does not conform to a spec. Let’s see what explain
|
121
|
+
# says about some non-conforming examples we’ve seen so far.
|
122
|
+
|
123
|
+
S.explain ns(:suit), 42
|
124
|
+
# >> val: 42 fails spec: :"Object/suit" predicate: [#<Set: {:club, :diamond, :heart, :spade}>, [42]]
|
125
|
+
|
126
|
+
S.explain ns(:big_even), 5
|
127
|
+
# >> val: 5 fails spec: :"Object/big_even" predicate: [#<Proc:0x007fb69b1908f8(&:even?)>, [5]]
|
128
|
+
|
129
|
+
S.explain ns(:name_or_id), :foo
|
130
|
+
# >> val: :foo fails spec: :"Object/name_or_id" at: [:name] predicate: [String, [:foo]]
|
131
|
+
# >> val: :foo fails spec: :"Object/name_or_id" at: [:id] predicate: [Integer, [:foo]]
|
132
|
+
|
133
|
+
# Let’s examine the output of the final example more closely. First note that
|
134
|
+
# there are two errors being reported - spec will evaluate all possible
|
135
|
+
# alternatives and report errors on every path. The parts of each error are:
|
136
|
+
|
137
|
+
# - val - the value in the user’s input that does not match
|
138
|
+
# - spec - the spec that was being evaluated
|
139
|
+
# - at - a path (an array of symbols) indicating the location within the spec
|
140
|
+
# where the error occurred - the tags in the path correspond to any tagged part
|
141
|
+
# in a spec (the alternatives in an or or alt, the parts of a cat, the keys in
|
142
|
+
# a map, etc)
|
143
|
+
# - predicate - the actual predicate that was not satsified by val
|
144
|
+
# - in - the key path through a nested data val to the failing value. In this
|
145
|
+
# example, the top-level value is the one that is failing so this is
|
146
|
+
# essentially an empty path and is omitted.
|
147
|
+
# - For the first reported error we can see that the value :foo did not satisfy
|
148
|
+
# the predicate String at the path :name in the spec ns(:name-or-id). The second
|
149
|
+
# reported error is similar but fails on the :id path instead. The actual value
|
150
|
+
# is a Symbol so neither is a match.
|
151
|
+
|
152
|
+
# In addition to explain, you can use explain_str to receive the error messages
|
153
|
+
# as a string or explain_data to receive the errors as data.
|
154
|
+
|
155
|
+
S.explain_data ns(:name_or_id), :foo
|
156
|
+
# => {:"Speculation/problems"=>
|
157
|
+
# [{:path=>[:name],
|
158
|
+
# :val=>:foo,
|
159
|
+
# :via=>[:"Object/name_or_id"],
|
160
|
+
# :in=>[],
|
161
|
+
# :pred=>[String, [:foo]]},
|
162
|
+
# {:path=>[:id],
|
163
|
+
# :val=>:foo,
|
164
|
+
# :via=>[:"Object/name_or_id"],
|
165
|
+
# :in=>[],
|
166
|
+
# :pred=>[Integer, [:foo]]}]}
|
167
|
+
|
168
|
+
## Entity hashes
|
169
|
+
|
170
|
+
# Ruby programs rely heavily on passing around hashes of data. (That may not be
|
171
|
+
# true...) A common approach in other libraries is to describe each entity
|
172
|
+
# type, combining both the keys it contains and the structure of their values.
|
173
|
+
# Rather than define attribute (key+value) specifications in the scope of the
|
174
|
+
# entity (the hash), specs assign meaning to individual attributes, then
|
175
|
+
# collect them into shahes using set semantics (on the keys). This approach
|
176
|
+
# allows us to start assigning (and sharing) semantics at the attribute level
|
177
|
+
# across our libraries and applications.
|
178
|
+
|
179
|
+
# This statement isn't quite true for Ruby's Ring equivalent, Rack:
|
180
|
+
# ~~For example, most Ring middleware functions modify the request or response
|
181
|
+
# map with unqualified keys. However, each middleware could instead use
|
182
|
+
# namespaced keys with registered semantics for those keys. The keys could then
|
183
|
+
# be checked for conformance, creating a system with greater opportunities for
|
184
|
+
# collaboration and consistency.~~
|
185
|
+
|
186
|
+
# Entity maps in spec are defined with keys:
|
187
|
+
|
188
|
+
email_regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/
|
189
|
+
|
190
|
+
S.def ns(:email_type), S.and(String, email_regex)
|
191
|
+
|
192
|
+
S.def ns(:acctid), Integer
|
193
|
+
S.def ns(:first_name), String
|
194
|
+
S.def ns(:last_name), String
|
195
|
+
S.def ns(:email), ns(:email_type)
|
196
|
+
|
197
|
+
S.def ns(:person), S.keys(:req => [ns(:first_name), ns(:last_name), ns(:email)], :opt => [ns(:phone)])
|
198
|
+
|
199
|
+
# This registers a ns(:person) spec with the required keys ns(:first-name),
|
200
|
+
# ns(:last_name), and ns(:email), with optional key ns(:phone). The hash spec
|
201
|
+
# never specifies the value spec for the attributes, only what attributes are
|
202
|
+
# required or optional.
|
203
|
+
|
204
|
+
# When conformance is checked on a hash, it does two things - checking that the
|
205
|
+
# required attributes are included, and checking that every registered key has
|
206
|
+
# a conforming value. We’ll see later where optional attributes can be useful.
|
207
|
+
# Also note that ALL attributes are checked via keys, not just those listed in
|
208
|
+
# the :req and :opt keys. Thus a bare S.keys is valid and will check all
|
209
|
+
# attributes of a map without checking which keys are required or optional.
|
210
|
+
|
211
|
+
S.valid? ns(:person), ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "elon@example.com" # => true
|
212
|
+
|
213
|
+
# Fails required key check
|
214
|
+
S.explain ns(:person), ns(:first_name) => "Elon"
|
215
|
+
# >> val: {:"Object/first_name"=>"Elon"} fails spec: :"Object/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
|
216
|
+
# >> val: {:"Object/first_name"=>"Elon"} fails spec: :"Object/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
|
217
|
+
|
218
|
+
# Fails attribute conformance
|
219
|
+
S.explain ns(:person), ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "n/a"
|
220
|
+
# >> In: [:"Object/email"] val: "n/a" fails spec: :"Object/email_type" at: [:"Object/email"] predicate: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/, ["n/a"]]
|
221
|
+
|
222
|
+
# Let’s take a moment to examine the explain error output on that final example:
|
223
|
+
|
224
|
+
# - in - the path within the data to the failing value (here, a key in the person instance)
|
225
|
+
# - val - the failing value, here "n/a"
|
226
|
+
# - spec - the spec that failed, here :my.domain/email
|
227
|
+
# - at - the path in the spec where the failing value is located
|
228
|
+
# - predicate - the predicate that failed, here (re-matches email-regex %)
|
229
|
+
|
230
|
+
# Much existing Ruby code does not use hashes with namespaced keys and so keys
|
231
|
+
# can also specify :req_un and :opt_un for required and optional unqualified
|
232
|
+
# keys. These variants specify namespaced keys used to find their
|
233
|
+
# specification, but the map only checks for the unqualified version of the
|
234
|
+
# keys.
|
235
|
+
|
236
|
+
# Let’s consider a person map that uses unqualified keys but checks conformance
|
237
|
+
# against the namespaced specs we registered earlier:
|
238
|
+
|
239
|
+
S.def :"unq/person", S.keys(:req_un => [ns(:first_name), ns(:last_name), ns(:email)],
|
240
|
+
:opt_un => [ns(:phone)])
|
241
|
+
|
242
|
+
S.conform :"unq/person", :first_name => "Elon", :last_name => "Musk", :email => "elon@example.com"
|
243
|
+
# => {:first_name=>"Elon", :last_name=>"Musk", :email=>"elon@example.com"}
|
244
|
+
|
245
|
+
S.explain :"unq/person", :first_name => "Elon", :last_name => "Musk", :email => "n/a"
|
246
|
+
# >> In: [:email] val: "n/a" fails spec: :"Object/email_type" at: [:email] predicate: [/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/, ["n/a"]]
|
247
|
+
|
248
|
+
S.explain :"unq/person", :first_name => "Elon"
|
249
|
+
# >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/last_name"]]
|
250
|
+
# >> val: {:first_name=>"Elon"} fails spec: :"unq/person" predicate: [#<Method: Speculation::Utils.key?>, [:"Object/email"]]
|
251
|
+
|
252
|
+
# Unqualified keys can also be used to validate record attributes # TODO for objects/structs
|
253
|
+
# Keyword args keys* - don't support
|
254
|
+
|
255
|
+
# Sometimes it will be convenient to declare entity maps in parts, either
|
256
|
+
# because there are different sources for requirements on an entity map or
|
257
|
+
# because there is a common set of keys and variant-specific parts. The S.merge
|
258
|
+
# spec can be used to combine multiple S.keys specs into a single spec that
|
259
|
+
# combines their requirements. For example consider two keys specs that define
|
260
|
+
# common animal attributes and some dog-specific ones. The dog entity itself
|
261
|
+
# can be described as a merge of those two attribute sets:
|
262
|
+
|
263
|
+
S.def :"animal/kind", String
|
264
|
+
S.def :"animal/says", String
|
265
|
+
S.def :"animal/common", S.keys(:req => [:"animal/kind", :"animal/says"])
|
266
|
+
S.def :"dog/tail?", ns(S, :boolean)
|
267
|
+
S.def :"dog/breed", String
|
268
|
+
S.def :"animal/dog", S.merge(:"animal/common", S.keys(:req => [:"dog/tail?", :"dog/breed"]))
|
269
|
+
|
270
|
+
S.valid? :"animal/dog", :"animal/kind" => "dog", :"animal/says" => "woof", :"dog/tail?" => true, :"dog/breed" => "retriever" # => true
|
271
|
+
|
272
|
+
## Multi-spec - don't support
|
273
|
+
|
274
|
+
## Collections
|
275
|
+
|
276
|
+
# A few helpers are provided for other special collection cases - coll_of,
|
277
|
+
# tuple, and hash_of.
|
278
|
+
|
279
|
+
# For the special case of a homogenous collection of arbitrary size, you can
|
280
|
+
# use coll_of to specify a collection of elements satisfying a predicate.
|
281
|
+
|
282
|
+
S.conform S.coll_of(Symbol), [:a, :b, :c] # => [:a, :b, :c]
|
283
|
+
S.conform S.coll_of(Numeric), Set[5, 10, 2] # => #<Set: {5, 10, 2}>
|
284
|
+
|
285
|
+
# Additionally, coll-of can be passed a number of keyword arg options:
|
286
|
+
|
287
|
+
# :kind - a predicate or spec that the incoming collection must satisfy, such as `Array`
|
288
|
+
# :count - specifies exact expected count
|
289
|
+
# :min_count, :max_count - checks that collection has `count.between?(min_count, max_count)`
|
290
|
+
# :distinct - checks that all elements are distinct
|
291
|
+
# :into - one of [], {}, or Set[] for output conformed value. If :into is not specified, the input collection type will be used.
|
292
|
+
|
293
|
+
# Following is an example utilizing some of these options to spec an array
|
294
|
+
# containing three distinct numbers conformed as a set and some of the errors
|
295
|
+
# for different kinds of invalid values:
|
296
|
+
|
297
|
+
S.def ns(:vnum3), S.coll_of(Numeric, :kind => Array, :count => 3, :distinct => true, :into => Set[])
|
298
|
+
S.conform ns(:vnum3), [1, 2, 3] # => #<Set: {1, 2, 3}>
|
299
|
+
S.explain ns(:vnum3), Set[1, 2, 3] # not an array
|
300
|
+
# >> val: #<Set: {1, 2, 3}> fails spec: :"Object/vnum3" predicate: [Array, [#<Set: {1, 2, 3}>]]
|
301
|
+
S.explain ns(:vnum3), [1, 1, 1] # not distinct
|
302
|
+
# >> val: [1, 1, 1] fails spec: :"Object/vnum3" predicate: [#<Method: Speculation::Utils.distinct?>, [[1, 1, 1]]]
|
303
|
+
S.explain ns(:vnum3), [1, 2, :a] # not a number
|
304
|
+
# >> In: [2] val: :a fails spec: :"Object/vnum3" predicate: [Numeric, [:a]]
|
305
|
+
|
306
|
+
# NOTE: Both coll-of and map-of will conform all of their elements, which may
|
307
|
+
# make them unsuitable for large collections. In that case, consider every or
|
308
|
+
# for maps every-kv.
|
309
|
+
|
310
|
+
# While coll-of is good for homogenous collections of any size, another case is
|
311
|
+
# a fixed-size positional collection with fields of known type at different
|
312
|
+
# positions. For that we have tuple.
|
313
|
+
|
314
|
+
S.def ns(:point), S.tuple(Float, Float, Float)
|
315
|
+
S.conform ns(:point), [1.5, 2.5, -0.5] # => [1.5, 2.5, -0.5]
|
316
|
+
|
317
|
+
# Note that in this case of a "point" structure with x/y/z values we actually
|
318
|
+
# had a choice of three possible specs:
|
319
|
+
|
320
|
+
# - Regular expression - S.cat :x => Float, :y => Float, :z => Float
|
321
|
+
# - Allows for matching nested structure (not needed here)
|
322
|
+
# - Conforms to hash with named keys based on the cat tags
|
323
|
+
# - Collection - S.coll_of Float
|
324
|
+
# - Designed for arbitrary size homogenous collections
|
325
|
+
# - Conforms to an array of the values
|
326
|
+
# - Tuple - S.tuple Float, Float, Float
|
327
|
+
# - Designed for fixed size with known positional "fields"
|
328
|
+
# - Conforms to an array of the values
|
329
|
+
|
330
|
+
# In this example, coll_of will match other (invalid) values as well (like
|
331
|
+
# [1.0] or [1.0 2.0 3.0 4.0]), so it is not a suitable choice - we want fixed
|
332
|
+
# fields. The choice between a regular expression and tuple here is to some
|
333
|
+
# degree a matter of taste, possibly informed by whether you expect either the
|
334
|
+
# tagged return values or error output to be better with one or the other.
|
335
|
+
|
336
|
+
# In addition to the support for information hashes via keys, spec also
|
337
|
+
# provides hash_of for maps with homogenous key and value predicates.
|
338
|
+
|
339
|
+
S.def ns(:scores), S.hash_of(String, Integer)
|
340
|
+
S.conform ns(:scores), "Sally" => 1000, "Joe" => 300 # => {"Sally"=>1000, "Joe"=>300}
|
341
|
+
|
342
|
+
# By default hash_of will validate but not conform keys because conformed keys
|
343
|
+
# might create key duplicates that would cause entries in the map to be
|
344
|
+
# overridden. If conformed keys are desired, pass the option
|
345
|
+
# `:conform_keys => # true`.
|
346
|
+
|
347
|
+
# You can also use the various count-related options on hash_of that you have
|
348
|
+
# with coll_of.
|
349
|
+
|
350
|
+
## Sequences
|
351
|
+
|
352
|
+
# Sometimes sequential data is used to encode additional structure. spec
|
353
|
+
# provides the standard regular expression operators to describe the structure
|
354
|
+
# of a sequential data value:
|
355
|
+
|
356
|
+
# - cat - concatenation of predicates/patterns
|
357
|
+
# - alt - choice among alternative predicates/patterns
|
358
|
+
# - zero_or_more - 0 or more of a predicate/pattern
|
359
|
+
# - one_or_more - 1 or more of a predicate/pattern
|
360
|
+
# - zero_or_one - 0 or 1 of a predicate/pattern
|
361
|
+
|
362
|
+
# Like or, both cat and alt tag their "parts" - these tags are then used in the
|
363
|
+
# conformed value to identify what was matched, to report errors, and more.
|
364
|
+
|
365
|
+
# Consider an ingredient represented by an array containing a quantity (number)
|
366
|
+
# and a unit (symbol). The spec for this data uses cat to specify the right
|
367
|
+
# components in the right order. Like predicates, regex operators are
|
368
|
+
# implicitly converted to specs when passed to functions like conform, valid?,
|
369
|
+
# etc.
|
370
|
+
|
371
|
+
S.def ns(:ingredient), S.cat(:quantity => Numeric, :unit => Symbol)
|
372
|
+
S.conform ns(:ingredient), [2, :teaspoon] # => {:quantity=>2, :unit=>:teaspoon}
|
373
|
+
|
374
|
+
# The data is conformed as a hash with the tags as keys. We can use explain to
|
375
|
+
# examine non-conforming data.
|
376
|
+
|
377
|
+
# pass string for unit instead of keyword
|
378
|
+
S.explain ns(:ingredient), [11, "peaches"]
|
379
|
+
# >> In: [1] val: "peaches" fails spec: :"Object/ingredient" at: [:unit] predicate: [Symbol, ["peaches"]]
|
380
|
+
|
381
|
+
# leave out the unit
|
382
|
+
S.explain ns(:ingredient), [2]
|
383
|
+
# >> val: [] fails spec: :"Object/ingredient" at: [:unit] predicate: [Symbol, []], "Insufficient input"
|
384
|
+
|
385
|
+
# Let’s now see the various occurence operators zero_or_more, one_or_more, and zero_or_one:
|
386
|
+
|
387
|
+
S.def ns(:seq_of_symbols), S.zero_or_more(Symbol)
|
388
|
+
S.conform ns(:seq_of_symbols), [:a, :b, :c] # => [:a, :b, :c]
|
389
|
+
S.explain ns(:seq_of_symbols), [10, 20]
|
390
|
+
# >> In: [0] val: 10 fails spec: :"Object/seq_of_symbols" predicate: [Symbol, [10]]
|
391
|
+
|
392
|
+
S.def ns(:odds_then_maybe_even), S.cat(:odds => S.one_or_more(:odd?.to_proc),
|
393
|
+
:even => S.zero_or_one(:even?.to_proc))
|
394
|
+
S.conform ns(:odds_then_maybe_even), [1, 3, 5, 100] # => {:odds=>[1, 3, 5], :even=>100}
|
395
|
+
|
396
|
+
S.conform ns(:odds_then_maybe_even), [1] # => {:odds=>[1]}
|
397
|
+
S.explain ns(:odds_then_maybe_even), [100]
|
398
|
+
# >> In: [0] val: 100 fails spec: :"Object/odds_then_maybe_even" at: [:odds] predicate: [#<Proc:0x007fb69a186ea8(&:odd?)>, [100]]
|
399
|
+
|
400
|
+
# opts are alternating symbols and booleans
|
401
|
+
S.def ns(:opts), S.zero_or_more(S.cat(:opt => Symbol, :val => ns(S, :boolean)))
|
402
|
+
S.conform ns(:opts), [:silent?, false, :verbose, true]
|
403
|
+
# => [{:opt=>:silent?, :val=>false}, {:opt=>:verbose, :val=>true}]
|
404
|
+
|
405
|
+
# Finally, we can use alt to specify alternatives within the sequential data.
|
406
|
+
# Like cat, alt requires you to tag each alternative but the conformed data is
|
407
|
+
# a vector of tag and value.
|
408
|
+
|
409
|
+
S.def ns(:config), S.zero_or_more(S.cat(:prop => String,
|
410
|
+
:val => S.alt(:s => String, :b => ns(S, :boolean))))
|
411
|
+
S.conform ns(:config), ["-server", "foo", "-verbose", true, "-user", "joe"]
|
412
|
+
# => [{:prop=>"-server", :val=>[:s, "foo"]},
|
413
|
+
# {:prop=>"-verbose", :val=>[:b, true]},
|
414
|
+
# {:prop=>"-user", :val=>[:s, "joe"]}]
|
415
|
+
|
416
|
+
# TODO: If you need a description of a specification, use describe to retrieve one.
|
417
|
+
|
418
|
+
# Spec also defines one additional regex operator, `constrained`, which takes a
|
419
|
+
# regex operator and constrains it with one or more additional predicates. This
|
420
|
+
# can be used to create regular expressions with additional constraints that
|
421
|
+
# would otherwise require custom predicates. For example, consider wanting to
|
422
|
+
# match only sequences with an even number of strings:
|
423
|
+
|
424
|
+
S.def ns(:even_strings), S.constrained(S.zero_or_more(String), ->(coll) { coll.count.even? })
|
425
|
+
|
426
|
+
S.valid? ns(:even_strings), ["a"] # => false
|
427
|
+
S.valid? ns(:even_strings), ["a", "b"] # => true
|
428
|
+
S.valid? ns(:even_strings), ["a", "b", "c"] # => false
|
429
|
+
S.valid? ns(:even_strings), ["a", "b", "c", "d"] # => true
|
430
|
+
|
431
|
+
# When regex ops are combined, they describe a single sequence. If you need to
|
432
|
+
# spec a nested sequential collection, you must use an explicit call to spec to
|
433
|
+
# start a new nested regex context. For example to describe a sequence like
|
434
|
+
# [:names, ["a", "b"], :nums, [1 2 3]], you need nested regular expressions to
|
435
|
+
# describe the inner sequential data:
|
436
|
+
|
437
|
+
S.def ns(:nested), S.cat(:names_sym => Set[:names],
|
438
|
+
:names => S.spec(S.zero_or_more(String)),
|
439
|
+
:nums_sym => Set[:nums],
|
440
|
+
:nums => S.spec(S.zero_or_more(Numeric)))
|
441
|
+
|
442
|
+
S.conform ns(:nested), [:names, ["a", "b"], :nums, [1, 2, 3]]
|
443
|
+
# => {:names_sym=>:names,
|
444
|
+
# :names=>["a", "b"],
|
445
|
+
# :nums_sym=>:nums,
|
446
|
+
# :nums=>[1, 2, 3]}
|
447
|
+
|
448
|
+
# If the specs were removed this spec would instead match a sequence like
|
449
|
+
# [:names, "a", "b", :nums, 1, 2, 3].
|
450
|
+
|
451
|
+
S.def ns(:unnested), S.cat(:names_sym => Set[:names],
|
452
|
+
:names => S.zero_or_more(String),
|
453
|
+
:nums_sym => Set[:nums],
|
454
|
+
:nums => S.zero_or_more(Numeric))
|
455
|
+
|
456
|
+
S.conform ns(:unnested), [:names, "a", "b", :nums, 1, 2, 3]
|
457
|
+
# => {:names_sym=>:names,
|
458
|
+
# :names=>["a", "b"],
|
459
|
+
# :nums_sym=>:nums,
|
460
|
+
# :nums=>[1, 2, 3]}
|
461
|
+
|
462
|
+
## Using spec for validation
|
463
|
+
|
464
|
+
# Now is a good time to step back and think about how spec can be used for
|
465
|
+
# runtime data validation.
|
466
|
+
|
467
|
+
# One way to use spec is to explicitly call valid? to verify input data passed
|
468
|
+
# to a function. ~~You can, for example, use the existing pre- and post-condition
|
469
|
+
# support built into defn:~~
|
470
|
+
|
471
|
+
def self.person_name(person)
|
472
|
+
raise "invalid" unless S.valid? ns(:person), person
|
473
|
+
name = "#{person[ns(:first_name)]} #{person[ns(:last_name)]}"
|
474
|
+
raise "invalid" unless S.valid? String, name
|
475
|
+
name
|
476
|
+
end
|
477
|
+
|
478
|
+
person_name 43 rescue $! # => #<RuntimeError: invalid>
|
479
|
+
person_name ns(:first_name) => "Elon", ns(:last_name) => "Musk", ns(:email) => "elon@example.com"
|
480
|
+
# => "Elon Musk"
|
481
|
+
|
482
|
+
# When the function is invoked with something that isn’t valid ns(:person) data,
|
483
|
+
# the pre-condition fails. Similarly, if there was a bug in our code and the
|
484
|
+
# output was not a string, the post-condition would fail.
|
485
|
+
|
486
|
+
# Another option is to use S.assert within your code to assert that a value
|
487
|
+
# satisfies a spec. On success the value is returned and on failure an
|
488
|
+
# assertion error is thrown. By default assertion checking is off - this can be
|
489
|
+
# changed by setting S.check_asserts or having the environment variable
|
490
|
+
# "SPECULATION_CHECK_ASSERTS=true".
|
491
|
+
|
492
|
+
def self.person_name(person)
|
493
|
+
p = S.assert ns(:person), person
|
494
|
+
"#{p[ns(:first_name)]} #{p[ns(:last_name)]}"
|
495
|
+
end
|
496
|
+
|
497
|
+
S.check_asserts = true
|
498
|
+
person_name 100 rescue $!
|
499
|
+
# => #<Speculation::Error: Spec assertion failed
|
500
|
+
# val: 100 fails predicate: [#<Method: Speculation::Utils.hash?>, [100]]
|
501
|
+
# Speculation/failure :assertion_failed
|
502
|
+
# {:"Speculation/problems"=>
|
503
|
+
# [{:path=>[],
|
504
|
+
# :pred=>[#<Method: Speculation::Utils.hash?>, [100]],
|
505
|
+
# :val=>100,
|
506
|
+
# :via=>[],
|
507
|
+
# :in=>[]}],
|
508
|
+
# :"Speculation/failure"=>:assertion_failed}
|
509
|
+
# >
|
510
|
+
|
511
|
+
# A deeper level of integration is to call conform and use the return value to
|
512
|
+
# destructure the input. This will be particularly useful for complex inputs
|
513
|
+
# with alternate options.
|
514
|
+
|
515
|
+
# Here we conform using the config specification defined above:
|
516
|
+
|
517
|
+
def self.set_config(prop, val)
|
518
|
+
# dummy fn
|
519
|
+
puts "set #{prop} #{val}"
|
520
|
+
end
|
521
|
+
|
522
|
+
def self.configure(input)
|
523
|
+
parsed = S.conform(ns(:config), input)
|
524
|
+
if parsed == ns(S, :invalid)
|
525
|
+
raise "Invalid input\n#{S.explain_str(ns(:config), input)}"
|
526
|
+
else
|
527
|
+
parsed.each do |config|
|
528
|
+
prop, val = config.values_at(:prop, :val)
|
529
|
+
_type, val = val
|
530
|
+
|
531
|
+
set_config(prop[1..-1], val)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
configure ["-server", "foo", "-verbose", true, "-user", "joe"]
|
537
|
+
# set server foo
|
538
|
+
# set verbose true
|
539
|
+
# set user joe
|
540
|
+
|
541
|
+
# Here configure calls conform to destructure the config input. The result is
|
542
|
+
# either the special :"Speculation/invalid" value or a destructured form of the
|
543
|
+
# result:
|
544
|
+
|
545
|
+
[{ :prop => "-server", :val => [:s, "foo"] },
|
546
|
+
{ :prop => "-verbose", :val => [:b, true] },
|
547
|
+
{ :prop => "-user", :val => [:s, "joe"] }]
|
548
|
+
|
549
|
+
# In the success case, the parsed input is transformed into the desired shape
|
550
|
+
# for further processing. In the error case, we call explain_str to generate
|
551
|
+
# an error message. The explain string contains information about what
|
552
|
+
# expression failed to conform, the path to that expression in the
|
553
|
+
# specification, and the predicate it was attempting to match.
|
554
|
+
|
555
|
+
## Spec’ing methods
|
556
|
+
|
557
|
+
# The pre- and post-condition example in the previous section hinted at an
|
558
|
+
# interesting question - how do we define the input and output specifications
|
559
|
+
# for a method.
|
560
|
+
|
561
|
+
# Spec has explicit support for this using fdef, which defines specifications
|
562
|
+
# for a function - the arguments and/or the return value spec, and optionally a
|
563
|
+
# function that can specify a relationship between args and return.
|
564
|
+
|
565
|
+
# Let’s consider a ranged-rand function that produces a random number in a
|
566
|
+
# range:
|
567
|
+
|
568
|
+
def self.ranged_rand(from, to)
|
569
|
+
rand(from...to)
|
570
|
+
end
|
571
|
+
|
572
|
+
# We can then provide a specification for that function:
|
573
|
+
|
574
|
+
S.fdef(method(:ranged_rand),
|
575
|
+
:args => S.and(S.cat(:start => Integer, :end => Integer), ->(args) { args[:start] < args[:end] }),
|
576
|
+
:ret => Integer,
|
577
|
+
:fn => S.and(->(fn) { fn[:ret] >= fn[:args][:start] },
|
578
|
+
->(fn) { fn[:ret] < fn[:args][:end] }))
|
579
|
+
|
580
|
+
# This function spec demonstrates a number of features. First the :args is a
|
581
|
+
# compound spec that describes the function arguments. This spec is invoked
|
582
|
+
# with the args in an array, as if they were invoked like `method.call(*args)`
|
583
|
+
# Because the args are sequential and the args are positional fields, they are
|
584
|
+
# almost always described using a regex op, like cat, alt, or zero_or_more.
|
585
|
+
|
586
|
+
# The second :args predicate takes as input the conformed result of the first
|
587
|
+
# predicate and verifies that start < end. The :ret spec indicates the return
|
588
|
+
# is also an integer. Finally, the :fn spec checks that the return value is >=
|
589
|
+
# start and < end.
|
590
|
+
|
591
|
+
# We’ll see later how we can use a function spec for development and testing.
|
592
|
+
|
593
|
+
## Higher order functions
|
594
|
+
|
595
|
+
# Higher order functions are common in ~Clojure~ Ruby and spec provides fspec
|
596
|
+
# to support spec’ing them.
|
597
|
+
|
598
|
+
# For example, consider the adder function:
|
599
|
+
|
600
|
+
def self.adder(x)
|
601
|
+
->(y) { x + y }
|
602
|
+
end
|
603
|
+
|
604
|
+
# adder returns a proc that adds x. We can declare a function spec for adder
|
605
|
+
# using fspec for the return value:
|
606
|
+
|
607
|
+
S.fdef method(:adder),
|
608
|
+
:args => S.cat(:x => Numeric),
|
609
|
+
:ret => S.fspec(:args => S.cat(:y => Numeric), :ret => Numeric),
|
610
|
+
:fn => ->(fn) { fn[:args][:x] == fn[:ret].call(0) }
|
611
|
+
|
612
|
+
# The :ret spec uses fspec to declare that the returning function takes and
|
613
|
+
# returns a number. Even more interesting, the :fn spec can state a general
|
614
|
+
# property that relates the :args (where we know x) and the result we get from
|
615
|
+
# invoking the function returned from adder, namely that adding 0 to it should
|
616
|
+
# return x.
|
617
|
+
|
618
|
+
## Macros - noop
|
619
|
+
|
620
|
+
## A game of cards
|
621
|
+
|
622
|
+
# Here’s a bigger set of specs to model a game of cards:
|
623
|
+
|
624
|
+
suit = Set[:club, :diamond, :heart, :spade]
|
625
|
+
rank = Set[:jack, :queen, :king, :ace].merge(2..10)
|
626
|
+
deck = rank.to_a.product(suit.to_a)
|
627
|
+
|
628
|
+
S.def ns(:card), S.tuple(rank, suit)
|
629
|
+
S.def ns(:hand), S.zero_or_more(ns(:card))
|
630
|
+
|
631
|
+
S.def ns(:name), String
|
632
|
+
S.def ns(:score), Integer
|
633
|
+
S.def ns(:player), S.keys(:req => [ns(:name), ns(:score), ns(:hand)])
|
634
|
+
|
635
|
+
S.def ns(:players), S.zero_or_more(ns(:player))
|
636
|
+
S.def ns(:deck), S.zero_or_more(ns(:card))
|
637
|
+
S.def ns(:game), S.keys(:req => [ns(:players), ns(:deck)])
|
638
|
+
|
639
|
+
# We can validate a piece of this data against the schema:
|
640
|
+
|
641
|
+
kenny = { ns(:name) => "Kenny Rogers",
|
642
|
+
ns(:score) => 100,
|
643
|
+
ns(:hand) => [] }
|
644
|
+
S.valid? ns(:player), kenny
|
645
|
+
# => true
|
646
|
+
|
647
|
+
# Or look at the errors we’ll get from some bad data:
|
648
|
+
|
649
|
+
S.explain ns(:game),
|
650
|
+
ns(:deck) => deck,
|
651
|
+
ns(:players) => [{ ns(:name) => "Kenny Rogers",
|
652
|
+
ns(:score) => 100,
|
653
|
+
ns(:hand) => [[2, :banana]] }]
|
654
|
+
# >> In: [:"Object/players", 0, :"Object/hand", 0, 1] val: :banana fails spec: :"Object/card" at: [:"Object/players", :"Object/hand", 1] predicate: [#<Set: {:club, :diamond, :heart, :spade}>, [:banana]]
|
655
|
+
|
656
|
+
# The error indicates the key path in the data structure down to the invalid
|
657
|
+
# value, the non-matching value, the spec part it’s trying to match, the path
|
658
|
+
# in that spec, and the predicate that failed.
|
659
|
+
|
660
|
+
# If we have a function `deal` that doles out some cards to the players we can
|
661
|
+
# spec that function to verify the arg and return value are both suitable
|
662
|
+
# data values. We can also specify a :fn spec to verify that the count of
|
663
|
+
# cards in the game before the deal equals the count of cards after the deal.
|
664
|
+
|
665
|
+
def self.total_cards(game)
|
666
|
+
game, players = game.values_at(ns(:game), ns(:players))
|
667
|
+
players.map { |player| player[ns(:hand)].count }.reduce(deck.count, &:+)
|
668
|
+
end
|
669
|
+
|
670
|
+
def self.deal(game)
|
671
|
+
# ...
|
672
|
+
end
|
673
|
+
|
674
|
+
S.fdef method(:deal),
|
675
|
+
:args => S.cat(:game => ns(:game)),
|
676
|
+
:ret => ns(:game),
|
677
|
+
:fn => ->(fn) { total_cards(fn[:args][:game]) == total_cards(fn[:ret]) }
|
678
|
+
|
679
|
+
## Generators
|
680
|
+
|
681
|
+
# A key design constraint of spec is that all specs are also designed to act as
|
682
|
+
# generators of sample data that conforms to the spec (a critical requirement
|
683
|
+
# for property-based testing).
|
684
|
+
|
685
|
+
## ~~Project Setup~~
|
686
|
+
|
687
|
+
# Nothing to do, Rantly is included by default. May look at removing the hard
|
688
|
+
# dependency in the future.
|
689
|
+
|
690
|
+
# In your code you also need to require the speculation/gen lib.
|
691
|
+
|
692
|
+
require "speculation/gen"
|
693
|
+
Gen = S::Gen
|
694
|
+
|
695
|
+
## Sampling Generators
|
696
|
+
|
697
|
+
# The gen function can be used to obtain the generator for any spec.
|
698
|
+
|
699
|
+
# Once you have obtained a generator with gen, there are several ways to use
|
700
|
+
# it. You can generate a single sample value with generate or a series of
|
701
|
+
# samples with sample. Let’s see some basic examples:
|
702
|
+
|
703
|
+
Gen.generate S.gen(Integer) # => 372495152381320358
|
704
|
+
Gen.generate S.gen(NilClass) # => nil
|
705
|
+
Gen.sample S.gen(String), 5
|
706
|
+
# => ["RhzOLQjmSjhWavH", "y", "", "O", "peoPwXHRBBAPjDxzEZQh"]
|
707
|
+
Gen.sample S.gen(Set[:club, :diamond, :heart, :spade]), 5
|
708
|
+
# => [:heart, :spade, :club, :spade, :spade]
|
709
|
+
|
710
|
+
Gen.sample S.gen(S.cat(:k => Symbol, :ns => S.one_or_more(Numeric))), 4
|
711
|
+
# => [[:csWKkimBORwN,
|
712
|
+
# -298753312314306397,
|
713
|
+
# -2303961522202434118,
|
714
|
+
# 1679934373136969303,
|
715
|
+
# -262631322747429978,
|
716
|
+
# 1.7157706401801108e+308,
|
717
|
+
# 1758361237993287532,
|
718
|
+
# 712842522394861335,
|
719
|
+
# -883871273503318653,
|
720
|
+
# 1283229873044628318,
|
721
|
+
# 1.5298057192258154e+308,
|
722
|
+
# 1.7789073686150528e+308,
|
723
|
+
# -2281793086040303873,
|
724
|
+
# 120746116914138063,
|
725
|
+
# -404134654833569820,
|
726
|
+
# -54740933266507251,
|
727
|
+
# 5.01892001701602e+307],
|
728
|
+
# [:RetYrsJr,
|
729
|
+
# 1.3391749738917395e+308,
|
730
|
+
# 1.0920197216545966e+307,
|
731
|
+
# 1.384947546752308e+307,
|
732
|
+
# 1.3364975035426882e+308,
|
733
|
+
# 327082393035103718,
|
734
|
+
# 1.0209866964240673e+308,
|
735
|
+
# 512415813150328683],
|
736
|
+
# [:UdDv,
|
737
|
+
# 3.0578102207508006e+307,
|
738
|
+
# 1.1626478137534508e+308,
|
739
|
+
# 1.7939796459941183e+308,
|
740
|
+
# 1494374259430455477,
|
741
|
+
# 1.342849042383955e+308,
|
742
|
+
# -281429214092326237,
|
743
|
+
# -552507314062007344,
|
744
|
+
# 4.1453903880025765e+307,
|
745
|
+
# -973157747452936365,
|
746
|
+
# 1.1388886925899274e+308,
|
747
|
+
# 2056792483501668313,
|
748
|
+
# 999682663796411736,
|
749
|
+
# 7.395274944717998e+306,
|
750
|
+
# -1514851160913660499,
|
751
|
+
# -2167762478595098510,
|
752
|
+
# 824382210168550458,
|
753
|
+
# 1614922845514653160],
|
754
|
+
# [:s,
|
755
|
+
# -234772724560973590,
|
756
|
+
# 1.0042104238108253e+308,
|
757
|
+
# 1.3942217537031457e+307,
|
758
|
+
# -1553774642616973743,
|
759
|
+
# -360282579504585923]]
|
760
|
+
|
761
|
+
# What about generating a random player in our card game?
|
762
|
+
|
763
|
+
Gen.generate S.gen(ns(:player))
|
764
|
+
# => {:"Object/name"=>"qrmY",
|
765
|
+
# :"Object/score"=>-188402685781919929,
|
766
|
+
# :"Object/hand"=>
|
767
|
+
# [[10, :heart],
|
768
|
+
# [:king, :heart],
|
769
|
+
# [2, :spade],
|
770
|
+
# [7, :heart],
|
771
|
+
# [9, :club],
|
772
|
+
# [7, :club],
|
773
|
+
# [10, :diamond],
|
774
|
+
# [:jack, :spade],
|
775
|
+
# [2, :diamond],
|
776
|
+
# [3, :diamond],
|
777
|
+
# [:king, :spade],
|
778
|
+
# [5, :spade],
|
779
|
+
# [10, :heart],
|
780
|
+
# [:king, :heart],
|
781
|
+
# [:jack, :spade],
|
782
|
+
# [:king, :spade],
|
783
|
+
# [:queen, :club],
|
784
|
+
# [6, :diamond],
|
785
|
+
# [5, :club],
|
786
|
+
# [6, :club]]}
|
787
|
+
|
788
|
+
# What about generating a whole game?
|
789
|
+
|
790
|
+
Gen.generate S.gen(ns(:game))
|
791
|
+
# it works! but the output is really long, so not including it here
|
792
|
+
|
793
|
+
# So we can now start with a spec, extract a generator, and generate some data.
|
794
|
+
# All generated data will conform to the spec we used as a generator. For specs
|
795
|
+
# that have a conformed value different than the original value (anything using
|
796
|
+
# S.or, S.cat, S.alt, etc) it can be useful to see a set of generated samples
|
797
|
+
# plus the result of conforming that sample data.
|
798
|
+
|
799
|
+
## Exercise
|
800
|
+
|
801
|
+
# For this we have `exercise`, which returns pairs of generated and conformed
|
802
|
+
# values for a spec. exercise by default produces 10 samples (like sample) but
|
803
|
+
# you can pass both functions a number indicating the number of samples to
|
804
|
+
# produce.
|
805
|
+
|
806
|
+
S.exercise S.cat(:k => Symbol, :ns => S.one_or_more(Numeric)), :n => 5
|
807
|
+
# => [[[:AXgNzoRmshVeKju,
|
808
|
+
# -817925373115395462,
|
809
|
+
# 1.5359311568381347e+308,
|
810
|
+
# 1.1061449248034022e+308,
|
811
|
+
# -235267876425474208,
|
812
|
+
# 3.955857252356689e+307,
|
813
|
+
# -889011872905836841,
|
814
|
+
# 9.082764829559406e+307,
|
815
|
+
# 3.8449893386631863e+307,
|
816
|
+
# 1399473921337276004,
|
817
|
+
# 1.1035252898212735e+308],
|
818
|
+
# {:k=>:AXgNzoRmshVeKju,
|
819
|
+
# :ns=>
|
820
|
+
# [-817925373115395462,
|
821
|
+
# 1.5359311568381347e+308,
|
822
|
+
# 1.1061449248034022e+308,
|
823
|
+
# -235267876425474208,
|
824
|
+
# 3.955857252356689e+307,
|
825
|
+
# -889011872905836841,
|
826
|
+
# 9.082764829559406e+307,
|
827
|
+
# 3.8449893386631863e+307,
|
828
|
+
# 1399473921337276004,
|
829
|
+
# 1.1035252898212735e+308]}],
|
830
|
+
# [[:Nsndjayf,
|
831
|
+
# 1.9984725870793707e+307,
|
832
|
+
# 1.5323527859487139e+308,
|
833
|
+
# 1.0526758425396865e+308,
|
834
|
+
# 2187215078751341740,
|
835
|
+
# 2000267805737910757,
|
836
|
+
# 672724827310048814,
|
837
|
+
# 7.353660057508847e+307,
|
838
|
+
# -499603991431322628,
|
839
|
+
# 823374880053618568,
|
840
|
+
# 988019501395130231,
|
841
|
+
# -85062962445868544,
|
842
|
+
# 1208854825028261939,
|
843
|
+
# -239585966232519771],
|
844
|
+
# {:k=>:Nsndjayf,
|
845
|
+
# :ns=>
|
846
|
+
# [1.9984725870793707e+307,
|
847
|
+
# 1.5323527859487139e+308,
|
848
|
+
# 1.0526758425396865e+308,
|
849
|
+
# 2187215078751341740,
|
850
|
+
# 2000267805737910757,
|
851
|
+
# 672724827310048814,
|
852
|
+
# 7.353660057508847e+307,
|
853
|
+
# -499603991431322628,
|
854
|
+
# 823374880053618568,
|
855
|
+
# 988019501395130231,
|
856
|
+
# -85062962445868544,
|
857
|
+
# 1208854825028261939,
|
858
|
+
# -239585966232519771]}],
|
859
|
+
# [[:kKknKqGtQjl, 1.781549997030396e+305, -2255917728752340059],
|
860
|
+
# {:k=>:kKknKqGtQjl,
|
861
|
+
# :ns=>[1.781549997030396e+305, -2255917728752340059]}],
|
862
|
+
# [[:OknzgVGj,
|
863
|
+
# -2263138309988902357,
|
864
|
+
# 6.780757328421502e+307,
|
865
|
+
# 1159675302983770930,
|
866
|
+
# 8.619504625294373e+307,
|
867
|
+
# -102111175606505256,
|
868
|
+
# 3.1369602174703924e+307,
|
869
|
+
# 714218663950371918,
|
870
|
+
# 1072428045010760820,
|
871
|
+
# 1.7120457957881442e+308,
|
872
|
+
# 1.7220639025345156e+308,
|
873
|
+
# 7.318059339504824e+307,
|
874
|
+
# -627281432214439965,
|
875
|
+
# 1285330282675190977,
|
876
|
+
# 5.624663033422957e+307],
|
877
|
+
# {:k=>:OknzgVGj,
|
878
|
+
# :ns=>
|
879
|
+
# [-2263138309988902357,
|
880
|
+
# 6.780757328421502e+307,
|
881
|
+
# 1159675302983770930,
|
882
|
+
# 8.619504625294373e+307,
|
883
|
+
# -102111175606505256,
|
884
|
+
# 3.1369602174703924e+307,
|
885
|
+
# 714218663950371918,
|
886
|
+
# 1072428045010760820,
|
887
|
+
# 1.7120457957881442e+308,
|
888
|
+
# 1.7220639025345156e+308,
|
889
|
+
# 7.318059339504824e+307,
|
890
|
+
# -627281432214439965,
|
891
|
+
# 1285330282675190977,
|
892
|
+
# 5.624663033422957e+307]}],
|
893
|
+
# [[:mifpKjpS, 3.8475669790437504e+307, 1.5541847940699583e+307],
|
894
|
+
# {:k=>:mifpKjpS,
|
895
|
+
# :ns=>[3.8475669790437504e+307, 1.5541847940699583e+307]}]]
|
896
|
+
|
897
|
+
S.exercise S.or(:k => Symbol, :s => String, :n => Numeric), :n => 5
|
898
|
+
# => [[-1310754584514288, [:n, -1310754584514288]],
|
899
|
+
# [872148706486332083, [:n, 872148706486332083]],
|
900
|
+
# [:rHCoqRLZYhzSgOu, [:k, :rHCoqRLZYhzSgOu]],
|
901
|
+
# [-395552003092497804, [:n, -395552003092497804]],
|
902
|
+
# [:WoaPnjB, [:k, :WoaPnjB]]]
|
903
|
+
|
904
|
+
# For spec’ed functions we also have exercise_fn, which generates sample args,
|
905
|
+
# invokes the spec’ed function and returns the args and the return value.
|
906
|
+
|
907
|
+
S.exercise_fn(method(:ranged_rand))
|
908
|
+
# => [[[-2128611012334186431, -1417738444057945122], -1635106169064592441],
|
909
|
+
# [[1514518280943101595, 1786254628919354373], 1739796291756227578],
|
910
|
+
# [[-46749061680797208, 822766248044755470], -7474228458851983],
|
911
|
+
# [[-649513218842008808, 1875894039691321060], -390581384114488816],
|
912
|
+
# [[858361555883341214, 1741658980258358628], 1374077212657449917],
|
913
|
+
# [[-1258388171360603963, -985723099401376708], -1123010455669592843],
|
914
|
+
# [[-1035489322616947034, 1688366643195138662], 441214083022620176],
|
915
|
+
# [[-2229284211372056198, -893085296484913242], -1161469637076511831],
|
916
|
+
# [[819684425123939548, 1044514159372510410], 971678102106589235],
|
917
|
+
# [[366502776249932529, 1318835861470496704], 377553467194155955]]
|
918
|
+
|
919
|
+
## Using S.and Generators
|
920
|
+
|
921
|
+
# All of the generators we’ve seen worked fine but there are a number of cases
|
922
|
+
# where they will need some additional help. One common case is when the
|
923
|
+
# predicate implicitly presumes values of a particular type but the spec does
|
924
|
+
# not specify them:
|
925
|
+
|
926
|
+
Gen.generate S.gen(:even?.to_proc) rescue $! # => #<Speculation::Error: unable to construct gen at: [] for: Speculation::Spec(#<Proc:0x007fb69b1908f8(&:even?)>) {:"Speculation/failure"=>:no_gen, :"Speculation/path"=>[]}\n>
|
927
|
+
|
928
|
+
# In this case spec was not able to find a generator for the even? predicate.
|
929
|
+
# Most of the primitive generators in spec are mapped to the common type
|
930
|
+
# predicates (classes, modules, built-in specs).
|
931
|
+
|
932
|
+
# However, spec is designed to support this case via `and` - the first
|
933
|
+
# predicate will determine the generator and subsequent branches will act as
|
934
|
+
# filters by applying the predicate to the produced values.
|
935
|
+
|
936
|
+
# If we modify our predicate to use an `and` and a predicate with a mapped
|
937
|
+
# generator, the even? can be used as a filter for generated values instead:
|
938
|
+
|
939
|
+
Gen.generate S.gen(S.and(Integer, :even?.to_proc))
|
940
|
+
# => 1875527059787064980
|
941
|
+
|
942
|
+
# We can use many predicates to further refine the generated values. For
|
943
|
+
# example, say we only wanted to generate numbers that were positive multiples
|
944
|
+
# of 3:
|
945
|
+
|
946
|
+
def self.divisible_by(n)
|
947
|
+
->(x) { (x % n).zero? }
|
948
|
+
end
|
949
|
+
|
950
|
+
Gen.sample S.gen(S.and(Integer, :positive?.to_proc, divisible_by(3)))
|
951
|
+
# => [1003257946641857673,
|
952
|
+
# 1302633092686504620,
|
953
|
+
# 1067379217208623728,
|
954
|
+
# 882135641374726149,
|
955
|
+
# 1933864978000820676,
|
956
|
+
# 235089151558168077,
|
957
|
+
# 470438340672134322,
|
958
|
+
# 2268668240213030931,
|
959
|
+
# 1061519505888350829,
|
960
|
+
# 1868667505095337938]
|
961
|
+
|
962
|
+
# However, it is possible to go too far with refinement and make something that
|
963
|
+
# fails to produce any values. The Rantly `guard` that implements the
|
964
|
+
# refinement will throw an error if the refinement predicate cannot be resolved
|
965
|
+
# within a relatively small number of attempts. For example, consider trying to
|
966
|
+
# generate strings that happen to contain the world "hello":
|
967
|
+
|
968
|
+
# hello, are you the one I'm looking for?
|
969
|
+
Gen.sample S.gen(S.and(String, ->(s) { s.include?("hello") })) rescue $!
|
970
|
+
# => #<Rantly::TooManyTries: Exceed gen limit 1000: 1001 failed guards)>
|
971
|
+
|
972
|
+
# Given enough time (maybe a lot of time), the generator probably would come up
|
973
|
+
# with a string like this, but the underlying `guard` will make only 100
|
974
|
+
# attempts to generate a value that passes the filter. This is a case where you
|
975
|
+
# will need to step in and provide a custom generator.
|
976
|
+
|
977
|
+
## Custom Generators
|
978
|
+
|
979
|
+
# Building your own generator gives you the freedom to be either narrower
|
980
|
+
# and/or be more explicit about what values you want to generate. Alternately,
|
981
|
+
# custom generators can be used in cases where conformant values can be
|
982
|
+
# generated more efficiently than using a base predicate plus filtering. Spec
|
983
|
+
# does not trust custom generators and any values they produce will also be
|
984
|
+
# checked by their associated spec to guarantee they pass conformance.
|
985
|
+
|
986
|
+
# There are three ways to build up custom generators - in decreasing order of
|
987
|
+
# preference:
|
988
|
+
|
989
|
+
# - Let spec create a generator based on a predicate/spec
|
990
|
+
# - Create your own generator using Rantly directly
|
991
|
+
|
992
|
+
# First consider a spec with a predicate to specify symbols from a particular
|
993
|
+
# namespace:
|
994
|
+
|
995
|
+
S.def ns(:syms), S.and(Symbol, ->(s) { S::NamespacedSymbols.namespace(s) == "my.domain" })
|
996
|
+
S.valid? ns(:syms), :"my.domain/name" # => true
|
997
|
+
Gen.sample S.gen(ns(:syms)) rescue $! # => #<Rantly::TooManyTries: Exceed gen limit 1000: 1001 failed guards)>
|
998
|
+
|
999
|
+
# The simplest way to start generating values for this spec is to have spec
|
1000
|
+
# create a generator from a fixed set of options. A set is a valid predicate
|
1001
|
+
# spec so we can create one and ask for it’s generator:
|
1002
|
+
|
1003
|
+
sym_gen = S.gen(Set[:"my.domain/name", :"my.domain/occupation", :"my.domain/id"])
|
1004
|
+
Gen.sample sym_gen, 5
|
1005
|
+
# => [:"my.domain/id",
|
1006
|
+
# :"my.domain/name",
|
1007
|
+
# :"my.domain/occupation",
|
1008
|
+
# :"my.domain/name",
|
1009
|
+
# :"my.domain/id"]
|
1010
|
+
|
1011
|
+
# To redefine our spec using this custom generator, use with_gen which takes a
|
1012
|
+
# spec and a replacement generator as a block:
|
1013
|
+
|
1014
|
+
gen = S.gen(Set[:"my.domain/name", :"my.domain/occupation", :"my.domain/id"])
|
1015
|
+
S.def(ns(:syms), S.with_gen(S.and(Symbol, ->(s) { S::NamespacedSymbols.namespace(s) == "my.domain" }), gen))
|
1016
|
+
|
1017
|
+
S.valid? ns(:syms), :"my.domain/name"
|
1018
|
+
Gen.sample S.gen(ns(:syms)), 5
|
1019
|
+
# => [:"my.domain/name",
|
1020
|
+
# :"my.domain/id",
|
1021
|
+
# :"my.domain/occupation",
|
1022
|
+
# :"my.domain/occupation",
|
1023
|
+
# :"my.domain/id"]
|
1024
|
+
|
1025
|
+
# TODO: make gens no-arg functions???
|
1026
|
+
# Note that with_gen (and other places that take a custom generator) take a
|
1027
|
+
# one-arg function that returns the generator, allowing it to be lazily
|
1028
|
+
# realized.
|
1029
|
+
|
1030
|
+
# One downside to this approach is we are missing what property testing is
|
1031
|
+
# really good at: automatically generating data across a wide search space to
|
1032
|
+
# find unexpected problems.
|
1033
|
+
|
1034
|
+
# Rantly has a small library of generators that can be utilized.
|
1035
|
+
|
1036
|
+
# In this case we want our keyword to have open names but fixed namespaces.
|
1037
|
+
# There are many ways to accomplish this but one of the simplest is to use fmap
|
1038
|
+
# to build up a keyword based on generated strings:
|
1039
|
+
|
1040
|
+
sym_gen_2 = ->(rantly) do
|
1041
|
+
size = rantly.range(1, 10)
|
1042
|
+
string = rantly.sized(size) { rantly.string(:alpha) }
|
1043
|
+
:"my.domain/#{string}"
|
1044
|
+
end
|
1045
|
+
Gen.sample sym_gen_2, 5 # => [:"my.domain/hLZnEpj", :"my.domain/kvy", :"my.domain/VqWbqD", :"my.domain/imq", :"my.domain/eHeZleWzj"]
|
1046
|
+
|
1047
|
+
# Returning to our "hello" example, we now have the tools to make that
|
1048
|
+
# generator:
|
1049
|
+
|
1050
|
+
S.def ns(:hello), S.with_gen(->(s) { s.include?("hello") }, ->(rantly) {
|
1051
|
+
s1 = rantly.sized(rantly.range(0, 10)) { rantly.string(:alpha) }
|
1052
|
+
s2 = rantly.sized(rantly.range(0, 10)) { rantly.string(:alpha) }
|
1053
|
+
"#{s1}hello#{s2}"
|
1054
|
+
})
|
1055
|
+
|
1056
|
+
Gen.sample S.gen(ns(:hello))
|
1057
|
+
# => ["XRLhtLshelloaY",
|
1058
|
+
# "tXxZQHhZOhelloJ",
|
1059
|
+
# "ExhellozzlPYz",
|
1060
|
+
# "MaiierIhelloel",
|
1061
|
+
# "WKZBJprQkhelloGdGToCbI",
|
1062
|
+
# "RDFCZhello",
|
1063
|
+
# "PXPsYwJLhellosYoYngd",
|
1064
|
+
# "SuhelloJ",
|
1065
|
+
# "wWhelloodQFFvdW",
|
1066
|
+
# "pNhello"]
|
1067
|
+
|
1068
|
+
# Here we generate a tuple of a random prefix and random suffix strings, then
|
1069
|
+
# insert "hello" bewteen them.
|
1070
|
+
|
1071
|
+
## Range Specs and Generators
|
1072
|
+
|
1073
|
+
# There are several cases where it’s useful to spec (and generate) values in a
|
1074
|
+
# range and spec provides helpers for these cases.
|
1075
|
+
|
1076
|
+
# For example, in the case of a range of integer values (for example, a bowling
|
1077
|
+
# roll), use int_in to spec a range:
|
1078
|
+
|
1079
|
+
S.def ns(:roll), S.int_in(0..10)
|
1080
|
+
Gen.sample S.gen(ns(:roll))
|
1081
|
+
# => [10, 0, 8, 2, 6, 10, 1, 10, 10, 0]
|
1082
|
+
|
1083
|
+
# spec also includes date_in for a range of dates:
|
1084
|
+
|
1085
|
+
S.def ns(:the_aughts), S.date_in(Date.new(2000, 1, 1)..Date.new(2010))
|
1086
|
+
Gen.sample S.gen(ns(:the_aughts)), 5
|
1087
|
+
# => [#<Date: 2003-07-06 ((2452827j,0s,0n),+0s,2299161j)>,
|
1088
|
+
# #<Date: 2009-04-13 ((2454935j,0s,0n),+0s,2299161j)>,
|
1089
|
+
# #<Date: 2009-08-03 ((2455047j,0s,0n),+0s,2299161j)>,
|
1090
|
+
# #<Date: 2006-07-07 ((2453924j,0s,0n),+0s,2299161j)>,
|
1091
|
+
# #<Date: 2001-06-12 ((2452073j,0s,0n),+0s,2299161j)>]
|
1092
|
+
|
1093
|
+
# spec also includes time_in for a range of times:
|
1094
|
+
|
1095
|
+
S.def ns(:the_aughts), S.time_in(Time.new(2000)..Time.new(2010))
|
1096
|
+
Gen.sample S.gen(ns(:the_aughts)), 5
|
1097
|
+
# => [2000-03-26 07:01:27 -0800,
|
1098
|
+
# 2009-03-28 12:00:18 -0700,
|
1099
|
+
# 2003-09-20 15:33:42 -0700,
|
1100
|
+
# 2007-06-26 20:07:13 -0700,
|
1101
|
+
# 2003-11-25 16:59:38 -0800]
|
1102
|
+
|
1103
|
+
# Finally, float_in has support for double ranges and special options for
|
1104
|
+
# checking special float values like NaN (not a number), Infinity, and
|
1105
|
+
# -Infinity.
|
1106
|
+
|
1107
|
+
S.def ns(:floats), S.float_in(:min => -100.0, :max => 100.0, :nan => false, :infinite => false)
|
1108
|
+
S.valid? ns(:floats), 2.9 # => true
|
1109
|
+
S.valid? ns(:floats), Float::INFINITY # => false
|
1110
|
+
Gen.sample S.gen(ns(:floats)), 5 # => [65.53711851243327, 67.31921045318401, -71.92560111608772, 81.66336359400515, -30.4921955594738]
|
1111
|
+
|
1112
|
+
## Instrumentation and Testing
|
1113
|
+
|
1114
|
+
# spec provides a set of development and testing functionality in the
|
1115
|
+
# Speculation::Test namespace, which we can include with:
|
1116
|
+
|
1117
|
+
require "speculation/test"
|
1118
|
+
STest = Speculation::Test
|
1119
|
+
|
1120
|
+
## Instrumentation
|
1121
|
+
|
1122
|
+
# Instrumentation validates that the :args spec is being invoked on
|
1123
|
+
# instrumented functions and thus provides validation for external uses of a
|
1124
|
+
# function. Let’s turn on instrumentation for our previously spec’ed
|
1125
|
+
# ranged-rand function:
|
1126
|
+
|
1127
|
+
STest.instrument method(:ranged_rand)
|
1128
|
+
|
1129
|
+
# If the function is invoked with args that do not conform with the :args spec
|
1130
|
+
# you will see an error like this:
|
1131
|
+
|
1132
|
+
ranged_rand 8, 5 rescue $!
|
1133
|
+
# => #<Speculation::Error: Call to 'main.ranged_rand' did not conform to spec:
|
1134
|
+
# val: {:start=>8, :end=>5} fails at: [:args] predicate: [#<Proc:0x007fb69a8a0fb8@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:548 (lambda)>, [{:start=>8, :end=>5}]]
|
1135
|
+
# Speculation/args [8, 5]
|
1136
|
+
# Speculation/failure :instrument
|
1137
|
+
# Speculation::Test/caller "/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:901:in `<main>'"
|
1138
|
+
# {:"Speculation/problems"=>
|
1139
|
+
# [{:path=>[:args],
|
1140
|
+
# :val=>{:start=>8, :end=>5},
|
1141
|
+
# :via=>[],
|
1142
|
+
# :in=>[],
|
1143
|
+
# :pred=>
|
1144
|
+
# [#<Proc:0x007fb69a8a0fb8@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:548 (lambda)>,
|
1145
|
+
# [{:start=>8, :end=>5}]]}],
|
1146
|
+
# :"Speculation/args"=>[8, 5],
|
1147
|
+
# :"Speculation/failure"=>:instrument,
|
1148
|
+
# :"Speculation::Test/caller"=>
|
1149
|
+
# "/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:901:in `<main>'"}
|
1150
|
+
# >
|
1151
|
+
|
1152
|
+
# The error fails in the second args predicate that checks `start < end`. Note
|
1153
|
+
# that the :ret and :fn specs are not checked with instrumentation as
|
1154
|
+
# validating the implementation should occur at testing time.
|
1155
|
+
|
1156
|
+
# Instrumentation can be turned off using the complementary function
|
1157
|
+
# unstrument. Instrumentation is likely to be useful at both development time
|
1158
|
+
# and during testing to discover errors in calling code. It is not recommended
|
1159
|
+
# to use instrumentation in production due to the overhead involved with
|
1160
|
+
# checking args specs.
|
1161
|
+
|
1162
|
+
## Testing
|
1163
|
+
|
1164
|
+
# We mentioned earlier that ~~clojure.spec.test~~ Speculation provides tools
|
1165
|
+
# for automatically testing functions. When functions have specs, we can use
|
1166
|
+
# check, to automatically generate tests that check the function using the
|
1167
|
+
# specs.
|
1168
|
+
|
1169
|
+
# check will generate arguments based on the :args spec for a function, invoke
|
1170
|
+
# the function, and check that the :ret and :fn specs were satisfied.
|
1171
|
+
|
1172
|
+
STest.check method(:ranged_rand)
|
1173
|
+
# => [{:spec=>Speculation::FSpec(main.ranged_rand),
|
1174
|
+
# :"Speculation::Test/ret"=>{:num_tests=>1000, :result=>true},
|
1175
|
+
# :method=>#<Method: main.ranged_rand>}]
|
1176
|
+
|
1177
|
+
# check also takes a number of options ~~that can be passed to test.check to
|
1178
|
+
# influence the test run~~, as well as the option to override generators for
|
1179
|
+
# parts of the spec, by either name or path.
|
1180
|
+
|
1181
|
+
# Imagine instead that we made an error in the ranged-rand code and swapped
|
1182
|
+
# start and end:
|
1183
|
+
|
1184
|
+
def self.ranged_rand(from, to) ## broken!
|
1185
|
+
(from + rand(to)).to_i
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
# This broken function will still create random integers, just not in the
|
1189
|
+
# expected range. Our :fn spec will detect the problem when checking the var:
|
1190
|
+
|
1191
|
+
STest.abbrev_result STest.check(method(:ranged_rand)).first
|
1192
|
+
# >> {:spec=>"Speculation::FSpec(main.ranged_rand)",
|
1193
|
+
# >> :method=>#<Method: main.ranged_rand>,
|
1194
|
+
# >> :failure=>
|
1195
|
+
# >> {:"Speculation/problems"=>
|
1196
|
+
# >> [{:path=>[:fn],
|
1197
|
+
# >> :val=>{:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0},
|
1198
|
+
# >> :via=>[],
|
1199
|
+
# >> :in=>[],
|
1200
|
+
# >> :pred=>
|
1201
|
+
# >> [#<Proc:0x007fb69a8a0c98@/var/folders/4l/j2mycv0j4rx7z47sp01r93vc3kfxzs/T/seeing_is_believing_temp_dir20170304-91770-1jtmc1y/program.rb:551 (lambda)>,
|
1202
|
+
# >> [{:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0}]]}],
|
1203
|
+
# >> :"Speculation::Test/args"=>[-1, 0],
|
1204
|
+
# >> :"Speculation::Test/val"=>
|
1205
|
+
# >> {:args=>{:start=>-1, :end=>0}, :block=>nil, :ret=>0},
|
1206
|
+
# >> :"Speculation/failure"=>:check_failed}}
|
1207
|
+
|
1208
|
+
# check has reported an error in the :fn spec. We can see the arguments passed
|
1209
|
+
# were -1 and 0 and the return value was -0, which is out of the expected
|
1210
|
+
# range (it should be less that the `end` argument).
|
1211
|
+
|
1212
|
+
# To test all of the spec’ed functions in a module/class (or multiple
|
1213
|
+
# module/classes), use enumerate-methods to generate the set of symbols
|
1214
|
+
# naming vars in the namespace:
|
1215
|
+
|
1216
|
+
STest.summarize_results STest.check(STest.enumerate_methods(self))
|
1217
|
+
# => {:total=>3, :check_failed=>2, :check_passed=>1}
|
1218
|
+
|
1219
|
+
# And you can check all of the spec’ed functions by calling STest.check without any arguments.
|
1220
|
+
|
1221
|
+
## Combining check and instrument
|
1222
|
+
|
1223
|
+
# While both `instrument` (for enabling :args checking) and `check` (for generating
|
1224
|
+
# tests of a function) are useful tools, they can be combined to provide even
|
1225
|
+
# deeper levels of test coverage.
|
1226
|
+
|
1227
|
+
# `instrument` takes a number of options for changing the behavior of
|
1228
|
+
# instrumented functions, including support for swapping in alternate
|
1229
|
+
# (narrower) specs, stubbing functions (by using the :ret spec to generate
|
1230
|
+
# results), or replacing functions with an alternate implementation.
|
1231
|
+
|
1232
|
+
# Consider the case where we have a low-level function that invokes a remote
|
1233
|
+
# service and a higher-level function that calls it.
|
1234
|
+
|
1235
|
+
# code under test
|
1236
|
+
|
1237
|
+
def self.invoke_service(service, request)
|
1238
|
+
# invokes remote service
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
def self.run_query(service, query)
|
1242
|
+
response = invoke_service(service, ns(:query) => query)
|
1243
|
+
result, error = response.values_at(ns(:result), ns(:error))
|
1244
|
+
result || error
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
# We can spec these functions using the following specs:
|
1248
|
+
|
1249
|
+
S.def ns(:query), String
|
1250
|
+
S.def ns(:request), S.keys(:req => [ns(:query)])
|
1251
|
+
S.def ns(:result), S.coll_of(String, :gen_max => 3)
|
1252
|
+
S.def ns(:error), Integer
|
1253
|
+
S.def ns(:response), S.or(:ok => S.keys(:req => [ns(:result)]),
|
1254
|
+
:err => S.keys(:req => [ns(:error)]))
|
1255
|
+
|
1256
|
+
S.fdef method(:invoke_service),
|
1257
|
+
:args => S.cat(:service => ns(S, :any), :request => ns(:request)),
|
1258
|
+
:ret => ns(:response)
|
1259
|
+
|
1260
|
+
S.fdef method(:run_query),
|
1261
|
+
:args => S.cat(:service => ns(S, :any), :query => String),
|
1262
|
+
:ret => S.or(:ok => ns(:result), :err => ns(:error))
|
1263
|
+
|
1264
|
+
# And then we want to test the behavior of run_query while stubbing out
|
1265
|
+
# invoke_service with instrument so that the remote service is not invoked:
|
1266
|
+
|
1267
|
+
STest.instrument method(:invoke_service), :stub => [method(:invoke_service)]
|
1268
|
+
|
1269
|
+
invoke_service nil, ns(:query) => "test"
|
1270
|
+
# => {:"Object/result"=>["LtqDYvzOfVzCHWN", "ZASNKhtkwBAXyTF"]}
|
1271
|
+
invoke_service nil, ns(:query) => "test"
|
1272
|
+
# => {:"Object/result"=>["yvccRd", "xREXEgc"]}
|
1273
|
+
STest.summarize_results STest.check(method(:run_query))
|
1274
|
+
# => {:total=>1, :check_passed=>1}
|
1275
|
+
|
1276
|
+
# The first call here instruments and stubs invoke_service. The second and
|
1277
|
+
# third calls demonstrate that calls to invoke_service now return generated
|
1278
|
+
# results (rather than hitting a service). Finally, we can use check on the
|
1279
|
+
# higher level function to test that it behaves properly based on the generated
|
1280
|
+
# stub results returned from invoke_service.
|
1281
|
+
|
1282
|
+
## Wrapping Up
|
1283
|
+
|
1284
|
+
# In this guide we have covered most of the features for designing and using
|
1285
|
+
# specs and generators. We expect to add some more advanced generator
|
1286
|
+
# techniques and help on testing in a future update.
|
1287
|
+
|
1288
|
+
# Original author of clojure.spec guide: Alex Miller
|