lib/prop_check/property.rb in prop_check-0.7.1 vs lib/prop_check/property.rb in prop_check-0.8.0

- old
+ new

@@ -1,27 +1,42 @@ require 'stringio' require "awesome_print" require 'prop_check/property/configuration' -require 'prop_check/property/check_evaluator' module PropCheck ## # Run properties class Property ## - # Call this with a keyword argument list of (symbol => generators) and a block. - # The block will then be executed many times, with the respective symbol names - # being defined as having a single generated value. + # 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(a: Generators.integer) do ... end` and forall(a: Generators.integer).check do ... end` are the same) - def self.forall(**bindings, &block) + # (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same) + def self.forall(*bindings, &block) - property = new(bindings) + property = new(*bindings) return property.check(&block) if block_given? property end @@ -43,15 +58,16 @@ yield(configuration) end attr_reader :bindings, :condition - def initialize(**bindings) - raise ArgumentError, 'No bindings specified!' if bindings.empty? + def initialize(*bindings, **kwbindings) + raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty? @bindings = bindings - @condition = -> { true } + @kwbindings = kwbindings + @condition = proc { true } @config = self.class.configuration end ## # Returns the configuration of this property @@ -85,19 +101,29 @@ # 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 = -> { instance_exec(&original_condition) && instance_exec(&condition) } + @condition = proc do |**kwargs| + original_condition.call(**kwargs) && condition.call(**kwargs) + end self end ## # Checks the property (after settings have been altered using the other instance methods in this class.) def check(&block) - binding_generator = PropCheck::Generators.fixed_hash(bindings) + gens = + if @kwbindings != {} + kwbinding_generator = PropCheck::Generators.fixed_hash(**@kwbindings) + @bindings + [kwbinding_generator] + else + @bindings + end + binding_generator = PropCheck::Generators.tuple(*gens) + # binding_generator = PropCheck::Generators.fixed_hash(**@kwbindings) n_runs = 0 n_successful = 0 # Loop stops at first exception @@ -111,10 +137,14 @@ end private def ensure_not_exhausted!(n_runs) return if n_runs >= @config.n_runs + raise_generator_exhausted! + end + + private 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. @@ -122,11 +152,11 @@ Try refining your generators instead. """ end private def check_attempt(generator_result, n_successful, &block) - CheckEvaluator.new(generator_result.root, &block).call + block.call(*generator_result.root) # immediately stop (without shrinnking) for when the app is asked # to close by outside intervention rescue SignalException, SystemExit raise @@ -158,12 +188,12 @@ n_runs = 0 size = 1 (0...@config.max_generate_attempts) .lazy .map { binding_generator.generate(size, rng) } - .reject { |val| val.root == :"_PropCheck.filter_me" } - .select { |val| CheckEvaluator.new(val.root, &@condition).call } + .reject { |val| val.root.any? { |elem| elem == :"_PropCheck.filter_me" }} + .select { |val| @condition.call(*val.root) } .map do |result| n_runs += 1 size += 1 result @@ -192,29 +222,33 @@ output end private def post_output(output, n_shrink_steps, shrunken_result, shrunken_exception) - output.puts '' - output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):" - output.puts "`#{print_roots(shrunken_result)}`" - output.puts "" - output.puts "Shrunken exception:\n---\n#{shrunken_exception}" - output.puts "---" - output.puts "" - + if n_shrink_steps == 0 + output.puts '(shrinking impossible)' + else + output.puts '' + output.puts "Shrunken input (after #{n_shrink_steps} shrink steps):" + output.puts "`#{print_roots(shrunken_result)}`" + output.puts "" + output.puts "Shrunken exception:\n---\n#{shrunken_exception}" + output.puts "---" + output.puts "" + end output end - private def print_roots(lazy_tree_hash) - # lazy_tree_hash.map do |name, val| - # "#{name} = #{val.inspect}" - # end.join(", ") - lazy_tree_hash.ai + private def print_roots(lazy_tree_val) + if lazy_tree_val.is_a?(Array) && lazy_tree_val.length == 1 && lazy_tree_val[0].is_a?(Hash) + lazy_tree_val[0].ai + else + lazy_tree_val.ai + end end - private def shrink(bindings_tree, io, &fun) + private def shrink(bindings_tree, io, &block) io.puts 'Shrinking...' if @config.verbose problem_child = bindings_tree siblings = problem_child.children.lazy parent_siblings = nil problem_exception = nil @@ -232,10 +266,10 @@ shrink_steps += 1 io.print '.' if @config.verbose begin - CheckEvaluator.new(sibling.root, &fun).call + block.call(*sibling.root) rescue Exception => e problem_child = sibling parent_siblings = siblings siblings = problem_child.children.lazy problem_exception = e