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