2018-01-08 21:36:55 -05:00
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
2018-02-13 23:47:57 -05:00
import argparse
2018-03-29 13:51:39 -04:00
import http . client
2018-01-08 21:36:55 -05:00
import os
import re
2019-11-21 17:28:46 -05:00
import shutil
2020-06-20 00:36:03 -04:00
import ssl
2018-01-08 21:36:55 -05:00
import subprocess
import sys
2018-03-20 11:11:33 -04:00
import time
2018-02-13 23:47:57 -05:00
import traceback
2018-01-08 21:36:55 -05:00
import urllib . error
import urllib . request
from textwrap import dedent
# Example: Checking out Revision e441a99009a557f82ea17ee9f9c3e9b89c75cee6 (refs/remotes/origin/master)
2018-02-13 23:47:57 -05:00
reGitRev = re . compile ( r ' Checking out Revision ( \ S+) \ s+ \ (refs/remotes/origin/([^)]+) ' )
2018-01-08 21:36:55 -05:00
2018-02-23 19:20:14 -05:00
# 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
# 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%%"
# ASF Jenkins example: [Lucene-Solr-Tests-master] $ /home/jenkins/tools/ant/apache-ant-1.8.4/bin/ant jenkins-hourly
# 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
# ASF Jenkins smoker example: [Lucene-Solr-SmokeRelease-master] $ /home/jenkins/tools/ant/apache-ant-1.8.4/bin/ant nightly-smoke
reAntInvocation = re . compile ( r ' \ bant(?: \ .bat)? \ s+.*(?:jenkins-(?:hourly|nightly)|nightly-smoke) ' )
reAntSysprops = re . compile ( r ' " -D[^ " ]+ " |-D[^=]+= " [^ " ]* " |-D \ S+ ' )
2022-05-09 23:03:55 -04:00
# Method example: NOTE: reproduce with: ant test -Dtestcase=ZkSolrClientTest -Dtests.method=testMultipleWatchesAsync -Dtests.seed=6EF5AB70F0032849 -Dtests.locale=he-IL -Dtests.timezone=NST -Dtests.asserts=true -Dtests.file.encoding=UTF-8
# Suite example: NOTE: reproduce with: ant test -Dtestcase=CloudSolrClientTest -Dtests.seed=DB2DF2D8228BAF27 -Dtests.multiplier=3 -Dtests.locale=es-AR -Dtests.timezone=America/Argentina/Cordoba -Dtests.asserts=true -Dtests.file.encoding=US-ASCII
2018-01-08 21:36:55 -05:00
reReproLine = re . compile ( r ' NOTE: \ s+reproduce \ s+with:( \ s+ant \ s+test \ s+-Dtestcase=( \ S+) \ s+(?:-Dtests.method= \ S+ \ s+)?(.*)) ' )
2018-02-13 23:47:57 -05:00
reTestsSeed = re . compile ( r ' -Dtests.seed= \ S+ \ s* ' )
2018-01-08 21:36:55 -05:00
# Example: https://jenkins.thetaphi.de/job/Lucene-Solr-master-Linux/21108/
reJenkinsURLWithoutConsoleText = re . compile ( r ' https?://.*/ \ d+/? \ Z ' , re . IGNORECASE )
reJavaFile = re . compile ( r ' (.*) \ .java \ Z ' )
2018-02-26 18:43:52 -05:00
reModule = re . compile ( r ' \ .[ \\ /](.*)[ \\ /]src[ \\ /] ' )
2018-01-08 21:36:55 -05:00
reTestOutputFile = re . compile ( r ' TEST-(.* \ .([^-.]+))(?:- \ d+)? \ .xml \ Z ' )
reErrorFailure = re . compile ( r ' (?:errors|failures)= " [^0] ' )
2018-02-16 10:45:46 -05:00
reGitMainBranch = re . compile ( r ' ^(?:master|branch_[x_ \ d]+)$ ' )
2018-01-08 21:36:55 -05:00
# consoleText from Policeman Jenkins's Windows jobs fails to decode as UTF-8
encoding = ' iso-8859-1 '
lastFailureCode = 0
gitCheckoutSucceeded = False
2018-02-13 23:47:57 -05:00
description = dedent ( ''' \
Must be run from a Lucene / Solr git workspace . Downloads the Jenkins
log pointed to by the given URL , parses it for Git revision and failed
Lucene / Solr tests , checks out the Git revision in the local workspace ,
groups the failed tests by module , then runs
' ant test -Dtest.dups= %d -Dtests.class= " *.test1[|*.test2[...]] " ... '
in each module of interest , failing at the end if any of the runs fails .
To control the maximum number of concurrent JVMs used for each module ' s
test run , set ' tests.jvms ' , e . g . in ~ / lucene . build . properties
''' )
defaultIters = 5
def readConfig ( ) :
parser = argparse . ArgumentParser ( formatter_class = argparse . RawDescriptionHelpFormatter ,
description = description )
parser . add_argument ( ' url ' , metavar = ' URL ' ,
help = ' Points to the Jenkins log to parse ' )
2018-02-22 16:52:09 -05:00
parser . add_argument ( ' --no-git ' , dest = ' useGit ' , action = ' store_false ' , default = True ,
help = ' Do not run " git " at all ' )
2018-02-13 23:47:57 -05:00
parser . add_argument ( ' --iters ' , dest = ' testIters ' , type = int , default = defaultIters , metavar = ' N ' ,
help = ' Number of iterations per test suite (default: %d ) ' % defaultIters )
return parser . parse_args ( )
2018-01-08 21:36:55 -05:00
def runOutput ( cmd ) :
print ( ' [repro] %s ' % cmd )
try :
return subprocess . check_output ( cmd . split ( ' ' ) , universal_newlines = True ) . strip ( )
except CalledProcessError as e :
raise RuntimeError ( " ERROR: Cmd ' %s ' failed with exit code %d and the following output: \n %s "
% ( cmd , e . returncode , e . output ) )
# Remembers non-zero exit code in lastFailureCode unless rememberFailure==False
def run ( cmd , rememberFailure = True ) :
global lastFailureCode
print ( ' [repro] %s ' % cmd )
code = os . system ( cmd )
if 0 != code and rememberFailure :
print ( ' \n [repro] Setting last failure code to %d \n ' % code )
lastFailureCode = code
return code
2018-03-20 11:11:33 -04:00
def fetchAndParseJenkinsLog ( url , numRetries ) :
2018-02-13 23:47:57 -05:00
global revisionFromLog
global branchFromLog
2018-02-23 19:20:14 -05:00
global antOptions
2018-02-13 23:47:57 -05:00
revisionFromLog = None
2018-02-23 19:20:14 -05:00
antOptions = ' '
2018-02-13 23:47:57 -05:00
tests = { }
2018-01-08 21:36:55 -05:00
print ( ' [repro] Jenkins log URL: %s \n ' % url )
try :
2020-06-20 00:36:03 -04:00
# HTTPS fails at certificate validation, see LUCENE-9412, PEP-476
context = ssl . _create_unverified_context ( )
with urllib . request . urlopen ( url , context = context ) as consoleText :
2018-01-08 21:36:55 -05:00
for rawLine in consoleText :
line = rawLine . decode ( encoding )
match = reGitRev . match ( line )
if match is not None :
2018-02-13 23:47:57 -05:00
revisionFromLog = match . group ( 1 )
branchFromLog = match . group ( 2 )
print ( ' [repro] Revision: %s \n ' % revisionFromLog )
2018-01-08 21:36:55 -05:00
else :
match = reReproLine . search ( line )
if match is not None :
print ( ' [repro] Repro line: %s \n ' % match . group ( 1 ) )
testcase = match . group ( 2 )
reproLineWithoutMethod = match . group ( 3 ) . strip ( )
tests [ testcase ] = reproLineWithoutMethod
2018-02-23 19:20:14 -05:00
else :
match = reAntInvocation . search ( line )
if match is not None :
antOptions = ' ' . join ( reAntSysprops . findall ( line ) )
2018-02-26 18:47:05 -05:00
if len ( antOptions ) > 0 :
print ( ' [repro] Ant options: %s ' % antOptions )
2018-01-08 21:36:55 -05:00
except urllib . error . URLError as e :
raise RuntimeError ( ' ERROR: fetching %s : %s ' % ( url , e ) )
2018-03-20 11:11:33 -04:00
except http . client . IncompleteRead as e :
if numRetries > 0 :
print ( ' [repro] Encountered IncompleteRead exception, pausing and then retrying... ' )
time . sleep ( 2 ) # pause for 2 seconds
return fetchAndParseJenkinsLog ( url , numRetries - 1 )
else :
print ( ' [repro] Encountered IncompleteRead exception, aborting after too many retries. ' )
raise RuntimeError ( ' ERROR: fetching %s : %s ' % ( url , e ) )
2018-02-13 23:47:57 -05:00
if revisionFromLog == None :
2018-01-08 21:36:55 -05:00
if reJenkinsURLWithoutConsoleText . match ( url ) :
print ( ' [repro] Not a Jenkins log. Appending " /consoleText " and retrying ... \n ' )
2018-03-20 11:11:33 -04:00
return fetchAndParseJenkinsLog ( url + ' /consoleText ' , numRetries )
2018-01-08 21:36:55 -05:00
else :
raise RuntimeError ( ' ERROR: %s does not appear to be a Jenkins log. ' % url )
if 0 == len ( tests ) :
print ( ' [repro] No " reproduce with " lines found; exiting. ' )
sys . exit ( 0 )
2018-02-13 23:47:57 -05:00
return tests
2018-01-08 21:36:55 -05:00
2018-02-22 16:52:09 -05:00
def prepareWorkspace ( useGit , gitRef ) :
2018-01-08 21:36:55 -05:00
global gitCheckoutSucceeded
2018-02-22 16:52:09 -05:00
if useGit :
2018-02-13 23:47:57 -05:00
code = run ( ' git fetch ' )
if 0 != code :
raise RuntimeError ( ' ERROR: " git fetch " failed. See above. ' )
2018-02-22 16:52:09 -05:00
checkoutCmd = ' git checkout %s ' % gitRef
code = run ( checkoutCmd )
if 0 != code :
2018-06-20 09:38:12 -04:00
addWantedBranchCmd = " git remote set-branches --add origin %s " % gitRef
2018-06-19 22:21:26 -04:00
checkoutBranchCmd = ' git checkout -t -b %s origin/ %s ' % ( gitRef , gitRef ) # Checkout remote branch as new local branch
2018-06-20 09:38:12 -04:00
print ( ' " %s " failed. Trying " %s " and " %s " . ' % ( checkoutCmd , addWantedBranchCmd , checkoutBranchCmd ) )
code = run ( addWantedBranchCmd )
if 0 != code :
raise RuntimeError ( ' ERROR: " %s " failed. See above. ' % addWantedBranchCmd )
2018-06-19 22:21:26 -04:00
code = run ( checkoutBranchCmd )
if 0 != code :
raise RuntimeError ( ' ERROR: " %s " failed. See above. ' % checkoutBranchCmd )
2018-02-22 16:52:09 -05:00
gitCheckoutSucceeded = True
run ( ' git merge --ff-only ' , rememberFailure = False ) # Ignore failure on non-branch ref
2018-01-08 21:36:55 -05:00
code = run ( ' ant clean ' )
if 0 != code :
raise RuntimeError ( ' ERROR: " ant clean " failed. See above. ' )
2018-02-13 23:47:57 -05:00
def groupTestsByModule ( tests ) :
modules = { }
2018-01-08 21:36:55 -05:00
for ( dir , _ , files ) in os . walk ( ' . ' ) :
for file in files :
match = reJavaFile . search ( file )
if match is not None :
test = match . group ( 1 )
if test in tests :
match = reModule . match ( dir )
module = match . group ( 1 )
if module not in modules :
modules [ module ] = set ( )
modules [ module ] . add ( test )
print ( ' [repro] Test suites by module: ' )
for module in modules :
print ( ' [repro] %s ' % module )
for test in modules [ module ] :
print ( ' [repro] %s ' % test )
2018-02-13 23:47:57 -05:00
return modules
2018-01-08 21:36:55 -05:00
2018-02-13 23:47:57 -05:00
def runTests ( testIters , modules , tests ) :
2018-01-08 21:36:55 -05:00
cwd = os . getcwd ( )
2018-02-23 19:20:14 -05:00
testCmdline = ' ant test-nocompile -Dtests.dups= %d -Dtests.maxfailures= %d -Dtests.class= " %s " -Dtests.showOutput=onerror %s %s '
2018-01-08 21:36:55 -05:00
for module in modules :
moduleTests = list ( modules [ module ] )
testList = ' | ' . join ( map ( lambda t : ' *. %s ' % t , moduleTests ) )
numTests = len ( moduleTests )
params = tests [ moduleTests [ 0 ] ] # Assumption: all tests in this module have the same cmdline params
os . chdir ( module )
code = run ( ' ant compile-test ' )
try :
2018-02-13 23:47:57 -05:00
if 0 != code :
2018-01-08 21:36:55 -05:00
raise RuntimeError ( " ERROR: Compile failed in %s / with code %d . See above. " % ( module , code ) )
2018-02-23 19:20:14 -05:00
run ( testCmdline % ( testIters , testIters * numTests , testList , antOptions , params ) )
2018-01-08 21:36:55 -05:00
finally :
os . chdir ( cwd )
2019-11-21 17:28:46 -05:00
def printAndMoveReports ( testIters , newSubDir , location ) :
2018-01-08 21:36:55 -05:00
failures = { }
for start in ( ' lucene/build ' , ' solr/build ' ) :
for ( dir , _ , files ) in os . walk ( start ) :
for file in files :
testOutputFileMatch = reTestOutputFile . search ( file )
if testOutputFileMatch is not None :
testcase = testOutputFileMatch . group ( 1 )
if testcase not in failures :
failures [ testcase ] = 0
2019-11-21 17:28:46 -05:00
filePath = os . path . join ( dir , file )
with open ( filePath , encoding = ' UTF-8 ' ) as testOutputFile :
2018-01-08 21:36:55 -05:00
for line in testOutputFile :
errorFailureMatch = reErrorFailure . search ( line )
if errorFailureMatch is not None :
failures [ testcase ] + = 1
break
2019-11-22 10:22:39 -05:00
# have to play nice with 'ant clean'...
2019-11-21 17:28:46 -05:00
newDirPath = os . path . join ( ' repro-reports ' , newSubDir , dir )
os . makedirs ( newDirPath , exist_ok = True )
os . rename ( filePath , os . path . join ( newDirPath , file ) )
2018-02-13 23:47:57 -05:00
print ( " [repro] Failures %s : " % location )
for testcase in sorted ( failures , key = lambda t : ( failures [ t ] , t ) ) : # sort by failure count, then by testcase
2018-01-08 21:36:55 -05:00
print ( " [repro] %d / %d failed: %s " % ( failures [ testcase ] , testIters , testcase ) )
2018-02-13 23:47:57 -05:00
return failures
2018-01-08 21:36:55 -05:00
2018-02-13 23:47:57 -05:00
def getLocalGitBranch ( ) :
2018-01-08 21:36:55 -05:00
origGitBranch = runOutput ( ' git rev-parse --abbrev-ref HEAD ' )
2018-02-13 23:47:57 -05:00
if origGitBranch == ' HEAD ' : # In detached HEAD state
2018-01-08 21:36:55 -05:00
origGitBranch = runOutput ( ' git rev-parse HEAD ' ) # Use the SHA when not on a branch
print ( ' [repro] Initial local git branch/revision: %s ' % origGitBranch )
2018-02-13 23:47:57 -05:00
return origGitBranch
2018-01-08 21:36:55 -05:00
def main ( ) :
2018-02-13 23:47:57 -05:00
config = readConfig ( )
2018-03-20 11:11:33 -04:00
tests = fetchAndParseJenkinsLog ( config . url , numRetries = 2 )
2018-02-22 16:52:09 -05:00
if config . useGit :
localGitBranch = getLocalGitBranch ( )
2018-01-08 21:36:55 -05:00
try :
2019-11-21 17:28:46 -05:00
# have to play nice with ant clean, so printAndMoveReports will move all the junit XML files here...
print ( ' [repro] JUnit rest result XML files will be moved to: ./repro-reports ' )
if os . path . isdir ( ' repro-reports ' ) :
print ( ' [repro] Deleting old ./repro-reports ' ) ;
shutil . rmtree ( ' repro-reports ' )
2018-02-22 16:52:09 -05:00
prepareWorkspace ( config . useGit , revisionFromLog )
2018-02-13 23:47:57 -05:00
modules = groupTestsByModule ( tests )
runTests ( config . testIters , modules , tests )
2019-11-21 17:28:46 -05:00
failures = printAndMoveReports ( config . testIters , ' orig ' ,
' w/original seeds ' + ( ' at %s ' % revisionFromLog if config . useGit else ' ' ) )
2018-02-13 23:47:57 -05:00
2018-02-22 16:52:09 -05:00
if config . useGit :
# Retest 100% failures at the tip of the branch
2018-02-13 23:47:57 -05:00
oldTests = tests
tests = { }
for fullClass in failures :
testcase = fullClass [ ( fullClass . rindex ( ' . ' ) + 1 ) : ]
if failures [ fullClass ] == config . testIters :
2018-02-22 16:52:09 -05:00
tests [ testcase ] = oldTests [ testcase ]
2018-02-13 23:47:57 -05:00
if len ( tests ) > 0 :
2018-02-22 16:52:09 -05:00
print ( ' \n [repro] Re-testing 100 %% failures at the tip of %s ' % branchFromLog )
2018-06-19 19:33:49 -04:00
prepareWorkspace ( True , branchFromLog )
2018-02-13 23:47:57 -05:00
modules = groupTestsByModule ( tests )
runTests ( config . testIters , modules , tests )
2019-11-21 17:28:46 -05:00
failures = printAndMoveReports ( config . testIters , ' branch-tip ' ,
' original seeds at the tip of %s ' % branchFromLog )
2018-02-22 16:52:09 -05:00
# Retest 100% tip-of-branch failures without a seed
oldTests = tests
tests = { }
for fullClass in failures :
testcase = fullClass [ ( fullClass . rindex ( ' . ' ) + 1 ) : ]
if failures [ fullClass ] == config . testIters :
tests [ testcase ] = re . sub ( reTestsSeed , ' ' , oldTests [ testcase ] )
if len ( tests ) > 0 :
print ( ' \n [repro] Re-testing 100 %% failures at the tip of %s without a seed ' % branchFromLog )
prepareWorkspace ( False , branchFromLog )
modules = groupTestsByModule ( tests )
runTests ( config . testIters , modules , tests )
2019-11-21 17:28:46 -05:00
printAndMoveReports ( config . testIters , ' branch-tip-no-seed ' ,
' at the tip of %s without a seed ' % branchFromLog )
2018-01-08 21:36:55 -05:00
except Exception as e :
2018-02-13 23:47:57 -05:00
print ( ' [repro] %s ' % traceback . format_exc ( ) )
2018-01-08 21:36:55 -05:00
sys . exit ( 1 )
finally :
2018-02-22 16:52:09 -05:00
if config . useGit and gitCheckoutSucceeded :
2018-02-13 23:47:57 -05:00
run ( ' git checkout %s ' % localGitBranch , rememberFailure = False ) # Restore original git branch/sha
2018-01-08 21:36:55 -05:00
print ( ' [repro] Exiting with code %d ' % lastFailureCode )
sys . exit ( lastFailureCode )
if __name__ == ' __main__ ' :
try :
main ( )
except KeyboardInterrupt :
print ( ' [repro] Keyboard interrupt...exiting ' )