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.
@@ -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