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