1#!/usr/bin/env python 2""" 3Parse the test file and return a dictionary. 4 5Quick usage:: 6 7 bin/maint/testparse.py -t src/ksp/ksp/examples/tutorials/ex1.c 8 9From the command line, it prints out the dictionary. 10This is meant to be used by other scripts, but it is 11useful to debug individual files. 12 13Example language 14---------------- 15 16/*TEST 17 build: 18 requires: moab 19 # This is equivalent to test: 20 testset: 21 args: -pc_type mg -ksp_type fgmres -da_refine 2 -ksp_monitor_short -mg_levels_ksp_monitor_short -mg_levels_ksp_norm_type unpreconditioned -ksp_view -pc_mg_type full 22 23 testset: 24 suffix: 2 25 nsize: 2 26 args: -pc_type mg -ksp_type fgmres -da_refine 2 -ksp_monitor_short -mg_levels_ksp_monitor_short -mg_levels_ksp_norm_type unpreconditioned -ksp_view -pc_mg_type full 27 28 testset: 29 suffix: 2 30 nsize: 2 31 args: -pc_type mg -ksp_type fgmres -da_refine 2 -ksp_monitor_short -mg_levels_ksp_monitor_short -mg_levels_ksp_norm_type unpreconditioned -ksp_view -pc_mg_type full 32 test: 33 34TEST*/ 35 36""" 37 38import os, re, glob, types 39from distutils.sysconfig import parse_makefile 40import sys 41import logging 42sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 43 44import inspect 45thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 46maintdir=os.path.join(os.path.join(os.path.dirname(thisscriptdir),'bin'),'maint') 47sys.path.insert(0,maintdir) 48# Don't print out trace when raise Exceptions 49sys.tracebacklimit = 0 50 51# These are special keys describing build 52buildkeys="requires TODO SKIP depends".split() 53 54acceptedkeys="test nsize requires command suffix args filter filter_output localrunfiles comments TODO SKIP output_file timeoutfactor".split() 55appendlist="args requires comments".split() 56 57import re 58 59def _stripIndent(block,srcfile,entireBlock=False,fileNums=[]): 60 """ 61 Go through and remove a level of indentation 62 Also strip of trailing whitespace 63 """ 64 # The first entry should be test: but it might be indented. 65 ext=os.path.splitext(srcfile)[1] 66 stripstr=" " 67 if len(fileNums)>0: lineNum=fileNums[0]-1 68 for lline in block.split("\n"): 69 if len(fileNums)>0: lineNum+=1 70 line=lline[1:] if lline.startswith("!") else lline 71 if not line.strip(): continue 72 if line.strip().startswith('#'): continue 73 if entireBlock: 74 var=line.split(":")[0].strip() 75 if not var in ['test','testset','build']: 76 raise Exception("Formatting error: Cannot find test in file: "+srcfile+" at line: "+str(lineNum)+"\n") 77 nspace=len(line)-len(line.lstrip(stripstr)) 78 newline=line[nspace:] 79 break 80 81 # Strip off any indentation for the whole string and any trailing 82 # whitespace for convenience 83 newTestStr="\n" 84 if len(fileNums)>0: lineNum=fileNums[0]-1 85 firstPass=True 86 for lline in block.split("\n"): 87 if len(fileNums)>0: lineNum+=1 88 line=lline[1:] if lline.startswith("!") else lline 89 if not line.strip(): continue 90 if line.strip().startswith('#'): 91 newTestStr+=line+'\n' 92 else: 93 newline=line[nspace:] 94 newTestStr+=newline.rstrip()+"\n" 95 # Do some basic indentation checks 96 if entireBlock: 97 # Don't need to check comment lines 98 if line.strip().startswith('#'): continue 99 if not newline.startswith(" "): 100 var=newline.split(":")[0].strip() 101 if not var in ['test','testset','build']: 102 err="Formatting error in file "+srcfile+" at line: " +line+"\n" 103 if len(fileNums)>0: 104 raise Exception(err+"Check indentation at line number: "+str(lineNum)) 105 else: 106 raise Exception(err) 107 else: 108 var=line.split(":")[0].strip() 109 if var in ['test','testset','build']: 110 subnspace=len(line)-len(line.lstrip(stripstr)) 111 if firstPass: 112 firstsubnspace=subnspace 113 firstPass=False 114 else: 115 if firstsubnspace!=subnspace: 116 err="Formatting subtest error in file "+srcfile+" at line: " +line+"\n" 117 if len(fileNums)>0: 118 raise Exception(err+"Check indentation at line number: "+str(lineNum)) 119 else: 120 raise Exception(err) 121 122 123 return newTestStr 124 125def parseLoopArgs(varset): 126 """ 127 Given: String containing loop variables 128 Return: tuple containing separate/shared and string of loop vars 129 """ 130 keynm=varset.split("{{")[0].strip() 131 if not keynm.strip(): keynm='nsize' 132 lvars=varset.split('{{')[1].split('}')[0] 133 suffx=varset.split('{{')[1].split('}')[1] 134 ftype='separate' if suffx.startswith('separate') else 'shared' 135 return keynm,lvars,ftype 136 137def _getSeparateTestvars(testDict): 138 """ 139 Given: dictionary that may have 140 Return: Variables that cause a test split 141 """ 142 vals=None 143 sepvars=[] 144 # Check nsize 145 if testDict.has_key('nsize'): 146 varset=testDict['nsize'] 147 if '{{' in varset: 148 keynm,lvars,ftype=parseLoopArgs(varset) 149 if ftype=='separate': sepvars.append(keynm) 150 151 # Now check args 152 if not testDict.has_key('args'): return sepvars 153 for varset in re.split('-(?=[a-zA-Z])',testDict['args']): 154 if not varset.strip(): continue 155 if '{{' in varset: 156 # Assuming only one for loop per var specification 157 keynm,lvars,ftype=parseLoopArgs(varset) 158 if ftype=='separate': sepvars.append(keynm) 159 160 return sepvars 161 162def _getVarVals(findvar,testDict): 163 """ 164 Given: variable that is either nsize or in args 165 Return: Values to loop over 166 """ 167 vals=None 168 newargs='' 169 if findvar=='nsize': 170 varset=testDict[findvar] 171 keynm,vals,ftype=parseLoopArgs('nsize '+varset) 172 else: 173 varlist=[] 174 for varset in re.split('-(?=[a-zA-Z])',testDict['args']): 175 if not varset.strip(): continue 176 if '{{' in varset: 177 keyvar,vals,ftype=parseLoopArgs(varset) 178 if keyvar!=findvar: 179 newargs+="-"+varset.strip()+" " 180 continue 181 else: 182 newargs+="-"+varset.strip()+" " 183 184 if not vals: raise StandardError("Could not find separate_testvar: "+findvar) 185 return vals,newargs 186 187def genTestsSeparateTestvars(intests,indicts): 188 """ 189 Given: testname, sdict with 'separate_testvars 190 Return: testnames,sdicts: List of generated tests 191 """ 192 testnames=[]; sdicts=[] 193 for i in range(len(intests)): 194 testname=intests[i]; sdict=indicts[i]; i+=1 195 separate_testvars=_getSeparateTestvars(sdict) 196 if len(separate_testvars)>0: 197 for kvar in separate_testvars: 198 kvals,newargs=_getVarVals(kvar,sdict) 199 # No errors means we are good to go 200 for val in kvals.split(): 201 kvardict=sdict.copy() 202 gensuffix="_"+kvar+"-"+val 203 newtestnm=testname+gensuffix 204 if sdict.has_key('suffix'): 205 kvardict['suffix']=sdict['suffix']+gensuffix 206 else: 207 kvardict['suffix']=gensuffix 208 if kvar=='nsize': 209 kvardict[kvar]=val 210 else: 211 kvardict['args']=newargs+"-"+kvar+" "+val 212 testnames.append(newtestnm) 213 sdicts.append(kvardict) 214 else: 215 testnames.append(testname) 216 sdicts.append(sdict) 217 return testnames,sdicts 218 219def genTestsSubtestSuffix(testnames,sdicts): 220 """ 221 Given: testname, sdict with separate_testvars 222 Return: testnames,sdicts: List of generated tests 223 """ 224 tnms=[]; sdcts=[] 225 for i in range(len(testnames)): 226 testname=testnames[i] 227 rmsubtests=[]; keepSubtests=False 228 if sdicts[i].has_key('subtests'): 229 for stest in sdicts[i]["subtests"]: 230 if sdicts[i][stest].has_key('suffix'): 231 rmsubtests.append(stest) 232 gensuffix="_"+sdicts[i][stest]['suffix'] 233 newtestnm=testname+gensuffix 234 tnms.append(newtestnm) 235 newsdict=sdicts[i].copy() 236 del newsdict['subtests'] 237 # Have to hand update 238 # Append 239 for kup in appendlist: 240 if sdicts[i][stest].has_key(kup): 241 if sdicts[i].has_key(kup): 242 newsdict[kup]=sdicts[i][kup]+" "+sdicts[i][stest][kup] 243 else: 244 newsdict[kup]=sdicts[i][stest][kup] 245 # Promote 246 for kup in acceptedkeys: 247 if kup in appendlist: continue 248 if sdicts[i][stest].has_key(kup): 249 newsdict[kup]=sdicts[i][stest][kup] 250 # Cleanup 251 for st in sdicts[i]["subtests"]: del newsdict[st] 252 sdcts.append(newsdict) 253 else: 254 keepSubtests=True 255 else: 256 tnms.append(testnames[i]) 257 sdcts.append(sdicts[i]) 258 # If a subtest without a suffix exists, then save it 259 if keepSubtests: 260 tnms.append(testnames[i]) 261 newsdict=sdicts[i].copy() 262 # Prune the tests to prepare for keeping 263 for rmtest in rmsubtests: 264 newsdict['subtests'].remove(rmtest) 265 del newsdict[rmtest] 266 sdcts.append(newsdict) 267 i+=1 268 return tnms,sdcts 269 270def splitTests(testname,sdict): 271 """ 272 Given: testname and YAML-generated dictionary 273 Return: list of names and dictionaries corresponding to each test 274 given that the YAML language allows for multiple tests 275 """ 276 277 # Order: Parent sep_tv, subtests suffix, subtests sep_tv 278 testnames,sdicts=genTestsSeparateTestvars([testname],[sdict]) 279 testnames,sdicts=genTestsSubtestSuffix(testnames,sdicts) 280 testnames,sdicts=genTestsSeparateTestvars(testnames,sdicts) 281 282 # Because I am altering the list, I do this in passes. Inelegant 283 284 return testnames, sdicts 285 286def parseTest(testStr,srcfile,verbosity): 287 """ 288 This parses an individual test 289 YAML is hierarchial so should use a state machine in the general case, 290 but in practice we only support two levels of test: 291 """ 292 basename=os.path.basename(srcfile) 293 # Handle the new at the begininng 294 bn=re.sub("new_","",basename) 295 # This is the default 296 testname="run"+os.path.splitext(bn)[0] 297 298 # Tests that have default everything (so empty effectively) 299 if len(testStr)==0: return testname, {} 300 301 striptest=_stripIndent(testStr,srcfile) 302 303 # go through and parse 304 subtestnum=0 305 subdict={} 306 comments=[] 307 indentlevel=0 308 for ln in striptest.split("\n"): 309 line=ln.split('#')[0].rstrip() 310 if verbosity>2: print line 311 comment=("" if len(ln.split("#"))>0 else " ".join(ln.split("#")[1:]).strip()) 312 if comment: comments.append(comment) 313 if not line.strip(): continue 314 lsplit=line.split(':') 315 if len(lsplit)==0: raise Exception("Missing : in line: "+line) 316 indentcount=lsplit[0].count(" ") 317 var=lsplit[0].strip() 318 val=line[line.find(':')+1:].strip() 319 if not var in acceptedkeys: raise Exception("Not a defined key: "+var+" from: "+line) 320 # Start by seeing if we are in a subtest 321 if line.startswith(" "): 322 subdict[subtestname][var]=val 323 if not indentlevel: indentlevel=indentcount 324 #if indentlevel!=indentcount: print "Error in indentation:", ln 325 # Determine subtest name and make dict 326 elif var=="test": 327 subtestname="test"+str(subtestnum) 328 subdict[subtestname]={} 329 if not subdict.has_key("subtests"): subdict["subtests"]=[] 330 subdict["subtests"].append(subtestname) 331 subtestnum=subtestnum+1 332 # The rest are easy 333 else: 334 # For convenience, it is sometimes convenient to list twice 335 if subdict.has_key(var): 336 if var in appendlist: 337 subdict[var]+=" "+val 338 else: 339 raise Exception(var+" entered twice: "+line) 340 else: 341 subdict[var]=val 342 if var=="suffix": 343 if len(val)>0: 344 testname=testname+"_"+val 345 346 if len(comments): subdict['comments']="\n".join(comments).lstrip("\n") 347 # A test block can create multiple tests. This does 348 # that logic 349 testnames,subdicts=splitTests(testname,subdict) 350 return testnames,subdicts 351 352def parseTests(testStr,srcfile,fileNums,verbosity): 353 """ 354 Parse the yaml string describing tests and return 355 a dictionary with the info in the form of: 356 testDict[test][subtest] 357 This is an inelegant parser as we do not wish to 358 introduce a new dependency by bringing in pyyaml. 359 The advantage is that validation can be done as 360 it is parsed (e.g., 'test' is the only top-level node) 361 """ 362 363 testDict={} 364 365 # The first entry should be test: but it might be indented. 366 newTestStr=_stripIndent(testStr,srcfile,entireBlock=True,fileNums=fileNums) 367 if verbosity>2: print srcfile 368 369 ## Check and see if we have build reuqirements 370 if "\nbuild:" in newTestStr: 371 testDict['build']={} 372 # The file info is already here and need to append 373 Part1=newTestStr.split("build:")[1] 374 fileInfo=re.split("\ntest(?:set)?:",newTestStr)[0] 375 for bkey in buildkeys: 376 if bkey+":" in fileInfo: 377 testDict['build'][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip() 378 #if verbosity>1: bkey+": "+testDict['build'][bkey] 379 380 # Now go through each test. First elem in split is blank 381 for test in re.split("\ntest(?:set)?:",newTestStr)[1:]: 382 testnames,subdicts=parseTest(test,srcfile,verbosity) 383 for i in range(len(testnames)): 384 if testDict.has_key(testnames[i]): 385 raise RuntimeError("Multiple test names specified: "+testnames[i]+" in file: "+srcfile) 386 testDict[testnames[i]]=subdicts[i] 387 388 return testDict 389 390def parseTestFile(srcfile,verbosity): 391 """ 392 Parse single example files and return dictionary of the form: 393 testDict[srcfile][test][subtest] 394 """ 395 debug=False 396 curdir=os.path.realpath(os.path.curdir) 397 basedir=os.path.dirname(os.path.realpath(srcfile)) 398 basename=os.path.basename(srcfile) 399 os.chdir(basedir) 400 401 testDict={} 402 sh=open(srcfile,"r"); fileStr=sh.read(); sh.close() 403 404 ## Start with doing the tests 405 # 406 fsplit=fileStr.split("/*TEST\n")[1:] 407 if len(fsplit)==0: 408 if debug: print "No test found in: "+srcfile 409 return {} 410 fstart=len(fileStr.split("/*TEST\n")[0].split("\n"))+1 411 # Allow for multiple "/*TEST" blocks even though it really should be 412 # one 413 srcTests=[] 414 for t in fsplit: srcTests.append(t.split("TEST*/")[0]) 415 testString=" ".join(srcTests) 416 if len(testString.strip())==0: 417 print "No test found in: "+srcfile 418 return {} 419 flen=len(testString.split("\n")) 420 fend=fstart+flen-1 421 fileNums=range(fstart,fend) 422 testDict[basename]=parseTests(testString,srcfile,fileNums,verbosity) 423 # Massage dictionary for build requirements 424 if 'build' in testDict[basename]: 425 testDict[basename].update(testDict[basename]['build']) 426 del testDict[basename]['build'] 427 428 429 os.chdir(curdir) 430 return testDict 431 432def parseTestDir(directory,verbosity): 433 """ 434 Parse single example files and return dictionary of the form: 435 testDict[srcfile][test][subtest] 436 """ 437 curdir=os.path.realpath(os.path.curdir) 438 basedir=os.path.realpath(directory) 439 os.chdir(basedir) 440 441 tDict={} 442 for test_file in glob.glob("new_ex*.*"): 443 tDict.update(parseTestFile(test_file,verbosity)) 444 445 os.chdir(curdir) 446 return tDict 447 448def printExParseDict(rDict): 449 """ 450 This is useful for debugging 451 """ 452 indent=" " 453 for sfile in rDict: 454 print sfile 455 sortkeys=rDict[sfile].keys() 456 sortkeys.sort() 457 for runex in sortkeys: 458 print indent+runex 459 if type(rDict[sfile][runex])==types.StringType: 460 print indent*2+rDict[sfile][runex] 461 else: 462 for var in rDict[sfile][runex]: 463 if var.startswith("test"): continue 464 print indent*2+var+": "+str(rDict[sfile][runex][var]) 465 if rDict[sfile][runex].has_key('subtests'): 466 for var in rDict[sfile][runex]['subtests']: 467 print indent*2+var 468 for var2 in rDict[sfile][runex][var]: 469 print indent*3+var2+": "+str(rDict[sfile][runex][var][var2]) 470 print "\n" 471 return 472 473def main(directory='',test_file='',verbosity=0): 474 475 if directory: 476 tDict=parseTestDir(directory,verbosity) 477 else: 478 tDict=parseTestFile(test_file,verbosity) 479 if verbosity>0: printExParseDict(tDict) 480 481 return 482 483if __name__ == '__main__': 484 import optparse 485 parser = optparse.OptionParser() 486 parser.add_option('-d', '--directory', dest='directory', 487 default="", help='Directory containing files to parse') 488 parser.add_option('-t', '--test_file', dest='test_file', 489 default="", help='Test file, e.g., ex1.c, to parse') 490 parser.add_option('-v', '--verbosity', dest='verbosity', 491 help='Verbosity of output by level: 1, 2, or 3', default=0) 492 opts, extra_args = parser.parse_args() 493 494 if extra_args: 495 import sys 496 sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args)) 497 exit(1) 498 if not opts.test_file and not opts.directory: 499 print "test file or directory is required" 500 parser.print_usage() 501 sys.exit() 502 503 # Need verbosity to be an integer 504 try: 505 verbosity=int(opts.verbosity) 506 except: 507 raise Exception("Error: Verbosity must be integer") 508 509 main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity) 510