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 "minitest/autorun"
require "digest"
module Minitest module Minitest
class Property < Test class Property < Test
@ -297,7 +299,7 @@ module Minitest
# You can safely omit implementing this at the cost of somewhat increased # You can safely omit implementing this at the cost of somewhat increased
# shrinking time. # shrinking time.
class CachedTestFunction class CachedTestFunction
def init(&test_function) def initialize(&test_function)
@test_function = test_function @test_function = test_function
# Tree nodes are either a point at which a choice occurs # Tree nodes are either a point at which a choice occurs
@ -320,53 +322,44 @@ module Minitest
node = node.fetch(c) node = node.fetch(c)
# mark_status was called, thus future choices # mark_status was called, thus future choices
# will be ignored. # will be ignored.
# if isinstance(node, Status): if node.is_a?(Status)
# assert node != Status.OVERRUN fail if node == Status::OVERRUN
# return node return node
end
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 rescue KeyError
end 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 end
# def __call__(self, choices: Sequence[int]) -> Status: test_case.status
# # XXX The type of node is problematic end
# 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
end end
class TestingState class TestingState
attr_reader :result, :valid_test_cases attr_reader :result, :valid_test_cases, :calls
def initialize(random:, test_function:, max_examples:) def initialize(random:, test_function:, max_examples:)
@random, @_test_function, @max_examples = 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 # 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 # where we try something that is e.g. a prefix of something we've
# previously tried, which is guaranteed not to work. # 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 consider = ->(choices) do
return true if choices == @result return true if choices == @result
test_function(TestCase.for_choices(choices)) == Status::INTERESTING cached.(choices) == Status::INTERESTING
end end
fail unless consider.(@result) fail unless consider.(@result)
@ -714,7 +703,28 @@ module Minitest
end end
class DirectoryDb 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
end end
@ -727,18 +737,24 @@ module Minitest
# Raised when a test has no valid examples. # Raised when a test has no valid examples.
class Unsatisfiable < StandardError; end class Unsatisfiable < StandardError; end
module Status class Status < Struct.new(:value)
# Test case didn't have enough data to complete # Test case didn't have enough data to complete
OVERRUN = 0 OVERRUN = self.new(0)
# Test case contained something that prevented completion # Test case contained something that prevented completion
INVALID = 1 INVALID = self.new(1)
# Test case completed just fine but was boring # Test case completed just fine but was boring
VALID = 2 VALID = self.new(2)
# Test case completed and was interesting # Test case completed and was interesting
INTERESTING = 3 INTERESTING = self.new(3)
include Comparable
def <=>(other)
value <=> other.value
end
end end
end end
end end
@ -812,9 +828,31 @@ class TestProperty < Minitest::Property
OUT OUT
end end
# def test_reuses_results_from_the_database(tmpdir): def test_reuses_results_from_the_database
# db = DirectoryDB(tmpdir) Dir.mktmpdir do |tmpdir|
# count = 0 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(): # def run():
# with pytest.raises(AssertionError): # with pytest.raises(AssertionError):
@ -853,9 +891,10 @@ class TestProperty < Minitest::Property
end end
def test_error_on_unbounded_test_function def test_error_on_unbounded_test_function
# TODO Make the warnings go away
orig_buffer_size = Minitest::Property::BUFFER_SIZE orig_buffer_size = Minitest::Property::BUFFER_SIZE
suppress_warnings do
Minitest::Property.const_set(:BUFFER_SIZE, 10) Minitest::Property.const_set(:BUFFER_SIZE, 10)
end
assert_raises(Unsatisfiable) do assert_raises(Unsatisfiable) do
run_test("error_on_unbounded_test_function", database: {}, max_examples: 5) do |test_case| run_test("error_on_unbounded_test_function", database: {}, max_examples: 5) do |test_case|
@ -865,27 +904,28 @@ class TestProperty < Minitest::Property
end end
end end
ensure ensure
suppress_warnings do
Minitest::Property.const_set(:BUFFER_SIZE, orig_buffer_size) Minitest::Property.const_set(:BUFFER_SIZE, orig_buffer_size)
end end
end
# def test_function_cache(): def test_function_cache
# def tf(tc): tf = ->(tc) do
# if tc.choice(1000) >= 200: tc.mark_status(Status::INTERESTING) if tc.choice(1_000) >= 200
# tc.mark_status(Status.INTERESTING) tc.reject if tc.choice(1).zero?
# if tc.choice(1) == 0: end
# tc.reject()
# state = State(Random(0), tf, 100)
# 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_equal Status::VALID, cache.([1, 1])
# assert cache([1]) == Status.OVERRUN assert_equal Status::OVERRUN, cache.([1])
# assert cache([1000]) == Status.INTERESTING assert_equal Status::INTERESTING, cache.([1_000])
# assert cache([1000]) == Status.INTERESTING assert_equal Status::INTERESTING, cache.([1_000])
# assert cache([1000, 1]) == Status.INTERESTING 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 # Targeting has a number of places it checks for whether we've exceeded the
# generation limits. This makes sure we've checked them all. # generation limits. This makes sure we've checked them all.
@ -1205,4 +1245,13 @@ class TestProperty < Minitest::Property
end end
end end
end end
private
def suppress_warnings
original_verbosity = $VERBOSE
$VERBOSE = nil
yield
$VERBOSE = original_verbosity
end
end end

Loading…
Cancel
Save