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 326 327def testSplit(striptest): 328 """ 329 Split up a test into lines, but use a shell parser to detect when newlines are within quotation marks 330 and keep those together 331 """ 332 import shlex 333 334 sl = shlex.shlex() 335 sl.whitespace_split = True # only split at whitespace 336 sl.commenters = '' 337 sl.push_source(striptest) 338 last_pos = sl.instream.tell() 339 try: 340 last_token = sl.read_token() 341 except ValueError: 342 print(striptest) 343 raise ValueError 344 last_line = '' 345 while last_token != '': 346 new_pos = sl.instream.tell() 347 block = striptest[last_pos:new_pos] 348 token_start = block.find(last_token) 349 leading = block[0:token_start] 350 trailing = block[(token_start + len(last_token)):] 351 leading_split = leading.split('\n') 352 if len(leading_split) > 1: 353 yield last_line 354 last_line = '' 355 last_line += leading_split[-1] 356 last_line += last_token 357 trailing_split = trailing.split('\n') 358 last_line += trailing_split[0] 359 if len(trailing_split) > 1: 360 yield last_line 361 last_line = '' 362 last_pos = new_pos 363 try: 364 last_token = sl.read_token() 365 except ValueError: 366 print(striptest) 367 raise ValueError 368 yield last_line 369 370 371def parseTest(testStr,srcfile,verbosity): 372 """ 373 This parses an individual test 374 Our YAML-like language is hierarchial so should use a state machine in the general case, 375 but in practice we only support two levels of test: 376 """ 377 basename=os.path.basename(srcfile) 378 # Handle the new at the begininng 379 bn=re.sub("new_","",basename) 380 # This is the default 381 testname="run"+os.path.splitext(bn)[0] 382 383 # Tests that have default everything (so empty effectively) 384 if len(testStr)==0: return [testname], [{}] 385 386 striptest=_stripIndent(testStr,srcfile) 387 388 # go through and parse 389 subtestnum=0 390 subdict={} 391 comments=[] 392 indentlevel=0 393 for ln in testSplit(striptest): 394 line=ln.split('#')[0].rstrip() 395 if verbosity>2: print(line) 396 comment=("" if len(ln.split("#"))>0 else " ".join(ln.split("#")[1:]).strip()) 397 if comment: comments.append(comment) 398 if not line.strip(): continue 399 lsplit=line.split(':') 400 if len(lsplit)==0: raise Exception("Missing : in line: "+line) 401 indentcount=lsplit[0].count(" ") 402 var=lsplit[0].strip() 403 val=line[line.find(':')+1:].strip() 404 if not var in acceptedkeys: raise Exception("Not a defined key: "+var+" from: "+line) 405 # Start by seeing if we are in a subtest 406 if line.startswith(" "): 407 if var in subdict[subtestname]: 408 subdict[subtestname][var]+=" "+val 409 else: 410 subdict[subtestname][var]=val 411 if not indentlevel: indentlevel=indentcount 412 #if indentlevel!=indentcount: print("Error in indentation:", ln) 413 # Determine subtest name and make dict 414 elif var=="test": 415 subtestname="test"+str(subtestnum) 416 subdict[subtestname]={} 417 if "subtests" not in subdict: subdict["subtests"]=[] 418 subdict["subtests"].append(subtestname) 419 subtestnum=subtestnum+1 420 # The rest are easy 421 else: 422 # For convenience, it is sometimes convenient to list twice 423 if var in subdict: 424 if var in appendlist: 425 subdict[var]+=" "+val 426 else: 427 raise Exception(var+" entered twice: "+line) 428 else: 429 subdict[var]=val 430 if var=="suffix": 431 if len(val)>0: 432 testname+="_"+val 433 434 if len(comments): subdict['comments']="\n".join(comments).lstrip("\n") 435 436 # A test block can create multiple tests. This does that logic 437 testnames,subdicts=splitTests(testname,subdict) 438 return testnames,subdicts 439 440def parseTests(testStr,srcfile,fileNums,verbosity): 441 """ 442 Parse the YAML-like string describing tests and return 443 a dictionary with the info in the form of: 444 testDict[test][subtest] 445 """ 446 447 testDict={} 448 449 # The first entry should be test: but it might be indented. 450 newTestStr=_stripIndent(testStr,srcfile,entireBlock=True,fileNums=fileNums) 451 if verbosity>2: print(srcfile) 452 453 ## Check and see if we have build requirements 454 addToRunRequirements=None 455 if "\nbuild:" in newTestStr: 456 testDict['build']={} 457 # The file info is already here and need to append 458 Part1=newTestStr.split("build:")[1] 459 fileInfo=re.split("\ntest(?:set)?:",newTestStr)[0] 460 for bkey in buildkeys: 461 if bkey+":" in fileInfo: 462 testDict['build'][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip() 463 #if verbosity>1: bkey+": "+testDict['build'][bkey] 464 # If a runtime requires are put into build, push them down to all run tests 465 # At this point, we are working with strings and not lists 466 if 'requires' in testDict['build']: 467 if 'datafilespath' in testDict['build']['requires']: 468 newreqs=re.sub('datafilespath','',testDict['build']['requires']) 469 testDict['build']['requires']=newreqs.strip() 470 addToRunRequirements='datafilespath' 471 472 473 # Now go through each test. First elem in split is blank 474 for test in re.split("\ntest(?:set)?:",newTestStr)[1:]: 475 testnames,subdicts=parseTest(test,srcfile,verbosity) 476 for i in range(len(testnames)): 477 if testnames[i] in testDict: 478 raise RuntimeError("Multiple test names specified: "+testnames[i]+" in file: "+srcfile) 479 # Add in build requirements that need to be moved 480 if addToRunRequirements: 481 if 'requires' in subdicts[i]: 482 subdicts[i]['requires']+=addToRunRequirements 483 else: 484 subdicts[i]['requires']=addToRunRequirements 485 testDict[testnames[i]]=subdicts[i] 486 487 return testDict 488 489def parseTestFile(srcfile,verbosity): 490 """ 491 Parse single example files and return dictionary of the form: 492 testDict[srcfile][test][subtest] 493 """ 494 debug=False 495 basename=os.path.basename(srcfile) 496 if basename=='makefile': return {} 497 498 curdir=os.path.realpath(os.path.curdir) 499 basedir=os.path.dirname(os.path.realpath(srcfile)) 500 os.chdir(basedir) 501 502 testDict={} 503 sh=open(basename,"r"); fileStr=sh.read(); sh.close() 504 505 ## Start with doing the tests 506 # 507 fsplit=fileStr.split("/*TEST\n")[1:] 508 fstart=len(fileStr.split("/*TEST\n")[0].split("\n"))+1 509 # Allow for multiple "/*TEST" blocks even though it really should be 510 # one 511 srcTests=[] 512 for t in fsplit: srcTests.append(t.split("TEST*/")[0]) 513 testString=" ".join(srcTests) 514 flen=len(testString.split("\n")) 515 fend=fstart+flen-1 516 fileNums=range(fstart,fend) 517 testDict[basename]=parseTests(testString,srcfile,fileNums,verbosity) 518 # Massage dictionary for build requirements 519 if 'build' in testDict[basename]: 520 testDict[basename].update(testDict[basename]['build']) 521 del testDict[basename]['build'] 522 523 524 os.chdir(curdir) 525 return testDict 526 527def parseTestDir(directory,verbosity): 528 """ 529 Parse single example files and return dictionary of the form: 530 testDict[srcfile][test][subtest] 531 """ 532 curdir=os.path.realpath(os.path.curdir) 533 basedir=os.path.realpath(directory) 534 os.chdir(basedir) 535 536 tDict={} 537 for test_file in sorted(glob.glob("new_ex*.*")): 538 tDict.update(parseTestFile(test_file,verbosity)) 539 540 os.chdir(curdir) 541 return tDict 542 543def printExParseDict(rDict): 544 """ 545 This is useful for debugging 546 """ 547 indent=" " 548 for sfile in rDict: 549 print(sfile) 550 sortkeys=list(rDict[sfile].keys()) 551 sortkeys.sort() 552 for runex in sortkeys: 553 if runex == 'requires': 554 print(indent+runex+':'+str(rDict[sfile][runex])) 555 continue 556 print(indent+runex) 557 if type(rDict[sfile][runex])==bytes: 558 print(indent*2+rDict[sfile][runex]) 559 else: 560 for var in rDict[sfile][runex]: 561 if var.startswith("test"): continue 562 print(indent*2+var+": "+str(rDict[sfile][runex][var])) 563 if 'subtests' in rDict[sfile][runex]: 564 for var in rDict[sfile][runex]['subtests']: 565 print(indent*2+var) 566 for var2 in rDict[sfile][runex][var]: 567 print(indent*3+var2+": "+str(rDict[sfile][runex][var][var2])) 568 print("\n") 569 return 570 571def main(directory='',test_file='',verbosity=0): 572 573 if directory: 574 tDict=parseTestDir(directory,verbosity) 575 else: 576 tDict=parseTestFile(test_file,verbosity) 577 if verbosity>0: printExParseDict(tDict) 578 579 return 580 581if __name__ == '__main__': 582 import optparse 583 parser = optparse.OptionParser() 584 parser.add_option('-d', '--directory', dest='directory', 585 default="", help='Directory containing files to parse') 586 parser.add_option('-t', '--test_file', dest='test_file', 587 default="", help='Test file, e.g., ex1.c, to parse') 588 parser.add_option('-v', '--verbosity', dest='verbosity', 589 help='Verbosity of output by level: 1, 2, or 3', default=0) 590 opts, extra_args = parser.parse_args() 591 592 if extra_args: 593 import sys 594 sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args)) 595 exit(1) 596 if not opts.test_file and not opts.directory: 597 print("test file or directory is required") 598 parser.print_usage() 599 sys.exit() 600 601 # Need verbosity to be an integer 602 try: 603 verbosity=int(opts.verbosity) 604 except: 605 raise Exception("Error: Verbosity must be integer") 606 607 main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity) 608