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