File: possible.rb

package info (click to toggle)
python-hypothesis 6.138.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,272 kB
  • sloc: python: 62,853; ruby: 1,107; sh: 253; makefile: 41; javascript: 6
file content (370 lines) | stat: -rw-r--r-- 11,340 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# frozen_string_literal: true

# @!visibility private
class HypothesisCoreRepeatValues
  def should_continue(source)
    result = _should_continue(source.wrapped_data)
    raise Hypothesis::DataOverflow if result.nil?
    result
  end
end

module Hypothesis
  class <<self
    include Hypothesis
  end

  # A Possible describes a range of valid values that
  # can result from a call to {Hypothesis#any}.
  # This class should not be subclassed directly, but
  # instead should always be constructed using methods
  # from {Hypothesis::Possibilities}.
  class Possible
    # @!visibility private
    include Hypothesis

    # A Possible value constructed by passing one of these
    # Possible values to the provided block.
    #
    # e.g. the Possible values of `integers.map { |i| i * 2 }`
    # are all even integers.
    #
    # @return [Possible]
    # @yield A possible value of self.
    def map
      Implementations::CompositePossible.new do
        yield any(self)
      end
    end

    alias collect map

    # One of these Possible values selected such that
    # the block returns a true value for it.
    #
    # e.g. the Possible values of
    # `integers.filter { |i| i % 2 == 0}`  are all even
    # integers (but will typically be less efficient
    # than the one suggested in {Possible#map}.
    #
    # @note Similar warnings to {Hypothesis#assume} apply
    #   here: If the condition is difficult to satisfy this
    #   may impact the performance and quality of your
    #   testing.
    #
    # @return [Possible]
    # @yield A possible value of self.
    def select
      Implementations::CompositePossible.new do
        result = nil
        4.times do |i|
          assume(i < 3)
          result = any self
          break if yield(result)
        end
        result
      end
    end

    alias filter select

    # @!visibility private
    module Implementations
      # @!visibility private
      class CompositePossible < Possible
        def initialize(block = nil, &implicit)
          @block = block || implicit
        end

        # @!visibility private
        def provide(&block)
          (@block || block).call
        end
      end

      # @!visibility private
      class PossibleFromCore < Possible
        def initialize(core_possible)
          @core_possible = core_possible
        end

        # @!visibility private
        def provide
          data = World.current_engine.current_source
          result = @core_possible.provide(data.wrapped_data)
          raise Hypothesis::DataOverflow if result.nil?
          result
        end
      end
    end
  end

  # A module of many common {Possible} implementations.
  # Rather than subclassing Possible yourself you should use
  # methods from this module to construct Possible values.`
  #
  # You can use methods from this module by including
  # Hypothesis::Possibilities in your tests, or by calling them
  # on the module object directly.
  #
  # Most methods in this module that return a Possible have
  # two names: A singular and a plural name. These are
  # simply aliases and are identical in every way, but are
  # provided to improve readability. For example
  # `any integer` reads better than `any integers`
  # but `arrays(of: integers)` reads better than
  # `arrays(of: integer)`.
  module Possibilities
    include Hypothesis

    class <<self
      include Possibilities
    end

    # built_as lets you chain multiple Possible values together,
    # by providing whatever value results from its block.
    #
    # For example the following provides an array plus some
    # element from that array:
    #
    # ```ruby
    #   built_as do
    #     ls = any array(of: integers)
    #     # Or min_size: 1 above, but this shows use of
    #     # assume
    #     assume ls.size > 0
    #     i = any element_of(ls)
    #     [ls, i]
    #   end
    # ```
    #
    # @return [Possible] A Possible whose possible values are
    #   any result from the passed block.
    def built_as(&block)
      Hypothesis::Possible::Implementations::CompositePossible.new(block)
    end

    alias values_built_as built_as

    # A Possible boolean value
    # @return [Possible]
    def booleans
      integers(min: 0, max: 1).map { |i| i == 1 }
    end

    alias boolean booleans

    # A Possible unicode codepoint.
    # @return [Possible]
    # @param min [Integer] The smallest codepoint to provide
    # @param max [Integer] The largest codepoint to provide
    def codepoints(min: 1, max: 1_114_111)
      base = integers(min: min, max: max)
      if min <= 126
        from(integers(min: min, max: [126, max].min), base)
      else
        base
      end
    end

    alias codepoint codepoints

    # A Possible String
    # @return [Possible]
    # @param codepoints [Possible, nil] The Possible codepoints
    #   that can be found in the string. If nil,
    #   will default to self.codepoints. These
    #   will be further filtered to ensure the generated string is
    #   valid.
    # @param min_size [Integer] The smallest valid length for a
    #   provided string
    # @param max_size [Integer] The largest valid length for a
    #   provided string
    def strings(codepoints: nil, min_size: 0, max_size: 10)
      codepoints = self.codepoints if codepoints.nil?
      codepoints = codepoints.select do |i|
        begin
          [i].pack('U*').codepoints
          true
        rescue ArgumentError
          false
        end
      end
      arrays(of: codepoints, min_size: min_size, max_size: max_size).map do |ls|
        ls.pack('U*')
      end
    end

    alias string strings

    # A Possible Hash, where all possible values have a fixed
    # shape.
    # This is used for hashes where you know exactly what the
    # keys are, and different keys may have different possible values.
    # For example, hashes_of_shape(a: integers, b: booleans)
    # will give you values like `{a: 11, b: false}`.
    # @return [Possible]
    # @param hash [Hash] A hash describing the values to provide.
    #  The keys will be present unmodified in the provided hashes,
    #  mapping to their Possible value in the result.
    def hashes_of_shape(hash)
      built_as do
        result = {}
        hash.each { |k, v| result[k] = any(v) }
        result
      end
    end

    alias hash_of_shape hashes_of_shape

    # A Possible Hash of variable shape.
    # @return [Possible]
    # @param keys [Possible] the possible keys
    # @param values [Possible] the possible values
    def hashes_with(keys:, values:, min_size: 0, max_size: 10)
      built_as do
        result = {}
        rep = HypothesisCoreRepeatValues.new(
          min_size, max_size, (min_size + max_size) * 0.5
        )
        source = World.current_engine.current_source
        while rep.should_continue(source)
          key = any keys
          if result.include?(key)
            rep.reject
          else
            result[key] = any values
          end
        end
        result
      end
    end

    alias hash_with hashes_with

    # A Possible Arrays of a fixed shape.
    # This is used for arrays where you know exactly how many
    # elements there are, and different values may be possible
    # at different positions.
    # For example, arrays_of_shape(strings, integers)
    # will give you values like ["a", 1]
    # @return [Possible]
    # @param elements [Array<Possible>] A variable number of Possible.
    #   values. The provided array will have this many values, with
    #   each value possible for the corresponding argument. If elements
    #   contains an array it will be flattened first, so e.g.
    #   arrays_of_shape(a, b) is equivalent to arrays_of_shape([a, b])
    def arrays_of_shape(*elements)
      elements = elements.flatten
      built_as do
        elements.map { |e| any e }.to_a
      end
    end

    alias array_of_shape arrays_of_shape

    # A Possible Array of variable shape.
    # This is used for arrays where the size may vary and the same values
    # are possible at any position.
    # For example, arrays(of: booleans) might provide [false, true, false].
    # @return [Possible]
    # @param of [Possible] The possible elements of the array.
    # @param min_size [Integer] The smallest valid size of a provided array
    # @param max_size [Integer] The largest valid size of a provided array
    def arrays(of:, min_size: 0, max_size: 10)
      built_as do
        result = []
        rep = HypothesisCoreRepeatValues.new(
          min_size, max_size, (min_size + max_size) * 0.5
        )
        source = World.current_engine.current_source
        result.push any(of) while rep.should_continue(source)
        result
      end
    end

    alias array arrays

    # A Possible where the possible values are any one of a number
    # of other possible values.
    # For example, from(strings, integers) could provide either of "a"
    # or 1.
    # @note This has a slightly non-standard aliasing. It reads more
    #   nicely if you write `any from(a, b, c)` but e.g.
    #   `arrays(of: mix_of(a, b, c))`.
    #
    # @return [Possible]
    # @param components [Array<Possible>] A number of Possible values,
    #   where the result will include any value possible from any of
    #   them. If components contains an
    #   array it will be flattened first, so e.g. from(a, b)
    #   is equivalent to from([a, b])
    def from(*components)
      components = components.flatten
      indexes = from_hypothesis_core(
        HypothesisCoreBoundedIntegers.new(components.size - 1)
      )
      built_as do
        i = any indexes
        any components[i]
      end
    end

    alias mix_of from

    # A Possible where any one of a fixed array of values is possible.
    # @note these values are provided as is, so if the provided
    #   values are mutated in the test you should be careful to make
    #   sure each test run gets a fresh value (if you use this Possible
    #   in line in the test you don't need to worry about this, this
    #   is only a problem if you define the Possible outside of your
    #   hypothesis block).
    # @return [Possible]
    # @param values [Enumerable] A collection of possible values.
    def element_of(values)
      values = values.to_a
      indexes = from_hypothesis_core(
        HypothesisCoreBoundedIntegers.new(values.size - 1)
      )
      built_as do
        values.fetch(any(indexes))
      end
    end

    alias elements_of element_of

    # A Possible integer
    # @return [Possible]
    # @param min [Integer] The smallest value integer to provide.
    # @param max [Integer] The largest value integer to provide.
    def integers(min: nil, max: nil)
      base = from_hypothesis_core HypothesisCoreIntegers.new
      if min.nil? && max.nil?
        base
      elsif min.nil?
        built_as { max - any(base).abs }
      elsif max.nil?
        built_as { min + any(base).abs }
      else
        bounded = from_hypothesis_core(
          HypothesisCoreBoundedIntegers.new(max - min)
        )
        if min.zero?
          bounded
        else
          built_as { min + any(bounded) }
        end
      end
    end

    alias integer integers

    private

    def from_hypothesis_core(core)
      Hypothesis::Possible::Implementations::PossibleFromCore.new(
        core
      )
    end
  end
end