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 13 14 15Example language 16---------------- 17/*T 18 Concepts: 19 requires: moab 20T*/ 21 22 23 24/*TEST 25 26 # This is equivalent to test: 27 testset: 28 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 29 30 testset: 31 suffix: 2 32 nsize: 2 33 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 34 35 testset: 36 suffix: 2 37 nsize: 2 38 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 39 test: 40 41TEST*/ 42 43""" 44 45import os, re, glob, types 46from distutils.sysconfig import parse_makefile 47import sys 48import logging 49sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 50 51import inspect 52thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 53maintdir=os.path.join(os.path.join(os.path.dirname(thisscriptdir),'bin'),'maint') 54sys.path.insert(0,maintdir) 55# Don't print out trace when raise Exceptions 56sys.tracebacklimit = 0 57 58# These are special keys describing build 59buildkeys="requires TODO SKIP depends".split() 60 61acceptedkeys="test nsize requires command suffix args filter filter_output localrunfiles comments TODO SKIP output_file".split() 62appendlist="args requires comments".split() 63 64import re 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=='test' or var=='testset'): 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=='test' or var=='testset'): 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=='test' or var=='testset': 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 130 return newTestStr 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() 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 testDict.has_key('nsize'): 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 not testDict.has_key('args'): return sepvars 160 for varset in re.split('-(?=[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 _getVarVals(findvar,testDict): 170 """ 171 Given: variable that is either nsize or in args 172 Return: Values to loop over 173 """ 174 vals=None 175 newargs='' 176 if findvar=='nsize': 177 varset=testDict[findvar] 178 keynm,vals,ftype=parseLoopArgs('nsize '+varset) 179 else: 180 varlist=[] 181 for varset in re.split('-(?=[a-zA-Z])',testDict['args']): 182 if not varset.strip(): continue 183 if '{{' in varset: 184 keyvar,vals,ftype=parseLoopArgs(varset) 185 if keyvar!=findvar: 186 newargs+="-"+varset.strip()+" " 187 continue 188 else: 189 newargs+="-"+varset.strip()+" " 190 191 if not vals: raise StandardError("Could not find separate_testvar: "+findvar) 192 return vals,newargs 193 194def genTestsSeparateTestvars(intests,indicts): 195 """ 196 Given: testname, sdict with 'separate_testvars 197 Return: testnames,sdicts: List of generated tests 198 """ 199 testnames=[]; sdicts=[] 200 for i in range(len(intests)): 201 testname=intests[i]; sdict=indicts[i]; i+=1 202 separate_testvars=_getSeparateTestvars(sdict) 203 if len(separate_testvars)>0: 204 for kvar in separate_testvars: 205 kvals,newargs=_getVarVals(kvar,sdict) 206 # No errors means we are good to go 207 for val in kvals.split(): 208 kvardict=sdict.copy() 209 gensuffix="_"+kvar+"-"+val 210 newtestnm=testname+gensuffix 211 if sdict.has_key('suffix'): 212 kvardict['suffix']=sdict['suffix']+gensuffix 213 else: 214 kvardict['suffix']=gensuffix 215 if kvar=='nsize': 216 kvardict[kvar]=val 217 else: 218 kvardict['args']=newargs+"-"+kvar+" "+val 219 testnames.append(newtestnm) 220 sdicts.append(kvardict) 221 else: 222 testnames.append(testname) 223 sdicts.append(sdict) 224 return testnames,sdicts 225 226def genTestsSubtestSuffix(testnames,sdicts): 227 """ 228 Given: testname, sdict with separate_testvars 229 Return: testnames,sdicts: List of generated tests 230 """ 231 tnms=[]; sdcts=[] 232 for i in range(len(testnames)): 233 testname=testnames[i] 234 rmsubtests=[]; keepSubtests=False 235 if sdicts[i].has_key('subtests'): 236 for stest in sdicts[i]["subtests"]: 237 if sdicts[i][stest].has_key('suffix'): 238 rmsubtests.append(stest) 239 gensuffix="_"+sdicts[i][stest]['suffix'] 240 newtestnm=testname+gensuffix 241 tnms.append(newtestnm) 242 newsdict=sdicts[i].copy() 243 del newsdict['subtests'] 244 # Have to hand update 245 # Append 246 for kup in appendlist: 247 if sdicts[i][stest].has_key(kup): 248 if sdicts[i].has_key(kup): 249 newsdict[kup]=sdicts[i][kup]+" "+sdicts[i][stest][kup] 250 else: 251 newsdict[kup]=sdicts[i][stest][kup] 252 # Promote 253 for kup in acceptedkeys: 254 if kup in appendlist: continue 255 if sdicts[i][stest].has_key(kup): 256 newsdict[kup]=sdicts[i][stest][kup] 257 # Cleanup 258 for st in sdicts[i]["subtests"]: del newsdict[st] 259 sdcts.append(newsdict) 260 else: 261 keepSubtests=True 262 else: 263 tnms.append(testnames[i]) 264 sdcts.append(sdicts[i]) 265 # If a subtest without a suffix exists, then save it 266 if keepSubtests: 267 tnms.append(testnames[i]) 268 newsdict=sdicts[i].copy() 269 # Prune the tests to prepare for keeping 270 for rmtest in rmsubtests: 271 newsdict['subtests'].remove(rmtest) 272 del newsdict[rmtest] 273 sdcts.append(newsdict) 274 i+=1 275 return tnms,sdcts 276 277def splitTests(testname,sdict): 278 """ 279 Given: testname and YAML-generated dictionary 280 Return: list of names and dictionaries corresponding to each test 281 given that the YAML language allows for multiple tests 282 """ 283 284 # Order: Parent sep_tv, subtests suffix, subtests sep_tv 285 testnames,sdicts=genTestsSeparateTestvars([testname],[sdict]) 286 testnames,sdicts=genTestsSubtestSuffix(testnames,sdicts) 287 testnames,sdicts=genTestsSeparateTestvars(testnames,sdicts) 288 289 # Because I am altering the list, I do this in passes. Inelegant 290 291 return testnames, sdicts 292 293def parseTest(testStr,srcfile,verbosity): 294 """ 295 This parses an individual test 296 YAML is hierarchial so should use a state machine in the general case, 297 but in practice we only support two levels of test: 298 """ 299 basename=os.path.basename(srcfile) 300 # Handle the new at the begininng 301 bn=re.sub("new_","",basename) 302 # This is the default 303 testname="run"+os.path.splitext(bn)[0] 304 305 # Tests that have default everything (so empty effectively) 306 if len(testStr)==0: return testname, {} 307 308 striptest=_stripIndent(testStr,srcfile) 309 310 # go through and parse 311 subtestnum=0 312 subdict={} 313 comments=[] 314 indentlevel=0 315 for ln in striptest.split("\n"): 316 line=ln.split('#')[0].rstrip() 317 if verbosity>2: print line 318 comment=("" if len(ln.split("#"))>0 else " ".join(ln.split("#")[1:]).strip()) 319 if comment: comments.append(comment) 320 if not line.strip(): continue 321 lsplit=line.split(':') 322 if len(lsplit)==0: raise Exception("Missing : in line: "+line) 323 indentcount=lsplit[0].count(" ") 324 var=lsplit[0].strip() 325 val=line[line.find(':')+1:].strip() 326 if not var in acceptedkeys: raise Exception("Not a defined key: "+var+" from: "+line) 327 # Start by seeing if we are in a subtest 328 if line.startswith(" "): 329 subdict[subtestname][var]=val 330 if not indentlevel: indentlevel=indentcount 331 #if indentlevel!=indentcount: print "Error in indentation:", ln 332 # Determine subtest name and make dict 333 elif var=="test": 334 subtestname="test"+str(subtestnum) 335 subdict[subtestname]={} 336 if not subdict.has_key("subtests"): subdict["subtests"]=[] 337 subdict["subtests"].append(subtestname) 338 subtestnum=subtestnum+1 339 # The rest are easy 340 else: 341 # For convenience, it is sometimes convenient to list twice 342 if subdict.has_key(var): 343 if var in appendlist: 344 subdict[var]+=" "+val 345 else: 346 raise Exception(var+" entered twice: "+line) 347 else: 348 subdict[var]=val 349 if var=="suffix": 350 if len(val)>0: 351 testname=testname+"_"+val 352 353 if len(comments): subdict['comments']="\n".join(comments).lstrip("\n") 354 # A test block can create multiple tests. This does 355 # that logic 356 testnames,subdicts=splitTests(testname,subdict) 357 return testnames,subdicts 358 359def parseTests(testStr,srcfile,fileNums,verbosity): 360 """ 361 Parse the yaml string describing tests and return 362 a dictionary with the info in the form of: 363 testDict[test][subtest] 364 This is an inelegant parser as we do not wish to 365 introduce a new dependency by bringing in pyyaml. 366 The advantage is that validation can be done as 367 it is parsed (e.g., 'test' is the only top-level node) 368 """ 369 370 testDict={} 371 372 # The first entry should be test: but it might be indented. 373 newTestStr=_stripIndent(testStr,srcfile,entireBlock=True,fileNums=fileNums) 374 if verbosity>2: print srcfile 375 376 # Now go through each test. First elem in split is blank 377 for test in re.split("\ntest(?:set)?:",newTestStr)[1:]: 378 testnames,subdicts=parseTest(test,srcfile,verbosity) 379 for i in range(len(testnames)): 380 if testDict.has_key(testnames[i]): 381 raise RuntimeError("Multiple test names specified: "+testnames[i]+" in file: "+srcfile) 382 testDict[testnames[i]]=subdicts[i] 383 384 return testDict 385 386def parseTestFile(srcfile,verbosity): 387 """ 388 Parse single example files and return dictionary of the form: 389 testDict[srcfile][test][subtest] 390 """ 391 debug=False 392 curdir=os.path.realpath(os.path.curdir) 393 basedir=os.path.dirname(os.path.realpath(srcfile)) 394 basename=os.path.basename(srcfile) 395 os.chdir(basedir) 396 397 testDict={} 398 sh=open(srcfile,"r"); fileStr=sh.read(); sh.close() 399 400 ## Start with doing the tests 401 # 402 fsplit=fileStr.split("/*TEST\n")[1:] 403 if len(fsplit)==0: 404 if debug: print "No test found in: "+srcfile 405 return {} 406 fstart=len(fileStr.split("/*TEST\n")[0].split("\n"))+1 407 # Allow for multiple "/*TEST" blocks even though it really should be 408 # one 409 srcTests=[] 410 for t in fsplit: srcTests.append(t.split("TEST*/")[0]) 411 testString=" ".join(srcTests) 412 if len(testString.strip())==0: 413 print "No test found in: "+srcfile 414 return {} 415 flen=len(testString.split("\n")) 416 fend=fstart+flen-1 417 fileNums=range(fstart,fend) 418 testDict[basename]=parseTests(testString,srcfile,fileNums,verbosity) 419 420 ## Check and see if we have build reuqirements 421 # 422 if "/*T\n" in fileStr or "/*T " in fileStr: 423 # The file info is already here and need to append 424 Part1=fileStr.split("T*/")[0] 425 fileInfo=Part1.split("/*T")[1] 426 for bkey in buildkeys: 427 if bkey+":" in fileInfo: 428 testDict[basename][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip() 429 430 os.chdir(curdir) 431 return testDict 432 433def parseTestDir(directory,verbosity): 434 """ 435 Parse single example files and return dictionary of the form: 436 testDict[srcfile][test][subtest] 437 """ 438 curdir=os.path.realpath(os.path.curdir) 439 basedir=os.path.realpath(directory) 440 os.chdir(basedir) 441 442 tDict={} 443 for test_file in glob.glob("new_ex*.*"): 444 tDict.update(parseTestFile(test_file,verbosity)) 445 446 os.chdir(curdir) 447 return tDict 448 449def printExParseDict(rDict): 450 """ 451 This is useful for debugging 452 """ 453 indent=" " 454 for sfile in rDict: 455 print sfile 456 sortkeys=rDict[sfile].keys() 457 sortkeys.sort() 458 for runex in sortkeys: 459 print indent+runex 460 if type(rDict[sfile][runex])==types.StringType: 461 print indent*2+rDict[sfile][runex] 462 else: 463 for var in rDict[sfile][runex]: 464 if var.startswith("test"): continue 465 print indent*2+var+": "+str(rDict[sfile][runex][var]) 466 if rDict[sfile][runex].has_key('subtests'): 467 for var in rDict[sfile][runex]['subtests']: 468 print indent*2+var 469 for var2 in rDict[sfile][runex][var]: 470 print indent*3+var2+": "+str(rDict[sfile][runex][var][var2]) 471 print "\n" 472 return 473 474def main(directory='',test_file='',verbosity=0): 475 476 if directory: 477 tDict=parseTestDir(directory,verbosity) 478 else: 479 tDict=parseTestFile(test_file,verbosity) 480 if verbosity>0: printExParseDict(tDict) 481 482 return 483 484if __name__ == '__main__': 485 import optparse 486 parser = optparse.OptionParser() 487 parser.add_option('-d', '--directory', dest='directory', 488 default="", help='Directory containing files to parse') 489 parser.add_option('-t', '--test_file', dest='test_file', 490 default="", help='Test file, e.g., ex1.c, to parse') 491 parser.add_option('-v', '--verbosity', dest='verbosity', 492 help='Verbosity of output by level: 1, 2, or 3', default=0) 493 opts, extra_args = parser.parse_args() 494 495 if extra_args: 496 import sys 497 sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args)) 498 exit(1) 499 if not opts.test_file and not opts.directory: 500 print "test file or directory is required" 501 parser.print_usage() 502 sys.exit() 503 504 # Need verbosity to be an integer 505 try: 506 verbosity=int(opts.verbosity) 507 except: 508 raise Exception("Error: Verbosity must be integer") 509 510 main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity) 511