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