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