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
|