You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

122 lines
3.4 KiB

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