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