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()))
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)
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
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.
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.')
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())
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.')
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
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
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
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)
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
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)
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
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)
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)
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)
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)