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