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