class PropCheck::Property

Create and run property-checks.

For simple usage, see `.forall`.

For advanced usage, call `PropCheck::Property.new(…)` and then configure it to your liking using e.g. `#with_config`, `#before`, `#after`, `#around` etc. Each of these methods will return a new `Property`, so earlier properties are not mutated. This allows you to re-use configuration and hooks between multiple tests.

Constants

Configuration

Public Class Methods

configuration() click to toggle source

Returns the default configuration of the library as it is configured right now for introspection.

For the configuration of a single property, check its `configuration` instance method. See PropCheck::Property::Configuration for more info on available settings.

# File lib/prop_check/property.rb, line 58
def self.configuration
  @configuration ||= Configuration.new
end
configure() { |configuration| ... } click to toggle source

Yields the library's configuration object for you to alter. See PropCheck::Property::Configuration for more info on available settings.

# File lib/prop_check/property.rb, line 65
def self.configure
  yield(configuration)
end
forall(*bindings, **kwbindings, &block) click to toggle source

Main entry-point to create (and possibly immediately run) a property-test.

This method accepts a list of generators and a block. The block will then be executed many times, passing the values generated by the generators as respective arguments:

“` include PropCheck::Generators PropCheck.forall(integer(), float()) { |x, y| … } “`

It is also possible (and recommended when having more than a few generators) to use a keyword-list of generators instead:

“` include PropCheck::Generators PropCheck.forall(x: integer(), y: float()) { |x:, y:| … } “`

If you do not pass a block right away, a Property object is returned, which you can call the other instance methods of this class on before finally passing a block to it using `#check`. (so `forall(Generators.integer) do |val| … end` and forall(Generators.integer).check do |val| … end` are the same)

# File lib/prop_check/property.rb, line 44
def self.forall(*bindings, **kwbindings, &block)
  property = new(*bindings, **kwbindings)

  return property.check(&block) if block_given?

  property
end
new(*bindings, **kwbindings) click to toggle source
# File lib/prop_check/property.rb, line 69
def initialize(*bindings, **kwbindings)
  @config = self.class.configuration
  @hooks = PropCheck::Hooks.new

  @gen = gen_from_bindings(bindings, kwbindings) unless bindings.empty? && kwbindings.empty?
  freeze
end

Public Instance Methods

after(&hook) click to toggle source

Calls `hook` after each time a check is run with new data.

This is useful to add teardown logic When called multiple times, earlier-added hooks will be called after `hook` is called.

# File lib/prop_check/property.rb, line 158
def after(&hook)
  duplicate = self.dup
  duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook))
  duplicate.freeze
  duplicate
end
around(&hook) click to toggle source

Calls `hook` around each time a check is run with new data.

`hook` should `yield` to the passed block.

When called multiple times, earlier-added hooks will be wrapped around `hook`.

Around hooks will be called after all `#before` hooks and before all `#after` hooks.

Note that if the block passed to `hook` raises an exception, it is possible for the code after `yield` not to be called. So make sure that cleanup logic is wrapped with the `ensure` keyword.

# File lib/prop_check/property.rb, line 178
def around(&hook)
  duplicate = self.dup
  duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook))
  duplicate.freeze
  duplicate
end
before(&hook) click to toggle source

Calls `hook` before each time a check is run with new data.

This is useful to add setup logic When called multiple times, earlier-added hooks will be called before `hook` is called.

# File lib/prop_check/property.rb, line 146
def before(&hook)
  duplicate = self.dup
  duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook))
  duplicate.freeze
  duplicate
end
check(&block) click to toggle source

Checks the property (after settings have been altered using the other instance methods in this class.)

# File lib/prop_check/property.rb, line 187
def check(&block)
  n_runs = 0
  n_successful = 0

  # Loop stops at first exception
  attempts_enum(@gen).each do |generator_result|
    n_runs += 1
    check_attempt(generator_result, n_successful, &block)
    n_successful += 1
  end

  ensure_not_exhausted!(n_runs)
end
configuration() click to toggle source

Returns the configuration of this property for introspection.

See PropCheck::Property::Configuration for more info on available settings.

# File lib/prop_check/property.rb, line 93
def configuration
  @config
end
where(&condition) click to toggle source

filters the generator using the given `condition`. The final property checking block will only be run if the condition is truthy.

If wanted, multiple `where`-conditions can be specified on a property. Be aware that if you filter away too much generated inputs, you might encounter a GeneratorExhaustedError. Only filter if you have few inputs to reject. Otherwise, improve your generators.

# File lib/prop_check/property.rb, line 131
def where(&condition)
  raise ArgumentError, 'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.' unless @gen

  duplicate = self.dup
  duplicate.instance_variable_set(:@gen, @gen.where(&condition))
  duplicate.freeze
  duplicate
end
with_bindings(*bindings, **kwbindings) click to toggle source
# File lib/prop_check/property.rb, line 114
def with_bindings(*bindings, **kwbindings)
  raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?

  duplicate = self.dup
  duplicate.instance_variable_set(:@gen, gen_from_bindings(bindings, kwbindings))
  duplicate.freeze
  duplicate
end
with_config(**config, &block) click to toggle source

Allows you to override the configuration of this property by giving a hash with new settings.

If no other changes need to occur before you want to check the property, you can immediately pass a block to this method. (so `forall(a: Generators.integer).with_config(verbose: true) do … end` is the same as `forall(a: Generators.integer).with_config(verbose: true).check do … end`)

# File lib/prop_check/property.rb, line 104
def with_config(**config, &block)
  duplicate = self.dup
  duplicate.instance_variable_set(:@config, @config.merge(config))
  duplicate.freeze

  return duplicate.check(&block) if block_given?

  duplicate
end

Private Instance Methods

attempts_enum(binding_generator) click to toggle source
# File lib/prop_check/property.rb, line 266
        def attempts_enum(binding_generator)
    @hooks
    .wrap_enum(raw_attempts_enum(binding_generator))
    .lazy
    .take(@config.n_runs)
end
check_attempt(generator_result, n_successful, &block) click to toggle source
# File lib/prop_check/property.rb, line 237
        def check_attempt(generator_result, n_successful, &block)
  PropCheck::Helper.call_splatted(generator_result.root, &block)

# immediately stop (without shrinnking) for when the app is asked
# to close by outside intervention
rescue SignalException, SystemExit
  raise

# We want to capture _all_ exceptions (even low-level ones) here,
# so we can shrink to find their cause.
# don't worry: they all get reraised
rescue Exception => e
  output, shrunken_result, shrunken_exception, n_shrink_steps = show_problem_output(e, generator_result, n_successful, &block)
  output_string = output.is_a?(StringIO) ? output.string : e.message

  e.define_singleton_method :prop_check_info do
    {
      original_input: generator_result.root,
      original_exception_message: e.message,
      shrunken_input: shrunken_result,
      shrunken_exception: shrunken_exception,
      n_successful: n_successful,
      n_shrink_steps: n_shrink_steps
    }
  end

  raise e, output_string, e.backtrace
end
ensure_not_exhausted!(n_runs) click to toggle source
# File lib/prop_check/property.rb, line 220
        def ensure_not_exhausted!(n_runs)
  return if n_runs >= @config.n_runs

  raise_generator_exhausted!
end
gen_from_bindings(bindings, kwbindings) click to toggle source
# File lib/prop_check/property.rb, line 201
            def gen_from_bindings(bindings, kwbindings)
      if bindings == [] && kwbindings != {}
        PropCheck::Generators.fixed_hash(**kwbindings)
      elsif bindings != [] && kwbindings == {}
        if bindings.size == 1
          bindings.first
        else
          PropCheck::Generators.tuple(*bindings)
        end
      else
        raise ArgumentError,
              'Attempted to use both normal and keyword bindings at the same time.
This is not supported because of the separation of positional and keyword arguments
(the old behaviour is deprecated in Ruby 2.7 and will be removed in 3.0)
c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
     '
      end
    end
raise_generator_exhausted!() click to toggle source
# File lib/prop_check/property.rb, line 226
        def raise_generator_exhausted!()
  raise Errors::GeneratorExhaustedError, """
    Could not perform `n_runs = #{@config.n_runs}` runs,
    (exhausted #{@config.max_generate_attempts} tries)
    because too few generator results were adhering to
    the `where` condition.

    Try refining your generators instead.
    """
end
raw_attempts_enum(binding_generator) click to toggle source
# File lib/prop_check/property.rb, line 273
        def raw_attempts_enum(binding_generator)
  rng = Random::DEFAULT
  size = 1
  (0...@config.max_generate_attempts)
    .lazy
    .map { binding_generator.generate(size: size, rng: rng, max_consecutive_attempts: @config.max_consecutive_attempts) }
    .map do |result|
      size += 1

      result
    end
end
show_problem_output(problem, generator_results, n_successful, &block) click to toggle source
# File lib/prop_check/property.rb, line 286
        def show_problem_output(problem, generator_results, n_successful, &block)
  output = @config.verbose ? STDOUT : StringIO.new
  output = PropCheck::Property::OutputFormatter.pre_output(output, n_successful, generator_results.root, problem)
  shrunken_result, shrunken_exception, n_shrink_steps = shrink(generator_results, output, &block)
  output = PropCheck::Property::OutputFormatter.post_output(output, n_shrink_steps, shrunken_result, shrunken_exception)

  [output, shrunken_result, shrunken_exception, n_shrink_steps]
end
shrink(bindings_tree, io, &block) click to toggle source
# File lib/prop_check/property.rb, line 295
        def shrink(bindings_tree, io, &block)
  PropCheck::Property::Shrinker.call(bindings_tree, io, @hooks, @config, &block)
end