gecko-dev/testing/mochitest/bisection.py

289 строки
12 KiB
Python

from __future__ import absolute_import, division, print_function
import math
import mozinfo
class Bisect(object):
"Class for creating, bisecting and summarizing for --bisect-chunk option."
def __init__(self, harness):
super(Bisect, self).__init__()
self.summary = []
self.contents = {}
self.repeat = 10
self.failcount = 0
self.max_failures = 3
def setup(self, tests):
"""This method is used to initialize various variables that are required
for test bisection"""
status = 0
self.contents.clear()
# We need totalTests key in contents for sanity check
self.contents["totalTests"] = tests
self.contents["tests"] = tests
self.contents["loop"] = 0
return status
def reset(self, expectedError, result):
"""This method is used to initialize self.expectedError and self.result
for each loop in runtests."""
self.expectedError = expectedError
self.result = result
def get_tests_for_bisection(self, options, tests):
"""Make a list of tests for bisection from a given list of tests"""
bisectlist = []
for test in tests:
bisectlist.append(test)
if test.endswith(options.bisectChunk):
break
return bisectlist
def pre_test(self, options, tests, status):
"""This method is used to call other methods for setting up variables and
getting the list of tests for bisection."""
if options.bisectChunk == "default":
return tests
# The second condition in 'if' is required to verify that the failing
# test is the last one.
elif "loop" not in self.contents or not self.contents["tests"][-1].endswith(
options.bisectChunk
):
tests = self.get_tests_for_bisection(options, tests)
status = self.setup(tests)
return self.next_chunk_binary(options, status)
def post_test(self, options, expectedError, result):
"""This method is used to call other methods to summarize results and check whether a
sanity check is done or not."""
self.reset(expectedError, result)
status = self.summarize_chunk(options)
# Check whether sanity check has to be done. Also it is necessary to check whether
# options.bisectChunk is present in self.expectedError as we do not want to run
# if it is "default".
if status == -1 and options.bisectChunk in self.expectedError:
# In case we have a debug build, we don't want to run a sanity
# check, will take too much time.
if mozinfo.info["debug"]:
return status
testBleedThrough = self.contents["testsToRun"][0]
tests = self.contents["totalTests"]
tests.remove(testBleedThrough)
# To make sure that the failing test is dependent on some other
# test.
if options.bisectChunk in testBleedThrough:
return status
status = self.setup(tests)
self.summary.append("Sanity Check:")
return status
def next_chunk_reverse(self, options, status):
"This method is used to bisect the tests in a reverse search fashion."
# Base Cases.
if self.contents["loop"] <= 1:
self.contents["testsToRun"] = self.contents["tests"]
if self.contents["loop"] == 1:
self.contents["testsToRun"] = [self.contents["tests"][-1]]
self.contents["loop"] += 1
return self.contents["testsToRun"]
if "result" in self.contents:
if self.contents["result"] == "PASS":
chunkSize = self.contents["end"] - self.contents["start"]
self.contents["end"] = self.contents["start"] - 1
self.contents["start"] = self.contents["end"] - chunkSize
# self.contents['result'] will be expected error only if it fails.
elif self.contents["result"] == "FAIL":
self.contents["tests"] = self.contents["testsToRun"]
status = 1 # for initializing
# initialize
if status:
totalTests = len(self.contents["tests"])
chunkSize = int(math.ceil(totalTests / 10.0))
self.contents["start"] = totalTests - chunkSize - 1
self.contents["end"] = totalTests - 2
start = self.contents["start"]
end = self.contents["end"] + 1
self.contents["testsToRun"] = self.contents["tests"][start:end]
self.contents["testsToRun"].append(self.contents["tests"][-1])
self.contents["loop"] += 1
return self.contents["testsToRun"]
def next_chunk_binary(self, options, status):
"This method is used to bisect the tests in a binary search fashion."
# Base cases.
if self.contents["loop"] <= 1:
self.contents["testsToRun"] = self.contents["tests"]
if self.contents["loop"] == 1:
self.contents["testsToRun"] = [self.contents["tests"][-1]]
self.contents["loop"] += 1
return self.contents["testsToRun"]
# Initialize the contents dict.
if status:
totalTests = len(self.contents["tests"])
self.contents["start"] = 0
self.contents["end"] = totalTests - 2
# pylint --py3k W1619
mid = (self.contents["start"] + self.contents["end"]) / 2
if "result" in self.contents:
if self.contents["result"] == "PASS":
self.contents["end"] = mid
elif self.contents["result"] == "FAIL":
self.contents["start"] = mid + 1
mid = (self.contents["start"] + self.contents["end"]) / 2
start = mid + 1
end = self.contents["end"] + 1
self.contents["testsToRun"] = self.contents["tests"][start:end]
if not self.contents["testsToRun"]:
self.contents["testsToRun"].append(self.contents["tests"][mid])
self.contents["testsToRun"].append(self.contents["tests"][-1])
self.contents["loop"] += 1
return self.contents["testsToRun"]
def summarize_chunk(self, options):
"This method is used summarize the results after the list of tests is run."
if options.bisectChunk == "default":
# if no expectedError that means all the tests have successfully
# passed.
if len(self.expectedError) == 0:
return -1
options.bisectChunk = self.expectedError.keys()[0]
self.summary.append("\tFound Error in test: %s" % options.bisectChunk)
return 0
# If options.bisectChunk is not in self.result then we need to move to
# the next run.
if options.bisectChunk not in self.result:
return -1
self.summary.append("\tPass %d:" % self.contents["loop"])
if len(self.contents["testsToRun"]) > 1:
self.summary.append(
"\t\t%d test files(start,end,failing). [%s, %s, %s]"
% (
len(self.contents["testsToRun"]),
self.contents["testsToRun"][0],
self.contents["testsToRun"][-2],
self.contents["testsToRun"][-1],
)
)
else:
self.summary.append("\t\t1 test file [%s]" % self.contents["testsToRun"][0])
return self.check_for_intermittent(options)
if self.result[options.bisectChunk] == "PASS":
self.summary.append("\t\tno failures found.")
if self.contents["loop"] == 1:
status = -1
else:
self.contents["result"] = "PASS"
status = 0
elif self.result[options.bisectChunk] == "FAIL":
if "expectedError" not in self.contents:
self.summary.append("\t\t%s failed." % self.contents["testsToRun"][-1])
self.contents["expectedError"] = self.expectedError[options.bisectChunk]
status = 0
elif (
self.expectedError[options.bisectChunk]
== self.contents["expectedError"]
):
self.summary.append(
"\t\t%s failed with expected error."
% self.contents["testsToRun"][-1]
)
self.contents["result"] = "FAIL"
status = 0
# This code checks for test-bleedthrough. Should work for any
# algorithm.
numberOfTests = len(self.contents["testsToRun"])
if numberOfTests < 3:
# This means that only 2 tests are run. Since the last test
# is the failing test itself therefore the bleedthrough
# test is the first test
self.summary.append(
"TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the "
"root cause for many of the above failures"
% self.contents["testsToRun"][0]
)
status = -1
else:
self.summary.append(
"\t\t%s failed with different error."
% self.contents["testsToRun"][-1]
)
status = -1
return status
def check_for_intermittent(self, options):
"This method is used to check whether a test is an intermittent."
if self.result[options.bisectChunk] == "PASS":
self.summary.append(
"\t\tThe test %s passed." % self.contents["testsToRun"][0]
)
if self.repeat > 0:
# loop is set to 1 to again run the single test.
self.contents["loop"] = 1
self.repeat -= 1
return 0
else:
if self.failcount > 0:
# -1 is being returned as the test is intermittent, so no need to bisect
# further.
return -1
# If the test does not fail even once, then proceed to next chunk for bisection.
# loop is set to 2 to proceed on bisection.
self.contents["loop"] = 2
return 1
elif self.result[options.bisectChunk] == "FAIL":
self.summary.append(
"\t\tThe test %s failed." % self.contents["testsToRun"][0]
)
self.failcount += 1
self.contents["loop"] = 1
self.repeat -= 1
# self.max_failures is the maximum number of times a test is allowed
# to fail to be called an intermittent. If a test fails more than
# limit set, it is a perma-fail.
if self.failcount < self.max_failures:
if self.repeat == 0:
# -1 is being returned as the test is intermittent, so no need to bisect
# further.
return -1
return 0
else:
self.summary.append(
"TEST-UNEXPECTED-FAIL | %s | Bleedthrough detected, this test is the "
"root cause for many of the above failures"
% self.contents["testsToRun"][0]
)
return -1
def print_summary(self):
"This method is used to print the recorded summary."
print("Bisection summary:")
for line in self.summary:
print(line)