Esempio n. 1
0
def main():
    """Prevent running two instances of autobisectjs concurrently - we don't want to confuse hg."""
    options = parseOpts()

    repo_dir = None
    if options.build_options:
        repo_dir = options.build_options.repo_dir

    with LockDir(
            sm_compile_helpers.get_lock_dir_path(
                Path.home(), options.nameOfTreeherderBranch, tbox_id="Tbox"
            ) if options.useTreeherderBinaries else sm_compile_helpers.
            get_lock_dir_path(Path.home(), repo_dir)):
        if options.useTreeherderBinaries:
            print_("TBD: We need to switch to the autobisect repository.",
                   flush=True)
            sys.exit(0)
        else:  # Bisect using local builds
            findBlamedCset(options, repo_dir,
                           compile_shell.makeTestRev(options))

        # Last thing we do while we have a lock.
        # Note that this only clears old *local* cached directories, not remote ones.
        rm_old_local_cached_dirs(
            sm_compile_helpers.ensure_cache_dir(Path.home()))
Esempio n. 2
0
def readIncompleteBuildTxtFile(txtFile, idNum):  # pylint: disable=missing-raises-doc,missing-param-doc,missing-type-doc
    """Read the INCOMPLETE_NOTE text file indicating that this particular build is incomplete."""
    with open(txtFile, 'r') as f:
        contentsF = f.read()
        if 'is incomplete.' not in contentsF:
            print_("Contents of %s is: %r" % (txtFile, contentsF), flush=True)
            raise Exception('Invalid ' + INCOMPLETE_NOTE + ' file contents.')
        else:
            print_("Examined build with numeric ID %s to be incomplete. Trying another build..." % idNum, flush=True)
Esempio n. 3
0
def getBuildOrNeighbour(isJsShell, preferredIndex, urls, buildType):  # pylint: disable=inconsistent-return-statements
    # pylint: disable=invalid-name,missing-param-doc,missing-return-doc,missing-return-type-doc,missing-type-doc
    # pylint: disable=too-many-branches,too-complex
    """Download a build. If the build is incomplete, find a working neighbour, then return results."""
    offset = None
    skippedChangesetNum = 0

    while True:
        if offset is None:
            offset = 0
        elif offset > 16:
            print_("Failed to find a working build after ~30 tries.", flush=True)
            return None, None, None, None
        elif offset > 0:
            # Stop once we are testing beyond the start & end entries of the list
            if (preferredIndex + offset >= len(urls)) and (preferredIndex - offset < 0):
                print_("Stop looping because everything within the range was tested.", flush=True)
                return None, None, None, None
            offset = -offset  # pylint: disable=invalid-unary-operand-type
        else:
            offset = -offset + 1  # Alternate between positive and negative offsets

        newIndex = preferredIndex + offset

        if newIndex < 0:
            continue
        elif newIndex >= len(urls):
            continue

        isWorking, idNum, tboxCacheFolder = getOneBuild(isJsShell, urls[newIndex], buildType)

        if isWorking:
            try:
                assertSaneJsBinary(tboxCacheFolder)
            except (KeyboardInterrupt, Exception) as e:  # pylint: disable=broad-except
                if 'Shell startup error' in repr(e):
                    writeIncompleteBuildTxtFile(urls[newIndex], tboxCacheFolder,
                                                sps.normExpUserPath(os.path.join(tboxCacheFolder,
                                                                                 INCOMPLETE_NOTE)),
                                                idNum)
                    continue
            return newIndex, idNum, tboxCacheFolder, skippedChangesetNum
        else:
            skippedChangesetNum += 1
            if len(urls) == 4:
                #  If we have [good, untested, incomplete, bad], after testing the middle changeset that
                #  has the "incomplete" result, the offset will push us the boundary changeset with the
                #  "bad" result. In this case, switch to the beginning changeset so the offset of 1 will
                #  push us into the untested changeset, avoiding a loop involving
                #  "incomplete->bad->incomplete->bad->..."
                #  See https://github.com/MozillaSecurity/funfuzz/issues/18
                preferredIndex = 0
Esempio n. 4
0
def writeIncompleteBuildTxtFile(url, cacheFolder, txtFile, num):  # pylint: disable=invalid-name,missing-param-doc
    # pylint: disable=missing-return-doc,missing-return-type-doc,missing-type-doc
    """Write a text file indicating that this particular build is incomplete."""
    if os.path.isdir(sps.normExpUserPath(os.path.join(cacheFolder, 'build', 'dist'))) or \
            os.path.isdir(sps.normExpUserPath(os.path.join(cacheFolder, 'build', 'download'))):
        sps.rmTreeIncludingReadOnly(sps.normExpUserPath(os.path.join(cacheFolder, 'build')))
    assert not os.path.isfile(txtFile), 'incompleteBuild.txt should not be present.'
    with open(txtFile, 'w') as f:
        f.write('This build with numeric ID ' + num + ' is incomplete.')
    assert num == getIdFromTboxUrl(url), 'The numeric ID ' + num + \
        ' has to be the one we downloaded from ' + url
    print_("Wrote a text file that indicates numeric ID %s has an incomplete build." % num, flush=True)
    return False  # False indicates that this text file has not yet been looked at.
Esempio n. 5
0
def assertSaneJsBinary(cacheF):  # pylint: disable=missing-param-doc,missing-raises-doc,missing-return-doc
    # pylint: disable=missing-return-type-doc,missing-type-doc
    """If the cache folder is present, check that the js binary is working properly."""
    if os.path.isdir(cacheF):
        fList = os.listdir(cacheF)
        if 'build' in fList:
            if INCOMPLETE_NOTE in fList:
                print_("%s has subdirectories: %s" % (cacheF, fList), flush=True)
                raise Exception("Downloaded binaries and incompleteBuild.txt should not both be "
                                "present together in this directory.")
            assert os.path.isdir(sps.normExpUserPath(os.path.join(cacheF, 'build', 'download')))
            assert os.path.isdir(sps.normExpUserPath(os.path.join(cacheF, 'build', 'dist')))
            assert os.path.isfile(sps.normExpUserPath(os.path.join(cacheF, 'build', 'dist',
                                                                   'js' + ('.exe' if sps.isWin else ''))))
            try:
                shellPath = getTboxJsBinPath(cacheF)
                # Ensure we don't fail because the shell lacks u+x
                if not os.access(shellPath, os.X_OK):
                    os.chmod(shellPath, stat.S_IXUSR)

                # tbpl binaries are always:
                # * run without Valgrind (they are not compiled with --enable-valgrind)
                retCode = inspect_shell.testBinary(shellPath, ['-e', '42'], False)[1]
                # Exit code -1073741515 on Windows shows up when a required DLL is not present.
                # This was testable at the time of writing, see bug 953314.
                isDllNotPresentWinStartupError = (sps.isWin and retCode == -1073741515)
                # We should have another condition here for non-Windows platforms but we do not yet
                # have a situation where we can test broken treeherder js shells on those platforms.
                if isDllNotPresentWinStartupError:
                    raise Exception('Shell startup error - a .dll file is probably not present.')
                elif retCode != 0:
                    raise Exception('Non-zero return code: ' + str(retCode))
                return True  # Binary is working correctly
            except (OSError, IOError):  # pylint: disable=overlapping-except
                raise Exception("Cache folder %s is corrupt, please delete it and try again." % cacheF)
        elif INCOMPLETE_NOTE in fList:
            return True
        else:
            raise Exception('Neither build/ nor INCOMPLETE_NOTE were found in the cache folder.')
    else:
        raise Exception('Cache folder ' + cacheF + ' is not found.')
Esempio n. 6
0
def main():
    """Prevent running two instances of autoBisectJs concurrently - we don't want to confuse hg."""
    options = parseOpts()

    if options.build_options:
        repoDir = options.build_options.repoDir

    with LockDir(
            compile_shell.getLockDirPath(options.nameOfTreeherderBranch,
                                         tboxIdentifier='Tbox') if options.
            useTreeherderBinaries else compile_shell.getLockDirPath(repoDir)):
        if options.useTreeherderBinaries:
            print_("TBD: We need to switch to the autobisect repository.",
                   flush=True)
            sys.exit(0)
        else:  # Bisect using local builds
            findBlamedCset(options, repoDir,
                           compile_shell.makeTestRev(options))

        # Last thing we do while we have a lock.
        # Note that this only clears old *local* cached directories, not remote ones.
        rmOldLocalCachedDirs(compile_shell.ensureCacheDir())
Esempio n. 7
0
def ensureCacheDirHasCorrectIdNum(cacheFolder):  # pylint: disable=missing-param-doc,missing-raises-doc,missing-type-doc
    """Ensure that the cache folder is named with the correct numeric ID."""
    srcUrlPath = sps.normExpUserPath(os.path.join(cacheFolder, 'build', 'download', 'source-url.txt'))
    if os.path.isfile(srcUrlPath):
        with open(srcUrlPath, 'r') as f:
            fContents = f.read().splitlines()

        idNumFolderName = cacheFolder.split('-')[-1]
        idNumSourceUrl = fContents[0].split('/')[-2]

        if idNumFolderName != idNumSourceUrl:
            print_(flush=True)
            print_("WARNING: Numeric ID in folder name (current value: %s) is not equal to , flush=True"
                   "the numeric ID from source URL (current value: %s)" % (idNumFolderName, idNumSourceUrl))
            print_(flush=True)
            raise Exception('Folder name numeric ID not equal to source URL numeric ID.')
Esempio n. 8
0
def testBuildOrNeighbour(options, preferredIndex, urls, buildType, testedIDs):  # pylint: disable=invalid-name
    # pylint: disable=missing-param-doc,missing-return-doc,missing-return-type-doc,missing-type-doc
    """Test the build. If the build is incomplete, find a working neighbour, then return results."""
    finalIndex, idNum, tboxCacheFolder, skippedNum = getBuildOrNeighbour(
        (not options.browserOptions), preferredIndex, urls, buildType
    )

    if idNum is None:
        result, reason = None, None
    elif idNum in list(testedIDs):
        print_("Retrieving previous test result: ", end=" ", flush=True)
        result, reason = testedIDs[idNum][2:4]
    else:
        # The build has not been tested before, so test it.
        testedIDs[idNum] = getTimestampAndHashFromTboxFiles(tboxCacheFolder)
        print_("Found binary in: %s" % tboxCacheFolder, flush=True)
        print_("Testing binary...", end=" ", flush=True)
        result, reason = isTboxBinInteresting(options, tboxCacheFolder, testedIDs[idNum][1])
        print_("Result: %s - %s" % (result, reason), flush=True)
        # Adds the result and reason to testedIDs
        testedIDs[idNum] = list(testedIDs[idNum]) + [result, reason]

    return idNum, result, reason, finalIndex, urls, testedIDs, skippedNum
Esempio n. 9
0
def parseOpts():  # pylint: disable=invalid-name,missing-docstring,missing-return-doc,missing-return-type-doc
    # pylint: disable=too-many-branches,too-complex,too-many-statements
    usage = 'Usage: %prog [options]'
    parser = OptionParser(usage)
    # http://docs.python.org/library/optparse.html#optparse.OptionParser.disable_interspersed_args
    parser.disable_interspersed_args()

    parser.set_defaults(
        resetRepoFirst=False,
        startRepo=None,
        endRepo='default',
        testInitialRevs=True,
        output='',
        watchExitCode=None,
        useInterestingnessTests=False,
        parameters=
        '-e 42',  # http://en.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy
        compilationFailedLabel='skip',
        build_options="",
        useTreeherderBinaries=False,
        nameOfTreeherderBranch='mozilla-inbound',
    )

    # Specify how the shell will be built.
    parser.add_option(
        '-b',
        '--build',
        dest='build_options',
        help='Specify js shell build options, e.g. -b "--enable-debug --32"'
        "(python -m funfuzz.js.build_options --help)")

    parser.add_option(
        '--resetToTipFirst',
        dest='resetRepoFirst',
        action='store_true',
        help="First reset to default tip overwriting all local changes. "
        "Equivalent to first executing `hg update -C default`. Defaults to '%default'."
    )

    # Specify the revisions between which to bisect.
    parser.add_option(
        '-s',
        '--startRev',
        dest='startRepo',
        help=
        "Earliest changeset/build numeric ID to consider (usually a 'good' cset). "
        "Defaults to the earliest revision known to work at all/available.")
    parser.add_option(
        '-e',
        '--endRev',
        dest='endRepo',
        help=
        "Latest changeset/build numeric ID to consider (usually a 'bad' cset). "
        "Defaults to the head of the main branch, 'default', or latest available build."
    )
    parser.add_option(
        '-k',
        '--skipInitialRevs',
        dest='testInitialRevs',
        action='store_false',
        help=
        "Skip testing the -s and -e revisions and automatically trust them as -g and -b."
    )

    # Specify the type of failure to look for.
    # (Optional -- by default, internalTestAndLabel will look for exit codes that indicate a crash or assert.)
    parser.add_option(
        '-o',
        '--output',
        dest='output',
        help="Stdout or stderr output to be observed. Defaults to '%default'. "
        "For assertions, set to 'ssertion fail'")
    parser.add_option(
        '-w',
        '--watchExitCode',
        dest='watchExitCode',
        type='int',
        help=
        "Look out for a specific exit code. Only this exit code will be considered 'bad'."
    )
    parser.add_option(
        '-i',
        '--useInterestingnessTests',
        dest='useInterestingnessTests',
        action="store_true",
        help="Interpret the final arguments as an interestingness test.")

    # Specify parameters for the js shell.
    parser.add_option(
        '-p',
        '--parameters',
        dest='parameters',
        help=
        'Specify parameters for the js shell, e.g. -p "-a --ion-eager testcase.js".'
    )

    # Specify how to treat revisions that fail to compile.
    # (You might want to add these to kbew.knownBrokenRanges in known_broken_earliest_working.)
    parser.add_option(
        '-l',
        '--compilationFailedLabel',
        dest='compilationFailedLabel',
        help="Specify how to treat revisions that fail to compile. "
        "(bad, good, or skip) Defaults to '%default'")

    parser.add_option(
        '-T',
        '--useTreeherderBinaries',
        dest='useTreeherderBinaries',
        action="store_true",
        help="Use treeherder binaries for quick bisection, assuming a fast "
        "internet connection. Defaults to '%default'")
    parser.add_option(
        '-N',
        '--nameOfTreeherderBranch',
        dest='nameOfTreeherderBranch',
        help='Name of the branch to download. Defaults to "%default"')

    (options, args) = parser.parse_args()
    if options.useTreeherderBinaries:
        print_(
            "TBD: Bisection using downloaded shells is temporarily not supported.",
            flush=True)
        sys.exit(0)

    options.build_options = build_options.parseShellOptions(
        options.build_options)
    options.skipRevs = ' + '.join(
        kbew.known_broken_ranges(options.build_options))

    options.paramList = [
        sps.normExpUserPath(x) for x in options.parameters.split(' ') if x
    ]
    # First check that the testcase is present.
    if '-e 42' not in options.parameters and not os.path.isfile(
            options.paramList[-1]):
        print_(flush=True)
        print_("List of parameters to be passed to the shell is: %s" %
               " ".join(options.paramList),
               flush=True)
        print_(flush=True)
        raise Exception('Testcase at ' + options.paramList[-1] +
                        ' is not present.')

    assert options.compilationFailedLabel in ('bad', 'good', 'skip')

    extraFlags = []  # pylint: disable=invalid-name

    if options.useInterestingnessTests:
        if len(args) < 1:
            print_("args are: %s" % args, flush=True)
            parser.error('Not enough arguments.')
        for a in args:  # pylint: disable=invalid-name
            if a.startswith("--flags="):
                extraFlags = a[8:].split(' ')  # pylint: disable=invalid-name
        options.testAndLabel = externalTestAndLabel(options, args)
    elif len(args) >= 1:
        parser.error('Too many arguments.')
    else:
        options.testAndLabel = internalTestAndLabel(options)

    earliestKnownQuery = kbew.earliest_known_working_rev(  # pylint: disable=invalid-name
        options.build_options, options.paramList + extraFlags,
        options.skipRevs)

    earliestKnown = ''  # pylint: disable=invalid-name

    if not options.useTreeherderBinaries:
        # pylint: disable=invalid-name
        earliestKnown = hg_helpers.getRepoHashAndId(
            options.build_options.repoDir, repoRev=earliestKnownQuery)[0]

    if options.startRepo is None:
        if options.useTreeherderBinaries:
            options.startRepo = 'default'
        else:
            options.startRepo = earliestKnown
    # elif not (options.useTreeherderBinaries or hg_helpers.isAncestor(options.build_options.repoDir,
    #                                                              earliestKnown, options.startRepo)):
    #     raise Exception('startRepo is not a descendant of kbew.earliestKnownWorkingRev for this configuration')
    #
    # if not options.useTreeherderBinaries and not hg_helpers.isAncestor(options.build_options.repoDir,
    #                                                                earliestKnown, options.endRepo):
    #     raise Exception('endRepo is not a descendant of kbew.earliestKnownWorkingRev for this configuration')

    if options.parameters == '-e 42':
        print_(
            "Note: since no parameters were specified, "
            "we're just ensuring the shell does not crash on startup/shutdown.",
            flush=True)

    if options.nameOfTreeherderBranch != 'mozilla-inbound' and not options.useTreeherderBinaries:
        raise Exception(
            'Setting the name of branches only works for treeherder shell bisection.'
        )

    return options
Esempio n. 10
0
def bisectLabel(hgPrefix, options, hgLabel, currRev, startRepo, endRepo):  # pylint: disable=invalid-name
    # pylint: disable=missing-param-doc,missing-raises-doc,missing-return-doc,missing-return-type-doc,missing-type-doc
    # pylint: disable=too-many-arguments
    """Tell hg what we learned about the revision."""
    assert hgLabel in ("good", "bad", "skip")
    outputResult = subprocess.run(
        hgPrefix + ["bisect", "-U", "--" + hgLabel, currRev],
        check=True,
        cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
        stdout=subprocess.PIPE,
        timeout=999).stdout.decode("utf-8", errors="replace")
    outputLines = outputResult.split("\n")

    repo_dir = None
    if options.build_options:
        repo_dir = options.build_options.repo_dir

    if re.compile(
            "Due to skipped revisions, the first (good|bad) revision could be any of:"
    ).match(outputLines[0]):
        print_(flush=True)
        print_(sanitizeCsetMsg(outputResult, repo_dir), flush=True)
        print_(flush=True)
        return None, None, None, startRepo, endRepo

    r = re.compile("The first (good|bad) revision is:")
    m = r.match(outputLines[0])
    if m:
        print_(flush=True)
        print_(flush=True)
        print_(
            "autobisectjs shows this is probably related to the following changeset:",
            flush=True)
        print_(flush=True)
        print_(sanitizeCsetMsg(outputResult, repo_dir), flush=True)
        print_(flush=True)
        blamedGoodOrBad = m.group(1)
        blamedRev = hg_helpers.get_cset_hash_from_bisect_msg(outputLines[1])
        return blamedGoodOrBad, blamedRev, None, startRepo, endRepo

    if options.testInitialRevs:
        return None, None, None, startRepo, endRepo

    # e.g. "Testing changeset 52121:573c5fa45cc4 (440 changesets remaining, ~8 tests)"
    sps.vdump(outputLines[0])

    currRev = hg_helpers.get_cset_hash_from_bisect_msg(outputLines[0])
    if currRev is None:
        print_("Resetting to default revision...", flush=True)
        subprocess.run(hgPrefix + ["update", "-C", "default"], check=True)
        hg_helpers.destroyPyc(repo_dir)
        raise Exception("hg did not suggest a changeset to test!")

    # Update the startRepo/endRepo values.
    start = startRepo
    end = endRepo
    if hgLabel == "bad":
        end = currRev
    elif hgLabel == "good":
        start = currRev
    elif hgLabel == "skip":
        pass

    return None, None, currRev, start, end
Esempio n. 11
0
def checkBlameParents(repo_dir, blamedRev, blamedGoodOrBad, labels, testRev,
                      startRepo, endRepo):
    """If bisect blamed a merge, try to figure out why."""
    repo_dir = str(repo_dir)
    bisectLied = False
    missedCommonAncestor = False

    hg_parent_output = subprocess.run(
        ["hg", "-R", str(repo_dir)] +
        ["parent", "--template={node|short},", "-r", blamedRev],
        check=True,
        cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
        stdout=subprocess.PIPE,
        timeout=99).stdout.decode("utf-8", errors="replace")
    parents = hg_parent_output.split(",")[:-1]

    if len(parents) == 1:
        return

    for p in parents:
        # Ensure we actually tested the parent.
        if labels.get(p) is None:
            print_(flush=True)
            print_(
                "Oops! We didn't test rev %s, a parent of the blamed revision! Let's do that now."
                % p,
                flush=True)
            if not hg_helpers.isAncestor(repo_dir, startRepo, p) and \
                    not hg_helpers.isAncestor(repo_dir, endRepo, p):
                print_(
                    "We did not test rev %s because it is not a descendant of either %s or %s."
                    % (p, startRepo, endRepo),
                    flush=True)
                # Note this in case we later decide the bisect result is wrong.
                missedCommonAncestor = True
            label = testRev(p)
            labels[p] = label
            print_("%s (%s) " % (label[0], label[1]), flush=True)
            print_(
                "As expected, the parent's label is the opposite of the blamed rev's label.",
                flush=True)

        # Check that the parent's label is the opposite of the blamed merge's label.
        if labels[p][0] == "skip":
            print_(
                'Parent rev %s was marked as "skip", so the regression window includes it.'
                % (p, ),
                flush=True)
        elif labels[p][0] == blamedGoodOrBad:
            print_("Bisect lied to us! Parent rev %s was also %s!" %
                   (p, blamedGoodOrBad),
                   flush=True)
            bisectLied = True
        else:
            assert labels[p][0] == {
                "good": "bad",
                "bad": "good"
            }[blamedGoodOrBad]

    # Explain why bisect blamed the merge.
    if bisectLied:
        if missedCommonAncestor:
            ca = hg_helpers.findCommonAncestor(repo_dir, parents[0],
                                               parents[1])
            print_(flush=True)
            print_(
                "Bisect blamed the merge because our initial range did not include one",
                flush=True)
            print_("of the parents.", flush=True)
            print_("The common ancestor of %s and %s is %s." %
                   (parents[0], parents[1], ca),
                   flush=True)
            label = testRev(ca)
            print_("%s (%s) " % (label[0], label[1]), flush=True)
            print_("Consider re-running autobisectjs with -s %s -e %s" %
                   (ca, blamedRev),
                   flush=True)
            print_(
                "in a configuration where earliestWorking is before the common ancestor.",
                flush=True)
        else:
            print_(flush=True)
            print_(
                "Most likely, bisect's result was unhelpful because one of the",
                flush=True)
            print_(
                'tested revisions was marked as "good" or "bad" for the wrong reason.',
                flush=True)
            print_(
                "I don't know which revision was incorrectly marked. Sorry.",
                flush=True)
    else:
        print_(flush=True)
        print_(
            "The bug was introduced by a merge (it was not present on either parent).",
            flush=True)
        print_(
            "I don't know which patches from each side of the merge contributed to the bug. Sorry.",
            flush=True)
Esempio n. 12
0
def parseOpts():  # pylint: disable=invalid-name,missing-docstring,missing-return-doc,missing-return-type-doc
    # pylint: disable=too-many-branches,too-complex,too-many-statements
    usage = "Usage: %prog [options]"
    parser = OptionParser(usage)
    # http://docs.python.org/library/optparse.html#optparse.OptionParser.disable_interspersed_args
    parser.disable_interspersed_args()

    parser.set_defaults(
        resetRepoFirst=False,
        startRepo=None,
        endRepo="default",
        testInitialRevs=True,
        output="",
        watchExitCode=None,
        useInterestingnessTests=False,
        parameters=
        "-e 42",  # http://en.wikipedia.org/wiki/The_Hitchhiker%27s_Guide_to_the_Galaxy
        compilationFailedLabel="skip",
        build_options="",
        useTreeherderBinaries=False,
        nameOfTreeherderBranch="mozilla-inbound",
    )

    # Specify how the shell will be built.
    parser.add_option(
        "-b",
        "--build",
        dest="build_options",
        help=(
            'Specify js shell build options, e.g. -b "--enable-debug --32"'
            "(%s -m funfuzz.js.build_options --help)" %
            re.search("python.*[2-3]", os.__file__).group(0).replace("/", "")))

    parser.add_option(
        "--resetToTipFirst",
        dest="resetRepoFirst",
        action="store_true",
        help="First reset to default tip overwriting all local changes. "
        'Equivalent to first executing `hg update -C default`. Defaults to "%default".'
    )

    # Specify the revisions between which to bisect.
    parser.add_option(
        "-s",
        "--startRev",
        dest="startRepo",
        help=
        'Earliest changeset/build numeric ID to consider (usually a "good" cset). '
        "Defaults to the earliest revision known to work at all/available.")
    parser.add_option(
        "-e",
        "--endRev",
        dest="endRepo",
        help=
        'Latest changeset/build numeric ID to consider (usually a "bad" cset). '
        'Defaults to the head of the main branch, "default", or latest available build.'
    )
    parser.add_option(
        "-k",
        "--skipInitialRevs",
        dest="testInitialRevs",
        action="store_false",
        help=
        "Skip testing the -s and -e revisions and automatically trust them as -g and -b."
    )

    # Specify the type of failure to look for.
    # (Optional -- by default, internalTestAndLabel will look for exit codes that indicate a crash or assert.)
    parser.add_option(
        "-o",
        "--output",
        dest="output",
        help='Stdout or stderr output to be observed. Defaults to "%default". '
        'For assertions, set to "ssertion fail"')
    parser.add_option(
        "-w",
        "--watchExitCode",
        dest="watchExitCode",
        type="int",
        help=
        'Look out for a specific exit code. Only this exit code will be considered "bad".'
    )
    parser.add_option(
        "-i",
        "--useInterestingnessTests",
        dest="useInterestingnessTests",
        action="store_true",
        help="Interpret the final arguments as an interestingness test.")

    # Specify parameters for the js shell.
    parser.add_option(
        "-p",
        "--parameters",
        dest="parameters",
        help=
        'Specify parameters for the js shell, e.g. -p "-a --ion-eager testcase.js".'
    )

    # Specify how to treat revisions that fail to compile.
    # (You might want to add these to kbew.knownBrokenRanges in known_broken_earliest_working.)
    parser.add_option(
        "-l",
        "--compilationFailedLabel",
        dest="compilationFailedLabel",
        help="Specify how to treat revisions that fail to compile. "
        '(bad, good, or skip) Defaults to "%default"')

    parser.add_option(
        "-T",
        "--useTreeherderBinaries",
        dest="useTreeherderBinaries",
        action="store_true",
        help="Use treeherder binaries for quick bisection, assuming a fast "
        'internet connection. Defaults to "%default"')
    parser.add_option(
        "-N",
        "--nameOfTreeherderBranch",
        dest="nameOfTreeherderBranch",
        help='Name of the branch to download. Defaults to "%default"')

    (options, args) = parser.parse_args()
    if options.useTreeherderBinaries:
        print_(
            "TBD: Bisection using downloaded shells is temporarily not supported.",
            flush=True)
        sys.exit(0)

    options.build_options = build_options.parse_shell_opts(
        options.build_options)
    options.skipRevs = " + ".join(
        kbew.known_broken_ranges(options.build_options))

    options.runtime_params = [x for x in options.parameters.split(" ") if x]

    # First check that the testcase is present.
    if "-e 42" not in options.parameters and not Path(
            options.runtime_params[-1]).expanduser().is_file():
        print_(flush=True)
        print_("List of parameters to be passed to the shell is: %s" %
               " ".join(options.runtime_params),
               flush=True)
        print_(flush=True)
        raise OSError("Testcase at %s is not present." %
                      options.runtime_params[-1])

    assert options.compilationFailedLabel in ("bad", "good", "skip")

    extraFlags = []  # pylint: disable=invalid-name

    if options.useInterestingnessTests:
        if len(args) < 1:
            print_("args are: %s" % args, flush=True)
            parser.error("Not enough arguments.")
        for a in args:  # pylint: disable=invalid-name
            if a.startswith("--flags="):
                extraFlags = a[8:].split(" ")  # pylint: disable=invalid-name
        options.testAndLabel = externalTestAndLabel(options, args)
    elif len(args) >= 1:
        parser.error("Too many arguments.")
    else:
        options.testAndLabel = internalTestAndLabel(options)

    earliestKnownQuery = kbew.earliest_known_working_rev(  # pylint: disable=invalid-name
        options.build_options, options.runtime_params + extraFlags,
        options.skipRevs)

    earliestKnown = ""  # pylint: disable=invalid-name

    if not options.useTreeherderBinaries:
        # pylint: disable=invalid-name
        earliestKnown = hg_helpers.get_repo_hash_and_id(
            options.build_options.repo_dir, repo_rev=earliestKnownQuery)[0]

    if options.startRepo is None:
        if options.useTreeherderBinaries:
            options.startRepo = "default"
        else:
            options.startRepo = earliestKnown
    # elif not (options.useTreeherderBinaries or hg_helpers.isAncestor(options.build_options.repo_dir,
    #                                                              earliestKnown, options.startRepo)):
    #     raise Exception("startRepo is not a descendant of kbew.earliestKnownWorkingRev for this configuration")
    #
    # if not options.useTreeherderBinaries and not hg_helpers.isAncestor(options.build_options.repo_dir,
    #                                                                earliestKnown, options.endRepo):
    #     raise Exception("endRepo is not a descendant of kbew.earliestKnownWorkingRev for this configuration")

    if options.parameters == "-e 42":
        print_(
            "Note: since no parameters were specified, "
            "we're just ensuring the shell does not crash on startup/shutdown.",
            flush=True)

    if options.nameOfTreeherderBranch != "mozilla-inbound" and not options.useTreeherderBinaries:
        raise Exception(
            "Setting the name of branches only works for treeherder shell bisection."
        )

    return options
Esempio n. 13
0
def findBlamedCset(options, repo_dir, testRev):  # pylint: disable=invalid-name,missing-docstring,too-complex
    # pylint: disable=too-many-locals,too-many-statements
    repo_dir = str(repo_dir)
    print_("%s | Bisecting on: %s" % (time.asctime(), repo_dir), flush=True)

    hgPrefix = ["hg", "-R", repo_dir]  # pylint: disable=invalid-name

    # Resolve names such as "tip", "default", or "52707" to stable hg hash ids, e.g. "9f2641871ce8".
    # pylint: disable=invalid-name
    realStartRepo = sRepo = hg_helpers.get_repo_hash_and_id(
        repo_dir, repo_rev=options.startRepo)[0]
    # pylint: disable=invalid-name
    realEndRepo = eRepo = hg_helpers.get_repo_hash_and_id(
        repo_dir, repo_rev=options.endRepo)[0]
    sps.vdump("Bisecting in the range " + sRepo + ":" + eRepo)

    # Refresh source directory (overwrite all local changes) to default tip if required.
    if options.resetRepoFirst:
        subprocess.run(hgPrefix + ["update", "-C", "default"], check=True)
        # Throws exit code 255 if purge extension is not enabled in .hgrc:
        subprocess.run(hgPrefix + ["purge", "--all"], check=True)

    # Reset bisect ranges and set skip ranges.
    subprocess.run(
        hgPrefix + ["bisect", "-r"],
        check=True,
        cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
        timeout=99)
    if options.skipRevs:
        subprocess.run(
            hgPrefix + ["bisect", "--skip", options.skipRevs],
            check=True,
            cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
            timeout=300)

    labels = {}
    # Specify `hg bisect` ranges.
    if options.testInitialRevs:
        currRev = eRepo  # If testInitialRevs mode is set, compile and test the latest rev first.
    else:
        labels[sRepo] = ("good", "assumed start rev is good")
        labels[eRepo] = ("bad", "assumed end rev is bad")
        subprocess.run(hgPrefix + ["bisect", "-U", "-g", sRepo], check=True)
        mid_bisect_output = subprocess.run(
            hgPrefix + ["bisect", "-U", "-b", eRepo],
            check=True,
            cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
            stdout=subprocess.PIPE,
            timeout=300).stdout.decode("utf-8", errors="replace")
        currRev = hg_helpers.get_cset_hash_from_bisect_msg(
            mid_bisect_output.split("\n"))

    iterNum = 1
    if options.testInitialRevs:
        iterNum -= 2

    skipCount = 0
    blamedGoodOrBad = None
    blamedRev = None

    while currRev is not None:
        startTime = time.time()
        label = testRev(currRev)
        labels[currRev] = label
        if label[0] == "skip":
            skipCount += 1
            # If we use "skip", we tell hg bisect to do a linear search to get around the skipping.
            # If the range is large, doing a bisect to find the start and endpoints of compilation
            # bustage would be faster. 20 total skips being roughly the time that the pair of
            # bisections would take.
            if skipCount > 20:
                print_("Skipped 20 times, stopping autobisectjs.", flush=True)
                break
        print_("%s (%s) " % (label[0], label[1]), end=" ", flush=True)

        if iterNum <= 0:
            print_("Finished testing the initial boundary revisions...",
                   end=" ",
                   flush=True)
        else:
            print_(
                "Bisecting for the n-th round where n is %s and 2^n is %s ..."
                % (iterNum, 2**iterNum),
                end=" ",
                flush=True)
        (blamedGoodOrBad, blamedRev, currRev, sRepo, eRepo) = \
            bisectLabel(hgPrefix, options, label[0], currRev, sRepo, eRepo)

        if options.testInitialRevs:
            options.testInitialRevs = False
            assert currRev is None
            currRev = sRepo  # If options.testInitialRevs is set, test earliest possible rev next.

        iterNum += 1
        endTime = time.time()
        oneRunTime = endTime - startTime
        print_("This iteration took %.3f seconds to run." % oneRunTime,
               flush=True)

    if blamedRev is not None:
        checkBlameParents(repo_dir, blamedRev, blamedGoodOrBad, labels,
                          testRev, realStartRepo, realEndRepo)

    sps.vdump("Resetting bisect")
    subprocess.run(hgPrefix + ["bisect", "-U", "-r"], check=True)

    sps.vdump("Resetting working directory")
    subprocess.run(
        hgPrefix + ["update", "-C", "-r", "default"],
        check=True,
        cwd=os.getcwdu() if sys.version_info.major == 2 else os.getcwd(),  # pylint: disable=no-member
        timeout=999)
    hg_helpers.destroyPyc(repo_dir)

    print_(time.asctime(), flush=True)
Esempio n. 14
0
def bisectLabel(hgPrefix, options, hgLabel, currRev, startRepo, endRepo):  # pylint: disable=invalid-name
    # pylint: disable=missing-param-doc,missing-raises-doc,missing-return-doc,missing-return-type-doc,missing-type-doc
    # pylint: disable=too-many-arguments
    """Tell hg what we learned about the revision."""
    assert hgLabel in ("good", "bad", "skip")
    outputResult = sps.captureStdout(hgPrefix +
                                     ['bisect', '-U', '--' +
                                      hgLabel, currRev])[0]
    outputLines = outputResult.split("\n")

    if options.build_options:
        repoDir = options.build_options.repoDir

    if re.compile(
            "Due to skipped revisions, the first (good|bad) revision could be any of:"
    ).match(outputLines[0]):
        print_(flush=True)
        print_(sanitizeCsetMsg(outputResult, repoDir), flush=True)
        print_(flush=True)
        return None, None, None, startRepo, endRepo

    r = re.compile("The first (good|bad) revision is:")
    m = r.match(outputLines[0])
    if m:
        print_(flush=True)
        print_(flush=True)
        print_(
            "autoBisect shows this is probably related to the following changeset:",
            flush=True)
        print_(flush=True)
        print_(sanitizeCsetMsg(outputResult, repoDir), flush=True)
        print_(flush=True)
        blamedGoodOrBad = m.group(1)
        blamedRev = hg_helpers.get_cset_hash_from_bisect_msg(outputLines[1])
        return blamedGoodOrBad, blamedRev, None, startRepo, endRepo

    if options.testInitialRevs:
        return None, None, None, startRepo, endRepo

    # e.g. "Testing changeset 52121:573c5fa45cc4 (440 changesets remaining, ~8 tests)"
    sps.vdump(outputLines[0])

    currRev = hg_helpers.get_cset_hash_from_bisect_msg(outputLines[0])
    if currRev is None:
        print_("Resetting to default revision...", flush=True)
        subprocess.check_call(hgPrefix + ['update', '-C', 'default'])
        hg_helpers.destroyPyc(repoDir)
        raise Exception("hg did not suggest a changeset to test!")

    # Update the startRepo/endRepo values.
    start = startRepo
    end = endRepo
    if hgLabel == 'bad':
        end = currRev
    elif hgLabel == 'good':
        start = currRev
    elif hgLabel == 'skip':
        pass

    return None, None, currRev, start, end
Esempio n. 15
0
def checkBlameParents(repoDir, blamedRev, blamedGoodOrBad, labels, testRev,
                      startRepo, endRepo):
    """If bisect blamed a merge, try to figure out why."""
    bisectLied = False
    missedCommonAncestor = False

    parents = sps.captureStdout(
        ["hg", "-R", repoDir] +
        ["parent", '--template={node|short},', "-r", blamedRev])[0].split(
            ",")[:-1]

    if len(parents) == 1:
        return

    for p in parents:
        # Ensure we actually tested the parent.
        if labels.get(p) is None:
            print_(flush=True)
            print_(
                "Oops! We didn't test rev %s, a parent of the blamed revision! Let's do that now."
                % p,
                flush=True)
            if not hg_helpers.isAncestor(repoDir, startRepo, p) and \
                    not hg_helpers.isAncestor(repoDir, endRepo, p):
                print_(
                    "We did not test rev %s because it is not a descendant of either %s or %s."
                    % (p, startRepo, endRepo),
                    flush=True)
                # Note this in case we later decide the bisect result is wrong.
                missedCommonAncestor = True
            label = testRev(p)
            labels[p] = label
            print_("%s (%s) " % (label[0], label[1]), flush=True)
            print_(
                "As expected, the parent's label is the opposite of the blamed rev's label.",
                flush=True)

        # Check that the parent's label is the opposite of the blamed merge's label.
        if labels[p][0] == "skip":
            print_(
                "Parent rev %s was marked as 'skip', so the regression window includes it."
                % (p, ),
                flush=True)
        elif labels[p][0] == blamedGoodOrBad:
            print_("Bisect lied to us! Parent rev %s was also %s!" %
                   (p, blamedGoodOrBad),
                   flush=True)
            bisectLied = True
        else:
            assert labels[p][0] == {
                'good': 'bad',
                'bad': 'good'
            }[blamedGoodOrBad]

    # Explain why bisect blamed the merge.
    if bisectLied:
        if missedCommonAncestor:
            ca = hg_helpers.findCommonAncestor(repoDir, parents[0], parents[1])
            print_(flush=True)
            print_(
                "Bisect blamed the merge because our initial range did not include one",
                flush=True)
            print_("of the parents.", flush=True)
            print_("The common ancestor of %s and %s is %s." %
                   (parents[0], parents[1], ca),
                   flush=True)
            label = testRev(ca)
            print_("%s (%s) " % (label[0], label[1]), flush=True)
            print_("Consider re-running autoBisect with -s %s -e %s" %
                   (ca, blamedRev),
                   flush=True)
            print_(
                "in a configuration where earliestWorking is before the common ancestor.",
                flush=True)
        else:
            print_(flush=True)
            print_(
                "Most likely, bisect's result was unhelpful because one of the",
                flush=True)
            print_(
                "tested revisions was marked as 'good' or 'bad' for the wrong reason.",
                flush=True)
            print_(
                "I don't know which revision was incorrectly marked. Sorry.",
                flush=True)
    else:
        print_(flush=True)
        print_(
            "The bug was introduced by a merge (it was not present on either parent).",
            flush=True)
        print_(
            "I don't know which patches from each side of the merge contributed to the bug. Sorry.",
            flush=True)
Esempio n. 16
0
def bisectUsingTboxBins(options):  # pylint: disable=invalid-name,missing-param-doc,missing-raises-doc,missing-type-doc
    # pylint: disable=too-complex,too-many-locals,too-many-statements
    """Download treeherder binaries and bisect them."""
    testedIDs = {}
    desiredArch = '32' if options.build_options.enable32 else '64'
    buildType = download_build.defaultBuildType(
        options.nameOfTreeherderBranch, desiredArch, options.build_options.enableDbg)

    # Get list of treeherder IDs
    urlsTbox = download_build.getBuildList(buildType, earliestBuild=options.startRepo, latestBuild=options.endRepo)

    # Download and test starting point.
    print_(flush=True)
    print_("Examining starting point...", flush=True)
    sID, startResult, _sReason, _sPosition, urlsTbox, testedIDs, _startSkippedNum = testBuildOrNeighbour(
        options, 0, urlsTbox, buildType, testedIDs)
    if sID is None:
        raise Exception('No complete builds were found.')
    print_("Numeric ID %s was tested." % sID, flush=True)

    # Download and test ending point.
    print_(flush=True)
    print_("Examining ending point...", flush=True)
    eID, endResult, _eReason, _ePosition, urlsTbox, testedIDs, _endSkippedNum = testBuildOrNeighbour(
        options, len(urlsTbox) - 1, urlsTbox, buildType, testedIDs)
    if eID is None:
        raise Exception('No complete builds were found.')
    print_("Numeric ID %s was tested." % eID, flush=True)

    if startResult == endResult:
        raise Exception('Starting and ending points should have opposite results')

    count = 0
    print_(flush=True)
    print_("Starting bisection...", flush=True)
    print_(flush=True)
    while count < MAX_ITERATIONS:
        sps.vdump('Unsorted dictionary of tested IDs is: ' + str(testedIDs))
        count += 1
        print_("Test number %d:" % count, flush=True)

        sortedUrlsTbox = sorted(urlsTbox)
        if len(sortedUrlsTbox) >= 3:
            mPosition = len(sortedUrlsTbox) // 2
        else:
            print_(flush=True)
            print_('WARNING: %s has size smaller than 3. Impossible to return "middle" element.' % (sortedUrlsTbox,),
                   flush=True)
            print_(flush=True)
            mPosition = len(sortedUrlsTbox)

        # Test the middle revision. If it is not a complete build, test ones around it.
        mID, mResult, _mReason, mPosition, urlsTbox, testedIDs, middleRevSkippedNum = testBuildOrNeighbour(
            options, mPosition, urlsTbox, buildType, testedIDs)
        if mID is None:
            print_("Middle ID is None.", flush=True)
            break

        # Refresh the range of treeherder IDs depending on mResult.
        if mResult == endResult:
            urlsTbox = urlsTbox[0:(mPosition + 1)]
        else:
            urlsTbox = urlsTbox[(mPosition):len(urlsTbox)]

        print_("Numeric ID %s was tested." % mID, end=" ", flush=True)

        #  Exit infinite loop once we have tested the starting point, ending point and any points
        #  in the middle with results returning "incomplete".
        if (len(urlsTbox) - middleRevSkippedNum) <= 2 and mID in testedIDs:
            break
        elif len(urlsTbox) < 2:
            print_("urlsTbox is: %s" % (urlsTbox,), flush=True)
            raise Exception('Length of urlsTbox should not be smaller than 2.')
        elif (len(testedIDs) - 2) > 30:
            raise Exception('Number of testedIDs has exceeded 30.')

        print_(showRemainingNumOfTests(urlsTbox), flush=True)

    print_(flush=True)
    sps.vdump('Build URLs are: ' + str(urlsTbox))
    assert getIdFromTboxUrl(urlsTbox[0]) in testedIDs, 'Starting ID should have been tested.'
    assert getIdFromTboxUrl(urlsTbox[-1]) in testedIDs, 'Ending ID should have been tested.'
    outputTboxBisectionResults(options, urlsTbox, testedIDs)
Esempio n. 17
0
def findBlamedCset(options, repoDir, testRev):  # pylint: disable=invalid-name,missing-docstring,too-complex
    # pylint: disable=too-many-locals,too-many-statements
    print_("%s | Bisecting on: %s" % (time.asctime(), repoDir), flush=True)

    hgPrefix = ['hg', '-R', repoDir]  # pylint: disable=invalid-name

    # Resolve names such as "tip", "default", or "52707" to stable hg hash ids, e.g. "9f2641871ce8".
    # pylint: disable=invalid-name
    realStartRepo = sRepo = hg_helpers.getRepoHashAndId(
        repoDir, repoRev=options.startRepo)[0]
    # pylint: disable=invalid-name
    realEndRepo = eRepo = hg_helpers.getRepoHashAndId(
        repoDir, repoRev=options.endRepo)[0]
    sps.vdump("Bisecting in the range " + sRepo + ":" + eRepo)

    # Refresh source directory (overwrite all local changes) to default tip if required.
    if options.resetRepoFirst:
        subprocess.check_call(hgPrefix + ['update', '-C', 'default'])
        # Throws exit code 255 if purge extension is not enabled in .hgrc:
        subprocess.check_call(hgPrefix + ['purge', '--all'])

    # Reset bisect ranges and set skip ranges.
    sps.captureStdout(hgPrefix + ['bisect', '-r'])
    if options.skipRevs:
        sps.captureStdout(hgPrefix + ['bisect', '--skip', options.skipRevs])

    labels = {}
    # Specify `hg bisect` ranges.
    if options.testInitialRevs:
        currRev = eRepo  # If testInitialRevs mode is set, compile and test the latest rev first.
    else:
        labels[sRepo] = ('good', 'assumed start rev is good')
        labels[eRepo] = ('bad', 'assumed end rev is bad')
        subprocess.check_call(hgPrefix + ['bisect', '-U', '-g', sRepo])
        currRev = hg_helpers.get_cset_hash_from_bisect_msg(
            sps.captureStdout(hgPrefix +
                              ['bisect', '-U', '-b', eRepo])[0].split('\n')[0])

    iterNum = 1
    if options.testInitialRevs:
        iterNum -= 2

    skipCount = 0
    blamedRev = None

    while currRev is not None:
        startTime = time.time()
        label = testRev(currRev)
        labels[currRev] = label
        if label[0] == 'skip':
            skipCount += 1
            # If we use "skip", we tell hg bisect to do a linear search to get around the skipping.
            # If the range is large, doing a bisect to find the start and endpoints of compilation
            # bustage would be faster. 20 total skips being roughly the time that the pair of
            # bisections would take.
            if skipCount > 20:
                print_("Skipped 20 times, stopping autoBisect.", flush=True)
                break
        print_("%s (%s) " % (label[0], label[1]), end=" ", flush=True)

        if iterNum <= 0:
            print_("Finished testing the initial boundary revisions...",
                   end=" ",
                   flush=True)
        else:
            print_(
                "Bisecting for the n-th round where n is %s and 2^n is %s ..."
                % (iterNum, 2**iterNum),
                end=" ",
                flush=True)
        (blamedGoodOrBad, blamedRev, currRev, sRepo, eRepo) = \
            bisectLabel(hgPrefix, options, label[0], currRev, sRepo, eRepo)

        if options.testInitialRevs:
            options.testInitialRevs = False
            assert currRev is None
            currRev = sRepo  # If options.testInitialRevs is set, test earliest possible rev next.

        iterNum += 1
        endTime = time.time()
        oneRunTime = endTime - startTime
        print_("This iteration took %.3f seconds to run." % oneRunTime,
               flush=True)

    if blamedRev is not None:
        checkBlameParents(repoDir, blamedRev, blamedGoodOrBad, labels, testRev,
                          realStartRepo, realEndRepo)

    sps.vdump("Resetting bisect")
    subprocess.check_call(hgPrefix + ['bisect', '-U', '-r'])

    sps.vdump("Resetting working directory")
    sps.captureStdout(hgPrefix + ['update', '-C', '-r', 'default'],
                      ignoreStderr=True)
    hg_helpers.destroyPyc(repoDir)

    print_(time.asctime(), flush=True)
Esempio n. 18
0
def outputTboxBisectionResults(options, interestingList, testedBuildsDict):  # pylint: disable=missing-param-doc
    # pylint: disable=missing-raises-doc,missing-type-doc,too-many-locals
    """Return formatted bisection results from using treeherder builds."""
    sTimestamp, sHash, sResult, _sReason = testedBuildsDict[getIdFromTboxUrl(interestingList[0])]
    eTimestamp, eHash, eResult, _eReason = testedBuildsDict[getIdFromTboxUrl(interestingList[-1])]

    print_(flush=True)
    print_("Parameters for compilation bisection:", flush=True)
    pOutput = '-p "' + options.parameters + '"' if options.parameters != '-e 42' else ''
    oOutput = '-o "' + options.output + '"' if options.output is not '' else ''  # pylint: disable=literal-comparison
    params = [i for i in ["-s " + sHash, "-e " + eHash, pOutput, oOutput, "-b <build parameters>"] if i]
    print_(" ".join(params), flush=True)

    print_(flush=True)
    print_("=== Treeherder Build Bisection Results by autoBisect ===", flush=True)
    print_(flush=True)
    print_('The "%s" changeset has the timestamp "%s" and the hash "%s".' % (sResult, sTimestamp, sHash), flush=True)
    print_('The "%s" changeset has the timestamp "%s" and the hash "%s".' % (eResult, eTimestamp, eHash), flush=True)

    # Are we describing a regression window or a fix window?
    if sResult == 'good' and eResult == 'bad':
        windowType = 'regression'
    elif sResult == 'bad' and eResult == 'good':
        windowType = 'fix'
    else:
        raise Exception('Unknown windowType because starting result is "%s" and ending result is "%s".' % (
            sResult, eResult))

    # Show an hgweb link
    pushlogWindow = "%s/pushloghtml?fromchange=%s&tochange=%s" % (
        getHgwebMozillaOrg(options.nameOfTreeherderBranch), sHash, eHash)
    print_(flush=True)
    print_("Likely %s window: %s" % (windowType, pushlogWindow), flush=True)
    print_(flush=True)