def __init__(self, apiroot, username, password, repoBase, options): self.apiroot = apiroot self.bz = BugzillaAgent(apiroot, username, password) self.repoBase = repoBase # Here we store the tip revision per repository for caching purposes self.tipRev = {} self.guessopts = {} #self.guessopts['mozilla-central'] = ['-m -n', '-m -n -a', '-m', '-j', '-j -m', '-j -m -a', None] #self.guessopts['ionmonkey'] = ['--ion -n -m', '--ion -n -m --ion-eager', None, '--ion-eager'] self.guessopts['ionmonkey'] = ['--ion -n -m', '--ion -n -m -a', '--ion -n -m --ion-eager', '--ion -n -m --ion-eager -a' ] self.guessopts['mozilla-central'] = [None, '--ion-eager', '-m --ion-eager', '-m -a --ion-eager', '--no-ion', '-a', '-a --no-ion', '-a --no-ti', '--no-jm', '-a --no-jm', '-m -n', '-m -n -a', '-m', '-j', '-j -m', '-j -m -a'] # Misc options self.options = options
def __init__(self, apiroot, username, password, repoBase, options): self.apiroot = apiroot self.bz = BugzillaAgent(apiroot, username, password) self.repoBase = repoBase # Here we store the tip revision per repository for caching purposes self.tipRev = {} self.allowedOpts = [ '--ion-eager', '--baseline-eager', '--ion-regalloc=backtracking', '--ion-regalloc=lsra', '--thread-count=2', '--ion-parallel-compile=off', '--ion-offthread-compile=off', '--ion-check-range-analysis', '--ion-gvn=pessimistic', '--ion-gvn=off', '--no-ion', '--no-baseline', '--arm-sim-icache-checks', '--arm-asm-nop-fill=1', '--no-threads', '--unboxed-objects', '--ion-fuzzer-checks', '--ion-extra-checks', '--arm-hwcap=vfp', '--ion-shared-stubs=on', '--ion-pgo=on', '-D' ] with open(os.path.join(repoBase, 'mozilla-central', 'config', 'milestone.txt'), 'rb') as fh: self.centralVersion = fh.readlines()[-1] self.centralVersion = int(self.centralVersion.split('.', 1)[0]) self.branches = ['mozilla-central', 'mozilla-aurora', 'mozilla-beta', 'mozilla-release'] # Misc options self.options = options
#bugPattern = /(bug\s#?|show_bug\.cgi\?id=)(\d+)/gi; #attachmentPattern = /(attachment\s#?)(\d+)/gi; #sourceUrlPattern = /bugzilla\.instantbird\.(?:org|com)/gi; return text # We can use "None" for both instead to not authenticate username, password = get_credentials() username2, password2 = get_credentials("bmo") # If you're stupid and type in the wrong password... #import keyring #keyring.set_password("bugzilla", username, "") # Load our agent for BMO bio = BugzillaAgent("https://api-dev.bugzilla.mozilla.org/instantbird/", username, password) bioUrl = "https://bugzilla.instantbird.org" #bmo = BMOAgent(username, password) bmo = CommentAgent("https://api-dev.bugzilla.mozilla.org/test/latest/", username2, password2) # Set whatever REST API options we want options = { 'changed_after': '2013-11-12', #'include_fields': '_default,attachments', 'include_fields': '_all', } # Get the bugs from the source. bugs = bio.get_bug_list(options)
class BugMonitor: def __init__(self, apiroot, username, password, repoBase, options): self.apiroot = apiroot self.bz = BugzillaAgent(apiroot, username, password) self.repoBase = repoBase # Here we store the tip revision per repository for caching purposes self.tipRev = {} self.allowedOpts = [ '--ion-eager', '--baseline-eager', '--ion-regalloc=backtracking', '--ion-regalloc=lsra', '--thread-count=2', '--ion-parallel-compile=off', '--ion-offthread-compile=off', '--ion-check-range-analysis', '--ion-gvn=pessimistic', '--ion-gvn=off', '--no-ion', '--no-baseline', '--arm-sim-icache-checks', '--arm-asm-nop-fill=1', '--no-threads', '--unboxed-objects', '--ion-fuzzer-checks', '--ion-extra-checks', '--arm-hwcap=vfp', '--ion-shared-stubs=on', '--ion-pgo=on', '-D' ] with open(os.path.join(repoBase, 'mozilla-central', 'config', 'milestone.txt'), 'rb') as fh: self.centralVersion = fh.readlines()[-1] self.centralVersion = int(self.centralVersion.split('.', 1)[0]) self.branches = ['mozilla-central', 'mozilla-aurora', 'mozilla-beta', 'mozilla-release'] # Misc options self.options = options def fetchBug(self, bug_id): bug = self.bz.get_bug(bug_id) if bug.depends_on != None and len(bug.depends_on) > 0: if isinstance(bug.depends_on[0], str): bug.depends_on = [ int("".join(bug.depends_on)) ] if bug.cf_crash_signature != None: bug.cf_crash_signature = bug.cf_crash_signature.replace("\r\n", "\n") return bug def postComment(self, bugnum, comment): url = urljoin(self.apiroot, 'bug/%s/comment?%s' % (bugnum, self.bz.qs())) return Comment(text=comment).post_to(url) def verifyFixedBug(self, bugnum, updateBug): # Fetch the bug bug = self.fetchBug(bugnum) bugModified = False bugVerified = False verifiedFlags = [] comments = [] if (bug.status == "RESOLVED" and bug.resolution == "FIXED"): result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): if updateBug: print "Marking bug " + str(bugnum) + " as verified fixed..." # Mark VERIFIED FIXED now bugVerified = True bugModified = True # Add a comment comments.append("JSBugMon: This bug has been automatically verified fixed.") else: print "Would mark bug " + str(bugnum) + " as verified fixed..." for branchNum in range(self.centralVersion - 3, self.centralVersion): statusFlagName = 'cf_status_firefox' + str(branchNum) if (bug.api_data[statusFlagName] == 'fixed'): branchRepo = self.branches[self.centralVersion - branchNum] branchRepoRev = self.hgFindFixParent(os.path.join(self.repoBase, branchRepo), bugnum) if branchRepoRev == None: print "Unable to find fix parent for bug %s on repository %s" % (str(bugnum), branchRepo) continue result = self.reproduceBug(bug, branchRepo, branchRepoRev) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): if updateBug: print "Marking bug " + str(bugnum) + " as verified fixed on Fx" + str(branchNum) + " ..." verifiedFlags.append(statusFlagName) bugModified = True comments.append("JSBugMon: This bug has been automatically verified fixed on Fx" + str(branchNum)) else: print "Would mark bug " + str(bugnum) + " as verified fixed on Fx" + str(branchNum) + " ..." if bugModified: while True: for flag in verifiedFlags: bug.api_data[flag] = 'verified' if bugVerified: bug.status = "VERIFIED" bug.api_data['cf_status_firefox' + str(self.centralVersion)] = 'verified' try: if bugModified: bug.put() break except Exception as e: print "Caught exception: " + str(e) print traceback.format_exc() time.sleep(1) except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) if len(comments) > 0: comment = "\n".join(comments) print "Commenting: " print comment self.postComment(bugnum, comment) return def confirmOpenBug(self, bugnum, updateBug, updateBugPositive): # Fetch the bug bug = self.fetchBug(bugnum) if (bug.status != "RESOLVED" and bug.status != "VERIFIED"): bugUpdateRequested = False bugConfirmRequested = False bugCloseRequested = False bugUpdated = False closeBug = False wbOpts = [] if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") # Explicitly marked to ignore this bug if ('ignore' in wbOpts): return if ('update' in wbOpts): bugUpdateRequested = True if ('reconfirm' in wbOpts): bugConfirmRequested = True if ('close' in wbOpts): bugCloseRequested = True result = self.reproduceBug(bug) comments = [] if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): if updateBugPositive or bugConfirmRequested: print "Marking bug " + str(bugnum) + " as confirmed on tip..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid (reproduced on revision " + result.tipRev + ").") bugUpdated = True else: print "Would mark bug " + str(bugnum) + " as confirmed on tip..." elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): if updateBug or bugUpdateRequested: print "Marking bug " + str(bugnum) + " as non-reproducing on tip..." # Add a comment comments.append("JSBugMon: The testcase found in this bug no longer reproduces (tried revision " + result.tipRev + ").") bugUpdated = True # Close bug only if requested to do so closeBug = bugCloseRequested else: print "Would mark bug " + str(bugnum) + " as non-reproducing on tip..." if bugUpdated: wbOpts.append('ignore') wbParts = filter(lambda x: len(x) > 0, map(str.rstrip, map(str.lstrip, re.split('\[jsbugmon:[^\]]+\]', bug.whiteboard)))) wbParts.append("[jsbugmon:" + ",".join(wbOpts) + "]") while True: try: # Fetch the bug again for updating bug = self.fetchBug(bugnum) # We add "ignore" to our bugmon options so we don't update the bug a second time bug.whiteboard = " ".join(wbParts) # Mark bug as WORKSFORME if confirmed to no longer reproduce if closeBug: bug.status = "RESOLVED" bug.resolution = "WORKSFORME" bug.put() break except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) if (len(comments) > 0): comment = "\n".join(comments) print "Posting comment: " print comment print "" self.postComment(bugnum, comment) return def processCommand(self, bugnum): # Fetch the bug bug = self.fetchBug(bugnum) bugUpdateRequested = False bugConfirmRequested = False bugCloseRequested = False bugVerifyRequested = False bugBisectRequested = False bugBisectFixRequested = False bugBisectForceCompile = False bugFailureMsg = None bugUpdated = False closeBug = False verifyBug = False wbOpts = [] if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") # Explicitly marked to ignore this bug if ('ignore' in wbOpts): return if ('update' in wbOpts): bugUpdateRequested = True if ('reconfirm' in wbOpts): bugConfirmRequested = True if ('close' in wbOpts): bugCloseRequested = True if ('verify' in wbOpts): bugVerifyRequested = True if ('bisect' in wbOpts): bugBisectRequested = True if ('bisectfix' in wbOpts): bugBisectFixRequested = True if ('bisect-force-compile' in wbOpts): bugBisectForceCompile = True print wbOpts comments = [] # Keep bisect comments separate so we can remove bisect/bisectfix commands separately bisectComments = [] bisectFixComments = [] result = None for opt in wbOpts: if (opt.find("=") > 0): (cmd,param) = opt.split('=') if (cmd != None and param != None): if (cmd == "verify-branch"): branches = param.split(';'); for branch in branches: if not branch in self.branches: continue print "Branch " + branch branchResult = self.reproduceBug(bug, branch) if (branchResult.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): print "Marking bug " + str(bugnum) + " as reproducing on branch " + branch + " ..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid on branch " + branch + " (reproduced on revision " + branchResult.tipRev + ").") elif (branchResult.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as non-reproducing on branch " + branch + " ..." comments.append("JSBugMon: The testcase found in this bug does not reproduce on branch " + branch + " (tried revision " + branchResult.tipRev + ").") else: print "Marking bug " + str(bugnum) + " as not processable ..." comments.append("JSBugMon: Command failed during processing this bug: " + opt + " (branch " + branch + ")") if bugVerifyRequested: if bug.status == "RESOLVED": if result == None: result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): print "Marking bug " + str(bugnum) + " as cannot verify fixed..." # Add a comment comments.append("JSBugMon: Cannot confirm fix, issue is still valid. (tried revision " + result.tipRev + ").") elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as verified fixed..." comments.append("JSBugMon: This bug has been automatically verified fixed. (tried revision " + result.tipRev + ").") verifyBug = True else: print "Marking bug " + str(bugnum) + " as not processable ..." comments.append("JSBugMon: Command failed during processing this bug: verify") if bugUpdateRequested: if bug.status != "RESOLVED" and bug.status != "VERIFIED": if result == None: try: result = self.reproduceBug(bug) except BugException as b: bugFailureMsg = "JSBugMon: Cannot process bug: " + str(b) except InternalException: # Propagate internal failures, don't update the bug raise except Exception as e: bugFailureMsg = "JSBugMon: Cannot process bug: Unknown exception (check manually)" print "Caught exception: " + str(e) print traceback.format_exc() if result != None: if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP or result.status == BugMonitorResult.statusCodes.REPRODUCED_SWITCHED): bugReproduced = True if bugConfirmRequested: print "Marking bug " + str(bugnum) + " as confirmed on tip..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid (reproduced on revision " + result.tipRev + ").") elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as non-reproducing on tip..." # Add a comment comments.append("JSBugMon: The testcase found in this bug no longer reproduces (tried revision " + result.tipRev + ").") if bugCloseRequested: closeBug = True elif (result.status == BugMonitorResult.statusCodes.FAILED): bugFailureMsg = "JSBugMon: Cannot process bug: Unable to automatically reproduce, please track manually." # If we already failed with the update command, don't try to bisect for now if bugFailureMsg != None: bugBisectRequested = False bugBisectFixRequested = False if bugBisectRequested and bug.status != "RESOLVED" and bug.status != "VERIFIED": if result == None: try: result = self.reproduceBug(bug) except BugException as b: bisectComments.append("JSBugMon: Bisection requested, failed due to error: " + str(b)) bisectComments.append("") if (result != None and (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP or result.status == BugMonitorResult.statusCodes.REPRODUCED_SWITCHED or result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED)): print "Bisecting bug " + str(bugnum) + " ..." bisectComment = self.bisectBug(bugnum, result, forceCompile=bugBisectForceCompile) if bisectComment != None: print bisectComment if len(bisectComment) > 0: bisectComments.append("JSBugMon: Bisection requested, result:") bisectComments.extend(bisectComment) else: bisectComments.append("JSBugMon: Bisection requested, failed due to error (try manually).") bisectComments.append(""); else: # Threat this as a temporary failure, don't remove the whiteboard tag bugBisectRequested = False if bugBisectFixRequested: if result == None: try: result = self.reproduceBug(bug) except BugException as b: bisectComments.append("JSBugMon: Fix Bisection requested, failed due to error: " + str(b)) bisectComments.append("") if (result != None and result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Bisecting fix for bug " + str(bugnum) + " ..." bisectComment = self.bisectBug(bugnum, result, bisectForFix=True, forceCompile=bugBisectForceCompile) if bisectComment != None: print bisectComment if len(bisectComment) > 0: bisectFixComments.append("JSBugMon: Fix Bisection requested, result:") bisectFixComments.extend(bisectComment) else: bisectFixComments.append("JSBugMon: Fix Bisection requested, failed due to error (try manually).") bisectFixComments.append(""); else: # Threat this as a temporary failure, don't remove the whiteboard tag bugBisectFixRequested = False wbParts = [] whiteBoardModified = False if closeBug or verifyBug or len(comments) > 0: whiteBoardModified = True wbOpts.append('ignore') if bugBisectRequested: whiteBoardModified = True wbOpts.remove('bisect') comments.extend(bisectComments) if bugBisectFixRequested and len(bisectFixComments) > 0: whiteBoardModified = True wbOpts.remove('bisectfix') comments.extend(bisectFixComments) if ((bugBisectRequested or (bugBisectFixRequested and len(bisectFixComments) > 0)) and bugBisectForceCompile): whiteBoardModified = True wbOpts.remove('bisect-force-compile') if bugFailureMsg != None and bugUpdateRequested: whiteBoardModified = True wbOpts.remove('update') comments.append(bugFailureMsg) if whiteBoardModified: wbParts = filter(lambda x: len(x) > 0, map(str.rstrip, map(str.lstrip, re.split('\[jsbugmon:[^\]]+\]', bug.whiteboard)))) wbParts.append("[jsbugmon:" + ",".join(wbOpts) + "]") while True: # Fetch the bug again bug = self.fetchBug(bugnum) bugModified = False # Mark bug as WORKSFORME if confirmed to no longer reproduce if closeBug: bugModified = True bug.status = "RESOLVED" bug.resolution = "WORKSFORME" # Mark bug as VERIFIED if we verified it successfully if verifyBug: bugModified = True bug.status = "VERIFIED" if whiteBoardModified: # We add "ignore" to our bugmon options so we don't update the bug a second time bugModified = True bug.whiteboard = " ".join(wbParts) try: if bugModified: bug.put() break except Exception as e: print "Caught exception: " + str(e) print traceback.format_exc() time.sleep(1) except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) if (len(comments) > 0): comment = "\n".join(comments) print "Posting comment: " print comment print "" self.postComment(bugnum, comment) return def bisectBug(self, bugnum, reproductionResult, bisectForFix=False, forceCompile=False): if forceCompile: return self.bisectBugCompile(bugnum, reproductionResult, bisectForFix) buildOpts = '-R %s' % (os.path.join(self.repoBase, reproductionResult.branchName)) if reproductionResult.buildFlags != None and len(reproductionResult.buildFlags) > 0: buildOpts += ' %s' % " ".join(reproductionResult.buildFlags) cmd = [ 'python', '/srv/repos/funfuzz/autobisect-js/autoBisect.py', '-T', '-b', buildOpts, '-p', " ".join(reproductionResult.testFlags) + " " + reproductionResult.testPath, '-i', 'crashes', '--timeout=10' ] print "DEBUG: Attempting binary bisection: %s" % str(cmd) outLines = None try: outLines = subprocess.check_output(cmd).split("\n"); except subprocess.CalledProcessError: # Threat this as a temporary failure, fallback to compiled bisection return self.bisectBugCompile(bugnum, reproductionResult, bisectForFix) retLines = [] found = False for outLine in outLines: if not found and (outLine.find("Build Bisection Results by autoBisect ===") != -1): found = True if found: retLines.append(outLine) if not found: # Binary bisection failed for some reason, fallback to compiled bisection return self.bisectBugCompile(bugnum, reproductionResult, bisectForFix) return retLines def bisectBugCompile(self, bugnum, reproductionResult, bisectForFix=False): # By default, bisect for the regressing changeset revFlag = '-e' if bisectForFix: revFlag = '-s' buildOpts = '-R %s' % (os.path.join(self.repoBase, reproductionResult.branchName)) if reproductionResult.buildFlags != None and len(reproductionResult.buildFlags) > 0: buildOpts += ' %s' % " ".join(reproductionResult.buildFlags) cmd = [ 'python', '/srv/repos/funfuzz/autobisect-js/autoBisect.py', '-b', buildOpts, revFlag, reproductionResult.origRev, '-p', " ".join(reproductionResult.testFlags) + " " + reproductionResult.testPath, '-i', 'crashes', '--timeout=10' ] print "DEBUG: %s" % str(cmd) outLines = None try: outLines = subprocess.check_output(cmd).split("\n"); except subprocess.CalledProcessError: # Threat this as a temporary failure return None retLines = [] found = False for outLine in outLines: if not found and (outLine.find("autoBisect shows this is probably related") != -1 or outLine.find("Due to skipped revisions") != -1): found = True if found: # Remove possible email address if outLine.find("user:"******"\s*<.+>", "", outLine) # autobisect emits a date at the end, skip that if (re.match("^\w+:", outLine) == None) and re.search("\s+\d{1,2}:\d{1,2}:\d{1,2}\s+", outLine) != None: continue retLines.append(outLine) return retLines def reproduceBug(self, bug, tipBranch=None, tipBranchRev=None): bugnum = str(bug.id) # Determine comment to look at and revision testCommentIdx = 0 rev = None if (tipBranch != None and tipBranchRev != None): rev = tipBranchRev if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") for opt in wbOpts: if (opt.find("=") > 0): (cmd,param) = opt.split('=') if (cmd != None and param != None): if (cmd == "origRev" and rev == None): rev = param elif (cmd == "testComment" and param.isdigit()): testCommentIdx = int(param) # Look for the first comment comment = bug.comments[testCommentIdx] if len(bug.comments) > testCommentIdx else None if (comment == None): raise BugException("Error: Specified bug does not have any comments") text = comment.text # Isolate revision to test for if (rev == None): rev = self.extractRevision(text) else: # Sanity check of the revision rev = self.extractRevision(rev) if (rev == None): raise BugException("Error: Failed to isolate original revision for test") buildFlags = [] checkFlags = ["--enable-more-deterministic", "--enable-simulator=arm", "--enable-arm-simulator", "--enable-debug", "--disable-debug", "--enable-optimize", "--disable-optimize"] for flag in checkFlags: if (re.search(flag + "[^-a-zA-Z0-9]", text) != None): buildFlags.append(flag) viableOptsList = [] opts = [] for opt in self.allowedOpts: if (text.find(opt) != -1): opts.append(opt) viableOptsList.append(opts) print "Extracted options: %s" % (' '.join(opts)) # Special hack for flags that changed if "--ion-parallel-compile=off" in opts: optsCopy = [] for opt in opts: if opt == "--ion-parallel-compile=off": optsCopy.append("--ion-offthread-compile=off") else: optsCopy.append(opt) viableOptsList.append(optsCopy) if (bug.version == "Trunk"): reponame = "mozilla-central" elif (bug.version == "Other Branch"): reponame = "ionmonkey" else: raise BugException("Error: Unsupported branch \"" + bug.version + "\" required by bug") # Default to using the bug.version field as repository specifier repoDir = os.path.join(self.repoBase, reponame) # If told to use a different tipBranch, use that for tip testing if (tipBranch == None): tipBranch = reponame tipRepoDir = os.path.join(self.repoBase, tipBranch) # If we are given a specific revision even for testing, then use # the tipBranch for all testing, including initial reproduction if (tipBranchRev != None): repoDir = tipRepoDir print "Using repository at %s with revision %s for initial reproduction" % (repoDir, rev) print "Using repository at %s with tip revision for testing" % (tipRepoDir) arch = None archList = None if (bug.platform == "x86_64"): arch = "64" elif (bug.platform == "x86"): arch = "32" elif (bug.platform == "All"): arch = "64" archList = [ "64", "32" ] # TODO: Detect native platform here # When auto-detecting, avoid using ARM simulator for now if "--enable-simulator=arm" in buildFlags: buildFlags.remove("--enable-simulator=arm") elif (bug.platform == "ARM"): arch = "32" buildFlags.append("--enable-simulator=arm") else: raise BugException("Error: Unsupported architecture \"" + bug.platform + "\" required by bug") # We need at least some shell to extract the test from the bug, # so we build a debug shell here already try: (testShell, testRev) = self.getShell("cache/", arch, "dbg", 0, rev, False, repoDir, buildFlags) except Exception: trace = sys.exc_info()[2] raise InternalException("Failed to compile tip shell (toolchain broken?)"), None, trace # If the file already exists, then we can reuse it if testCommentIdx > 0: testFile = "bug" + str(bugnum) + "-" + str(testCommentIdx) + ".js" else: testFile = "bug" + str(bugnum) + ".js" if (os.path.exists(testFile)): print "Using existing (cached) testfile " + testFile else: # We need to detect where our test is. blocks = text.split("\n\n") found = False cnt = 0 for i,block in enumerate(blocks): # Write our test to file outFile = open(testFile, "w") outFile.write(block) outFile.close() print "Testing syntax with shell %s" % testShell (err, ret) = testBinary(testShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") < 0): # We have found the test (or maybe only the start of the test) # Try adding more code until we hit an error or are out of # blocks. oldBlock = block curBlock = block for j,block in enumerate(blocks): if j > i: curBlock = curBlock + "\n" + block # Write our test to file outFile = open(testFile, "w") outFile.write(curBlock) outFile.close() (err, ret) = testBinary(testShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") >= 0): # Too much, write oldBlock and break outFile = open(testFile, "w") outFile.write(oldBlock) outFile.close() break else: oldBlock = curBlock found = True print "Isolated possible testcase starting in textblock " + str(cnt) break cnt += 1 if not found: # First try to find a suitable attachment attachments = [a for a in bug.attachments if not bool(int(a.is_obsolete))] for attachment in attachments: # Seriously, we don't need anything larger than 512kb here^^ if (attachment.size <= 512*1024): # Refetch attachment with content url = urljoin(self.apiroot, 'attachment/%s/?%s&attachmentdata=1' % (attachment.id, self.bz.qs())) attachment = attachment.get(url) try: rawData = base64.b64decode(attachment.data) # Write our data to file outFile = open(testFile, "w") outFile.write(rawData) outFile.close() (err, ret) = testBinary(testShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") < 0): # Found something that looks like JS :) found = True break except TypeError: pass # If we still haven't found any test, give up here... if not found: # Ensure we don't cache the wrong test os.remove(testFile) raise BugException("Error: Failed to isolate test from comment") (oouterr, oret) = (None, None) (origShell, origRev) = (None, None) # If we have an exact architecture, we will only test that if (archList == None): archList = [ arch ] for compileType in ['dbg', 'opt']: for archType in archList: try: (origShell, origRev) = self.getShell("cache/", archType, compileType, 0, rev, False, repoDir, buildFlags) except Exception: # Unlike compilation failures on tip, we must not ignore compilation failures with the original # revision, as it could mean that the bug was filed with a broken revision. raise BugException("Error: Failed to compile specified revision %s (maybe try another?)" % rev) for opts in viableOptsList: (oouterr, oret) = testBinary(origShell, testFile, opts , 0, verbose=self.options.verbose, timeout=30) if (oret < 0): break # If we reproduced with one arch, then we don't need to try the others if (oret < 0): break print "" # If we reproduced with dbg, then we don't need to try opt if (oret < 0): break # Check if we reproduced at all (dbg or opt) if (oret < 0): print "" print "Successfully reproduced bug (exit code " + str(oret) + ") on original revision " + rev + ":" errl = oouterr.split("\n") if len(errl) > 2: errl = errl[-2:] for err in errl: print err # Try running on tip now print "Testing bug on tip..." # Update to tip and cache result: updated = False if not self.tipRev.has_key(tipRepoDir): # If we don't know the tip revision for this branch, update and get it self.tipRev[tipRepoDir] = self.hgUpdate(tipRepoDir) updated = True try: (tipShell, tipRev) = self.getShell("cache/", archType, compileType, 0, self.tipRev[tipRepoDir], updated, tipRepoDir, buildFlags) except Exception: trace = sys.exc_info()[2] raise InternalException("Failed to compile tip shell (toolchain broken?)"), None, trace tipOpts = None for opts in viableOptsList: (touterr, tret) = testBinary(tipShell, testFile, opts , 0, verbose=self.options.verbose, timeout=30) if (tret < 0): tipOpts = opts break if (tret < 0): if (tret == oret): if (opts == tipOpts): print "Result: Bug still reproduces" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.REPRODUCED_TIP) else: print "Result: Bug still reproduces, but with different options: " + " ".join(tipOpts) # TODO need another code here in the future return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.REPRODUCED_TIP) else: # Unlikely but possible, switched signal print "Result: Bug now reproduces with signal " + str(tret) + " (previously " + str(oret) + ")" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.REPRODUCED_SWITCHED) else: print "Result: Bug no longer reproduces" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.REPRODUCED_FIXED) else: print "Error: Failed to reproduce bug on original revision" #return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.FAILED) return BugMonitorResult(reponame, rev, None, opts, testFile, archType, compileType, buildFlags, BugMonitorResult.statusCodes.FAILED) def extractOptions(self, text): ret = re.compile('((?: \-[a-z])+)', re.DOTALL).search(text) if (ret != None and ret.groups > 1): return ret.group(1).lstrip().split(" ") return None def extractRevision(self, text): if (text == None): return None tokens = text.split(' ') for token in tokens: if (re.match('^[a-f0-9]{12}[^a-f0-9]?', token)): return token[0:12] return None def hgFindFixParent(self, repoDir, bugNum): prevRev = None hgOut = captureStdout(['hg', 'log', '-l', '10000', '--template', '{node} {desc}\n'], ignoreStderr=True, currWorkingDir=repoDir)[0].split("\n") for line in reversed(hgOut): line = line.split(' ', 1) if len(line) < 2: continue rev = line[0][0:12] if (line[1].find(str(bugNum)) != -1): return prevRev prevRev = rev return None def hgUpdate(self, repoDir, rev=None): try: print "Running hg update..." if (rev != None): captureStdout(['hg', 'update', '-C', '-r', rev], ignoreStderr=True, currWorkingDir=repoDir) else: captureStdout(['hg', 'update', '-C'], ignoreStderr=True, currWorkingDir=repoDir) hgIdCmdList = ['hg', 'identify', repoDir] # In Windows, this throws up a warning about failing to set color mode to win32. if platform.system() == 'Windows': hgIdFull = captureStdout(hgIdCmdList, currWorkingDir=repoDir, ignoreStderr=True)[0] else: hgIdFull = captureStdout(hgIdCmdList, currWorkingDir=repoDir)[0] hgIdChangesetHash = hgIdFull.split(' ')[0] #os.chdir(savedPath) return hgIdChangesetHash except: print "Unexpected error while updating HG:", sys.exc_info()[0] sys.exit(1) def getShell(self, shellCacheDir, archNum, compileType, valgrindSupport, rev, updated, repoDir, buildFlags=None): shell = None # This code maps the old "-c dbg / -c opt" configurations to their configurations haveDebugOptFlags = False if buildFlags != None: haveDebugOptFlags = ('--enable-debug' in buildFlags) or ('--disable-debug' in buildFlags) or ('--enable-optimize' in buildFlags) or ('--disable-optimize' in buildFlags) print "haveDebugOptFlags: %s %s" % (str(haveDebugOptFlags), " ".join(buildFlags)) if compileType == 'dbg': if buildFlags != None: if not haveDebugOptFlags: buildFlags.append('--enable-debug') buildFlags.append('--enable-optimize') else: buildFlags = [ '--enable-debug', '--enable-optimize' ] elif compileType == 'opt': if buildFlags != None: if not haveDebugOptFlags: buildFlags.append('--disable-debug') buildFlags.append('--enable-optimize') else: buildFlags = [ '--disable-debug', '--enable-optimize' ] if archNum == "32": buildFlags.append('--32') buildOpts = '-R %s' % (repoDir) if buildFlags != None and len(buildFlags) > 0: buildOpts += ' %s' % " ".join(buildFlags) if (shell == None): if (rev == None): rev = self.hgUpdate(repoDir, rev) print "Compiling a new shell for tip (revision " + rev + ")" else: print "Compiling a new shell for revision " + rev shell = captureStdout(['/srv/repos/funfuzz/js/compileShell.py', '-b', buildOpts, '-r', rev])[0].split("\n")[-1] return (shell, rev)
class BugMonitor: def __init__(self, apiroot, username, password, repoBase, options): self.apiroot = apiroot self.bz = BugzillaAgent(apiroot, username, password) self.repoBase = repoBase # Here we store the tip revision per repository for caching purposes self.tipRev = {} self.guessopts = {} #self.guessopts['mozilla-central'] = ['-m -n', '-m -n -a', '-m', '-j', '-j -m', '-j -m -a', None] #self.guessopts['ionmonkey'] = ['--ion -n -m', '--ion -n -m --ion-eager', None, '--ion-eager'] self.guessopts['ionmonkey'] = ['--ion -n -m', '--ion -n -m -a', '--ion -n -m --ion-eager', '--ion -n -m --ion-eager -a' ] self.guessopts['mozilla-central'] = [None, '--ion-eager', '-m --ion-eager', '-m -a --ion-eager', '--no-ion', '-a', '-a --no-ion', '-a --no-ti', '--no-jm', '-a --no-jm', '-m -n', '-m -n -a', '-m', '-j', '-j -m', '-j -m -a'] # Misc options self.options = options def fetchBug(self, bug_id): bug = self.bz.get_bug(bug_id) if bug.depends_on != None and len(bug.depends_on) > 0: if isinstance(bug.depends_on[0], str): bug.depends_on = [ int("".join(bug.depends_on)) ] if bug.cf_crash_signature != None: bug.cf_crash_signature = bug.cf_crash_signature.replace("\r\n", "\n") return bug def postComment(self, bugnum, comment): url = urljoin(self.apiroot, 'bug/%s/comment?%s' % (bugnum, self.bz.qs())) return Comment(text=comment).post_to(url) def verifyFixedBug(self, bugnum, updateBug): # Fetch the bug bug = self.fetchBug(bugnum) if (bug.status == "RESOLVED" and bug.resolution == "FIXED"): result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): if updateBug: print "Marking bug " + str(bugnum) + " as verified fixed..." while True: try: bug = self.fetchBug(bugnum) # Mark VERIFIED FIXED now bug.status = "VERIFIED" bug.put() break except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) # Add a comment self.postComment(bugnum, "JSBugMon: This bug has been automatically verified fixed.") else: print "Would mark bug " + str(bugnum) + " as verified fixed..." return def confirmOpenBug(self, bugnum, updateBug, updateBugPositive): # Fetch the bug bug = self.fetchBug(bugnum) if (bug.status != "RESOLVED" and bug.status != "VERIFIED"): bugUpdateRequested = False bugConfirmRequested = False bugCloseRequested = False bugUpdated = False closeBug = False wbOpts = [] if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") # Explicitly marked to ignore this bug if ('ignore' in wbOpts): return if ('update' in wbOpts): bugUpdateRequested = True if ('reconfirm' in wbOpts): bugConfirmRequested = True if ('close' in wbOpts): bugCloseRequested = True result = self.reproduceBug(bug) comments = [] if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): if updateBugPositive or bugConfirmRequested: print "Marking bug " + str(bugnum) + " as confirmed on tip..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid (reproduced on revision " + result.tipRev + ").") bugUpdated = True else: print "Would mark bug " + str(bugnum) + " as confirmed on tip..." elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): if updateBug or bugUpdateRequested: print "Marking bug " + str(bugnum) + " as non-reproducing on tip..." # Add a comment comments.append("JSBugMon: The testcase found in this bug no longer reproduces (tried revision " + result.tipRev + ").") bugUpdated = True # Close bug only if requested to do so closeBug = bugCloseRequested else: print "Would mark bug " + str(bugnum) + " as non-reproducing on tip..." if bugUpdated: wbOpts.append('ignore') wbParts = filter(lambda x: len(x) > 0, map(str.rstrip, map(str.lstrip, re.split('\[jsbugmon:[^\]]+\]', bug.whiteboard)))) wbParts.append("[jsbugmon:" + ",".join(wbOpts) + "]") while True: try: # Fetch the bug again for updating bug = self.fetchBug(bugnum) # We add "ignore" to our bugmon options so we don't update the bug a second time bug.whiteboard = " ".join(wbParts) # Mark bug as WORKSFORME if confirmed to no longer reproduce if closeBug: bug.status = "RESOLVED" bug.resolution = "WORKSFORME" bug.put() break except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) if (len(comments) > 0): comment = "\n".join(comments) print "Posting comment: " print comment print "" self.postComment(bugnum, comment) return def processCommand(self, bugnum): # Fetch the bug bug = self.fetchBug(bugnum) bugUpdateRequested = False bugConfirmRequested = False bugCloseRequested = False bugVerifyRequested = False bugBisectRequested = False bugBisectFixRequested = False bugFailureMsg = None bugUpdated = False closeBug = False verifyBug = False wbOpts = [] if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") # Explicitly marked to ignore this bug if ('ignore' in wbOpts): return if ('update' in wbOpts): bugUpdateRequested = True if ('reconfirm' in wbOpts): bugConfirmRequested = True if ('close' in wbOpts): bugCloseRequested = True if ('verify' in wbOpts): bugVerifyRequested = True if ('bisect' in wbOpts): bugBisectRequested = True if ('bisectfix' in wbOpts): bugBisectFixRequested = True print wbOpts comments = [] # Keep bisect comments separate so we can remove bisect/bisectfix commands separately bisectComments = [] bisectFixComments = [] result = None for opt in wbOpts: if (opt.find("=") > 0): (cmd,param) = opt.split('=') if (cmd != None and param != None): if (cmd == "verify-branch"): branches = param.split(';'); for branch in branches: if not branch in ['mozilla-aurora', 'mozilla-beta', 'mozilla-release', 'mozilla-esr17']: continue print "Branch " + branch branchResult = self.reproduceBug(bug, branch) if (branchResult.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): print "Marking bug " + str(bugnum) + " as reproducing on branch " + branch + " ..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid on branch " + branch + " (reproduced on revision " + branchResult.tipRev + ").") elif (branchResult.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as non-reproducing on branch " + branch + " ..." comments.append("JSBugMon: The testcase found in this bug does not reproduce on branch " + branch + " (tried revision " + branchResult.tipRev + ").") else: print "Marking bug " + str(bugnum) + " as not processable ..." comments.append("JSBugMon: Command failed during processing this bug: " + opt + " (branch " + branch + ")") if bugVerifyRequested: if bug.status == "RESOLVED": if result == None: result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP): print "Marking bug " + str(bugnum) + " as cannot verify fixed..." # Add a comment comments.append("JSBugMon: Cannot confirm fix, issue is still valid. (tried revision " + result.tipRev + ").") elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as verified fixed..." comments.append("JSBugMon: This bug has been automatically verified fixed. (tried revision " + result.tipRev + ").") verifyBug = True else: print "Marking bug " + str(bugnum) + " as not processable ..." comments.append("JSBugMon: Command failed during processing this bug: verify") if bugUpdateRequested: if bug.status != "RESOLVED" and bug.status != "VERIFIED": if result == None: try: result = self.reproduceBug(bug) except BugException as b: bugFailureMsg = "JSBugMon: Cannot process bug: " + str(b) except Exception as e: bugFailureMsg = "JSBugMon: Cannot process bug: Unknown exception (check manually)" print "Caught exception: " + str(e) print traceback.format_exc() if result != None: if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP or result.status == BugMonitorResult.statusCodes.REPRODUCED_SWITCHED): bugReproduced = True if bugConfirmRequested: print "Marking bug " + str(bugnum) + " as confirmed on tip..." # Add a comment comments.append("JSBugMon: This bug has been automatically confirmed to be still valid (reproduced on revision " + result.tipRev + ").") elif (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Marking bug " + str(bugnum) + " as non-reproducing on tip..." # Add a comment comments.append("JSBugMon: The testcase found in this bug no longer reproduces (tried revision " + result.tipRev + ").") if bugCloseRequested: closeBug = True elif (result.status == BugMonitorResult.statusCodes.FAILED): bugFailureMsg = "JSBugMon: Cannot process bug: Unable to automatically reproduce, please track manually." if bugBisectRequested and bug.status != "RESOLVED" and bug.status != "VERIFIED": if result == None: result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_TIP or result.status == BugMonitorResult.statusCodes.REPRODUCED_SWITCHED or BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Bisecting bug " + str(bugnum) + " ..." bisectComment = self.bisectBug(bugnum, result) print bisectComment if len(bisectComment) > 0: bisectComments.append("JSBugMon: Bisection requested, result:") bisectComments.extend(bisectComment) else: bisectComments.append("JSBugMon: Bisection requested, failed due to error (try manually).") bisectComments.append(""); if bugBisectFixRequested: if result == None: result = self.reproduceBug(bug) if (result.status == BugMonitorResult.statusCodes.REPRODUCED_FIXED): print "Bisecting fix for bug " + str(bugnum) + " ..." bisectComment = self.bisectBug(bugnum, result, True) print bisectComment if len(bisectComment) > 0: bisectFixComments.append("JSBugMon: Fix Bisection requested, result:") bisectFixComments.extend(bisectComment) else: bisectFixComments.append("JSBugMon: Fix Bisection requested, failed due to error (try manually).") bisectFixComments.append(""); wbParts = [] whiteBoardModified = False if closeBug or verifyBug or len(comments) > 0: whiteBoardModified = True wbOpts.append('ignore') if bugBisectRequested: whiteBoardModified = True wbOpts.remove('bisect') comments.extend(bisectComments) if bugBisectFixRequested and len(bisectFixComments) > 0: whiteBoardModified = True wbOpts.remove('bisectfix') comments.extend(bisectFixComments) if bugFailureMsg != None and bugUpdateRequested: whiteBoardModified = True wbOpts.remove('update') comments.append(bugFailureMsg) if whiteBoardModified: wbParts = filter(lambda x: len(x) > 0, map(str.rstrip, map(str.lstrip, re.split('\[jsbugmon:[^\]]+\]', bug.whiteboard)))) wbParts.append("[jsbugmon:" + ",".join(wbOpts) + "]") while True: # Fetch the bug again bug = self.fetchBug(bugnum) bugModified = False # Mark bug as WORKSFORME if confirmed to no longer reproduce if closeBug: bugModified = True bug.status = "RESOLVED" bug.resolution = "WORKSFORME" # Mark bug as VERIFIED if we verified it successfully if verifyBug: bugModified = True bug.status = "VERIFIED" if whiteBoardModified: # We add "ignore" to our bugmon options so we don't update the bug a second time bugModified = True bug.whiteboard = " ".join(wbParts) try: if bugModified: bug.put() break except Exception as e: print "Caught exception: " + str(e) print traceback.format_exc() time.sleep(1) except: print "Failed to submit bug change, sleeping one second and retrying..." time.sleep(1) if (len(comments) > 0): comment = "\n".join(comments) print "Posting comment: " print comment print "" self.postComment(bugnum, comment) return def bisectBug(self, bugnum, reproductionResult, bisectForFix=False): # By default, bisect for the regressing changeset revFlag = '-e' if bisectForFix: revFlag = '-s' cmd = [ 'python', '/srv/repos/fuzzing/autobisect-js/autoBisect.py', '-R', os.path.join(self.repoBase, reproductionResult.branchName), '-a', reproductionResult.arch, '-c', reproductionResult.ctype, revFlag, reproductionResult.origRev, '-p', " ".join(reproductionResult.testFlags) + " " + reproductionResult.testPath, '-i', 'crashes', '--timeout=10' ] outLines = subprocess.check_output(cmd).split("\n"); retLines = [] found = False for outLine in outLines: if not found and (outLine.find("autoBisect shows this is probably related") != -1 or outLine.find("Due to skipped revisions") != -1): found = True if found: # Remove possible email address if outLine.find("user:"******"\s*<.+>", "", outLine) # autobisect emits a date at the end, skip that if (re.match("^\w+:", outLine) == None) and re.search("\s+\d{1,2}:\d{1,2}:\d{1,2}\s+", outLine) != None: continue retLines.append(outLine) return retLines def reproduceBug(self, bug, tipBranch=None): # Fetch the bug #bug = self.fetchBug(bugnum) bugnum = str(bug.id) # Determine comment to look at and revision testCommentIdx = 0 rev = None if (bug.whiteboard != None): ret = re.compile('\[jsbugmon:([^\]]+)\]').search(bug.whiteboard) if (ret != None and ret.groups > 1): wbOpts = ret.group(1).split(",") for opt in wbOpts: if (opt.find("=") > 0): (cmd,param) = opt.split('=') if (cmd != None and param != None): if (cmd == "origRev"): rev = param elif (cmd == "testComment" and param.isdigit()): testCommentIdx = int(param) # Look for the first comment comment = bug.comments[testCommentIdx] if len(bug.comments) > testCommentIdx else None if (comment == None): raise BugException("Error: Specified bug does not have any comments") text = comment.text # Isolate revision to test for if (rev == None): rev = self.extractRevision(text) else: # Sanity check of the revision rev = self.extractRevision(rev) if (rev == None): raise BugException("Error: Failed to isolate original revision for test") opts = None tipOpts = None # Isolate options for testing, not explicitly instructed to guess if not self.options.guessopts: opts = self.extractOptions(text) if (opts == None): print "Warning: No options found, will try to guess" arch = None archList = None if (bug.platform == "x86_64"): arch = "64" elif (bug.platform == "x86"): arch = "32" elif (bug.platform == "All"): arch = "64" archList = [ "64", "32" ] # TODO: Detect native platform here else: raise BugException("Error: Unsupported architecture \"" + bug.platform + "\" required by bug") if (bug.version == "Trunk"): reponame = "mozilla-central" elif (bug.version == "Other Branch"): reponame = "ionmonkey" else: raise BugException("Error: Unsupported branch \"" + bug.version + "\" required by bug") if (tipBranch == None): tipBranch = reponame print "Repobase: " + self.repoBase print "Reponame: " + reponame repoDir = os.path.join(self.repoBase, reponame) tipRepoDir = os.path.join(self.repoBase, tipBranch) # We need at least some shell to extract the test from the bug, # so we build a debug tip shell here already updated = False if not self.tipRev.has_key(repoDir): # If we don't know the tip revision for this branch, update and get it self.tipRev[repoDir] = self.hgUpdate(repoDir) updated = True (tipShell, tipRev) = self.getShell("cache/", arch, "dbg", 0, self.tipRev[repoDir], updated, repoDir) # If the file already exists, then we can reuse it if testCommentIdx > 0: testFile = "bug" + str(bugnum) + "-" + str(testCommentIdx) + ".js" else: testFile = "bug" + str(bugnum) + ".js" if (os.path.exists(testFile)): print "Using existing (cached) testfile " + testFile else: # We need to detect where our test is. blocks = text.split("\n\n") found = False cnt = 0 for i,block in enumerate(blocks): # Write our test to file outFile = open(testFile, "w") outFile.write(block) outFile.close() (err, ret) = testBinary(tipShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") < 0): # We have found the test (or maybe only the start of the test) # Try adding more code until we hit an error or are out of # blocks. oldBlock = block curBlock = block for j,block in enumerate(blocks): if j > i: curBlock = curBlock + "\n" + block # Write our test to file outFile = open(testFile, "w") outFile.write(curBlock) outFile.close() (err, ret) = testBinary(tipShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") >= 0): # Too much, write oldBlock and break outFile = open(testFile, "w") outFile.write(oldBlock) outFile.close() break else: oldBlock = curBlock found = True print "Isolated possible testcase starting in textblock " + str(cnt) break cnt += 1 if not found: # First try to find a suitable attachment attachments = [a for a in bug.attachments if not bool(int(a.is_obsolete))] for attachment in attachments: # Seriously, we don't need anything larger than 512kb here^^ if (attachment.size <= 512*1024): # Refetch attachment with content url = urljoin(self.apiroot, 'attachment/%s/?%s&attachmentdata=1' % (attachment.id, self.bz.qs())) attachment = attachment.get(url) try: rawData = base64.b64decode(attachment.data) # Write our data to file outFile = open(testFile, "w") outFile.write(rawData) outFile.close() (err, ret) = testBinary(tipShell, testFile, [], 0, timeout=30) if (err.find("SyntaxError") < 0): # Found something that looks like JS :) found = True break except TypeError: pass # If we still haven't found any test, give up here... if not found: # Ensure we don't cache the wrong test os.remove(testFile) raise BugException("Error: Failed to isolate test from comment") (oouterr, oret) = (None, None) (origShell, origRev) = (None, None) # If we have an exact architecture, we will only test that if (archList == None): archList = [ arch ] for compileType in ['dbg', 'opt']: for archType in archList: # Update to tip and cache result: updated = False if not self.tipRev.has_key(tipRepoDir): # If we don't know the tip revision for this branch, update and get it self.tipRev[tipRepoDir] = self.hgUpdate(tipRepoDir) updated = True (tipShell, tipRev) = self.getShell("cache/", archType, compileType, 0, self.tipRev[tipRepoDir], updated, tipRepoDir) (origShell, origRev) = self.getShell("cache/", archType, compileType, 0, rev, False, repoDir) if (opts != None): (oouterr, oret) = testBinary(origShell, testFile, opts , 0, verbose=self.options.verbose, timeout=30) else: print "Guessing options...", guessopts = self.guessopts[reponame] for opt in guessopts: topts = [] if opt == None: print " no options", else: print " " + opt, topts = opt.split(' ') (oouterr, oret) = testBinary(origShell, testFile, topts , 0, verbose=self.options.verbose, timeout=30) if (oret < 0): opts = topts break; # If we reproduced with one arch, then we don't need to try the others if (oret < 0): break; print "" # If we reproduced with dbg, then we don't need to try opt if (oret < 0): break; # Check if we reproduced at all (dbg or opt) if (oret < 0): print "" print "Successfully reproduced bug (exit code " + str(oret) + ") on original revision " + rev + ":" errl = oouterr.split("\n") if len(errl) > 2: errl = errl[-2:] for err in errl: print err if (opts != None): # Try running on tip now print "Testing bug on tip..." if self.options.guessopts: guessopts = self.guessopts[reponame] for opt in guessopts: tipOpts = [] if opt == None: print " no options", else: print " " + opt, tipOpts = opt.split(' ') (touterr, tret) = testBinary(tipShell, testFile, tipOpts , 0, verbose=self.options.verbose, timeout=30) if (tret < 0): break; else: tipOpts = opts (touterr, tret) = testBinary(tipShell, testFile, tipOpts , 0, verbose=self.options.verbose, timeout=30) else: print "" if (tret < 0): if (tret == oret): if (opts == tipOpts): print "Result: Bug still reproduces" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, BugMonitorResult.statusCodes.REPRODUCED_TIP) else: print "Result: Bug still reproduces, but with different options: " + " ".join(tipOpts) # TODO need another code here in the future return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, BugMonitorResult.statusCodes.REPRODUCED_TIP) else: # Unlikely but possible, switched signal print "Result: Bug now reproduces with signal " + str(tret) + " (previously " + str(oret) + ")" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, BugMonitorResult.statusCodes.REPRODUCED_SWITCHED) else: print "Result: Bug no longer reproduces" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, BugMonitorResult.statusCodes.REPRODUCED_FIXED) else: print "Error: Failed to reproduce bug on original revision" return BugMonitorResult(reponame, rev, self.tipRev[tipRepoDir], opts, testFile, archType, compileType, BugMonitorResult.statusCodes.FAILED) def extractOptions(self, text): ret = re.compile('((?: \-[a-z])+)', re.DOTALL).search(text) if (ret != None and ret.groups > 1): return ret.group(1).lstrip().split(" ") return None def extractRevision(self, text): if (text == None): return None tokens = text.split(' ') for token in tokens: if (re.match('^[a-f0-9]{12}[^a-f0-9]?', token)): return token[0:12] return None def hgUpdate(self, repoDir, rev=None): print "Running hg update..." if (rev != None): captureStdout(['hg', 'update', '-r', rev], ignoreStderr=True, currWorkingDir=repoDir) else: captureStdout(['hg', 'update'], ignoreStderr=True, currWorkingDir=repoDir) hgIdCmdList = ['hg', 'identify', repoDir] # In Windows, this throws up a warning about failing to set color mode to win32. if platform.system() == 'Windows': hgIdFull = captureStdout(hgIdCmdList, currWorkingDir=repoDir, ignoreStderr=True)[0] else: hgIdFull = captureStdout(hgIdCmdList, currWorkingDir=repoDir)[0] hgIdChangesetHash = hgIdFull.split(' ')[0] #os.chdir(savedPath) return hgIdChangesetHash def getCachedShell(self, shellCacheDir, archNum, compileType, valgrindSupport, rev): cachedShell = os.path.join(shellCacheDir, shellName(archNum, compileType, rev, valgrindSupport)) if os.path.exists(cachedShell): return cachedShell return None def getShell(self, shellCacheDir, archNum, compileType, valgrindSupport, rev, updated, repoDir): shell = self.getCachedShell(shellCacheDir, archNum, compileType, valgrindSupport, rev) updRev = None if (shell == None): if updated: updRev = rev else: updRev = self.hgUpdate(repoDir, rev) if (rev == None): print "Compiling a new shell for tip (revision " + updRev + ")" else: print "Compiling a new shell for revision " + updRev shell = makeShell(shellCacheDir, repoDir, archNum, compileType, valgrindSupport, updRev, True) return (shell, updRev)