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