hypothesis-specs 0.0.3
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 +7 -0
- data/CHANGELOG.md +10 -0
- data/Cargo.toml +11 -0
- data/LICENSE.txt +8 -0
- data/README.markdown +86 -0
- data/Rakefile +145 -0
- data/ext/Makefile +7 -0
- data/ext/extconf.rb +5 -0
- data/lib/hypothesis.rb +223 -0
- data/lib/hypothesis/engine.rb +85 -0
- data/lib/hypothesis/errors.rb +28 -0
- data/lib/hypothesis/possible.rb +369 -0
- data/lib/hypothesis/testcase.rb +44 -0
- data/lib/hypothesis/world.rb +9 -0
- data/src/data.rs +99 -0
- data/src/distributions.rs +238 -0
- data/src/engine.rs +400 -0
- data/src/lib.rs +170 -0
- metadata +91 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'helix_runtime'
|
4
|
+
require 'hypothesis-ruby-core/native'
|
5
|
+
|
6
|
+
module Hypothesis
|
7
|
+
class Engine
|
8
|
+
attr_reader :current_source
|
9
|
+
attr_accessor :is_find
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
seed = Random.rand(2**64 - 1)
|
13
|
+
@core_engine = HypothesisCoreEngine.new(
|
14
|
+
seed, options.fetch(:max_examples)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
loop do
|
20
|
+
core = @core_engine.new_source
|
21
|
+
break if core.nil?
|
22
|
+
@current_source = TestCase.new(core)
|
23
|
+
begin
|
24
|
+
result = yield(@current_source)
|
25
|
+
if is_find && result
|
26
|
+
@core_engine.finish_interesting(core)
|
27
|
+
else
|
28
|
+
@core_engine.finish_valid(core)
|
29
|
+
end
|
30
|
+
rescue UnsatisfiedAssumption
|
31
|
+
@core_engine.finish_invalid(core)
|
32
|
+
rescue DataOverflow
|
33
|
+
@core_engine.finish_overflow(core)
|
34
|
+
rescue Exception
|
35
|
+
raise if is_find
|
36
|
+
@core_engine.finish_interesting(core)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@current_source = nil
|
40
|
+
core = @core_engine.failing_example
|
41
|
+
if core.nil?
|
42
|
+
raise Unsatisfiable if @core_engine.was_unsatisfiable
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
if is_find
|
47
|
+
@current_source = TestCase.new(core, record_draws: true)
|
48
|
+
yield @current_source
|
49
|
+
else
|
50
|
+
@current_source = TestCase.new(core, print_draws: true)
|
51
|
+
|
52
|
+
begin
|
53
|
+
yield @current_source
|
54
|
+
rescue Exception => e
|
55
|
+
givens = @current_source.print_log
|
56
|
+
given_str = givens.each_with_index.map do |(name, s), i|
|
57
|
+
name = "##{i + 1}" if name.nil?
|
58
|
+
"Given #{name}: #{s}"
|
59
|
+
end.to_a
|
60
|
+
|
61
|
+
if e.respond_to? :hypothesis_data
|
62
|
+
e.hypothesis_data[0] = given_str
|
63
|
+
else
|
64
|
+
original_to_s = e.to_s
|
65
|
+
original_inspect = e.inspect
|
66
|
+
|
67
|
+
class <<e
|
68
|
+
attr_accessor :hypothesis_data
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
['', hypothesis_data[0], '', hypothesis_data[1]].join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
['', hypothesis_data[0], '', hypothesis_data[2]].join("\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
e.hypothesis_data = [given_str, original_to_s, original_inspect]
|
79
|
+
end
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hypothesis
|
4
|
+
# A generic superclass for all errors thrown by
|
5
|
+
# Hypothesis.
|
6
|
+
class HypothesisError < RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Indicates that Hypothesis was not able to find
|
10
|
+
# enough valid examples for the test to be meaningful.
|
11
|
+
# (Currently this is only thrown if Hypothesis did not
|
12
|
+
# find *any* valid examples).
|
13
|
+
class Unsatisfiable < HypothesisError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Indicates that the Hypothesis API has been used
|
17
|
+
# incorrectly in some manner.
|
18
|
+
class UsageError < HypothesisError
|
19
|
+
end
|
20
|
+
|
21
|
+
# @!visibility private
|
22
|
+
class UnsatisfiedAssumption < HypothesisError
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
class DataOverflow < HypothesisError
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,369 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
class HypothesisCoreRepeatValues
|
5
|
+
def should_continue(source)
|
6
|
+
result = _should_continue(source.wrapped_data)
|
7
|
+
raise Hypothesis::DataOverflow if result.nil?
|
8
|
+
result
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Hypothesis
|
13
|
+
class <<self
|
14
|
+
include Hypothesis
|
15
|
+
end
|
16
|
+
|
17
|
+
# A Possible describes a range of valid values that
|
18
|
+
# can result from a call to {Hypothesis#any}.
|
19
|
+
# This class should not be subclassed directly, but
|
20
|
+
# instead should always be constructed using methods
|
21
|
+
# from {Hypothesis::Possibilities}.
|
22
|
+
class Possible
|
23
|
+
# @!visibility private
|
24
|
+
include Hypothesis
|
25
|
+
|
26
|
+
# A Possible value constructed by passing one of these
|
27
|
+
# Possible values to the provided block.
|
28
|
+
#
|
29
|
+
# e.g. the Possible values of `integers.map { |i| i * 2 }`
|
30
|
+
# are all even integers.
|
31
|
+
#
|
32
|
+
# @return [Possible]
|
33
|
+
# @yield A possible value of self.
|
34
|
+
def map
|
35
|
+
Implementations::CompositePossible.new do
|
36
|
+
yield any(self)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
alias collect map
|
41
|
+
|
42
|
+
# One of these Possible values selected such that
|
43
|
+
# the block returns a true value for it.
|
44
|
+
#
|
45
|
+
# e.g. the Possible values of
|
46
|
+
# `integers.filter { |i| i % 2 == 0}` are all even
|
47
|
+
# integers (but will typically be less efficient
|
48
|
+
# than the one suggested in {Possible#map}.
|
49
|
+
#
|
50
|
+
# @note Similar warnings to {Hypothesis#assume} apply
|
51
|
+
# here: If the condition is difficult to satisfy this
|
52
|
+
# may impact the performance and quality of your
|
53
|
+
# testing.
|
54
|
+
#
|
55
|
+
# @return [Possible]
|
56
|
+
# @yield A possible value of self.
|
57
|
+
def select
|
58
|
+
Implementations::CompositePossible.new do
|
59
|
+
result = nil
|
60
|
+
4.times do |i|
|
61
|
+
assume(i < 3)
|
62
|
+
result = any self
|
63
|
+
break if yield(result)
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
alias filter select
|
70
|
+
|
71
|
+
# @!visibility private
|
72
|
+
module Implementations
|
73
|
+
# @!visibility private
|
74
|
+
class CompositePossible < Possible
|
75
|
+
def initialize(block = nil, &implicit)
|
76
|
+
@block = block || implicit
|
77
|
+
end
|
78
|
+
|
79
|
+
# @!visibility private
|
80
|
+
def provide(&block)
|
81
|
+
(@block || block).call
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @!visibility private
|
86
|
+
class PossibleFromCore < Possible
|
87
|
+
def initialize(core_possible)
|
88
|
+
@core_possible = core_possible
|
89
|
+
end
|
90
|
+
|
91
|
+
# @!visibility private
|
92
|
+
def provide
|
93
|
+
data = World.current_engine.current_source
|
94
|
+
result = @core_possible.provide(data.wrapped_data)
|
95
|
+
raise Hypothesis::DataOverflow if result.nil?
|
96
|
+
result
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# A module of many common {Possible} implementations.
|
103
|
+
# Rather than subclassing Possible yourself you should use
|
104
|
+
# methods from this module to construct Possible values.`
|
105
|
+
#
|
106
|
+
# You can use methods from this module by including
|
107
|
+
# Hypothesis::Possibilities in your tests, or by calling them
|
108
|
+
# on the module object directly.
|
109
|
+
#
|
110
|
+
# Most methods in this module that return a Possible have
|
111
|
+
# two names: A singular and a plural name. These are
|
112
|
+
# simply aliases and are identical in every way, but are
|
113
|
+
# provided to improve readability. For example
|
114
|
+
# `any integer` reads better than `given integers`
|
115
|
+
# but `arrays(of: integers)` reads better than
|
116
|
+
# `arrays(of: integer)`.
|
117
|
+
module Possibilities
|
118
|
+
include Hypothesis
|
119
|
+
|
120
|
+
class <<self
|
121
|
+
include Possibilities
|
122
|
+
end
|
123
|
+
|
124
|
+
# built_as lets you chain multiple Possible values together,
|
125
|
+
# by providing whatever value results from its block.
|
126
|
+
#
|
127
|
+
# For example the following provides a array plus some
|
128
|
+
# element from that array:
|
129
|
+
#
|
130
|
+
# ```ruby
|
131
|
+
# built_as do
|
132
|
+
# ls = any array(of: integers)
|
133
|
+
# # Or min_size: 1 above, but this shows use of
|
134
|
+
# # assume
|
135
|
+
# assume ls.size > 0
|
136
|
+
# i = any element_of(ls)
|
137
|
+
# [ls, i]
|
138
|
+
# ```
|
139
|
+
#
|
140
|
+
# @return [Possible] A Possible whose possible values are
|
141
|
+
# any result from the passed block.
|
142
|
+
def built_as(&block)
|
143
|
+
Hypothesis::Possible::Implementations::CompositePossible.new(block)
|
144
|
+
end
|
145
|
+
|
146
|
+
alias values_built_as built_as
|
147
|
+
|
148
|
+
# A Possible boolean value
|
149
|
+
# @return [Possible]
|
150
|
+
def booleans
|
151
|
+
integers(min: 0, max: 1).map { |i| i == 1 }
|
152
|
+
end
|
153
|
+
|
154
|
+
alias boolean booleans
|
155
|
+
|
156
|
+
# A Possible unicode codepoint.
|
157
|
+
# @return [Possible]
|
158
|
+
# @param min [Integer] The smallest codepoint to provide
|
159
|
+
# @param max [Integer] The largest codepoint to provide
|
160
|
+
def codepoints(min: 1, max: 1_114_111)
|
161
|
+
base = integers(min: min, max: max)
|
162
|
+
if min <= 126
|
163
|
+
from(integers(min: min, max: [126, max].min), base)
|
164
|
+
else
|
165
|
+
base
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
alias codepoint codepoints
|
170
|
+
|
171
|
+
# A Possible String
|
172
|
+
# @return [Possible]
|
173
|
+
# @param codepoints [Possible, nil] The Possible codepoints
|
174
|
+
# that can be found in the string. If nil,
|
175
|
+
# will default to self.codepoints. These
|
176
|
+
# will be further filtered to ensure the generated string is
|
177
|
+
# valid.
|
178
|
+
# @param min_size [Integer] The smallest valid length for a
|
179
|
+
# provided string
|
180
|
+
# @param max_size [Integer] The smallest valid length for a
|
181
|
+
# provided string
|
182
|
+
def strings(codepoints: nil, min_size: 0, max_size: 10)
|
183
|
+
codepoints = self.codepoints if codepoints.nil?
|
184
|
+
codepoints = codepoints.select do |i|
|
185
|
+
begin
|
186
|
+
[i].pack('U*').codepoints
|
187
|
+
true
|
188
|
+
rescue ArgumentError
|
189
|
+
false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
arrays(of: codepoints, min_size: min_size, max_size: max_size).map do |ls|
|
193
|
+
ls.pack('U*')
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
alias string strings
|
198
|
+
|
199
|
+
# A Possible Hash, where all possible values have a fixed
|
200
|
+
# shape.
|
201
|
+
# This is used for hashes where you know exactly what the
|
202
|
+
# keys are, and different keys may have different possible values.
|
203
|
+
# For example, hashes_of_shape(a: integers, b: booleans)
|
204
|
+
# will give you values like `{a: 11, b: false}`.
|
205
|
+
# @return [Possible]
|
206
|
+
# @param hash [Hash] A hash describing the values to provide.
|
207
|
+
# The keys will be present unmodified in the provided hashes,
|
208
|
+
# mapping to their Possible value in the result.
|
209
|
+
def hashes_of_shape(hash)
|
210
|
+
built_as do
|
211
|
+
result = {}
|
212
|
+
hash.each { |k, v| result[k] = any(v) }
|
213
|
+
result
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
alias hash_of_shape hashes_of_shape
|
218
|
+
|
219
|
+
# A Possible Hash of variable shape.
|
220
|
+
# @return [Possible]
|
221
|
+
# @param keys [Possible] the possible keys
|
222
|
+
# @param values [Possible] the possible values
|
223
|
+
def hashes_with(keys:, values:, min_size: 0, max_size: 10)
|
224
|
+
built_as do
|
225
|
+
result = {}
|
226
|
+
rep = HypothesisCoreRepeatValues.new(
|
227
|
+
min_size, max_size, (min_size + max_size) * 0.5
|
228
|
+
)
|
229
|
+
source = World.current_engine.current_source
|
230
|
+
while rep.should_continue(source)
|
231
|
+
key = any keys
|
232
|
+
if result.include?(key)
|
233
|
+
rep.reject
|
234
|
+
else
|
235
|
+
result[key] = any values
|
236
|
+
end
|
237
|
+
end
|
238
|
+
result
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
alias hash_with hashes_with
|
243
|
+
|
244
|
+
# A Possible Arrays of a fixed shape.
|
245
|
+
# This is used for arrays where you know exactly how many
|
246
|
+
# elements there are, and different values may be possible
|
247
|
+
# at different positions.
|
248
|
+
# For example, arrays_of_shape(strings, integers)
|
249
|
+
# will give you values like ["a", 1]
|
250
|
+
# @return [Possible]
|
251
|
+
# @param elements [Array<Possible>] A variable number of Possible.
|
252
|
+
# values. The provided array will have this many values, with
|
253
|
+
# each value possible for the corresponding argument. If elements
|
254
|
+
# contains an array it will be flattened first, so e.g.
|
255
|
+
# arrays_of_shape(a, b) is equivalent to arrays_of_shape([a, b])
|
256
|
+
def arrays_of_shape(*elements)
|
257
|
+
elements = elements.flatten
|
258
|
+
built_as do
|
259
|
+
elements.map { |e| any e }.to_a
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
alias array_of_shape arrays_of_shape
|
264
|
+
|
265
|
+
# A Possible Array of variable shape.
|
266
|
+
# This is used for arrays where all of the elements come from
|
267
|
+
# the size may vary and the same values are possible at any position.
|
268
|
+
# For example, arrays(booleans) might provide [false, true, false].
|
269
|
+
# @return [Possible]
|
270
|
+
# @param of [Possible] The possible elements of the array.
|
271
|
+
# @param min_size [Integer] The smallest valid size of a provided array
|
272
|
+
# @param max_size [Integer] The largest valid size of a provided array
|
273
|
+
def arrays(of:, min_size: 0, max_size: 10)
|
274
|
+
built_as do
|
275
|
+
result = []
|
276
|
+
rep = HypothesisCoreRepeatValues.new(
|
277
|
+
min_size, max_size, (min_size + max_size) * 0.5
|
278
|
+
)
|
279
|
+
source = World.current_engine.current_source
|
280
|
+
result.push any(of) while rep.should_continue(source)
|
281
|
+
result
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
alias array arrays
|
286
|
+
|
287
|
+
# A Possible where the possible values are any one of a number
|
288
|
+
# of other possible values.
|
289
|
+
# For example, from(strings, integers) could provide either of "a"
|
290
|
+
# or 1.
|
291
|
+
# @note This has a slightly non-standard aliasing. It reads more
|
292
|
+
# nicely if you write `any from(a, b, c)` but e.g.
|
293
|
+
# `arrays(of: mix_of(a, b, c))`.
|
294
|
+
#
|
295
|
+
# @return [Possible]
|
296
|
+
# @param components [Array<Possible>] A number of Possible values,
|
297
|
+
# where the result will include any value possible from any of
|
298
|
+
# them. If components contains an
|
299
|
+
# array it will be flattened first, so e.g. from(a, b)
|
300
|
+
# is equivalent to from([a, b])
|
301
|
+
def from(*components)
|
302
|
+
components = components.flatten
|
303
|
+
indexes = from_hypothesis_core(
|
304
|
+
HypothesisCoreBoundedIntegers.new(components.size - 1)
|
305
|
+
)
|
306
|
+
built_as do
|
307
|
+
i = any indexes
|
308
|
+
any components[i]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
alias mix_of from
|
313
|
+
|
314
|
+
# A Possible where any one of a fixed array of values is possible.
|
315
|
+
# @note these values are provided as is, so if the provided
|
316
|
+
# values are mutated in the test you should be careful to make
|
317
|
+
# sure each test run gets a fresh value (if you use this Possible
|
318
|
+
# in line in the test you don't need to worry about this, this
|
319
|
+
# is only a problem if you define the Possible outside of your
|
320
|
+
# hypothesis block).
|
321
|
+
# @return [Possible]
|
322
|
+
# @param values [Enumerable] A collection of possible values.
|
323
|
+
def element_of(values)
|
324
|
+
values = values.to_a
|
325
|
+
indexes = from_hypothesis_core(
|
326
|
+
HypothesisCoreBoundedIntegers.new(values.size - 1)
|
327
|
+
)
|
328
|
+
built_as do
|
329
|
+
values.fetch(any(indexes))
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
alias elements_of element_of
|
334
|
+
|
335
|
+
# A Possible integer
|
336
|
+
# @return [Possible]
|
337
|
+
# @param min [Integer] The smallest value integer to provide.
|
338
|
+
# @param max [Integer] The largest value integer to provide.
|
339
|
+
def integers(min: nil, max: nil)
|
340
|
+
base = from_hypothesis_core HypothesisCoreIntegers.new
|
341
|
+
if min.nil? && max.nil?
|
342
|
+
base
|
343
|
+
elsif min.nil?
|
344
|
+
built_as { max - any(base).abs }
|
345
|
+
elsif max.nil?
|
346
|
+
built_as { min + any(base).abs }
|
347
|
+
else
|
348
|
+
bounded = from_hypothesis_core(
|
349
|
+
HypothesisCoreBoundedIntegers.new(max - min)
|
350
|
+
)
|
351
|
+
if min.zero?
|
352
|
+
bounded
|
353
|
+
else
|
354
|
+
built_as { min + any(bounded) }
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
alias integer integers
|
360
|
+
|
361
|
+
private
|
362
|
+
|
363
|
+
def from_hypothesis_core(core)
|
364
|
+
Hypothesis::Possible::Implementations::PossibleFromCore.new(
|
365
|
+
core
|
366
|
+
)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|