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