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 output_file: output/ex25_1.out 29 30 test: 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 35TEST*/ 36 37""" 38 39import os, re, glob, types 40from distutils.sysconfig import parse_makefile 41import sys 42import logging 43sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 44 45import inspect 46thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 47maintdir=os.path.join(os.path.join(os.path.dirname(thisscriptdir),'bin'),'maint') 48sys.path.insert(0,maintdir) 49 50# These are special keys describing build 51buildkeys="requires TODO SKIP depends".split() 52 53 54import re 55def extractForVars(argStr): 56 """ 57 Given: 'args: -bs {{1 2 3 4 5}} -pc_type {{cholesky sor}} -ksp_monitor' 58 Return: 59 args: -ksp_monitor 60 forlist="bs pc_type" # Don't assume OrderedDict 61 forargs[bs]['val']="1 2 3 4 5" 62 forargs[pc_type]['val']="cholesky sor" 63 """ 64 loopstr=argStr 65 forargs={} 66 forlist=[] 67 newargs="" 68 for varset in re.split('-(?=[a-zA-Z])',loopstr): 69 if not varset.strip(): continue 70 if len(re.findall('{{(.*?)}}',varset))>0: 71 # Assuming only one for loop per var specification 72 forvar=varset.split("{{")[0].strip() 73 forlist.append(forvar) 74 forargs[forvar]={} 75 forargs[forvar]['val']=re.findall('{{(.*?)}}',varset)[0] 76 else: 77 newargs=newargs+"-"+varset+" " 78 79 return forlist,forargs,newargs.strip() 80 81 82def _stripIndent(block,srcfile): 83 """ 84 Go through and remove a level of indentation 85 Also strip of trailing whitespace 86 """ 87 # The first entry should be test: but it might be indented. 88 ext=os.path.splitext(srcfile)[1] 89 for lline in block.split("\n"): 90 line=lline[1:] if lline.startswith("!") else lline 91 line=line.split('#')[0] 92 if not line.strip(): continue 93 stripstr=" " 94 nspace=len(line)-len(line.lstrip(stripstr)) 95 newline=line[nspace:] 96 break 97 98 # Strip off any indentation for the whole string and any trailing 99 # whitespace for convenience 100 newTestStr="\n" 101 for lline in block.split("\n"): 102 line=lline[1:] if lline.startswith("!") else lline 103 if not line.strip(): continue 104 newline=line[nspace:] 105 newTestStr=newTestStr+newline.rstrip()+"\n" 106 107 return newTestStr 108 109def parseTestInsert(dct,var,val,prefix=''): 110 """ 111 Before inserting a string, look for implicit loop variables ({{a,b}}), and convert them into references to 112 automatically generated explicity loop variables in the dictionary (e.g., create the entry {var_0: {{a,b}}} and 113 replace '{{a,b}}' with @VAR_0@) 114 """ 115 count = 0 116 impLoop = re.search('{{.*?}}',val) 117 while impLoop: 118 if impLoop.group(0) == val: 119 break 120 expLoop = '_'.join([prefix,str(var),str(count)]) 121 while dct.has_key(expLoop): 122 count += 1 123 expLoop = '_'.join([prefix,str(var),str(count)]) 124 dct[expLoop] = impLoop.group(0) 125 subs = '@'+expLoop.upper()+'@' 126 val = re.sub(impLoop.group(0),subs,val,count=1) 127 impLoop = re.search('{{.*?}}',val) 128 dct[var] = val 129 130 131def parseTest(testStr,srcfile): 132 """ 133 This parses an individual test 134 YAML is hierarchial so should use a state machine in the general case, 135 but in practice we only support two levels of test: 136 """ 137 basename=os.path.basename(srcfile) 138 # Handle the new at the begininng 139 bn=re.sub("new_","",basename) 140 # This is the default 141 testname="run"+os.path.splitext(bn)[0] 142 143 # Tests that have default everything (so empty effectively) 144 if len(testStr)==0: return testname, {} 145 146 striptest=_stripIndent(testStr,srcfile) 147 148 # go through and parse 149 subtestnum=0 150 subdict={} 151 comments=[] 152 indentlevel=0 153 for ln in striptest.split("\n"): 154 line=ln.split('#')[0] 155 comment=("" if len(ln.split("#"))==1 else " ".join(ln.split("#")[1:]).strip()) 156 if comment: comments.append(comment) 157 if not line.strip(): continue 158 indentcount=line.split(":")[0].count(" ") 159 var=line.split(":")[0].strip() 160 val=line.split(":")[1].strip() 161 # Start by seeing if we are in a subtest 162 if line.startswith(" "): 163 #subdict[subtestname][var]=val 164 if not indentlevel: indentlevel=indentcount 165 #if indentlevel!=indentcount: print "Error in indentation:", ln 166 parseTestInsert(subdict[subtestname],var,val,prefix=subtestname) 167 # Determine subtest name and make dict 168 elif var=="test": 169 subtestname="test"+str(subtestnum) 170 subdict[subtestname]={} 171 if not subdict.has_key("subtests"): subdict["subtests"]=[] 172 subdict["subtests"].append(subtestname) 173 subtestnum=subtestnum+1 174 # The rest are easy 175 else: 176 parseTestInsert(subdict,var,val) 177 if var=="suffix": 178 if len(val)>0: 179 testname=testname+"_"+val 180 181 if len(comments): subdict['comments']="\n".join(comments).lstrip("\n") 182 return testname,subdict 183 184def parseTests(testStr,srcfile): 185 """ 186 Parse the yaml string describing tests and return 187 a dictionary with the info in the form of: 188 testDict[test][subtest] 189 This is an inelegant parser as we do not wish to 190 introduce a new dependency by bringing in pyyaml. 191 The advantage is that validation can be done as 192 it is parsed (e.g., 'test' is the only top-level node) 193 """ 194 195 testDict={} 196 197 # The first entry should be test: but it might be indented. 198 newTestStr=_stripIndent(testStr,srcfile) 199 200 # Now go through each test. First elem in split is blank 201 for test in newTestStr.split("\ntest:\n")[1:]: 202 testname,subdict=parseTest(test,srcfile) 203 if testDict.has_key(testname): 204 print "Multiple test names specified: "+testname+" in file: "+srcfile 205 testDict[testname]=subdict 206 207 return testDict 208 209def parseTestFile(srcfile): 210 """ 211 Parse single example files and return dictionary of the form: 212 testDict[srcfile][test][subtest] 213 """ 214 debug=False 215 curdir=os.path.realpath(os.path.curdir) 216 basedir=os.path.dirname(os.path.realpath(srcfile)) 217 basename=os.path.basename(srcfile) 218 os.chdir(basedir) 219 220 testDict={} 221 sh=open(srcfile,"r"); fileStr=sh.read(); sh.close() 222 223 ## Start with doing the tests 224 # 225 fsplit=fileStr.split("/*TEST\n")[1:] 226 if len(fsplit)==0: 227 if debug: print "No test found in: "+srcfile 228 return {} 229 # Allow for multiple "/*TEST" blocks even though it really should be 230 # on 231 srcTests=[] 232 for t in fsplit: srcTests.append(t.split("TEST*/")[0]) 233 testString=" ".join(srcTests) 234 if len(testString.strip())==0: 235 print "No test found in: "+srcfile 236 return {} 237 testDict[basename]=parseTests(testString,srcfile) 238 239 ## Check and see if we have build reuqirements 240 # 241 if "/*T\n" in fileStr or "/*T " in fileStr: 242 # The file info is already here and need to append 243 Part1=fileStr.split("T*/")[0] 244 fileInfo=Part1.split("/*T")[1] 245 for bkey in buildkeys: 246 if bkey+":" in fileInfo: 247 testDict[basename][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip() 248 249 os.chdir(curdir) 250 return testDict 251 252def parseTestDir(directory): 253 """ 254 Parse single example files and return dictionary of the form: 255 testDict[srcfile][test][subtest] 256 """ 257 curdir=os.path.realpath(os.path.curdir) 258 basedir=os.path.realpath(directory) 259 os.chdir(basedir) 260 261 tDict={} 262 for test_file in glob.glob("new_ex*.*"): 263 tDict.update(parseTestFile(test_file)) 264 265 os.chdir(curdir) 266 return tDict 267 268def printExParseDict(rDict): 269 """ 270 This is useful for debugging 271 """ 272 indent=" " 273 for sfile in rDict: 274 print "\n\n"+sfile 275 for runex in rDict[sfile]: 276 print indent+runex 277 if type(rDict[sfile][runex])==types.StringType: 278 print indent*2+rDict[sfile][runex] 279 else: 280 for var in rDict[sfile][runex]: 281 if var.startswith("test"): 282 print indent*2+var 283 for var2 in rDict[sfile][runex][var]: 284 print indent*3+var2+": "+str(rDict[sfile][runex][var][var2]) 285 else: 286 print indent*2+var+": "+str(rDict[sfile][runex][var]) 287 return 288 289def main(directory='',test_file='',verbosity=0): 290 291 if directory: 292 tDict=parseTestDir(directory) 293 else: 294 tDict=parseTestFile(test_file) 295 if verbosity>0: printExParseDict(tDict) 296 297 return 298 299if __name__ == '__main__': 300 import optparse 301 parser = optparse.OptionParser() 302 parser.add_option('-d', '--directory', dest='directory', 303 default="", help='Directory containing files to parse') 304 parser.add_option('-t', '--test_file', dest='test_file', 305 default="", help='Test file, e.g., ex1.c, to parse') 306 parser.add_option('-v', '--verbosity', dest='verbosity', 307 help='Verbosity of output by level: 1, 2, or 3', default=0) 308 opts, extra_args = parser.parse_args() 309 310 if extra_args: 311 import sys 312 sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args)) 313 exit(1) 314 if not opts.test_file and not opts.directory: 315 print "test file or directory is required" 316 parser.print_usage() 317 sys.exit() 318 319 # Need verbosity to be an integer 320 try: 321 verbosity=int(opts.verbosity) 322 except: 323 raise Exception("Error: Verbosity must be integer") 324 325 main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity) 326