|
|
|
@ -1,5 +1,7 @@
|
|
|
|
|
require "minitest/autorun"
|
|
|
|
|
|
|
|
|
|
require "digest"
|
|
|
|
|
|
|
|
|
|
module Minitest
|
|
|
|
|
|
|
|
|
|
class Property < Test
|
|
|
|
@ -297,7 +299,7 @@ module Minitest
|
|
|
|
|
# You can safely omit implementing this at the cost of somewhat increased
|
|
|
|
|
# shrinking time.
|
|
|
|
|
class CachedTestFunction
|
|
|
|
|
def init(&test_function)
|
|
|
|
|
def initialize(&test_function)
|
|
|
|
|
@test_function = test_function
|
|
|
|
|
|
|
|
|
|
# Tree nodes are either a point at which a choice occurs
|
|
|
|
@ -320,53 +322,44 @@ module Minitest
|
|
|
|
|
node = node.fetch(c)
|
|
|
|
|
# mark_status was called, thus future choices
|
|
|
|
|
# will be ignored.
|
|
|
|
|
# if isinstance(node, Status):
|
|
|
|
|
# assert node != Status.OVERRUN
|
|
|
|
|
# return node
|
|
|
|
|
if node.is_a?(Status)
|
|
|
|
|
fail if node == Status::OVERRUN
|
|
|
|
|
return node
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
# If we never entered an unknown region of the tree
|
|
|
|
|
# or hit a Status value, then we know that another
|
|
|
|
|
# choice will be made next and the result will overrun.
|
|
|
|
|
return Status::OVERRUN
|
|
|
|
|
rescue KeyError
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# We now have to actually call the test function to find out what
|
|
|
|
|
# happens.
|
|
|
|
|
test_case = TestCase.for_choices(choices)
|
|
|
|
|
@test_function.(test_case)
|
|
|
|
|
fail if test_case.status.nil?
|
|
|
|
|
|
|
|
|
|
# We enter the choices made in a tree.
|
|
|
|
|
node = @tree
|
|
|
|
|
test_case.choices.each.with_index do |c, i|
|
|
|
|
|
if i + 1 < test_case.choices.length || test_case.status == Status::OVERRUN
|
|
|
|
|
if node.has_key?(c)
|
|
|
|
|
node = node[c]
|
|
|
|
|
else
|
|
|
|
|
node = node[c] = {}
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
node[c] = test_case.status
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# def __call__(self, choices: Sequence[int]) -> Status:
|
|
|
|
|
# # XXX The type of node is problematic
|
|
|
|
|
# node: Any = self.tree
|
|
|
|
|
# try:
|
|
|
|
|
# for c in choices:
|
|
|
|
|
# node = node[c]
|
|
|
|
|
# # mark_status was called, thus future choices
|
|
|
|
|
# # will be ignored.
|
|
|
|
|
# if isinstance(node, Status):
|
|
|
|
|
# assert node != Status.OVERRUN
|
|
|
|
|
# return node
|
|
|
|
|
# # If we never entered an unknown region of the tree
|
|
|
|
|
# # or hit a Status value, then we know that another
|
|
|
|
|
# # choice will be made next and the result will overrun.
|
|
|
|
|
# return Status.OVERRUN
|
|
|
|
|
# except KeyError:
|
|
|
|
|
# pass
|
|
|
|
|
|
|
|
|
|
# # We now have to actually call the test function to find out
|
|
|
|
|
# # what happens.
|
|
|
|
|
# test_case = TestCase.for_choices(choices)
|
|
|
|
|
# self.test_function(test_case)
|
|
|
|
|
# assert test_case.status is not None
|
|
|
|
|
|
|
|
|
|
# # We enter the choices made in a tree.
|
|
|
|
|
# node = self.tree
|
|
|
|
|
# for i, c in enumerate(test_case.choices):
|
|
|
|
|
# if i + 1 < len(test_case.choices) or test_case.status == Status.OVERRUN:
|
|
|
|
|
# try:
|
|
|
|
|
# node = node[c]
|
|
|
|
|
# except KeyError:
|
|
|
|
|
# node = node.setdefault(c, {})
|
|
|
|
|
# else:
|
|
|
|
|
# node[c] = test_case.status
|
|
|
|
|
# return test_case.status
|
|
|
|
|
test_case.status
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class TestingState
|
|
|
|
|
attr_reader :result, :valid_test_cases
|
|
|
|
|
attr_reader :result, :valid_test_cases, :calls
|
|
|
|
|
|
|
|
|
|
def initialize(random:, test_function:, max_examples:)
|
|
|
|
|
@random, @_test_function, @max_examples = random, test_function, max_examples
|
|
|
|
@ -506,16 +499,12 @@ module Minitest
|
|
|
|
|
# reevaluating it in those cases. This also allows us to catch cases
|
|
|
|
|
# where we try something that is e.g. a prefix of something we've
|
|
|
|
|
# previously tried, which is guaranteed not to work.
|
|
|
|
|
# cached = CachedTestFunction(self.test_function)
|
|
|
|
|
cached = CachedTestFunction.new {|tc| test_function(tc) }
|
|
|
|
|
|
|
|
|
|
# def consider(choices: array[int]) -> bool:
|
|
|
|
|
# if choices == self.result:
|
|
|
|
|
# return True
|
|
|
|
|
# return cached(choices) == Status.INTERESTING
|
|
|
|
|
consider = ->(choices) do
|
|
|
|
|
return true if choices == @result
|
|
|
|
|
|
|
|
|
|
test_function(TestCase.for_choices(choices)) == Status::INTERESTING
|
|
|
|
|
cached.(choices) == Status::INTERESTING
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fail unless consider.(@result)
|
|
|
|
@ -714,7 +703,28 @@ module Minitest
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
class DirectoryDb
|
|
|
|
|
def initialize(directory)
|
|
|
|
|
def initialize(dir)
|
|
|
|
|
@dir = dir
|
|
|
|
|
Dir.mkdir(@dir)
|
|
|
|
|
rescue SystemCallError => e
|
|
|
|
|
raise unless e.errno == Errno::EEXIST::Errno
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def [](key)
|
|
|
|
|
f = file(key)
|
|
|
|
|
return nil unless File.exist?(f)
|
|
|
|
|
|
|
|
|
|
File.read(f)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def []=(key, value)
|
|
|
|
|
File.write(file(key), value)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def file(key)
|
|
|
|
|
File.join(@dir, Digest::SHA1.hexdigest(key)[0...10])
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
@ -727,18 +737,24 @@ module Minitest
|
|
|
|
|
# Raised when a test has no valid examples.
|
|
|
|
|
class Unsatisfiable < StandardError; end
|
|
|
|
|
|
|
|
|
|
module Status
|
|
|
|
|
class Status < Struct.new(:value)
|
|
|
|
|
# Test case didn't have enough data to complete
|
|
|
|
|
OVERRUN = 0
|
|
|
|
|
OVERRUN = self.new(0)
|
|
|
|
|
|
|
|
|
|
# Test case contained something that prevented completion
|
|
|
|
|
INVALID = 1
|
|
|
|
|
INVALID = self.new(1)
|
|
|
|
|
|
|
|
|
|
# Test case completed just fine but was boring
|
|
|
|
|
VALID = 2
|
|
|
|
|
VALID = self.new(2)
|
|
|
|
|
|
|
|
|
|
# Test case completed and was interesting
|
|
|
|
|
INTERESTING = 3
|
|
|
|
|
INTERESTING = self.new(3)
|
|
|
|
|
|
|
|
|
|
include Comparable
|
|
|
|
|
|
|
|
|
|
def <=>(other)
|
|
|
|
|
value <=> other.value
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
@ -812,9 +828,31 @@ class TestProperty < Minitest::Property
|
|
|
|
|
OUT
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# def test_reuses_results_from_the_database(tmpdir):
|
|
|
|
|
# db = DirectoryDB(tmpdir)
|
|
|
|
|
# count = 0
|
|
|
|
|
def test_reuses_results_from_the_database
|
|
|
|
|
Dir.mktmpdir do |tmpdir|
|
|
|
|
|
db = DirectoryDb.new(tmpdir)
|
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
|
|
run = -> {
|
|
|
|
|
assert_raises(Minitest::Assertion) do
|
|
|
|
|
run_test("reuses_results_from_the_database", database: db, quiet: true) do |test_case|
|
|
|
|
|
count += 1
|
|
|
|
|
assert test_case.choice(10_000) < 10
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run.()
|
|
|
|
|
|
|
|
|
|
assert_equal 1, Dir.children(tmpdir).length
|
|
|
|
|
prev_count = count
|
|
|
|
|
|
|
|
|
|
run.()
|
|
|
|
|
|
|
|
|
|
assert_equal 1, Dir.children(tmpdir).length
|
|
|
|
|
assert_equal prev_count + 2, count
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# def run():
|
|
|
|
|
# with pytest.raises(AssertionError):
|
|
|
|
@ -853,9 +891,10 @@ class TestProperty < Minitest::Property
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def test_error_on_unbounded_test_function
|
|
|
|
|
# TODO Make the warnings go away
|
|
|
|
|
orig_buffer_size = Minitest::Property::BUFFER_SIZE
|
|
|
|
|
suppress_warnings do
|
|
|
|
|
Minitest::Property.const_set(:BUFFER_SIZE, 10)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
assert_raises(Unsatisfiable) do
|
|
|
|
|
run_test("error_on_unbounded_test_function", database: {}, max_examples: 5) do |test_case|
|
|
|
|
@ -865,27 +904,28 @@ class TestProperty < Minitest::Property
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
ensure
|
|
|
|
|
suppress_warnings do
|
|
|
|
|
Minitest::Property.const_set(:BUFFER_SIZE, orig_buffer_size)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# def test_function_cache():
|
|
|
|
|
# def tf(tc):
|
|
|
|
|
# if tc.choice(1000) >= 200:
|
|
|
|
|
# tc.mark_status(Status.INTERESTING)
|
|
|
|
|
# if tc.choice(1) == 0:
|
|
|
|
|
# tc.reject()
|
|
|
|
|
|
|
|
|
|
# state = State(Random(0), tf, 100)
|
|
|
|
|
def test_function_cache
|
|
|
|
|
tf = ->(tc) do
|
|
|
|
|
tc.mark_status(Status::INTERESTING) if tc.choice(1_000) >= 200
|
|
|
|
|
tc.reject if tc.choice(1).zero?
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# cache = CachedTestFunction(state.test_function)
|
|
|
|
|
state = TestingState.new(random: Random.new(0), test_function: tf, max_examples: 100)
|
|
|
|
|
cache = CachedTestFunction.new {|tc| state.test_function(tc) }
|
|
|
|
|
|
|
|
|
|
# assert cache([1, 1]) == Status.VALID
|
|
|
|
|
# assert cache([1]) == Status.OVERRUN
|
|
|
|
|
# assert cache([1000]) == Status.INTERESTING
|
|
|
|
|
# assert cache([1000]) == Status.INTERESTING
|
|
|
|
|
# assert cache([1000, 1]) == Status.INTERESTING
|
|
|
|
|
assert_equal Status::VALID, cache.([1, 1])
|
|
|
|
|
assert_equal Status::OVERRUN, cache.([1])
|
|
|
|
|
assert_equal Status::INTERESTING, cache.([1_000])
|
|
|
|
|
assert_equal Status::INTERESTING, cache.([1_000])
|
|
|
|
|
assert_equal Status::INTERESTING, cache.([1_000, 1])
|
|
|
|
|
|
|
|
|
|
# assert state.calls == 2
|
|
|
|
|
assert_equal 2, state.calls
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Targeting has a number of places it checks for whether we've exceeded the
|
|
|
|
|
# generation limits. This makes sure we've checked them all.
|
|
|
|
@ -1205,4 +1245,13 @@ class TestProperty < Minitest::Property
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
def suppress_warnings
|
|
|
|
|
original_verbosity = $VERBOSE
|
|
|
|
|
$VERBOSE = nil
|
|
|
|
|
yield
|
|
|
|
|
$VERBOSE = original_verbosity
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|