blob: d14ece5d39ad94db17abee5b2df34a4ae5fbce08 [file] [log] [blame]
import random
TESTRESULT_NOFAILURE = "NoFailure"
TESTRESULT_KEEPSUFFIX = "KeepSuffix"
TESTRESULT_KEEPPREFIX = "KeepPrefix"
TESTRESULTS = set([TESTRESULT_NOFAILURE, TESTRESULT_KEEPSUFFIX,
TESTRESULT_KEEPPREFIX])
class ListReducer(object):
"""Reduce lists of objects. Inspired by llvm bugpoint"""
def __init__(self, l):
self.target_list = l
# Maximal number of allowed splitting iterations,
# before the elements are randomly shuffled.
self.max_iters_without_progress = 3
# Maximal number of allowed single-element trim iterations. We add a
# threshhold here as single-element reductions may otherwise take a
# very long time to complete.
self.max_trim_iterations_without_back_jump = 3
self.shuffling_enabled = True
self.num_iters_without_progress = 0
self.mid_top = 0
self.max_iters = self.max_iters_without_progress
def _reset_progress(self):
self.max_iters = self.max_iters_without_progress
self.num_iters_without_progress = 0
def run_test(self, prefix, suffix):
raise RuntimeError("Abstract method")
def _should_continue(self, result):
if result == TESTRESULT_KEEPPREFIX:
# We are done, we have the base case and the base case fails.
if len(self.target_list) == 1:
return {'should_continue': False, 'result': True}
else:
# There is an error, and we can narrow it down further.
return {'should_continue': True, 'result': None}
if result == TESTRESULT_KEEPSUFFIX:
raise RuntimeError("ListReducer internal error: Selected empty "
"set!")
raise RuntimeError('Unknown test result: %s' % result)
def _test_shuffle_slow_converging_list(self):
if not self.shuffling_enabled or \
self.num_iters_without_progress <= self.max_iters_without_progress:
return
print("*** Testing shuffled set...")
shuffled_list = list(self.target_list)
random.shuffle(shuffled_list)
# TODO: Is this correct? I guess we are always doing something.
self.num_iters_without_progress = 0
# Check that the random shuffle does not lose the bug.
(result, _, _) = self.run_test(shuffled_list, [])
if result != TESTRESULT_KEEPPREFIX:
# If we fail here, disable any further shuffling...
self.shuffling_enabled = False
print("*** Shuffling hides the bug...")
return
self.mid_top = len(shuffled_list)
self.max_iters = self.max_iters + 2
print("*** Shuffling does not hide the bug...")
self.target_list = shuffled_list
def _test_prefix_suffix(self, mid, prefix, suffix):
(result, prefix, suffix) = self.run_test(prefix, suffix)
if result == TESTRESULT_KEEPSUFFIX:
# The property still holds. We can just drop the prefix
# elements, and shorten the list to the "kept" elements.
self.target_list = suffix
self.mid_top = len(self.target_list)
# Reset the progress threshold
self._reset_progress()
return False
if result == TESTRESULT_KEEPPREFIX:
# The predicate still holds, shorten the list to the prefix
# elements.
self.target_list = prefix
self.mid_top = len(self.target_list)
self._reset_progress()
return False
assert(result == TESTRESULT_NOFAILURE)
# The property does not hold. Some of the elements we removed must
# be necessary to maintain the property.
self.mid_top = mid
self.num_iters_without_progress = \
self.num_iters_without_progress + 1
return False
def _trim_target_list(self):
self.mid_top = len(self.target_list)
self.max_iters = self.max_iters_without_progress
# Binary split reduction loop
while self.mid_top > 1:
# If the loop doesn't make satisfying progress, try shuffling.
# The purpose of shuffling is to avoid the heavy tails of the
# distribution (improving the speed of convergence).
self._test_shuffle_slow_converging_list()
# Split the list into a prefix, suffix list and then run test on
# those.
mid = self.mid_top/2
if not self._test_prefix_suffix(mid, self.target_list[:mid],
self.target_list[mid:]):
# If we returned false, then we did some sort of work and there
# was not an error, so continue.
continue
# Otherwise, the test routine signaled an error, so return True to
# signal error.
return True
# If we reach this point, return False, we have no further work we can
# do.
return False
def _trim_try_backjump_and_trim_suffix(self):
backjump_probability = 10
if len(self.target_list) <= 2:
return False
changed = True
trim_iters = 0
# Trimming loop
while changed:
changed = False
# If the binary split reduction loop made an unfortunate sequence
# of splits, the trimming loop might be left off with a huge
# number of remaining elements (large search space). Backjumping
# out of that search space and attempting a different split can
# significantly improve the convergence speed.
if random.randint(0, 100) < backjump_probability:
return True
# Check interior elements, using an offset to make sure we do not
# skip elements when we trim.
offset = 0
for i in range(1, len(self.target_list)-1):
real_i = i + offset
test_list = self.target_list[real_i:]
(result, prefix, suffix) = self.run_test([], test_list)
if result == TESTRESULT_KEEPSUFFIX:
# We can trim the list!
self.target_list = test_list
offset = offset - 1
changed = True
if trim_iters >= self.max_trim_iterations_without_back_jump:
return False
trim_iters = trim_iters + 1
def reduce_list(self):
random.seed(0x6e5ea738) # Seed the random number generator
(result, self.target_list, kept) = self.run_test(self.target_list, [])
assert(result in TESTRESULTS)
(should_continue, result) = self._should_continue(result)
if not should_continue:
return result
# Now try to trim the list.
should_backjump = True
while should_backjump:
# If self._trim_target_list returns True, then we failed to
# reduce. Bail!
if self._trim_target_list():
return False
# Finally decide if we should back_jump
should_backjump = self._trim_try_backjump_and_trim_suffix()
# There are some failure and we've narrowed them down
return True