Skip to content

Commit

Permalink
Merge pull request #8 from wuest/short-circuit-invalid-case
Browse files Browse the repository at this point in the history
Improve signed int handling, speed up where blocks
  • Loading branch information
wuest authored Sep 16, 2024
2 parents d660d63 + 19a2e6c commit defa118
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 98 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Lint/BinaryOperatorWithIdenticalOperands:
Exclude:
- 'test/property_test.rb'

Lint/SuppressedException:
Exclude:
- 'lib/minitest/proptest/property.rb'

Metrics/BlockNesting:
Exclude:
- 'lib/minitest/proptest/property.rb'
Expand Down
36 changes: 8 additions & 28 deletions lib/minitest/proptest/gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,47 +287,27 @@ def for(*classes)

generator_for(Int8) do
r = sized(0xff)
(r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
end.with_shrink_function do |i|
j = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
integral_shrink.call(j)
end
(r & 0x80).zero? ? r : -((r ^ 0x7f) - 0x7f)
end.with_shrink_function(&integral_shrink)

generator_for(Int16) do
r = sized(0xffff)
(r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
end.with_shrink_function do |i|
j = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
integral_shrink.call(j)
end
(r & 0x8000).zero? ? r : -((r ^ 0x7fff) - 0x7fff)
end.with_shrink_function(&integral_shrink)

generator_for(Int32) do
r = sized(0xffffffff)
(r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
end.with_shrink_function do |i|
j = if (i & 0x80000000).zero?
i
else
-(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
end
integral_shrink.call(j)
end
(r & 0x80000000).zero? ? r : -((r ^ 0x7fffffff) - 0x7fffffff)
end.with_shrink_function(&integral_shrink)

generator_for(Int64) do
r = sized(0xffffffffffffffff)
if (r & 0x8000000000000000).zero?
r
else
-(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
-((r ^ 0x7fffffffffffffff) - 0x7fffffffffffffff)
end
end.with_shrink_function do |i|
j = if (i & 0x8000000000000000).zero?
i
else
-(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
end
integral_shrink.call(j)
end
end.with_shrink_function(&integral_shrink)

generator_for(UInt8) do
sized(0xff)
Expand Down
6 changes: 5 additions & 1 deletion lib/minitest/proptest/gen/value_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ def score_function(v)
def shrink_candidates
fs = @type_parameters.map { |x| x.method(:shrink_function) }
os = score
candidates = shrink_function(*fs, value)
# Ensure that the end of the shrink attempt will contain the original
# value. This is necessary to guarantee that the shrink process
# produces at least one failure for the purpose of capturing variable
# assignment.
candidates = shrink_function(*fs, value) + [value]
candidates
.map { |c| [force(c).score, c] }
.reject { |(s, _)| s > os }
Expand Down
109 changes: 56 additions & 53 deletions lib/minitest/proptest/property.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Property
require 'minitest/assertions'
include Minitest::Assertions

class InvalidProperty < StandardError; end

attr_reader :calls, :result, :status, :trivial

attr_accessor :assertions
Expand Down Expand Up @@ -47,7 +49,6 @@ def initialize(
@max_shrinks = max_shrinks
@status = Status.unknown
@trivial = false
@valid_test_case = true
@result = nil
@exception = nil
@calls = 0
Expand Down Expand Up @@ -126,40 +127,35 @@ def arbitrary(*classes)
end

def where(&b)
@valid_test_case &= b.call
raise InvalidProperty unless b.call
end

def iterate!
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
@valid_test_case = true
@generated = []
@generator = ::Minitest::Proptest::Gen.new(@random)
@calls += 1

success = begin
instance_eval(&@test_proc)
rescue Minitest::Assertion
if @valid_test_case
@result = @generated
@status = Status.interesting
end
rescue => e
raise e if @valid_test_case
end
if @valid_test_case && success
@status = Status.valid if @status.unknown?
@valid_test_cases += 1
elsif @valid_test_case
begin
if instance_eval(&@test_proc)
@status = Status.valid if @status.unknown?
@valid_test_cases += 1
else
@result = @generated
@status = Status.interesting
end
rescue Minitest::Assertion
@result = @generated
@status = Status.interesting
rescue InvalidProperty
rescue => e
@status = Status.invalid
@exception = e
end

@status = Status.exhausted if @calls >= @max_success * (@max_discard_ratio + 1)
@trivial = true if @generated.empty?
end
rescue => e
@status = Status.invalid
@exception = e
end

def rerun!
Expand All @@ -181,22 +177,22 @@ def rerun!
end

@generator = ::Minitest::Proptest::Gen.new(@random)
success = begin
instance_eval(&@test_proc)
rescue Minitest::Assertion
!@valid_test_case
rescue => e
if @valid_test_case
@status = Status.invalid
@exception = e
false
end
end
if success || !@valid_test_case
@generated = []
elsif @valid_test_case
begin
if instance_eval(&@test_proc)
@generated = []
else
@result = @generated
@status = Status.interesting
end
rescue Minitest::Assertion
@result = @generated
@status = Status.interesting
rescue InvalidProperty
@generated = []
rescue => e
@result = @generated
@status = Status.invalid
@exception = e
end

# Clean up after we're done
Expand All @@ -215,6 +211,12 @@ def shrink!
candidates = @generated.map(&:shrink_candidates)
old_arbitrary = @arbitrary

# Using a TracePoint to determine variable assignments at the time of
# the failure only occurs within shrink! - this is a deliberate decision
# which eliminates all time lost in iterate! to optimize for the success
# case. The tradeoff is that if all shrinking fails, one additional
# cycle (with the values which produced the original failure) will be
# required.
local_variables = {}
tracepoint = TracePoint.new(:b_return) do |trace|
if trace.path == @filename && trace.method_id.to_s == @methodname
Expand Down Expand Up @@ -249,27 +251,28 @@ def shrink!
@valid_test_case = true

@generator = ::Minitest::Proptest::Gen.new(@random)
if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
success = begin
tracepoint.enable
instance_eval(&@test_proc)
rescue Minitest::Assertion
false
rescue => e
next unless @valid_test_case

@status = Status.invalid
@excption = e
break
ensure
tracepoint.disable
end

if !success && @valid_test_case
# The first hit is guaranteed to be the best scoring since the
# shrink candidates are pre-sorted.
if to_test[run[:run]].map(&:first).reduce(&:+) <= best_score
begin
tracepoint.enable
unless instance_eval(&@test_proc)
# The first hit is guaranteed to be the best scoring since the
# shrink candidates are pre-sorted.
best_generated = @generated
break
end
rescue Minitest::Assertion
best_generated = @generated
break
rescue InvalidProperty
# Invalid test case generated- continue
rescue => e
next unless @valid_test_case

@status = Status.invalid
@excption = e
break
ensure
tracepoint.disable
end
end

Expand Down
32 changes: 16 additions & 16 deletions test/property_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_shrink_int8
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0x7f && x >= -0x80 &&
( n < 0 ? x <= 1 : x >= -1 )
Expand All @@ -27,7 +27,7 @@ def test_shrink_int16
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0x7fff && x >= -0x8000 &&
( n < 0 ? x <= 1 : x >= -1 )
Expand All @@ -43,7 +43,7 @@ def test_shrink_int32
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0x7fffffff && x >= -0x80000000 &&
( n < 0 ? x <= 1 : x >= -1 )
Expand All @@ -59,7 +59,7 @@ def test_shrink_int64
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0x7fffffffffffffff && x >= -0x8000000000000000 &&
( n < 0 ? x <= 1 : x >= -1 )
Expand All @@ -75,7 +75,7 @@ def test_shrink_int
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0x7fffffffffffffff && x >= -0x8000000000000000 &&
( n < 0 ? x <= 1 : x >= -1 )
Expand All @@ -91,7 +91,7 @@ def test_shrink_uint8
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0xff &&
x >= 0
Expand All @@ -107,7 +107,7 @@ def test_shrink_uint16
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0xffff &&
x >= 0
Expand All @@ -123,7 +123,7 @@ def test_shrink_uint32
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0xffffffff &&
x >= 0
Expand All @@ -139,7 +139,7 @@ def test_shrink_uint64
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.abs < n.abs &&
x.abs <= n.abs &&
score == x.abs &&
x <= 0xffffffffffffffff &&
x >= 0
Expand All @@ -158,7 +158,7 @@ def test_shrink_float32
if x.nan? || x.infinite?
score.zero?
else
score == x.abs.ceil && x.abs < n.abs
score == x.abs.ceil && x.abs <= n.abs
end
end
end
Expand All @@ -175,7 +175,7 @@ def test_shrink_float64
if x.nan? || x.infinite?
score.zero?
else
score == x.abs.ceil && x.abs < n.abs
score == x.abs.ceil && x.abs <= n.abs
end
end
end
Expand All @@ -192,7 +192,7 @@ def test_shrink_float
if x.nan? || x.infinite?
score.zero?
else
score == x.abs.ceil && x.abs < n.abs
score == x.abs.ceil && x.abs <= n.abs
end
end
end
Expand Down Expand Up @@ -251,7 +251,7 @@ def test_shrink_range
candidates = g.shrink_candidates

candidates.all? do |score, _|
score < g.score
score <= g.score
end
end
end
Expand All @@ -264,7 +264,7 @@ def test_shrink_rational
candidates = g.shrink_candidates

candidates.all? do |score, _|
score < g.score
score <= g.score
end
end
end
Expand All @@ -277,7 +277,7 @@ def test_shrink_char
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.ord < c.ord &&
x.ord <= c.ord &&
score == x.ord &&
x.ord <= 0xff && x.ord >= 0
end
Expand All @@ -292,7 +292,7 @@ def test_shrink_asciichar
candidates = g.shrink_candidates

candidates.all? do |score, x|
x.ord < c.ord &&
x.ord <= c.ord &&
score == x.ord &&
x.ord <= 0x7f && x.ord >= 0
end
Expand Down
Loading

0 comments on commit defa118

Please sign in to comment.