xref: /Lucene/dev-tools/scripts/reproduceJenkinsFailures.py (revision 591d844e806c4887974d887097fe8401f66f7049)
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