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