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