Class: PropCheck::Generator

Inherits:
Object
  • Object
show all
Defined in:
lib/prop_check/generator.rb

Overview

A `Generator` is a special kind of 'proc' that, given a size an random number generator state, will generate a (finite) LazyTree of output values:

The root of this tree is the value to be used during testing, and the children are 'smaller' values related to the root, to be used during the shrinking phase.

Constant Summary collapse

@@default_size =
10
@@default_rng =
Random.new
@@max_consecutive_attempts =
100
@@default_kwargs =
{size: @@default_size, rng: @@default_rng, max_consecutive_attempts: @@max_consecutive_attempts}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Generator

Being a special kind of Proc, a Generator wraps a block.


18
19
20
# File 'lib/prop_check/generator.rb', line 18

def initialize(&block)
  @block = block
end

Class Method Details

.wrap(val) ⇒ Object

Creates a 'constant' generator that always returns the same value, regardless of `size` or `rng`.

Keen readers may notice this as the Monadic 'pure'/'return' implementation for Generators.

>> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
=> [2, 79]

70
71
72
# File 'lib/prop_check/generator.rb', line 70

def self.wrap(val)
  Generator.new { LazyTree.wrap(val) }
end

Instance Method Details

#bind(&generator_proc) ⇒ Object

Create a generator whose implementation depends on the output of another generator. this allows us to compose multiple generators.

Keen readers may notice this as the Monadic 'bind' (sometimes known as '>>=') implementation for Generators.

>> Generators.integer.bind { |a| Generators.integer.bind { |b| Generator.wrap([a , b]) } }.call(size: 100, rng: Random.new(42))
=> [2, 79]

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/prop_check/generator.rb', line 82

def bind(&generator_proc)
  # Generator.new do |size, rng|
  #   outer_result = generate(size, rng)
  #   outer_result.map do |outer_val|
  #     inner_generator = generator_proc.call(outer_val)
  #     inner_generator.generate(size, rng)
  #   end.flatten
  # end
  Generator.new do |**kwargs|
    outer_result = self.generate(**kwargs)
    outer_result.bind do |outer_val|
      inner_generator = generator_proc.call(outer_val)
      inner_generator.generate(**kwargs)
    end
  end
end

#call(**kwargs) ⇒ Object

Generates a value, and only return this value (drop information for shrinking)

>> Generators.integer.call(size: 1000, rng: Random.new(42))
=> 126

49
50
51
# File 'lib/prop_check/generator.rb', line 49

def call(**kwargs)
  generate(**@@default_kwargs.merge(kwargs)).root
end

#generate(**kwargs) ⇒ Object

Given a `size` (integer) and a random number generator state `rng`, generate a LazyTree.


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/prop_check/generator.rb', line 25

def generate(**kwargs)
  kwargs = @@default_kwargs.merge(kwargs)
  max_consecutive_attempts = kwargs[:max_consecutive_attempts]

  (0..max_consecutive_attempts).each do
    res = @block.call(**kwargs)
    next if res.root == :"_PropCheck.filter_me"

    return res
  end

  raise Errors::GeneratorExhaustedError, """
  Exhausted #{max_consecutive_attempts} consecutive generation attempts.

  Probably too few generator results were adhering to a `where` condition.
  """
end

#map(&proc) ⇒ Object

Creates a new Generator that returns a value by running `proc` on the output of the current Generator.

>> Generators.choose(32..128).map(&:chr).call(size: 10, rng: Random.new(42))
=> "S"

104
105
106
107
108
109
# File 'lib/prop_check/generator.rb', line 104

def map(&proc)
  Generator.new do |**kwargs|
    result = self.generate(**kwargs)
    result.map(&proc)
  end
end

#resize(&proc) ⇒ Object

Resizes the generator to either grow faster or smaller than normal.

`proc` takes the current size as input and is expected to return the new size. a size should always be a nonnegative integer.

>> Generators.integer.resize{}

131
132
133
134
135
136
# File 'lib/prop_check/generator.rb', line 131

def resize(&proc)
  Generator.new do |size:, **other_kwargs|
    new_size = proc.call(size)
    self.generate(**other_kwargs, size: new_size)
  end
end

#sample(num_of_samples = 10, **kwargs) ⇒ Object

Returns `num_of_samples` values from calling this Generator. This is mostly useful for debugging if a generator behaves as you intend it to.


56
57
58
59
60
# File 'lib/prop_check/generator.rb', line 56

def sample(num_of_samples = 10, **kwargs)
  num_of_samples.times.map do
    call(**@@default_kwargs.merge(kwargs))
  end
end

#where(&condition) ⇒ Object

Creates a new Generator that only produces a value when the block `condition` returns a truthy value.


113
114
115
116
117
118
119
120
121
122
# File 'lib/prop_check/generator.rb', line 113

def where(&condition)
  self.map do |result|
    # if condition.call(*result)
    if PropCheck::Helper.call_splatted(result, &condition)
      result
    else
      :"_PropCheck.filter_me"
    end
  end
end