Alpha Chen 2 years ago
parent d0eb77e347
commit 686d41feae
Signed by: alpha
SSH Key Fingerprint: SHA256:3fOT8fiYQG/aK9ntivV3Bqtg8AYQ7q4nV6ZgihOA20g

@ -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
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
# 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
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
Minitest::Property.const_set(:BUFFER_SIZE, 10)
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
Minitest::Property.const_set(:BUFFER_SIZE, orig_buffer_size)
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

Loading…
Cancel
Save