xref: /petsc/config/testparse.py (revision c0658d2af0617fd5f0839559dfeb92f88412867f)
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 parseTest(testStr,srcfile):
110  """
111  This parses an individual test
112  YAML is hierarchial so should use a state machine in the general case,
113  but in practice we only support two levels of test:
114  """
115  basename=os.path.basename(srcfile)
116  # Handle the new at the begininng
117  bn=re.sub("new_","",basename)
118  # This is the default
119  testname="run"+os.path.splitext(bn)[0]
120
121  # Tests that have default everything (so empty effectively)
122  if len(testStr)==0: return testname, {}
123
124  striptest=_stripIndent(testStr,srcfile)
125
126  # go through and parse
127  subtestnum=0
128  subdict={}
129  comments=[]
130  indentlevel=0
131  for ln in striptest.split("\n"):
132    line=ln.split('#')[0]
133    comment=("" if len(ln.split("#"))==1 else " ".join(ln.split("#")[1:]).strip())
134    if comment: comments.append(comment)
135    if not line.strip(): continue
136    indentcount=line.split(":")[0].count(" ")
137    var=line.split(":")[0].strip()
138    val=line.split(":")[1].strip()
139    # Start by seeing if we are in a subtest
140    if line.startswith(" "):
141      subdict[subtestname][var]=val
142      if not indentlevel: indentlevel=indentcount
143      #if indentlevel!=indentcount: print "Error in indentation:", ln
144    # Determine subtest name and make dict
145    elif var=="test":
146      subtestname="test"+str(subtestnum)
147      subdict[subtestname]={}
148      if not subdict.has_key("subtests"): subdict["subtests"]=[]
149      subdict["subtests"].append(subtestname)
150      subtestnum=subtestnum+1
151    # The rest are easy
152    else:
153      subdict[var]=val
154      if var=="suffix":
155        if len(val)>0:
156          testname=testname+"_"+val
157
158  if len(comments): subdict['comments']="\n".join(comments).lstrip("\n")
159  return testname,subdict
160
161def parseTests(testStr,srcfile):
162  """
163  Parse the yaml string describing tests and return
164  a dictionary with the info in the form of:
165    testDict[test][subtest]
166  This is an inelegant parser as we do not wish to
167  introduce a new dependency by bringing in pyyaml.
168  The advantage is that validation can be done as
169  it is parsed (e.g., 'test' is the only top-level node)
170  """
171
172  testDict={}
173
174  # The first entry should be test: but it might be indented.
175  newTestStr=_stripIndent(testStr,srcfile)
176
177  # Now go through each test.  First elem in split is blank
178  for test in newTestStr.split("\ntest:\n")[1:]:
179    testname,subdict=parseTest(test,srcfile)
180    if testDict.has_key(testname):
181      print "Multiple test names specified: "+testname+" in file: "+srcfile
182    testDict[testname]=subdict
183
184  return testDict
185
186def parseTestFile(srcfile):
187  """
188  Parse single example files and return dictionary of the form:
189    testDict[srcfile][test][subtest]
190  """
191  debug=False
192  curdir=os.path.realpath(os.path.curdir)
193  basedir=os.path.dirname(os.path.realpath(srcfile))
194  basename=os.path.basename(srcfile)
195  os.chdir(basedir)
196
197  testDict={}
198  sh=open(srcfile,"r"); fileStr=sh.read(); sh.close()
199
200  ## Start with doing the tests
201  #
202  fsplit=fileStr.split("/*TEST\n")[1:]
203  if len(fsplit)==0:
204    if debug: print "No test found in: "+srcfile
205    return {}
206  # Allow for multiple "/*TEST" blocks even though it really should be
207  # on
208  srcTests=[]
209  for t in fsplit: srcTests.append(t.split("TEST*/")[0])
210  testString=" ".join(srcTests)
211  if len(testString.strip())==0:
212    print "No test found in: "+srcfile
213    return {}
214  testDict[basename]=parseTests(testString,srcfile)
215
216  ## Check and see if we have build reuqirements
217  #
218  if "/*T\n" in fileStr or "/*T " in fileStr:
219    # The file info is already here and need to append
220    Part1=fileStr.split("T*/")[0]
221    fileInfo=Part1.split("/*T")[1]
222    for bkey in buildkeys:
223      if bkey+":" in fileInfo:
224        testDict[basename][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip()
225
226  os.chdir(curdir)
227  return testDict
228
229def parseTestDir(directory):
230  """
231  Parse single example files and return dictionary of the form:
232    testDict[srcfile][test][subtest]
233  """
234  curdir=os.path.realpath(os.path.curdir)
235  basedir=os.path.realpath(directory)
236  os.chdir(basedir)
237
238  tDict={}
239  for test_file in glob.glob("new_ex*.*"):
240    tDict.update(parseTestFile(test_file))
241
242  os.chdir(curdir)
243  return tDict
244
245def printExParseDict(rDict):
246  """
247  This is useful for debugging
248  """
249  indent="   "
250  for sfile in rDict:
251    print "\n\n"+sfile
252    for runex in rDict[sfile]:
253      print indent+runex
254      if type(rDict[sfile][runex])==types.StringType:
255        print indent*2+rDict[sfile][runex]
256      else:
257        for var in rDict[sfile][runex]:
258          if var.startswith("test"):
259            print indent*2+var
260            for var2 in rDict[sfile][runex][var]:
261              print indent*3+var2+": "+str(rDict[sfile][runex][var][var2])
262          else:
263            print indent*2+var+": "+str(rDict[sfile][runex][var])
264  return
265
266def main(directory='',test_file='',verbosity=0):
267
268    if directory:
269      tDict=parseTestDir(directory)
270    else:
271      tDict=parseTestFile(test_file)
272    if verbosity>0: printExParseDict(tDict)
273
274    return
275
276if __name__ == '__main__':
277    import optparse
278    parser = optparse.OptionParser()
279    parser.add_option('-d', '--directory', dest='directory',
280                      default="", help='Directory containing files to parse')
281    parser.add_option('-t', '--test_file', dest='test_file',
282                      default="", help='Test file, e.g., ex1.c, to parse')
283    parser.add_option('-v', '--verbosity', dest='verbosity',
284                      help='Verbosity of output by level: 1, 2, or 3', default=0)
285    opts, extra_args = parser.parse_args()
286
287    if extra_args:
288        import sys
289        sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args))
290        exit(1)
291    if not opts.test_file and not opts.directory:
292      print "test file or directory is required"
293      parser.print_usage()
294      sys.exit()
295
296    # Need verbosity to be an integer
297    try:
298      verbosity=int(opts.verbosity)
299    except:
300      raise Exception("Error: Verbosity must be integer")
301
302    main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity)
303