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.
480 lines
12 KiB
480 lines
12 KiB
2 years ago
|
require "test_helper"
|
||
|
|
||
|
class Minitest::ThesisTest < Minitest::Thesis
|
||
|
class Failure < StandardError; end
|
||
|
|
||
|
def test_finds_small_list
|
||
|
(0...10).each do |seed|
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("finds_small_list", database: {}, random: Random.new(seed)) do |test_case|
|
||
|
ls = test_case.any(lists(integers(0, 10_000)))
|
||
|
assert ls.sum <= 1_000
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
any(lists(integers(0, 10000))): [1001]
|
||
|
OUT
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Minithesis can't really handle shrinking arbitrary monadic bind, but length
|
||
|
# parameters are a common case of monadic bind that it has a little bit of
|
||
|
# special casing for. This test ensures that that special casing works.
|
||
|
#
|
||
|
# The problem is that if you generate a list by drawing a length and then
|
||
|
# drawing that many elements, you can end up with something like ``[1001, 0,
|
||
|
# 0]`` then deleting those zeroes in the middle is a pain. minithesis will
|
||
|
# solve this by first sorting those elements, so that we have ``[0, 0,
|
||
|
# 1001]``, and then lowering the length by two, turning it into ``[1001]`` as
|
||
|
# desired.
|
||
|
def test_finds_small_list_even_with_bad_lists
|
||
|
bad_list = Possibility.new(
|
||
|
->(tc) { n = tc.choice(10); Array.new(n) { tc.choice(10_000) }},
|
||
|
name: "bad_list",
|
||
|
)
|
||
|
|
||
|
(0...10).each do |seed|
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("finds_small_list_even_with_bad_lists", database: {}, random: Random.new(seed)) do |test_case|
|
||
|
ls = test_case.any(bad_list)
|
||
|
assert ls.sum <= 1_000
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
any(bad_list): [1001]
|
||
|
OUT
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_reduces_additive_pairs
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("reduces_additive_pairs", database: {}, max_examples: 10_000) do |test_case|
|
||
|
m = test_case.choice(1000)
|
||
|
n = test_case.choice(1000)
|
||
|
assert m + n <= 1000
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
choice(1000): 1
|
||
|
choice(1000): 1000
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
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 test_test_cases_satisfy_preconditions
|
||
|
run_test("test_cases_satisfy_preconditions", database: {}) do |test_case|
|
||
|
n = test_case.choice(10)
|
||
|
test_case.assume(n != 0)
|
||
|
refute_equal 0, n
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_error_on_too_strict_precondition
|
||
|
assert_raises(Unsatisfiable) do
|
||
|
run_test("error_on_too_strict_precondition", database: {}) do |test_case|
|
||
|
n = test_case.choice(10)
|
||
|
test_case.reject
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_error_on_unbounded_test_function
|
||
|
orig_buffer_size = BUFFER_SIZE
|
||
|
suppress_warnings do
|
||
|
Minitest::Thesis.const_set(:BUFFER_SIZE, 10)
|
||
|
end
|
||
|
|
||
|
assert_raises(Unsatisfiable) do
|
||
|
run_test("error_on_unbounded_test_function", database: {}, max_examples: 5) do |test_case|
|
||
|
loop do
|
||
|
test_case.choice(10)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
ensure
|
||
|
suppress_warnings do
|
||
|
Minitest::Thesis.const_set(:BUFFER_SIZE, orig_buffer_size)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
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
|
||
|
|
||
|
state = TestingState.new(random: Random.new(0), test_function: tf, max_examples: 100)
|
||
|
cache = CachedTestFunction.new {|tc| state.test_function(tc) }
|
||
|
|
||
|
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_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.
|
||
|
def test_max_examples_is_not_exceeded
|
||
|
(1...100).each do |max_examples|
|
||
|
calls = 0
|
||
|
|
||
|
run_test(
|
||
|
"max_examples_is_not_exceeded",
|
||
|
database: {},
|
||
|
random: Random.new(0),
|
||
|
max_examples:,
|
||
|
) do |tc|
|
||
|
m = 10000
|
||
|
n = tc.choice(m)
|
||
|
calls += 1
|
||
|
tc.target(n * (m - n))
|
||
|
end
|
||
|
|
||
|
assert_equal max_examples, calls
|
||
|
end
|
||
|
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.
|
||
|
def test_finds_a_local_maximum
|
||
|
(0...100).each do |seed|
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test(
|
||
|
"finds_a_local_maximum",
|
||
|
database: {},
|
||
|
random: Random.new(seed),
|
||
|
max_examples: 200,
|
||
|
quiet: true
|
||
|
) do |tc|
|
||
|
m = tc.choice(1000)
|
||
|
n = tc.choice(1000)
|
||
|
score = -((m - 500) ** 2 + (n - 500) ** 2)
|
||
|
tc.target(score)
|
||
|
assert m != 500 || n != 500
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_can_target_a_score_upwards_to_interesting
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("can_target_a_score_upwards_to_interesting", database: {}, max_examples: 1000) do |test_case|
|
||
|
n = test_case.choice(1000)
|
||
|
m = test_case.choice(1000)
|
||
|
score = n + m
|
||
|
test_case.target(score)
|
||
|
assert score < 2000
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
choice(1000): 1000
|
||
|
choice(1000): 1000
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
def test_can_target_a_score_upwards_without_failing
|
||
|
max_score = 0
|
||
|
|
||
|
run_test("can_target_a_score_upwards_without_failing", database: {}, max_examples: 1000) do |test_case|
|
||
|
n = test_case.choice(1000)
|
||
|
m = test_case.choice(1000)
|
||
|
score = n + m
|
||
|
test_case.target(score)
|
||
|
max_score = [score, max_score].max
|
||
|
end
|
||
|
|
||
|
assert_equal 2000, max_score
|
||
|
end
|
||
|
|
||
|
def test_targeting_when_most_do_not_benefit
|
||
|
big = 10_000
|
||
|
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("targeting_when_most_do_not_benefit", database: {}, max_examples: 1000) do |test_case|
|
||
|
test_case.choice(1000)
|
||
|
test_case.choice(1000)
|
||
|
score = test_case.choice(big)
|
||
|
test_case.target(score)
|
||
|
assert score < big
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
choice(1000): 0
|
||
|
choice(1000): 0
|
||
|
choice(#{big}): #{big}
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
def test_can_target_a_score_downwards
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("can_target_a_score_downwards", database: {}, max_examples: 1000) do |test_case|
|
||
|
n = test_case.choice(1000)
|
||
|
m = test_case.choice(1000)
|
||
|
score = n + m
|
||
|
test_case.target(-score)
|
||
|
assert score.positive?
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
choice(1000): 0
|
||
|
choice(1000): 0
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
def test_prints_a_top_level_weighted
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("prints_a_top_level_weighted", database: {}, max_examples: 1000) do |test_case|
|
||
|
assert test_case.weighted(0.5).nonzero?
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
weighted(0.5): 0
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
def test_errors_when_using_frozen
|
||
|
tc = TestCase.for_choices([0])
|
||
|
tc.status = Status::VALID
|
||
|
|
||
|
assert_raises(Frozen) do
|
||
|
tc.mark_status(Status::INTERESTING)
|
||
|
end
|
||
|
|
||
|
assert_raises(Frozen) do
|
||
|
tc.choice(10)
|
||
|
end
|
||
|
|
||
|
assert_raises(Frozen) do
|
||
|
tc.forced_choice(10)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_errors_on_too_large_choice
|
||
|
tc = TestCase.for_choices([0])
|
||
|
assert_raises(RangeError) do
|
||
|
tc.choice(2 ** 64)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_can_choose_full_64_bits
|
||
|
run_test("can_choose_full_64_bits", database: {}) do |tc|
|
||
|
tc.choice(2 ** 64 - 1)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_mapped_possibility
|
||
|
run_test("mapped_possibility", database: {}) do |tc|
|
||
|
n = tc.any(integers(0, 5).map {|n| n * 2 })
|
||
|
assert n.even?
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_selected_possibility
|
||
|
run_test("selected_possibility", database: {}) do |tc|
|
||
|
n = tc.any(integers(0, 5).satisfying(&:even?))
|
||
|
assert n.even?
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_bound_possibility
|
||
|
run_test("bound_possibility", database: {}) do |tc|
|
||
|
m, n = tc.any(
|
||
|
integers(0, 5).bind {|m| tuples(just(m), integers(m, m + 10)) }
|
||
|
)
|
||
|
assert (m..m+10).cover?(n)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_cannot_witness_nothing
|
||
|
assert_raises(Unsatisfiable) do
|
||
|
run_test("cannot_witness_nothing", database: {}) do |tc|
|
||
|
tc.any(nothing)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_cannot_witness_empty_mix_of
|
||
|
assert_raises(Unsatisfiable) do
|
||
|
run_test("cannot_witness_empty_mix_of", database: {}) do |tc|
|
||
|
tc.any(mix_of)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_can_draw_mixture
|
||
|
run_test("can_draw_mixture", database: {}) do |tc|
|
||
|
m = tc.any(mix_of(integers(-5, 0), integers(2, 5)))
|
||
|
assert (-5..5).cover?(m)
|
||
|
refute_equal 1, m
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# This test is very hard to trigger without targeting, and targeting will
|
||
|
# tend to overshoot the score, so we will see multiple interesting test cases
|
||
|
# before shrinking.
|
||
|
def test_target_and_reduce
|
||
|
out, _ = capture_io do
|
||
|
assert_raises(Minitest::Assertion) do
|
||
|
run_test("target_and_reduce", database: {}) do |tc|
|
||
|
m = tc.choice(100_000)
|
||
|
tc.target(m)
|
||
|
assert m <= 99_900
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
assert_equal <<~OUT, out
|
||
|
choice(100000): 99901
|
||
|
OUT
|
||
|
end
|
||
|
|
||
|
def test_impossible_weighted
|
||
|
assert_raises(Failure) do
|
||
|
run_test("impossible_weighted", database: {}, quiet: true) do |tc|
|
||
|
tc.choice(1)
|
||
|
10.times do
|
||
|
assert false unless tc.weighted(0.0).zero?
|
||
|
end
|
||
|
raise Failure if tc.choice(1).zero?
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_guaranteed_weighted
|
||
|
assert_raises(Failure) do
|
||
|
run_test("guaranteed_weighted", database: {}, quiet: true) do |tc|
|
||
|
if tc.weighted(1.0).nonzero?
|
||
|
tc.choice(1)
|
||
|
raise Failure
|
||
|
else
|
||
|
assert false
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_size_bounds_on_list
|
||
|
run_test("size_bounds_on_list", database: {}) do |tc|
|
||
|
ls = tc.any(lists(integers(0, 10), min_size: 1, max_size: 3))
|
||
|
assert (1..3).cover?(ls.length)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_forced_choice_bounds
|
||
|
assert_raises(RangeError) do
|
||
|
run_test("forced_choice_bounds", database: {}) do |tc|
|
||
|
tc.forced_choice(2 ** 64)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_failure_from_hypothesis_1
|
||
|
assert_raises(Failure) do
|
||
|
run_test("failure_from_hypothesis_1", database: {}, random: Random.new(100), max_examples: 1000, quiet: true) do |tc|
|
||
|
n1 = tc.weighted(0.0)
|
||
|
if n1.zero?
|
||
|
n2 = tc.choice(511)
|
||
|
if n2 == 112
|
||
|
n3 = tc.choice(511)
|
||
|
if n3 == 124
|
||
|
raise Failure
|
||
|
elsif n3 == 93
|
||
|
raise Failure
|
||
|
else
|
||
|
tc.mark_status(Status::INVALID)
|
||
|
end
|
||
|
elsif n2 == 93
|
||
|
raise Failure
|
||
|
else
|
||
|
tc.mark_status(Status::INVALID)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def test_failure_from_hypothesis_2
|
||
|
assert_raises(Failure) do
|
||
|
run_test("failure_from_hypothesis_2", database: {}, random: Random.new(0), max_examples: 1000, quiet: true) do |tc|
|
||
|
n1 = tc.choice(6)
|
||
|
if n1 == 6
|
||
|
n2 = tc.weighted(0.0)
|
||
|
if n2.zero?
|
||
|
raise Failure
|
||
|
end
|
||
|
elsif n1 == 4
|
||
|
n3 = tc.choice(0)
|
||
|
if n3 == 0
|
||
|
raise Failure
|
||
|
else
|
||
|
tc.mark_status(Status::INVALID)
|
||
|
end
|
||
|
elsif n1 == 2
|
||
|
raise Failure
|
||
|
else
|
||
|
tc.mark_status(Status::INVALID)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
private
|
||
|
|
||
|
def suppress_warnings
|
||
|
original_verbosity = $VERBOSE
|
||
|
$VERBOSE = nil
|
||
|
yield
|
||
|
$VERBOSE = original_verbosity
|
||
|
end
|
||
|
end
|