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
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
|