speculation 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -0
- data/.travis.yml +2 -4
- data/README.md +22 -12
- data/Rakefile +5 -9
- data/bin/console +54 -7
- data/examples/codebreaker.rb +246 -0
- data/examples/spec_guide.rb +1288 -0
- data/lib/speculation.rb +145 -146
- data/lib/speculation/error.rb +4 -3
- data/lib/speculation/gen.rb +51 -47
- data/lib/speculation/identifier.rb +7 -7
- data/lib/speculation/namespaced_symbols.rb +26 -19
- data/lib/speculation/pmap.rb +9 -10
- data/lib/speculation/spec_impl/and_spec.rb +3 -4
- data/lib/speculation/spec_impl/every_spec.rb +24 -24
- data/lib/speculation/spec_impl/f_spec.rb +32 -35
- data/lib/speculation/spec_impl/hash_spec.rb +33 -41
- data/lib/speculation/spec_impl/merge_spec.rb +2 -3
- data/lib/speculation/spec_impl/nilable_spec.rb +8 -9
- data/lib/speculation/spec_impl/or_spec.rb +5 -7
- data/lib/speculation/spec_impl/regex_spec.rb +2 -3
- data/lib/speculation/spec_impl/spec.rb +3 -5
- data/lib/speculation/spec_impl/tuple_spec.rb +8 -10
- data/lib/speculation/test.rb +126 -101
- data/lib/speculation/utils.rb +31 -5
- data/lib/speculation/version.rb +1 -1
- data/speculation.gemspec +0 -1
- metadata +30 -44
- data/lib/speculation/conj.rb +0 -32
- data/lib/speculation/utils_specs.rb +0 -57
@@ -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
|