require_relative "error" require_relative "status" module Minitest::Thesis # Represents a single generated test case, which consists of an underlying # set of choices that produce possibilities. class TestCase # Returns a test case that makes this series of choices. def self.for_choices(choices, print_results: false) self.new(prefix: choices, random: nil, max_size: choices.length, print_results:) end attr_accessor :status attr_reader :choices, :targeting_score def initialize(prefix:, random:, max_size: Float::INFINITY, print_results: false) @prefix, @random, @max_size, @print_results = prefix, random, max_size, print_results @choices = [] @status = nil @depth = 0 @targeting_score = nil end # Returns a number in the range [0, n] def choice(n) result = make_choice(n) { @random.rand(n) } puts "choice(#{n}): #{result}" if should_print? result end # Return True with probability `p`. def weighted(p) result = if p <= 0 then forced_choice(0) elsif p >= 1 then forced_choice(1) else make_choice(1) { (@random.rand <= p) ? 1 : 0 } end puts "weighted(#{p}): #{result}" if should_print? result end # Inserts a fake choice into the choice sequence, as if some call to # choice() had returned `n`. You almost never need this, but sometimes it # can be a useful hint to the shrinker. def forced_choice(n) raise RangeError.new("Invalid choice #{n}") if n.bit_length > 64 || n.negative? raise Frozen unless @status.nil? mark_status(Status::OVERRUN) if @choices.length >= @max_size choices << n n end # Mark this test case as invalid. def reject = mark_status(Status::INVALID) # If this precondition is not met, abort the test and mark this test case as invalid. def assume(precondition) return if precondition reject end # Set a score to maximize. Multiple calls to this function will override previous ones. # # The name and idea come from Löscher, Andreas, and Konstantinos Sagonas. # "Targeted property-based testing." ISSTA. 2017, but the implementation # is based on that found in Hypothesis, which is not that similar to # anything described in the paper. def target(score) = @targeting_score = score # Return a possible value from `possibility`. def any(possibility) begin @depth += 1 result = possibility.produce.(self) ensure @depth -= 1 end puts "any(#{possibility}): #{result}" if should_print? result end # Set the status and raise StopTest. def mark_status(status) raise Frozen unless self.status.nil? @status = status raise StopTest end private def should_print? = @print_results && @depth.zero? # Make a choice in [0, n], by calling rnd_method if randomness is needed. def make_choice(n, &rnd_method) raise RangeError.new("Invalid choice #{n}") if n.bit_length > 64 || n.negative? raise Frozen unless @status.nil? mark_status(Status::OVERRUN) if @choices.length >= @max_size result = if @choices.length < @prefix.length @prefix[@choices.length] else rnd_method.() end @choices << result mark_status(Status::INVALID) if result > n result end end end