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