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