1#!/usr/bin/env python 2from __future__ import print_function 3import glob, os, re 4import optparse 5import inspect 6 7""" 8Quick script for parsing the output of the test system and summarizing the results. 9""" 10 11def inInstallDir(): 12 """ 13 When petsc is installed then this file in installed in: 14 <PREFIX>/share/petsc/examples/config/gmakegentest.py 15 otherwise the path is: 16 <PETSC_DIR>/config/gmakegentest.py 17 We use this difference to determine if we are in installdir 18 """ 19 thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 20 dirlist=thisscriptdir.split(os.path.sep) 21 if len(dirlist)>4: 22 lastfour=os.path.sep.join(dirlist[len(dirlist)-4:]) 23 if lastfour==os.path.join('share','petsc','examples','config'): 24 return True 25 else: 26 return False 27 else: 28 return False 29 30def summarize_results(directory,make,ntime,etime): 31 ''' Loop over all of the results files and summarize the results''' 32 startdir = os.getcwd() 33 try: 34 os.chdir(directory) 35 except OSError: 36 print('# No tests run') 37 return 38 summary={'total':0,'success':0,'failed':0,'failures':[],'todo':0,'skip':0, 39 'time':0, 'cputime':0} 40 timesummary={} 41 cputimesummary={} 42 timelist=[] 43 for cfile in glob.glob('*.counts'): 44 with open(cfile, 'r') as f: 45 for line in f: 46 l = line.split() 47 if l[0] == 'failures': 48 if len(l)>1: 49 summary[l[0]] += l[1:] 50 elif l[0] == 'time': 51 if len(l)==1: continue 52 summary[l[0]] += float(l[1]) 53 summary['cputime'] += float(l[2]) 54 timesummary[cfile]=float(l[1]) 55 cputimesummary[cfile]=float(l[2]) 56 timelist.append(float(l[1])) 57 elif l[0] not in summary: 58 continue 59 else: 60 summary[l[0]] += int(l[1]) 61 62 failstr=' '.join(summary['failures']) 63 print("\n# -------------") 64 print("# Summary ") 65 print("# -------------") 66 if failstr.strip(): print("# FAILED " + failstr) 67 68 for t in "success failed todo skip".split(): 69 percent=summary[t]/float(summary['total'])*100 70 print("# %s %d/%d tests (%3.1f%%)" % (t, summary[t], summary['total'], percent)) 71 print("#") 72 if etime: 73 print("# Wall clock time for tests: %s sec"% etime) 74 print("# Approximate CPU time (not incl. build time): %s sec"% summary['cputime']) 75 76 if failstr.strip(): 77 fail_targets=( 78 re.sub('cmd-','', 79 re.sub('diff-','',failstr+' ')) 80 ) 81 print(fail_targets) 82 # Strip off characters from subtests 83 fail_list=[] 84 for failure in fail_targets.split(): 85 if failure.split('-')[1].count('_')>1: 86 froot=failure.split('-')[0] 87 flabel='_'.join(failure.split('-')[1].split('_')[0:1]) 88 fail_list.append(froot+'-'+flabel+'_*') 89 elif failure.count('-')>1: 90 fail_list.append('-'.join(failure.split('-')[:-1])) 91 else: 92 fail_list.append(failure) 93 fail_list=list(set(fail_list)) 94 fail_targets=' '.join(fail_list) 95 96 #Make the message nice 97 makefile="gmakefile.test" if inInstallDir() else "gmakefile" 98 99 print("#\n# To rerun failed tests: ") 100 print("# "+make+" -f "+makefile+" test globsearch='" + fail_targets.strip()+"'") 101 102 if ntime>0: 103 print("#\n# Timing summary (actual test time / total CPU time): ") 104 timelist=list(set(timelist)) 105 timelist.sort(reverse=True) 106 nlim=(ntime if ntime<len(timelist) else len(timelist)) 107 # Do a double loop to sort in order 108 for timelimit in timelist[0:nlim]: 109 for cf in timesummary: 110 if timesummary[cf] == timelimit: 111 print("# %s: %.2f sec / %.2f sec" % (re.sub('.counts','',cf), timesummary[cf], cputimesummary[cf])) 112 os.chdir(startdir) 113 return 114 115def get_test_data(directory): 116 """ 117 Create dictionary structure with test data 118 """ 119 startdir= os.getcwd() 120 try: 121 os.chdir(directory) 122 except OSError: 123 print('# No tests run') 124 return 125 # loop over *.counts files for all the problems tested in the test suite 126 testdata = {} 127 for cfile in glob.glob('*.counts'): 128 # first we get rid of the .counts extension, then we split the name in two 129 # to recover the problem name and the package it belongs to 130 fname = cfile.split('.')[0] 131 testname = fname.split('-') 132 probname = '' 133 for i in range(1,len(testname)): 134 probname += testname[i] 135 # we split the package into its subcomponents of PETSc module (e.g.: snes) 136 # and test type (e.g.: tutorial) 137 testname_list = testname[0].split('_') 138 pkgname = testname_list[0] 139 testtype = testname_list[-1] 140 # in order to correct assemble the folder path for problem outputs, we 141 # iterate over any possible subpackage names and test suffixes 142 testname_short = testname_list[:-1] 143 prob_subdir = os.path.join('', *testname_short) 144 probfolder = 'run%s'%probname 145 probdir = os.path.join('..', prob_subdir, 'examples', testtype, probfolder) 146 if not os.path.exists(probdir): 147 probfolder = probfolder.split('_')[0] 148 probdir = os.path.join('..', prob_subdir, 'examples', testtype, probfolder) 149 probfullpath=os.path.normpath(os.path.join(directory,probdir)) 150 # assemble the final full folder path for problem outputs and read the files 151 try: 152 with open('%s/diff-%s.out'%(probdir, probfolder),'r') as probdiff: 153 difflines = probdiff.readlines() 154 except IOError: 155 difflines = [] 156 try: 157 with open('%s/%s.err'%(probdir, probfolder),'r') as probstderr: 158 stderrlines = probstderr.readlines() 159 except IOError: 160 stderrlines = [] 161 try: 162 with open('%s/%s.tmp'%(probdir, probname), 'r') as probstdout: 163 stdoutlines = probstdout.readlines() 164 except IOError: 165 stdoutlines = [] 166 # join the package, subpackage and problem type names into a "class" 167 classname = pkgname 168 for item in testname_list[1:]: 169 classname += '.%s'%item 170 # if this is the first time we see this package, initialize its dict 171 if pkgname not in testdata.keys(): 172 testdata[pkgname] = { 173 'total':0, 174 'success':0, 175 'failed':0, 176 'errors':0, 177 'todo':0, 178 'skip':0, 179 'time':0, 180 'problems':{} 181 } 182 # add the dict for the problem into the dict for the package 183 testdata[pkgname]['problems'][probname] = { 184 'classname':classname, 185 'time':0, 186 'failed':False, 187 'skipped':False, 188 'diff':difflines, 189 'stdout':stdoutlines, 190 'stderr':stderrlines, 191 'probdir':probfullpath, 192 'fullname':fname 193 } 194 # process the *.counts file and increment problem status trackers 195 if len(testdata[pkgname]['problems'][probname]['stderr'])>0: 196 testdata[pkgname]['errors'] += 1 197 with open(cfile, 'r') as f: 198 for line in f: 199 l = line.split() 200 if l[0] == 'time': 201 if len(l)==1: continue 202 testdata[pkgname]['problems'][probname][l[0]] = float(l[1]) 203 testdata[pkgname][l[0]] += float(l[1]) 204 elif l[0] in testdata[pkgname].keys(): 205 num_int=int(l[1]) 206 testdata[pkgname][l[0]] += num_int 207 if l[0] in ['failed','skip'] and num_int: 208 testdata[pkgname]['problems'][probname][l[0]] = True 209 else: 210 continue 211 os.chdir(startdir) # Keep function in good state 212 return testdata 213 214def show_fail(testdata): 215 """ Show the failures and commands to run them 216 """ 217 for pkg in testdata.keys(): 218 testsuite = testdata[pkg] 219 for prob in testsuite['problems'].keys(): 220 p = testsuite['problems'][prob] 221 cdbase='cd '+p['probdir']+' && ' 222 if p['skipped']: 223 # if we got here, the TAP output shows a skipped test 224 pass 225 elif len(p['stderr'])>0: 226 # if we got here, the test crashed with an error 227 # we show the stderr output under <error> 228 shbase=os.path.join(p['probdir'], p['fullname']) 229 shfile=shbase+".sh" 230 if not os.path.exists(shfile): 231 shfile=glob.glob(shbase+"*")[0] 232 with open(shfile, 'r') as sh: 233 cmd = sh.read() 234 print(p['fullname']+': '+cdbase+cmd.split('>')[0]) 235 elif len(p['diff'])>0: 236 # if we got here, the test output did not match the stored output file 237 # we show the diff between new output and old output under <failure> 238 shbase=os.path.join(p['probdir'], 'diff-'+p['fullname']) 239 shfile=shbase+".sh" 240 if not os.path.exists(shfile): 241 shfile=glob.glob(shbase+"*")[0] 242 with open(shfile, 'r') as sh: 243 cmd = sh.read() 244 print(p['fullname']+': '+cdbase+cmd.split('>')[0]) 245 pass 246 return 247 248def generate_xml(testdata,directory): 249 """ write testdata information into a jUnit formatted XLM file 250 """ 251 startdir= os.getcwd() 252 try: 253 os.chdir(directory) 254 except OSError: 255 print('# No tests run') 256 return 257 junit = open('../testresults.xml', 'w') 258 junit.write('<?xml version="1.0" ?>\n') 259 junit.write('<testsuites>\n') 260 for pkg in testdata.keys(): 261 testsuite = testdata[pkg] 262 junit.write(' <testsuite errors="%i" failures="%i" name="%s" tests="%i">\n'%( 263 testsuite['errors'], testsuite['failed'], pkg, testsuite['total'])) 264 for prob in testsuite['problems'].keys(): 265 p = testsuite['problems'][prob] 266 junit.write(' <testcase classname="%s" name="%s" time="%f">\n'%( 267 p['classname'], prob, p['time'])) 268 if p['skipped']: 269 # if we got here, the TAP output shows a skipped test 270 junit.write(' <skipped/>\n') 271 elif len(p['stderr'])>0: 272 # if we got here, the test crashed with an error 273 # we show the stderr output under <error> 274 junit.write(' <error type="crash">\n') 275 junit.write("<![CDATA[\n") # CDATA is necessary to preserve whitespace 276 for line in p['stderr']: 277 junit.write("%s\n"%line.rstrip()) 278 junit.write("]]>") 279 junit.write(' </error>\n') 280 elif len(p['diff'])>0: 281 # if we got here, the test output did not match the stored output file 282 # we show the diff between new output and old output under <failure> 283 junit.write(' <failure type="output">\n') 284 junit.write("<![CDATA[\n") # CDATA is necessary to preserve whitespace 285 for line in p['diff']: 286 junit.write("%s\n"%line.rstrip()) 287 junit.write("]]>") 288 junit.write(' </failure>\n') 289 elif len(p['stdout'])>0: 290 # if we got here, the test succeeded so we just show the stdout 291 # for manual sanity-checks 292 junit.write(' <system-out>\n') 293 junit.write("<![CDATA[\n") # CDATA is necessary to preserve whitespace 294 count = 0 295 for line in p['stdout']: 296 junit.write("%s\n"%line.rstrip()) 297 count += 1 298 if count >= 1024: 299 break 300 junit.write("]]>") 301 junit.write(' </system-out>\n') 302 junit.write(' </testcase>\n') 303 junit.write(' </testsuite>\n') 304 junit.write('</testsuites>') 305 junit.close() 306 os.chdir(startdir) 307 return 308 309def main(): 310 parser = optparse.OptionParser(usage="%prog [options]") 311 parser.add_option('-d', '--directory', dest='directory', 312 help='Directory containing results of petsc test system', 313 default=os.path.join(os.environ.get('PETSC_ARCH',''), 314 'tests','counts')) 315 parser.add_option('-e', '--elapsed_time', dest='elapsed_time', 316 help='Report elapsed time in output', 317 default=None) 318 parser.add_option('-m', '--make', dest='make', 319 help='make executable to report in summary', 320 default='make') 321 parser.add_option('-t', '--time', dest='time', 322 help='-t n: Report on the n number expensive jobs', 323 default=0) 324 parser.add_option('-f', '--fail', dest='show_fail', action="store_true", 325 help='Show the failed tests and how to run them') 326 options, args = parser.parse_args() 327 328 # Process arguments 329 if len(args) > 0: 330 parser.print_usage() 331 return 332 333 334 if not options.show_fail: 335 summarize_results(options.directory,options.make,int(options.time),options.elapsed_time) 336 testresults=get_test_data(options.directory) 337 338 if options.show_fail: 339 show_fail(testresults) 340 else: 341 generate_xml(testresults, options.directory) 342 343if __name__ == "__main__": 344 main() 345