1#!/usr/bin/env python 2""" 3Parse the test file and return a dictionary. 4 5Quick usage:: 6 7 lib/petsc/bin/maint/testparse.py -t src/ksp/ksp/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""" 37from __future__ import print_function 38 39import os, re, glob, types 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 49# These are special keys describing build 50buildkeys="requires TODO SKIP depends".split() 51 52acceptedkeys="test nsize requires command suffix args filter filter_output localrunfiles comments TODO SKIP output_file timeoutfactor".split() 53appendlist="args requires comments".split() 54 55import re 56 57def getDefaultOutputFileRoot(testname): 58 """ 59 Given testname, give DefaultRoot and DefaultOutputFilename 60 e.g., runex1 gives ex1_1, output/ex1_1.out 61 """ 62 defroot=(re.sub("run","",testname) if testname.startswith("run") else testname) 63 if not "_" in defroot: defroot=defroot+"_1" 64 return defroot 65 66def _stripIndent(block,srcfile,entireBlock=False,fileNums=[]): 67 """ 68 Go through and remove a level of indentation 69 Also strip of trailing whitespace 70 """ 71 # The first entry should be test: but it might be indented. 72 ext=os.path.splitext(srcfile)[1] 73 stripstr=" " 74 if len(fileNums)>0: lineNum=fileNums[0]-1 75 for lline in block.split("\n"): 76 if len(fileNums)>0: lineNum+=1 77 line=lline[1:] if lline.startswith("!") else lline 78 if not line.strip(): continue 79 if line.strip().startswith('#'): continue 80 if entireBlock: 81 var=line.split(":")[0].strip() 82 if not var in ['test','testset','build']: 83 raise Exception("Formatting error: Cannot find test in file: "+srcfile+" at line: "+str(lineNum)+"\n") 84 nspace=len(line)-len(line.lstrip(stripstr)) 85 newline=line[nspace:] 86 break 87 88 # Strip off any indentation for the whole string and any trailing 89 # whitespace for convenience 90 newTestStr="\n" 91 if len(fileNums)>0: lineNum=fileNums[0]-1 92 firstPass=True 93 for lline in block.split("\n"): 94 if len(fileNums)>0: lineNum+=1 95 line=lline[1:] if lline.startswith("!") else lline 96 if not line.strip(): continue 97 if line.strip().startswith('#'): 98 newTestStr+=line+'\n' 99 else: 100 newline=line[nspace:] 101 newTestStr+=newline.rstrip()+"\n" 102 # Do some basic indentation checks 103 if entireBlock: 104 # Don't need to check comment lines 105 if line.strip().startswith('#'): continue 106 if not newline.startswith(" "): 107 var=newline.split(":")[0].strip() 108 if not var in ['test','testset','build']: 109 err="Formatting error in file "+srcfile+" at line: " +line+"\n" 110 if len(fileNums)>0: 111 raise Exception(err+"Check indentation at line number: "+str(lineNum)) 112 else: 113 raise Exception(err) 114 else: 115 var=line.split(":")[0].strip() 116 if var in ['test','testset','build']: 117 subnspace=len(line)-len(line.lstrip(stripstr)) 118 if firstPass: 119 firstsubnspace=subnspace 120 firstPass=False 121 else: 122 if firstsubnspace!=subnspace: 123 err="Formatting subtest error in file "+srcfile+" at line: " +line+"\n" 124 if len(fileNums)>0: 125 raise Exception(err+"Check indentation at line number: "+str(lineNum)) 126 else: 127 raise Exception(err) 128 129 # Allow line continuation character '\' 130 return newTestStr.replace('\\\n', ' ') 131 132def parseLoopArgs(varset): 133 """ 134 Given: String containing loop variables 135 Return: tuple containing separate/shared and string of loop vars 136 """ 137 keynm=varset.split("{{")[0].strip().lstrip('-') 138 if not keynm.strip(): keynm='nsize' 139 lvars=varset.split('{{')[1].split('}')[0] 140 suffx=varset.split('{{')[1].split('}')[1] 141 ftype='separate' if suffx.startswith('separate') else 'shared' 142 return keynm,lvars,ftype 143 144def _getSeparateTestvars(testDict): 145 """ 146 Given: dictionary that may have 147 Return: Variables that cause a test split 148 """ 149 vals=None 150 sepvars=[] 151 # Check nsize 152 if 'nsize' in testDict: 153 varset=testDict['nsize'] 154 if '{{' in varset: 155 keynm,lvars,ftype=parseLoopArgs(varset) 156 if ftype=='separate': sepvars.append(keynm) 157 158 # Now check args 159 if 'args' not in testDict: return sepvars 160 for varset in re.split('(^|\W)-(?=[a-zA-Z])',testDict['args']): 161 if not varset.strip(): continue 162 if '{{' in varset: 163 # Assuming only one for loop per var specification 164 keynm,lvars,ftype=parseLoopArgs(varset) 165 if ftype=='separate': sepvars.append(keynm) 166 167 return sepvars 168 169def _getNewArgs(args): 170 """ 171 Given: String that has args that might have loops in them 172 Return: All of the arguments/values that do not have 173 for 'separate output' in for loops 174 """ 175 newargs='' 176 if not args.strip(): return args 177 for varset in re.split('(^|\W)-(?=[a-zA-Z])',args): 178 if not varset.strip(): continue 179 if '{{' in varset and 'separate' in varset: continue 180 newargs+="-"+varset.strip()+" " 181 182 return newargs 183 184def _getVarVals(findvar,testDict): 185 """ 186 Given: variable that is either nsize or in args 187 Return: Values to loop over and the other arguments 188 Note that we keep the other arguments even if they have 189 for loops to enable stepping through all of the for lops 190 """ 191 save_vals=None 192 if findvar=='nsize': 193 varset=testDict[findvar] 194 keynm,save_vals,ftype=parseLoopArgs('nsize '+varset) 195 else: 196 varlist=[] 197 for varset in re.split('-(?=[a-zA-Z])',testDict['args']): 198 if not varset.strip(): continue 199 if '{{' not in varset: continue 200 keyvar,vals,ftype=parseLoopArgs(varset) 201 if keyvar==findvar: 202 save_vals=vals 203 204 if not save_vals: raise Exception("Could not find separate_testvar: "+findvar) 205 return save_vals 206 207def genTestsSeparateTestvars(intests,indicts,final=False): 208 """ 209 Given: testname, sdict with 'separate_testvars 210 Return: testnames,sdicts: List of generated tests 211 The tricky part here is the {{ ... }separate output} 212 that can be used multiple times 213 """ 214 testnames=[]; sdicts=[] 215 for i in range(len(intests)): 216 testname=intests[i]; sdict=indicts[i]; i+=1 217 separate_testvars=_getSeparateTestvars(sdict) 218 if len(separate_testvars)>0: 219 sep_dicts=[sdict.copy()] 220 if 'args' in sep_dicts[0]: 221 sep_dicts[0]['args']=_getNewArgs(sdict['args']) 222 sep_testnames=[testname] 223 for kvar in separate_testvars: 224 kvals=_getVarVals(kvar,sdict) 225 226 # Have to do loop over previous var/val combos as well 227 # and accumulate as we go 228 val_testnames=[]; val_dicts=[] 229 for val in kvals.split(): 230 gensuffix="_"+kvar+"-"+val.replace(',','__') 231 for kvaltestnm in sep_testnames: 232 val_testnames.append(kvaltestnm+gensuffix) 233 for kv in sep_dicts: 234 kvardict=kv.copy() 235 # If the last var then we have the final version 236 if 'suffix' in sdict: 237 kvardict['suffix']+=gensuffix 238 else: 239 kvardict['suffix']=gensuffix 240 if kvar=='nsize': 241 kvardict[kvar]=val 242 else: 243 kvardict['args']+="-"+kvar+" "+val+" " 244 val_dicts.append(kvardict) 245 sep_testnames=val_testnames 246 sep_dicts=val_dicts 247 testnames+=sep_testnames 248 sdicts+=sep_dicts 249 else: 250 # These are plain vanilla tests (no subtests, no loops) that 251 # do not have a suffix. This makes the targets match up with 252 # the output file (testname_1.out) 253 if final: 254 if '_' not in testname: testname+='_1' 255 testnames.append(testname) 256 sdicts.append(sdict) 257 return testnames,sdicts 258 259def genTestsSubtestSuffix(testnames,sdicts): 260 """ 261 Given: testname, sdict with separate_testvars 262 Return: testnames,sdicts: List of generated tests 263 """ 264 tnms=[]; sdcts=[] 265 for i in range(len(testnames)): 266 testname=testnames[i] 267 rmsubtests=[]; keepSubtests=False 268 if 'subtests' in sdicts[i]: 269 for stest in sdicts[i]["subtests"]: 270 if 'suffix' in sdicts[i][stest]: 271 rmsubtests.append(stest) 272 gensuffix="_"+sdicts[i][stest]['suffix'] 273 newtestnm=testname+gensuffix 274 tnms.append(newtestnm) 275 newsdict=sdicts[i].copy() 276 del newsdict['subtests'] 277 # Have to hand update 278 # Append 279 for kup in appendlist: 280 if kup in sdicts[i][stest]: 281 if kup in sdicts[i]: 282 newsdict[kup]=sdicts[i][kup]+" "+sdicts[i][stest][kup] 283 else: 284 newsdict[kup]=sdicts[i][stest][kup] 285 # Promote 286 for kup in acceptedkeys: 287 if kup in appendlist: continue 288 if kup in sdicts[i][stest]: 289 newsdict[kup]=sdicts[i][stest][kup] 290 # Cleanup 291 for st in sdicts[i]["subtests"]: del newsdict[st] 292 sdcts.append(newsdict) 293 else: 294 keepSubtests=True 295 else: 296 tnms.append(testnames[i]) 297 sdcts.append(sdicts[i]) 298 # If a subtest without a suffix exists, then save it 299 if keepSubtests: 300 tnms.append(testnames[i]) 301 newsdict=sdicts[i].copy() 302 # Prune the tests to prepare for keeping 303 for rmtest in rmsubtests: 304 newsdict['subtests'].remove(rmtest) 305 del newsdict[rmtest] 306 sdcts.append(newsdict) 307 i+=1 308 return tnms,sdcts 309 310def splitTests(testname,sdict): 311 """ 312 Given: testname and dictionary generated from the YAML-like definition 313 Return: list of names and dictionaries corresponding to each test 314 given that the YAML-like language allows for multiple tests 315 """ 316 317 # Order: Parent sep_tv, subtests suffix, subtests sep_tv 318 testnames,sdicts=genTestsSeparateTestvars([testname],[sdict]) 319 testnames,sdicts=genTestsSubtestSuffix(testnames,sdicts) 320 testnames,sdicts=genTestsSeparateTestvars(testnames,sdicts,final=True) 321 322 # Because I am altering the list, I do this in passes. Inelegant 323 324 return testnames, sdicts 325 326def parseTest(testStr,srcfile,verbosity): 327 """ 328 This parses an individual test 329 Our YAML-like language is hierarchial so should use a state machine in the general case, 330 but in practice we only support two levels of test: 331 """ 332 basename=os.path.basename(srcfile) 333 # Handle the new at the begininng 334 bn=re.sub("new_","",basename) 335 # This is the default 336 testname="run"+os.path.splitext(bn)[0] 337 338 # Tests that have default everything (so empty effectively) 339 if len(testStr)==0: return [testname], [{}] 340 341 striptest=_stripIndent(testStr,srcfile) 342 343 # go through and parse 344 subtestnum=0 345 subdict={} 346 comments=[] 347 indentlevel=0 348 for ln in striptest.split("\n"): 349 line=ln.split('#')[0].rstrip() 350 if verbosity>2: print(line) 351 comment=("" if len(ln.split("#"))>0 else " ".join(ln.split("#")[1:]).strip()) 352 if comment: comments.append(comment) 353 if not line.strip(): continue 354 lsplit=line.split(':') 355 if len(lsplit)==0: raise Exception("Missing : in line: "+line) 356 indentcount=lsplit[0].count(" ") 357 var=lsplit[0].strip() 358 val=line[line.find(':')+1:].strip() 359 if not var in acceptedkeys: raise Exception("Not a defined key: "+var+" from: "+line) 360 # Start by seeing if we are in a subtest 361 if line.startswith(" "): 362 if var in subdict[subtestname]: 363 subdict[subtestname][var]+=" "+val 364 else: 365 subdict[subtestname][var]=val 366 if not indentlevel: indentlevel=indentcount 367 #if indentlevel!=indentcount: print("Error in indentation:", ln) 368 # Determine subtest name and make dict 369 elif var=="test": 370 subtestname="test"+str(subtestnum) 371 subdict[subtestname]={} 372 if "subtests" not in subdict: subdict["subtests"]=[] 373 subdict["subtests"].append(subtestname) 374 subtestnum=subtestnum+1 375 # The rest are easy 376 else: 377 # For convenience, it is sometimes convenient to list twice 378 if var in subdict: 379 if var in appendlist: 380 subdict[var]+=" "+val 381 else: 382 raise Exception(var+" entered twice: "+line) 383 else: 384 subdict[var]=val 385 if var=="suffix": 386 if len(val)>0: 387 testname+="_"+val 388 389 if len(comments): subdict['comments']="\n".join(comments).lstrip("\n") 390 391 # A test block can create multiple tests. This does that logic 392 testnames,subdicts=splitTests(testname,subdict) 393 return testnames,subdicts 394 395def parseTests(testStr,srcfile,fileNums,verbosity): 396 """ 397 Parse the YAML-like string describing tests and return 398 a dictionary with the info in the form of: 399 testDict[test][subtest] 400 """ 401 402 testDict={} 403 404 # The first entry should be test: but it might be indented. 405 newTestStr=_stripIndent(testStr,srcfile,entireBlock=True,fileNums=fileNums) 406 if verbosity>2: print(srcfile) 407 408 ## Check and see if we have build requirements 409 addToRunRequirements=None 410 if "\nbuild:" in newTestStr: 411 testDict['build']={} 412 # The file info is already here and need to append 413 Part1=newTestStr.split("build:")[1] 414 fileInfo=re.split("\ntest(?:set)?:",newTestStr)[0] 415 for bkey in buildkeys: 416 if bkey+":" in fileInfo: 417 testDict['build'][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip() 418 #if verbosity>1: bkey+": "+testDict['build'][bkey] 419 # If a runtime requires are put into build, push them down to all run tests 420 # At this point, we are working with strings and not lists 421 if 'requires' in testDict['build']: 422 if 'datafilespath' in testDict['build']['requires']: 423 newreqs=re.sub('datafilespath','',testDict['build']['requires']) 424 testDict['build']['requires']=newreqs.strip() 425 addToRunRequirements='datafilespath' 426 427 428 # Now go through each test. First elem in split is blank 429 for test in re.split("\ntest(?:set)?:",newTestStr)[1:]: 430 testnames,subdicts=parseTest(test,srcfile,verbosity) 431 for i in range(len(testnames)): 432 if testnames[i] in testDict: 433 raise RuntimeError("Multiple test names specified: "+testnames[i]+" in file: "+srcfile) 434 # Add in build requirements that need to be moved 435 if addToRunRequirements: 436 if 'requires' in subdicts[i]: 437 subdicts[i]['requires']+=addToRunRequirements 438 else: 439 subdicts[i]['requires']=addToRunRequirements 440 testDict[testnames[i]]=subdicts[i] 441 442 return testDict 443 444def parseTestFile(srcfile,verbosity): 445 """ 446 Parse single example files and return dictionary of the form: 447 testDict[srcfile][test][subtest] 448 """ 449 debug=False 450 basename=os.path.basename(srcfile) 451 if basename=='makefile': return {} 452 453 curdir=os.path.realpath(os.path.curdir) 454 basedir=os.path.dirname(os.path.realpath(srcfile)) 455 os.chdir(basedir) 456 457 testDict={} 458 sh=open(basename,"r"); fileStr=sh.read(); sh.close() 459 460 ## Start with doing the tests 461 # 462 fsplit=fileStr.split("/*TEST\n")[1:] 463 fstart=len(fileStr.split("/*TEST\n")[0].split("\n"))+1 464 # Allow for multiple "/*TEST" blocks even though it really should be 465 # one 466 srcTests=[] 467 for t in fsplit: srcTests.append(t.split("TEST*/")[0]) 468 testString=" ".join(srcTests) 469 flen=len(testString.split("\n")) 470 fend=fstart+flen-1 471 fileNums=range(fstart,fend) 472 testDict[basename]=parseTests(testString,srcfile,fileNums,verbosity) 473 # Massage dictionary for build requirements 474 if 'build' in testDict[basename]: 475 testDict[basename].update(testDict[basename]['build']) 476 del testDict[basename]['build'] 477 478 479 os.chdir(curdir) 480 return testDict 481 482def parseTestDir(directory,verbosity): 483 """ 484 Parse single example files and return dictionary of the form: 485 testDict[srcfile][test][subtest] 486 """ 487 curdir=os.path.realpath(os.path.curdir) 488 basedir=os.path.realpath(directory) 489 os.chdir(basedir) 490 491 tDict={} 492 for test_file in sorted(glob.glob("new_ex*.*")): 493 tDict.update(parseTestFile(test_file,verbosity)) 494 495 os.chdir(curdir) 496 return tDict 497 498def printExParseDict(rDict): 499 """ 500 This is useful for debugging 501 """ 502 indent=" " 503 for sfile in rDict: 504 print(sfile) 505 sortkeys=list(rDict[sfile].keys()) 506 sortkeys.sort() 507 for runex in sortkeys: 508 if runex == 'requires': 509 print(indent+runex+':'+str(rDict[sfile][runex])) 510 continue 511 print(indent+runex) 512 if type(rDict[sfile][runex])==bytes: 513 print(indent*2+rDict[sfile][runex]) 514 else: 515 for var in rDict[sfile][runex]: 516 if var.startswith("test"): continue 517 print(indent*2+var+": "+str(rDict[sfile][runex][var])) 518 if 'subtests' in rDict[sfile][runex]: 519 for var in rDict[sfile][runex]['subtests']: 520 print(indent*2+var) 521 for var2 in rDict[sfile][runex][var]: 522 print(indent*3+var2+": "+str(rDict[sfile][runex][var][var2])) 523 print("\n") 524 return 525 526def main(directory='',test_file='',verbosity=0): 527 528 if directory: 529 tDict=parseTestDir(directory,verbosity) 530 else: 531 tDict=parseTestFile(test_file,verbosity) 532 if verbosity>0: printExParseDict(tDict) 533 534 return 535 536if __name__ == '__main__': 537 import optparse 538 parser = optparse.OptionParser() 539 parser.add_option('-d', '--directory', dest='directory', 540 default="", help='Directory containing files to parse') 541 parser.add_option('-t', '--test_file', dest='test_file', 542 default="", help='Test file, e.g., ex1.c, to parse') 543 parser.add_option('-v', '--verbosity', dest='verbosity', 544 help='Verbosity of output by level: 1, 2, or 3', default=0) 545 opts, extra_args = parser.parse_args() 546 547 if extra_args: 548 import sys 549 sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args)) 550 exit(1) 551 if not opts.test_file and not opts.directory: 552 print("test file or directory is required") 553 parser.print_usage() 554 sys.exit() 555 556 # Need verbosity to be an integer 557 try: 558 verbosity=int(opts.verbosity) 559 except: 560 raise Exception("Error: Verbosity must be integer") 561 562 main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity) 563