1f7e166e7SSteve Rowe# Licensed to the Apache Software Foundation (ASF) under one or more 2f7e166e7SSteve Rowe# contributor license agreements. See the NOTICE file distributed with 3f7e166e7SSteve Rowe# this work for additional information regarding copyright ownership. 4f7e166e7SSteve Rowe# The ASF licenses this file to You under the Apache License, Version 2.0 5f7e166e7SSteve Rowe# (the "License"); you may not use this file except in compliance with 6f7e166e7SSteve Rowe# the License. You may obtain a copy of the License at 7f7e166e7SSteve Rowe# 8f7e166e7SSteve Rowe# http://www.apache.org/licenses/LICENSE-2.0 9f7e166e7SSteve Rowe# 10f7e166e7SSteve Rowe# Unless required by applicable law or agreed to in writing, software 11f7e166e7SSteve Rowe# distributed under the License is distributed on an "AS IS" BASIS, 12f7e166e7SSteve Rowe# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13f7e166e7SSteve Rowe# See the License for the specific language governing permissions and 14f7e166e7SSteve Rowe# limitations under the License. 15f7e166e7SSteve Rowe 16a07493d5SSteve Roweimport argparse 171ce72537SSteve Roweimport http.client 18f7e166e7SSteve Roweimport os 19f7e166e7SSteve Roweimport re 202d1e67c8SChris Hostetterimport shutil 21*591d844eSMike Drobimport ssl 22f7e166e7SSteve Roweimport subprocess 23f7e166e7SSteve Roweimport sys 240c61c857SSteve Roweimport time 25a07493d5SSteve Roweimport traceback 26f7e166e7SSteve Roweimport urllib.error 27f7e166e7SSteve Roweimport urllib.request 28f7e166e7SSteve Rowefrom textwrap import dedent 29f7e166e7SSteve Rowe 30f7e166e7SSteve Rowe# Example: Checking out Revision e441a99009a557f82ea17ee9f9c3e9b89c75cee6 (refs/remotes/origin/master) 31a07493d5SSteve RowereGitRev = re.compile(r'Checking out Revision (\S+)\s+\(refs/remotes/origin/([^)]+)') 32f7e166e7SSteve Rowe 3388e00d08SSteve Rowe# Policeman Jenkins example: [Lucene-Solr-7.x-Linux] $ /var/lib/jenkins/tools/hudson.tasks.Ant_AntInstallation/ANT_1.8.2/bin/ant "-Dargs=-XX:-UseCompressedOops -XX:+UseConcMarkSweepGC" jenkins-hourly 3488e00d08SSteve Rowe# Policeman Jenkins Windows example: [Lucene-Solr-master-Windows] $ cmd.exe /C "C:\Users\jenkins\tools\hudson.tasks.Ant_AntInstallation\ANT_1.8.2\bin\ant.bat '"-Dargs=-client -XX:+UseConcMarkSweepGC"' jenkins-hourly && exit %%ERRORLEVEL%%" 3588e00d08SSteve Rowe# ASF Jenkins example: [Lucene-Solr-Tests-master] $ /home/jenkins/tools/ant/apache-ant-1.8.4/bin/ant jenkins-hourly 3688e00d08SSteve Rowe# ASF Jenkins nightly example: [checkout] $ /home/jenkins/tools/ant/apache-ant-1.8.4/bin/ant -file build.xml -Dtests.multiplier=2 -Dtests.linedocsfile=/home/jenkins/jenkins-slave/workspace/Lucene-Solr-NightlyTests-master/test-data/enwiki.random.lines.txt jenkins-nightly 3788e00d08SSteve Rowe# ASF Jenkins smoker example: [Lucene-Solr-SmokeRelease-master] $ /home/jenkins/tools/ant/apache-ant-1.8.4/bin/ant nightly-smoke 3888e00d08SSteve RowereAntInvocation = re.compile(r'\bant(?:\.bat)?\s+.*(?:jenkins-(?:hourly|nightly)|nightly-smoke)') 3988e00d08SSteve RowereAntSysprops = re.compile(r'"-D[^"]+"|-D[^=]+="[^"]*"|-D\S+') 4088e00d08SSteve Rowe 41f7e166e7SSteve Rowe# Method example: NOTE: reproduce with: ant test -Dtestcase=ZkSolrClientTest -Dtests.method=testMultipleWatchesAsync -Dtests.seed=6EF5AB70F0032849 -Dtests.slow=true -Dtests.locale=he-IL -Dtests.timezone=NST -Dtests.asserts=true -Dtests.file.encoding=UTF-8 42f7e166e7SSteve Rowe# Suite example: NOTE: reproduce with: ant test -Dtestcase=CloudSolrClientTest -Dtests.seed=DB2DF2D8228BAF27 -Dtests.multiplier=3 -Dtests.slow=true -Dtests.locale=es-AR -Dtests.timezone=America/Argentina/Cordoba -Dtests.asserts=true -Dtests.file.encoding=US-ASCII 43f7e166e7SSteve RowereReproLine = re.compile(r'NOTE:\s+reproduce\s+with:(\s+ant\s+test\s+-Dtestcase=(\S+)\s+(?:-Dtests.method=\S+\s+)?(.*))') 44a07493d5SSteve RowereTestsSeed = re.compile(r'-Dtests.seed=\S+\s*') 45f7e166e7SSteve Rowe 46f7e166e7SSteve Rowe# Example: https://jenkins.thetaphi.de/job/Lucene-Solr-master-Linux/21108/ 47f7e166e7SSteve RowereJenkinsURLWithoutConsoleText = re.compile(r'https?://.*/\d+/?\Z', re.IGNORECASE) 48f7e166e7SSteve Rowe 49f7e166e7SSteve RowereJavaFile = re.compile(r'(.*)\.java\Z') 50606e91c2SSteve RowereModule = re.compile(r'\.[\\/](.*)[\\/]src[\\/]') 51f7e166e7SSteve RowereTestOutputFile = re.compile(r'TEST-(.*\.([^-.]+))(?:-\d+)?\.xml\Z') 52f7e166e7SSteve RowereErrorFailure = re.compile(r'(?:errors|failures)="[^0]') 53e71286c8SSteve RowereGitMainBranch = re.compile(r'^(?:master|branch_[x_\d]+)$') 54f7e166e7SSteve Rowe 55f7e166e7SSteve Rowe# consoleText from Policeman Jenkins's Windows jobs fails to decode as UTF-8 56f7e166e7SSteve Roweencoding = 'iso-8859-1' 57f7e166e7SSteve Rowe 58f7e166e7SSteve RowelastFailureCode = 0 59f7e166e7SSteve RowegitCheckoutSucceeded = False 60f7e166e7SSteve Rowe 61a07493d5SSteve Rowedescription = dedent('''\ 62a07493d5SSteve Rowe Must be run from a Lucene/Solr git workspace. Downloads the Jenkins 63a07493d5SSteve Rowe log pointed to by the given URL, parses it for Git revision and failed 64a07493d5SSteve Rowe Lucene/Solr tests, checks out the Git revision in the local workspace, 65a07493d5SSteve Rowe groups the failed tests by module, then runs 66a07493d5SSteve Rowe 'ant test -Dtest.dups=%d -Dtests.class="*.test1[|*.test2[...]]" ...' 67a07493d5SSteve Rowe in each module of interest, failing at the end if any of the runs fails. 68a07493d5SSteve Rowe To control the maximum number of concurrent JVMs used for each module's 69a07493d5SSteve Rowe test run, set 'tests.jvms', e.g. in ~/lucene.build.properties 70a07493d5SSteve Rowe ''') 71a07493d5SSteve RowedefaultIters = 5 72a07493d5SSteve Rowe 73a07493d5SSteve Rowedef readConfig(): 74a07493d5SSteve Rowe parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, 75a07493d5SSteve Rowe description=description) 76a07493d5SSteve Rowe parser.add_argument('url', metavar='URL', 77a07493d5SSteve Rowe help='Points to the Jenkins log to parse') 78edd54e55SSteve Rowe parser.add_argument('--no-git', dest='useGit', action='store_false', default=True, 79edd54e55SSteve Rowe help='Do not run "git" at all') 80a07493d5SSteve Rowe parser.add_argument('--iters', dest='testIters', type=int, default=defaultIters, metavar='N', 81a07493d5SSteve Rowe help='Number of iterations per test suite (default: %d)' % defaultIters) 82a07493d5SSteve Rowe return parser.parse_args() 83a07493d5SSteve Rowe 84f7e166e7SSteve Rowedef runOutput(cmd): 85f7e166e7SSteve Rowe print('[repro] %s' % cmd) 86f7e166e7SSteve Rowe try: 87f7e166e7SSteve Rowe return subprocess.check_output(cmd.split(' '), universal_newlines=True).strip() 88f7e166e7SSteve Rowe except CalledProcessError as e: 89f7e166e7SSteve Rowe raise RuntimeError("ERROR: Cmd '%s' failed with exit code %d and the following output:\n%s" 90f7e166e7SSteve Rowe % (cmd, e.returncode, e.output)) 91f7e166e7SSteve Rowe 92f7e166e7SSteve Rowe# Remembers non-zero exit code in lastFailureCode unless rememberFailure==False 93f7e166e7SSteve Rowedef run(cmd, rememberFailure=True): 94f7e166e7SSteve Rowe global lastFailureCode 95f7e166e7SSteve Rowe print('[repro] %s' % cmd) 96f7e166e7SSteve Rowe code = os.system(cmd) 97f7e166e7SSteve Rowe if 0 != code and rememberFailure: 98f7e166e7SSteve Rowe print('\n[repro] Setting last failure code to %d\n' % code) 99f7e166e7SSteve Rowe lastFailureCode = code 100f7e166e7SSteve Rowe return code 101f7e166e7SSteve Rowe 1020c61c857SSteve Rowedef fetchAndParseJenkinsLog(url, numRetries): 103a07493d5SSteve Rowe global revisionFromLog 104a07493d5SSteve Rowe global branchFromLog 10588e00d08SSteve Rowe global antOptions 106a07493d5SSteve Rowe revisionFromLog = None 10788e00d08SSteve Rowe antOptions = '' 108a07493d5SSteve Rowe tests = {} 109f7e166e7SSteve Rowe print('[repro] Jenkins log URL: %s\n' % url) 110f7e166e7SSteve Rowe try: 111*591d844eSMike Drob # HTTPS fails at certificate validation, see LUCENE-9412, PEP-476 112*591d844eSMike Drob context = ssl._create_unverified_context() 113*591d844eSMike Drob with urllib.request.urlopen(url, context=context) as consoleText: 114f7e166e7SSteve Rowe for rawLine in consoleText: 115f7e166e7SSteve Rowe line = rawLine.decode(encoding) 116f7e166e7SSteve Rowe match = reGitRev.match(line) 117f7e166e7SSteve Rowe if match is not None: 118a07493d5SSteve Rowe revisionFromLog = match.group(1) 119a07493d5SSteve Rowe branchFromLog = match.group(2) 120a07493d5SSteve Rowe print('[repro] Revision: %s\n' % revisionFromLog) 121f7e166e7SSteve Rowe else: 122f7e166e7SSteve Rowe match = reReproLine.search(line) 123f7e166e7SSteve Rowe if match is not None: 124f7e166e7SSteve Rowe print('[repro] Repro line: %s\n' % match.group(1)) 125f7e166e7SSteve Rowe testcase = match.group(2) 126f7e166e7SSteve Rowe reproLineWithoutMethod = match.group(3).strip() 127f7e166e7SSteve Rowe tests[testcase] = reproLineWithoutMethod 12888e00d08SSteve Rowe else: 12988e00d08SSteve Rowe match = reAntInvocation.search(line) 13088e00d08SSteve Rowe if match is not None: 13188e00d08SSteve Rowe antOptions = ' '.join(reAntSysprops.findall(line)) 13288e00d08SSteve Rowe if len(antOptions) > 0: 13388e00d08SSteve Rowe print('[repro] Ant options: %s' % antOptions) 134f7e166e7SSteve Rowe except urllib.error.URLError as e: 135f7e166e7SSteve Rowe raise RuntimeError('ERROR: fetching %s : %s' % (url, e)) 1360c61c857SSteve Rowe except http.client.IncompleteRead as e: 1370c61c857SSteve Rowe if numRetries > 0: 1380c61c857SSteve Rowe print('[repro] Encountered IncompleteRead exception, pausing and then retrying...') 1390c61c857SSteve Rowe time.sleep(2) # pause for 2 seconds 1400c61c857SSteve Rowe return fetchAndParseJenkinsLog(url, numRetries - 1) 1410c61c857SSteve Rowe else: 1420c61c857SSteve Rowe print('[repro] Encountered IncompleteRead exception, aborting after too many retries.') 1430c61c857SSteve Rowe raise RuntimeError('ERROR: fetching %s : %s' % (url, e)) 144f7e166e7SSteve Rowe 145a07493d5SSteve Rowe if revisionFromLog == None: 146f7e166e7SSteve Rowe if reJenkinsURLWithoutConsoleText.match(url): 147f7e166e7SSteve Rowe print('[repro] Not a Jenkins log. Appending "/consoleText" and retrying ...\n') 1480c61c857SSteve Rowe return fetchAndParseJenkinsLog(url + '/consoleText', numRetries) 149f7e166e7SSteve Rowe else: 150f7e166e7SSteve Rowe raise RuntimeError('ERROR: %s does not appear to be a Jenkins log.' % url) 151f7e166e7SSteve Rowe if 0 == len(tests): 152f7e166e7SSteve Rowe print('[repro] No "reproduce with" lines found; exiting.') 153f7e166e7SSteve Rowe sys.exit(0) 154a07493d5SSteve Rowe return tests 155f7e166e7SSteve Rowe 156edd54e55SSteve Rowedef prepareWorkspace(useGit, gitRef): 157f7e166e7SSteve Rowe global gitCheckoutSucceeded 158edd54e55SSteve Rowe if useGit: 159a07493d5SSteve Rowe code = run('git fetch') 160f7e166e7SSteve Rowe if 0 != code: 161a07493d5SSteve Rowe raise RuntimeError('ERROR: "git fetch" failed. See above.') 162a07493d5SSteve Rowe checkoutCmd = 'git checkout %s' % gitRef 163a07493d5SSteve Rowe code = run(checkoutCmd) 164a07493d5SSteve Rowe if 0 != code: 165bcbcb16bSSteve Rowe addWantedBranchCmd = "git remote set-branches --add origin %s" % gitRef 16674bd5d56SSteve Rowe checkoutBranchCmd = 'git checkout -t -b %s origin/%s' % (gitRef, gitRef) # Checkout remote branch as new local branch 167bcbcb16bSSteve Rowe print('"%s" failed. Trying "%s" and "%s".' % (checkoutCmd, addWantedBranchCmd, checkoutBranchCmd)) 168bcbcb16bSSteve Rowe code = run(addWantedBranchCmd) 169bcbcb16bSSteve Rowe if 0 != code: 170bcbcb16bSSteve Rowe raise RuntimeError('ERROR: "%s" failed. See above.' % addWantedBranchCmd) 17174bd5d56SSteve Rowe code = run(checkoutBranchCmd) 17274bd5d56SSteve Rowe if 0 != code: 17374bd5d56SSteve Rowe raise RuntimeError('ERROR: "%s" failed. See above.' % checkoutBranchCmd) 174f7e166e7SSteve Rowe gitCheckoutSucceeded = True 175e71286c8SSteve Rowe run('git merge --ff-only', rememberFailure=False) # Ignore failure on non-branch ref 176edd54e55SSteve Rowe 177f7e166e7SSteve Rowe code = run('ant clean') 178f7e166e7SSteve Rowe if 0 != code: 179f7e166e7SSteve Rowe raise RuntimeError('ERROR: "ant clean" failed. See above.') 180f7e166e7SSteve Rowe 181a07493d5SSteve Rowedef groupTestsByModule(tests): 182a07493d5SSteve Rowe modules = {} 183f7e166e7SSteve Rowe for (dir, _, files) in os.walk('.'): 184f7e166e7SSteve Rowe for file in files: 185f7e166e7SSteve Rowe match = reJavaFile.search(file) 186f7e166e7SSteve Rowe if match is not None: 187f7e166e7SSteve Rowe test = match.group(1) 188f7e166e7SSteve Rowe if test in tests: 189f7e166e7SSteve Rowe match = reModule.match(dir) 190f7e166e7SSteve Rowe module = match.group(1) 191f7e166e7SSteve Rowe if module not in modules: 192f7e166e7SSteve Rowe modules[module] = set() 193f7e166e7SSteve Rowe modules[module].add(test) 194f7e166e7SSteve Rowe print('[repro] Test suites by module:') 195f7e166e7SSteve Rowe for module in modules: 196f7e166e7SSteve Rowe print('[repro] %s' % module) 197f7e166e7SSteve Rowe for test in modules[module]: 198f7e166e7SSteve Rowe print('[repro] %s' % test) 199a07493d5SSteve Rowe return modules 200f7e166e7SSteve Rowe 201a07493d5SSteve Rowedef runTests(testIters, modules, tests): 202f7e166e7SSteve Rowe cwd = os.getcwd() 20388e00d08SSteve Rowe testCmdline = 'ant test-nocompile -Dtests.dups=%d -Dtests.maxfailures=%d -Dtests.class="%s" -Dtests.showOutput=onerror %s %s' 204f7e166e7SSteve Rowe for module in modules: 205f7e166e7SSteve Rowe moduleTests = list(modules[module]) 206f7e166e7SSteve Rowe testList = '|'.join(map(lambda t: '*.%s' % t, moduleTests)) 207f7e166e7SSteve Rowe numTests = len(moduleTests) 208f7e166e7SSteve Rowe params = tests[moduleTests[0]] # Assumption: all tests in this module have the same cmdline params 209f7e166e7SSteve Rowe os.chdir(module) 210f7e166e7SSteve Rowe code = run('ant compile-test') 211f7e166e7SSteve Rowe try: 212a07493d5SSteve Rowe if 0 != code: 213f7e166e7SSteve Rowe raise RuntimeError("ERROR: Compile failed in %s/ with code %d. See above." % (module, code)) 21488e00d08SSteve Rowe run(testCmdline % (testIters, testIters * numTests, testList, antOptions, params)) 215f7e166e7SSteve Rowe finally: 216f7e166e7SSteve Rowe os.chdir(cwd) 217f7e166e7SSteve Rowe 2182d1e67c8SChris Hostetterdef printAndMoveReports(testIters, newSubDir, location): 219f7e166e7SSteve Rowe failures = {} 220f7e166e7SSteve Rowe for start in ('lucene/build', 'solr/build'): 221f7e166e7SSteve Rowe for (dir, _, files) in os.walk(start): 222f7e166e7SSteve Rowe for file in files: 223f7e166e7SSteve Rowe testOutputFileMatch = reTestOutputFile.search(file) 224f7e166e7SSteve Rowe if testOutputFileMatch is not None: 225f7e166e7SSteve Rowe testcase = testOutputFileMatch.group(1) 226f7e166e7SSteve Rowe if testcase not in failures: 227f7e166e7SSteve Rowe failures[testcase] = 0 2282d1e67c8SChris Hostetter filePath = os.path.join(dir, file) 2292d1e67c8SChris Hostetter with open(filePath, encoding='UTF-8') as testOutputFile: 230f7e166e7SSteve Rowe for line in testOutputFile: 231f7e166e7SSteve Rowe errorFailureMatch = reErrorFailure.search(line) 232f7e166e7SSteve Rowe if errorFailureMatch is not None: 233f7e166e7SSteve Rowe failures[testcase] += 1 234f7e166e7SSteve Rowe break 235acd56b35SChris Hostetter # have to play nice with 'ant clean'... 2362d1e67c8SChris Hostetter newDirPath = os.path.join('repro-reports', newSubDir, dir) 2372d1e67c8SChris Hostetter os.makedirs(newDirPath, exist_ok=True) 2382d1e67c8SChris Hostetter os.rename(filePath, os.path.join(newDirPath, file)) 239a07493d5SSteve Rowe print("[repro] Failures%s:" % location) 240a07493d5SSteve Rowe for testcase in sorted(failures, key=lambda t: (failures[t],t)): # sort by failure count, then by testcase 241f7e166e7SSteve Rowe print("[repro] %d/%d failed: %s" % (failures[testcase], testIters, testcase)) 242a07493d5SSteve Rowe return failures 243f7e166e7SSteve Rowe 244a07493d5SSteve Rowedef getLocalGitBranch(): 245f7e166e7SSteve Rowe origGitBranch = runOutput('git rev-parse --abbrev-ref HEAD') 246a07493d5SSteve Rowe if origGitBranch == 'HEAD': # In detached HEAD state 247f7e166e7SSteve Rowe origGitBranch = runOutput('git rev-parse HEAD') # Use the SHA when not on a branch 248f7e166e7SSteve Rowe print('[repro] Initial local git branch/revision: %s' % origGitBranch) 249a07493d5SSteve Rowe return origGitBranch 250f7e166e7SSteve Rowe 251f7e166e7SSteve Rowedef main(): 252a07493d5SSteve Rowe config = readConfig() 2530c61c857SSteve Rowe tests = fetchAndParseJenkinsLog(config.url, numRetries = 2) 254edd54e55SSteve Rowe if config.useGit: 255a07493d5SSteve Rowe localGitBranch = getLocalGitBranch() 256f7e166e7SSteve Rowe 257f7e166e7SSteve Rowe try: 2582d1e67c8SChris Hostetter # have to play nice with ant clean, so printAndMoveReports will move all the junit XML files here... 2592d1e67c8SChris Hostetter print('[repro] JUnit rest result XML files will be moved to: ./repro-reports') 2602d1e67c8SChris Hostetter if os.path.isdir('repro-reports'): 2612d1e67c8SChris Hostetter print('[repro] Deleting old ./repro-reports'); 2622d1e67c8SChris Hostetter shutil.rmtree('repro-reports') 263edd54e55SSteve Rowe prepareWorkspace(config.useGit, revisionFromLog) 264a07493d5SSteve Rowe modules = groupTestsByModule(tests) 265a07493d5SSteve Rowe runTests(config.testIters, modules, tests) 2662d1e67c8SChris Hostetter failures = printAndMoveReports(config.testIters, 'orig', 2672d1e67c8SChris Hostetter ' w/original seeds' + (' at %s' % revisionFromLog if config.useGit else '')) 2682d1e67c8SChris Hostetter 269a07493d5SSteve Rowe 270edd54e55SSteve Rowe if config.useGit: 271a07493d5SSteve Rowe # Retest 100% failures at the tip of the branch 272a07493d5SSteve Rowe oldTests = tests 273a07493d5SSteve Rowe tests = {} 274a07493d5SSteve Rowe for fullClass in failures: 275a07493d5SSteve Rowe testcase = fullClass[(fullClass.rindex('.') + 1):] 276a07493d5SSteve Rowe if failures[fullClass] == config.testIters: 277a07493d5SSteve Rowe tests[testcase] = oldTests[testcase] 278a07493d5SSteve Rowe if len(tests) > 0: 279a07493d5SSteve Rowe print('\n[repro] Re-testing 100%% failures at the tip of %s' % branchFromLog) 280d04acc95SSteve Rowe prepareWorkspace(True, branchFromLog) 281a07493d5SSteve Rowe modules = groupTestsByModule(tests) 282a07493d5SSteve Rowe runTests(config.testIters, modules, tests) 2832d1e67c8SChris Hostetter failures = printAndMoveReports(config.testIters, 'branch-tip', 2842d1e67c8SChris Hostetter ' original seeds at the tip of %s' % branchFromLog) 285a07493d5SSteve Rowe 286a07493d5SSteve Rowe # Retest 100% tip-of-branch failures without a seed 287a07493d5SSteve Rowe oldTests = tests 288a07493d5SSteve Rowe tests = {} 289a07493d5SSteve Rowe for fullClass in failures: 290a07493d5SSteve Rowe testcase = fullClass[(fullClass.rindex('.') + 1):] 291a07493d5SSteve Rowe if failures[fullClass] == config.testIters: 292a07493d5SSteve Rowe tests[testcase] = re.sub(reTestsSeed, '', oldTests[testcase]) 293a07493d5SSteve Rowe if len(tests) > 0: 294a07493d5SSteve Rowe print('\n[repro] Re-testing 100%% failures at the tip of %s without a seed' % branchFromLog) 295a07493d5SSteve Rowe prepareWorkspace(False, branchFromLog) 296a07493d5SSteve Rowe modules = groupTestsByModule(tests) 297a07493d5SSteve Rowe runTests(config.testIters, modules, tests) 2982d1e67c8SChris Hostetter printAndMoveReports(config.testIters, 'branch-tip-no-seed', 2992d1e67c8SChris Hostetter ' at the tip of %s without a seed' % branchFromLog) 300f7e166e7SSteve Rowe except Exception as e: 301a07493d5SSteve Rowe print('[repro] %s' % traceback.format_exc()) 302f7e166e7SSteve Rowe sys.exit(1) 303f7e166e7SSteve Rowe finally: 304edd54e55SSteve Rowe if config.useGit and gitCheckoutSucceeded: 305a07493d5SSteve Rowe run('git checkout %s' % localGitBranch, rememberFailure=False) # Restore original git branch/sha 306f7e166e7SSteve Rowe 307f7e166e7SSteve Rowe print('[repro] Exiting with code %d' % lastFailureCode) 308f7e166e7SSteve Rowe sys.exit(lastFailureCode) 309f7e166e7SSteve Rowe 310f7e166e7SSteve Roweif __name__ == '__main__': 311f7e166e7SSteve Rowe try: 312f7e166e7SSteve Rowe main() 313f7e166e7SSteve Rowe except KeyboardInterrupt: 314f7e166e7SSteve Rowe print('[repro] Keyboard interrupt...exiting') 315