testing/mochitest/bisection.py
author Nicholas Nethercote <nnethercote@mozilla.com>
Wed, 09 Aug 2017 20:41:38 +1000
changeset 427564 14de940bb317a497f50f6e80a1b5439fe9d049a6
parent 360963 cce196d1d7b48958abcb1b23bc3afce6f223f074
child 436713 dbe60b600dc0b9aabce3d197b438e4a6816f117e
permissions -rw-r--r--
Bug 1386103 (part 1, attempt 3) - Specify nsAuto[C]String storage size via template parameter. r=dbaron.

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

        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