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