From 686d41feaebb2a4bc6c1004ee922e3131c8bdbed Mon Sep 17 00:00:00 2001 From: Alpha Chen Date: Wed, 16 Nov 2022 12:59:10 -0800 Subject: [PATCH] mu --- test/test_helper.rb | 197 +++++++++++++++++++++++++++----------------- 1 file changed, 123 insertions(+), 74 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index 1a69950..b480b26 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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