xref: /petsc/config/testparse.py (revision e9b06b45184094998db48a805ae9104ad43d3ca8)
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