lib/prop_check/property.rb in prop_check-0.13.0 vs lib/prop_check/property.rb in prop_check-0.14.0

- old
+ new

@@ -5,13 +5,19 @@ require 'prop_check/property/output_formatter' require 'prop_check/property/shrinker' require 'prop_check/hooks' module PropCheck ## - # Run properties + # 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. class Property - ## # 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 @@ -34,11 +40,10 @@ # 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) def self.forall(*bindings, **kwbindings, &block) - property = new(*bindings, **kwbindings) return property.check(&block) if block_given? property @@ -59,23 +64,29 @@ # See PropCheck::Property::Configuration for more info on available settings. def self.configure yield(configuration) end - attr_reader :bindings, :condition - def initialize(*bindings, **kwbindings) - raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty? - - # @bindings = bindings - # @kwbindings = kwbindings - @gen = gen_from_bindings(bindings, kwbindings) - @condition = proc { true } @config = self.class.configuration @hooks = PropCheck::Hooks.new + + @gen = gen_from_bindings(bindings, kwbindings) unless bindings.empty? && kwbindings.empty? + freeze end + # [:condition, :config, :hooks, :gen].each do |symbol| + # define_method(symbol) do + # self.instance_variable_get("@#{symbol}") + # end + + # protected define_method("#{symbol}=") do |value| + # duplicate = self.dup + # duplicate.instance_variable_set("@#{symbol}", value) + # duplicate + # end + ## # Returns the configuration of this property # for introspection. # # See PropCheck::Property::Configuration for more info on available settings. @@ -89,55 +100,68 @@ # # 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`) def with_config(**config, &block) - @config = @config.merge(config) + duplicate = self.dup + duplicate.instance_variable_set(:@config, @config.merge(config)) + duplicate.freeze - return self.check(&block) if block_given? + return duplicate.check(&block) if block_given? - self + duplicate end + 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 + ## # 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. def where(&condition) - # original_condition = @condition.dup - # @condition = proc do |val| - # call_splatted(val, &original_condition) && call_splatted(val, &condition) - # # original_condition.call(val) && condition.call(val) - # end - @gen = @gen.where(&condition) + raise ArgumentError, 'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.' unless @gen - self + duplicate = self.dup + duplicate.instance_variable_set(:@gen, @gen.where(&condition)) + duplicate.freeze + duplicate end ## # 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. def before(&hook) - @hooks.add_before(&hook) - self + duplicate = self.dup + duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook)) + duplicate.freeze + duplicate end ## # 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. def after(&hook) - @hooks.add_after(&hook) - self + duplicate = self.dup + duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook)) + duplicate.freeze + duplicate end ## # Calls `hook` around each time a check is run with new data. # @@ -150,12 +174,14 @@ # # 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. def around(&hook) - @hooks.add_around(&hook) - self + duplicate = self.dup + duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook)) + duplicate.freeze + duplicate end ## # Checks the property (after settings have been altered using the other instance methods in this class.) def check(&block) @@ -236,9 +262,11 @@ raise e, output_string, e.backtrace end private def attempts_enum(binding_generator) + ap @hooks + @hooks .wrap_enum(raw_attempts_enum(binding_generator)) .lazy .take(@config.n_runs) end