1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
require_relative "error"
require_relative "status"
module Minitest::Thesis
# Represents a single generated test case, which consists of an underlying
# set of choices that produce possibilities.
class TestCase
# Returns a test case that makes this series of choices.
def self.for_choices(choices, print_results: false)
self.new(prefix: choices, random: nil, max_size: choices.length, print_results:)
end
attr_accessor :status
attr_reader :choices, :targeting_score
def initialize(prefix:, random:, max_size: Float::INFINITY, print_results: false)
@prefix, @random, @max_size, @print_results = prefix, random, max_size, print_results
@choices = []
@status = nil
@depth = 0
@targeting_score = nil
end
# Returns a number in the range [0, n]
def choice(n)
result = make_choice(n) { @random.rand(n+1) }
puts "choice(#{n}): #{result.inspect}" if should_print?
result
end
# Return True with probability `p`.
def weighted(p)
result = if p <= 0 then forced_choice(0)
elsif p >= 1 then forced_choice(1)
else make_choice(1) { (@random.rand <= p) ? 1 : 0 }
end
puts "weighted(#{p}): #{result.inspect}" if should_print?
result
end
# Inserts a fake choice into the choice sequence, as if some call to
# choice() had returned `n`. You almost never need this, but sometimes it
# can be a useful hint to the shrinker.
def forced_choice(n)
raise RangeError.new("Invalid choice #{n}") if n.bit_length > 64 || n.negative?
raise Frozen unless @status.nil?
mark_status(Status::OVERRUN) if @choices.length >= @max_size
choices << n
n
end
# Mark this test case as invalid.
def reject = mark_status(Status::INVALID)
# If this precondition is not met, abort the test and mark this test case as invalid.
def assume(precondition)
return if precondition
reject
end
# Set a score to maximize. Multiple calls to this function will override previous ones.
#
# The name and idea come from Löscher, Andreas, and Konstantinos Sagonas.
# "Targeted property-based testing." ISSTA. 2017, but the implementation
# is based on that found in Hypothesis, which is not that similar to
# anything described in the paper.
def target(score) = @targeting_score = score
# Return a possible value from `possibility`.
def any(possibility)
begin
@depth += 1
result = possibility.produce.(self)
ensure
@depth -= 1
end
puts "any(#{possibility}): #{result.inspect}" if should_print?
result
end
# Set the status and raise StopTest.
def mark_status(status)
raise Frozen unless self.status.nil?
@status = status
raise StopTest
end
private
def should_print? = @print_results && @depth.zero?
# Make a choice in [0, n], by calling rnd_method if randomness is needed.
def make_choice(n, &rnd_method)
raise RangeError.new("Invalid choice #{n}") if n.bit_length > 64 || n.negative?
raise Frozen unless @status.nil?
mark_status(Status::OVERRUN) if @choices.length >= @max_size
result = if @choices.length < @prefix.length
@prefix[@choices.length]
else
rnd_method.()
end
@choices << result
mark_status(Status::INVALID) if result > n
result
end
end
end