xref: /petsc/config/testparse.py (revision 8722ed3a1ce3ec382c5e0ec553555fda119baaf0)
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   # This is equivalent to test:
27   testset:
28      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
29
30   testset:
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
35   testset:
36      suffix: 2
37      nsize: 2
38      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
39      test:
40
41TEST*/
42
43"""
44
45import os, re, glob, types
46from distutils.sysconfig import parse_makefile
47import sys
48import logging
49sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
50
51import inspect
52thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
53maintdir=os.path.join(os.path.join(os.path.dirname(thisscriptdir),'bin'),'maint')
54sys.path.insert(0,maintdir)
55
56# These are special keys describing build
57buildkeys="requires TODO SKIP depends".split()
58
59acceptedkeys="test nsize requires command suffix args filter filter_output localrunfiles comments TODO SKIP output_file".split()
60appendlist="args requires comments".split()
61
62import re
63
64def _stripIndent(block,srcfile,entireBlock=False,fileNums=[]):
65  """
66  Go through and remove a level of indentation
67  Also strip of trailing whitespace
68  """
69  # The first entry should be test: but it might be indented.
70  ext=os.path.splitext(srcfile)[1]
71  stripstr=" "
72  for lline in block.split("\n"):
73    line=lline[1:] if lline.startswith("!") else lline
74    if not line.strip(): continue
75    if line.strip().startswith('#'): continue
76    if entireBlock:
77      var=line.split(":")[0].strip()
78      if not (var=='test' or var=='testset'):
79        raise Exception("Formatting error in finding test in file: "+srcfile+"\n")
80    nspace=len(line)-len(line.lstrip(stripstr))
81    newline=line[nspace:]
82    break
83
84  # Strip off any indentation for the whole string and any trailing
85  # whitespace for convenience
86  newTestStr="\n"
87  if len(fileNums)>0: lineNum=fileNums[0]-1
88  firstPass=True
89  for lline in block.split("\n"):
90    if len(fileNums)>0: lineNum+=1
91    line=lline[1:] if lline.startswith("!") else lline
92    if not line.strip(): continue
93    if line.strip().startswith('#'):
94      newTestStr+=line+'\n'
95    else:
96      newline=line[nspace:]
97      newTestStr+=newline.rstrip()+"\n"
98    # Do some basic indentation checks
99    if entireBlock:
100      # Don't need to check comment lines
101      if line.strip().startswith('#'): continue
102      if not newline.startswith(" "):
103        var=newline.split(":")[0].strip()
104        if not (var=='test' or var=='testset'):
105          err="Formatting error in file "+srcfile+" at line: " +line+"\n"
106          if len(fileNums)>0:
107            raise Exception(err+"Check indentation at line number: "+str(lineNum))
108          else:
109            raise Exception(err)
110      else:
111        var=line.split(":")[0].strip()
112        if var=='test' or var=='testset':
113          subnspace=len(line)-len(line.lstrip(stripstr))
114          if firstPass:
115            firstsubnspace=subnspace
116            firstPass=False
117          else:
118            if firstsubnspace!=subnspace:
119              err="Formatting subtest error in file "+srcfile+" at line: " +line+"\n"
120              if len(fileNums)>0:
121                raise Exception(err+"Check indentation at line number: "+str(lineNum))
122              else:
123                raise Exception(err)
124
125
126  return newTestStr
127
128def _getNewArgs(kvar,val,argStr):
129  """
130  Given:   String containing arguments
131  Return:  String without the value in it
132  """
133  return newTestStr
134
135def _getSeparateTestvars(testDict):
136  """
137  Given: dictionary that may have
138  Return:  Variables that cause a test split
139  """
140  vals=None
141  sepvars=[]
142  # Check nsize
143  if testDict.has_key('nsize'):
144    varset=testDict['nsize']
145    if len(re.findall('{{(.*?)}(?:[\w\s]*)?}',varset))>0:
146      if 'separate' in re.findall('{{(?:.*?)}([\w\s]*)?}',varset)[0]:
147        sepvars.append('nsize')
148
149  # Now check args
150  if not testDict.has_key('args'): return sepvars
151  for varset in re.split('-(?=[a-zA-Z])',testDict['args']):
152    if not varset.strip(): continue
153    if len(re.findall('{{(.*?)}(?:[\w\s]*)?}',varset))>0:
154      # Assuming only one for loop per var specification
155      keyvar=varset.split("{{")[0].strip()
156      fortype=re.findall('{{(?:.*?)}([\w\s]*)?}',varset)[0]
157      if 'separate' in fortype: sepvars.append(keyvar)
158
159  return sepvars
160
161def _getVarVals(findvar,testDict):
162  """
163  Given: variable that is either nsize or in args
164  Return:  Values to loop over
165  """
166  vals=None
167  newargs=''
168  if findvar=='nsize':
169    varset=testDict[findvar]
170    vals=re.findall('{{(.*?)}(?:[\w\s]*)?}',varset)[0]
171  else:
172    varlist=[]
173    for varset in re.split('-(?=[a-zA-Z])',testDict['args']):
174      if not varset.strip(): continue
175      if len(re.findall('{{(.*?)}(?:[\w\s]*)?}',varset))>0:
176        # Assuming only one for loop per var specification
177        keyvar=varset.split("{{")[0].strip()
178        if keyvar!=findvar:
179          newargs+="-"+varset.strip()+" "
180          continue
181        vals=re.findall('{{(.*?)}(?:[\w\s]*)?}',varset)[0]
182      else:
183        newargs+="-"+varset.strip()+" "
184
185  if not vals: raise StandardError("Could not find separate_testvar: "+findvar)
186  return vals,newargs
187
188def genTestsSeparateTestvars(intests,indicts):
189  """
190  Given: testname, sdict with 'separate_testvars
191  Return: testnames,sdicts: List of generated tests
192  """
193  testnames=[]; sdicts=[]
194  for i in range(len(intests)):
195    testname=intests[i]; sdict=indicts[i]; i+=1
196    separate_testvars=_getSeparateTestvars(sdict)
197    if len(separate_testvars)>0:
198      for kvar in separate_testvars:
199        kvals,newargs=_getVarVals(kvar,sdict)
200        # No errors means we are good to go
201        for val in kvals.split():
202          kvardict=sdict.copy()
203          gensuffix="_"+kvar+"-"+val
204          newtestnm=testname+gensuffix
205          if sdict.has_key('suffix'):
206            kvardict['suffix']=sdict['suffix']+gensuffix
207          else:
208            kvardict['suffix']=gensuffix
209          if kvar=='nsize':
210            kvardict[kvar]=val
211          else:
212            kvardict['args']=newargs+"-"+kvar+" "+val
213          testnames.append(newtestnm)
214          sdicts.append(kvardict)
215    else:
216      testnames.append(testname)
217      sdicts.append(sdict)
218  return testnames,sdicts
219
220def genTestsSubtestSuffix(testnames,sdicts):
221  """
222  Given: testname, sdict with separate_testvars
223  Return: testnames,sdicts: List of generated tests
224  """
225  tnms=[]; sdcts=[]
226  for i in range(len(testnames)):
227    testname=testnames[i]
228    rmsubtests=[]; keepSubtests=False
229    if sdicts[i].has_key('subtests'):
230      for stest in sdicts[i]["subtests"]:
231        if sdicts[i][stest].has_key('suffix'):
232          rmsubtests.append(stest)
233          gensuffix="_"+sdicts[i][stest]['suffix']
234          newtestnm=testname+gensuffix
235          tnms.append(newtestnm)
236          newsdict=sdicts[i].copy()
237          del newsdict['subtests']
238          # Have to hand update
239          # Append
240          for kup in appendlist:
241            if sdicts[i][stest].has_key(kup):
242              if sdicts[i].has_key(kup):
243                newsdict[kup]=sdicts[i][kup]+" "+sdicts[i][stest][kup]
244              else:
245                newsdict[kup]=sdicts[i][stest][kup]
246          # Promote
247          for kup in acceptedkeys:
248            if kup in appendlist: continue
249            if sdicts[i][stest].has_key(kup):
250              newsdict[kup]=sdicts[i][stest][kup]
251          # Cleanup
252          for st in sdicts[i]["subtests"]: del newsdict[st]
253          sdcts.append(newsdict)
254        else:
255          keepSubtests=True
256    else:
257      tnms.append(testnames[i])
258      sdcts.append(sdicts[i])
259    # If a subtest without a suffix exists, then save it
260    if keepSubtests:
261      tnms.append(testnames[i])
262      newsdict=sdicts[i].copy()
263      # Prune the tests to prepare for keeping
264      for rmtest in rmsubtests:
265        newsdict['subtests'].remove(rmtest)
266        del newsdict[rmtest]
267      sdcts.append(newsdict)
268    i+=1
269  return tnms,sdcts
270
271def splitTests(testname,sdict):
272  """
273  Given: testname and YAML-generated dictionary
274  Return: list of names and dictionaries corresponding to each test
275          given that the YAML language allows for multiple tests
276  """
277
278  # Order: Parent sep_tv, subtests suffix, subtests sep_tv
279  testnames,sdicts=genTestsSeparateTestvars([testname],[sdict])
280  testnames,sdicts=genTestsSubtestSuffix(testnames,sdicts)
281  testnames,sdicts=genTestsSeparateTestvars(testnames,sdicts)
282
283  # Because I am altering the list, I do this in passes.  Inelegant
284
285  return testnames, sdicts
286
287def parseTest(testStr,srcfile,verbosity):
288  """
289  This parses an individual test
290  YAML is hierarchial so should use a state machine in the general case,
291  but in practice we only support two levels of test:
292  """
293  basename=os.path.basename(srcfile)
294  # Handle the new at the begininng
295  bn=re.sub("new_","",basename)
296  # This is the default
297  testname="run"+os.path.splitext(bn)[0]
298
299  # Tests that have default everything (so empty effectively)
300  if len(testStr)==0: return testname, {}
301
302  striptest=_stripIndent(testStr,srcfile)
303
304  # go through and parse
305  subtestnum=0
306  subdict={}
307  comments=[]
308  indentlevel=0
309  for ln in striptest.split("\n"):
310    line=ln.split('#')[0].rstrip()
311    if verbosity>2: print line
312    comment=("" if len(ln.split("#"))>0 else " ".join(ln.split("#")[1:]).strip())
313    if comment: comments.append(comment)
314    if not line.strip(): continue
315    lsplit=line.split(':')
316    if len(lsplit)==0: raise Exception("Missing : in line: "+line)
317    indentcount=lsplit[0].count(" ")
318    var=lsplit[0].strip()
319    val=line[line.find(':')+1:].strip()
320    if not var in acceptedkeys: raise Exception("Not a defined key: "+var+" from:  "+line)
321    # Start by seeing if we are in a subtest
322    if line.startswith(" "):
323      subdict[subtestname][var]=val
324      if not indentlevel: indentlevel=indentcount
325      #if indentlevel!=indentcount: print "Error in indentation:", ln
326    # Determine subtest name and make dict
327    elif var=="test":
328      subtestname="test"+str(subtestnum)
329      subdict[subtestname]={}
330      if not subdict.has_key("subtests"): subdict["subtests"]=[]
331      subdict["subtests"].append(subtestname)
332      subtestnum=subtestnum+1
333    # The rest are easy
334    else:
335      # For convenience, it is sometimes convenient to list twice
336      if subdict.has_key(var):
337        if var in appendlist:
338          subdict[var]+=" "+val
339        else:
340          raise Exception(var+" entered twice: "+line)
341      else:
342        subdict[var]=val
343      if var=="suffix":
344        if len(val)>0:
345          testname=testname+"_"+val
346
347  if len(comments): subdict['comments']="\n".join(comments).lstrip("\n")
348  # A test block can create multiple tests.  This does
349  # that logic
350  testnames,subdicts=splitTests(testname,subdict)
351  return testnames,subdicts
352
353def parseTests(testStr,srcfile,fileNums,verbosity):
354  """
355  Parse the yaml string describing tests and return
356  a dictionary with the info in the form of:
357    testDict[test][subtest]
358  This is an inelegant parser as we do not wish to
359  introduce a new dependency by bringing in pyyaml.
360  The advantage is that validation can be done as
361  it is parsed (e.g., 'test' is the only top-level node)
362  """
363
364  testDict={}
365
366  # The first entry should be test: but it might be indented.
367  newTestStr=_stripIndent(testStr,srcfile,entireBlock=True,fileNums=fileNums)
368  if verbosity>2: print srcfile
369
370  # Now go through each test.  First elem in split is blank
371  for test in re.split("\ntest(?:set)?:",newTestStr)[1:]:
372    testnames,subdicts=parseTest(test,srcfile,verbosity)
373    for i in range(len(testnames)):
374      if testDict.has_key(testnames[i]):
375        raise Error("Multiple test names specified: "+testname+" in file: "+srcfile)
376      testDict[testnames[i]]=subdicts[i]
377
378  return testDict
379
380def parseTestFile(srcfile,verbosity):
381  """
382  Parse single example files and return dictionary of the form:
383    testDict[srcfile][test][subtest]
384  """
385  debug=False
386  curdir=os.path.realpath(os.path.curdir)
387  basedir=os.path.dirname(os.path.realpath(srcfile))
388  basename=os.path.basename(srcfile)
389  os.chdir(basedir)
390
391  testDict={}
392  sh=open(srcfile,"r"); fileStr=sh.read(); sh.close()
393
394  ## Start with doing the tests
395  #
396  fsplit=fileStr.split("/*TEST\n")[1:]
397  if len(fsplit)==0:
398    if debug: print "No test found in: "+srcfile
399    return {}
400  fstart=len(fileStr.split("/*TEST\n")[0].split("\n"))+1
401  # Allow for multiple "/*TEST" blocks even though it really should be
402  # on
403  srcTests=[]
404  for t in fsplit: srcTests.append(t.split("TEST*/")[0])
405  testString=" ".join(srcTests)
406  if len(testString.strip())==0:
407    print "No test found in: "+srcfile
408    return {}
409  flen=len(testString.split("\n"))
410  fend=fstart+flen-1
411  fileNums=range(fstart,fend)
412  testDict[basename]=parseTests(testString,srcfile,fileNums,verbosity)
413
414  ## Check and see if we have build reuqirements
415  #
416  if "/*T\n" in fileStr or "/*T " in fileStr:
417    # The file info is already here and need to append
418    Part1=fileStr.split("T*/")[0]
419    fileInfo=Part1.split("/*T")[1]
420    for bkey in buildkeys:
421      if bkey+":" in fileInfo:
422        testDict[basename][bkey]=fileInfo.split(bkey+":")[1].split("\n")[0].strip()
423
424  os.chdir(curdir)
425  return testDict
426
427def parseTestDir(directory,verbosity):
428  """
429  Parse single example files and return dictionary of the form:
430    testDict[srcfile][test][subtest]
431  """
432  curdir=os.path.realpath(os.path.curdir)
433  basedir=os.path.realpath(directory)
434  os.chdir(basedir)
435
436  tDict={}
437  for test_file in glob.glob("new_ex*.*"):
438    tDict.update(parseTestFile(test_file,verbosity))
439
440  os.chdir(curdir)
441  return tDict
442
443def printExParseDict(rDict):
444  """
445  This is useful for debugging
446  """
447  indent="   "
448  for sfile in rDict:
449    print sfile
450    sortkeys=rDict[sfile].keys()
451    sortkeys.sort()
452    for runex in sortkeys:
453      print indent+runex
454      if type(rDict[sfile][runex])==types.StringType:
455        print indent*2+rDict[sfile][runex]
456      else:
457        for var in rDict[sfile][runex]:
458          if var.startswith("test"): continue
459          print indent*2+var+": "+str(rDict[sfile][runex][var])
460        if rDict[sfile][runex].has_key('subtests'):
461          for var in rDict[sfile][runex]['subtests']:
462            print indent*2+var
463            for var2 in rDict[sfile][runex][var]:
464              print indent*3+var2+": "+str(rDict[sfile][runex][var][var2])
465      print "\n"
466  return
467
468def main(directory='',test_file='',verbosity=0):
469
470    if directory:
471      tDict=parseTestDir(directory,verbosity)
472    else:
473      tDict=parseTestFile(test_file,verbosity)
474    if verbosity>0: printExParseDict(tDict)
475
476    return
477
478if __name__ == '__main__':
479    import optparse
480    parser = optparse.OptionParser()
481    parser.add_option('-d', '--directory', dest='directory',
482                      default="", help='Directory containing files to parse')
483    parser.add_option('-t', '--test_file', dest='test_file',
484                      default="", help='Test file, e.g., ex1.c, to parse')
485    parser.add_option('-v', '--verbosity', dest='verbosity',
486                      help='Verbosity of output by level: 1, 2, or 3', default=0)
487    opts, extra_args = parser.parse_args()
488
489    if extra_args:
490        import sys
491        sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args))
492        exit(1)
493    if not opts.test_file and not opts.directory:
494      print "test file or directory is required"
495      parser.print_usage()
496      sys.exit()
497
498    # Need verbosity to be an integer
499    try:
500      verbosity=int(opts.verbosity)
501    except:
502      raise Exception("Error: Verbosity must be integer")
503
504    main(directory=opts.directory,test_file=opts.test_file,verbosity=verbosity)
505