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