xref: /petsc/config/gmakegentest.py (revision 7d5fd1e4d9337468ad3f05b65b7facdcd2dfd2a4) !
1#!/usr/bin/env python
2
3from __future__ import print_function
4import pickle
5import os,shutil, string, re
6import sys
7import logging, time
8import types
9sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
10from collections import defaultdict
11from gmakegen import *
12
13import inspect
14thisscriptdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
15sys.path.insert(0,thisscriptdir)
16import testparse
17import example_template
18
19
20"""
21
22There are 2 modes of running tests: Normal builds and run from prefix of
23install.  They affect where to find things:
24
25
26Case 1.  Normal builds:
27
28     +---------------------+----------------------------------+
29     | PETSC_DIR           | <git dir>                        |
30     +---------------------+----------------------------------+
31     | PETSC_ARCH          | arch-foo                         |
32     +---------------------+----------------------------------+
33     | PETSC_LIBDIR        | PETSC_DIR/PETSC_ARCH/lib         |
34     +---------------------+----------------------------------+
35     | PETSC_EXAMPLESDIR   | PETSC_DIR/src                    |
36     +---------------------+----------------------------------+
37     | PETSC_TESTDIR       | PETSC_DIR/PETSC_ARCH/tests       |
38     +---------------------+----------------------------------+
39     | PETSC_GMAKEFILETEST | PETSC_DIR/gmakefile.test         |
40     +---------------------+----------------------------------+
41     | PETSC_GMAKEGENTEST  | PETSC_DIR/config/gmakegentest.py |
42     +---------------------+----------------------------------+
43
44
45Case 2.  From install dir:
46
47     +---------------------+-------------------------------------------------------+
48     | PETSC_DIR           | <prefix dir>                                          |
49     +---------------------+-------------------------------------------------------+
50     | PETSC_ARCH          | ''                                                    |
51     +---------------------+-------------------------------------------------------+
52     | PETSC_LIBDIR        | PETSC_DIR/PETSC_ARCH/lib                              |
53     +---------------------+-------------------------------------------------------+
54     | PETSC_EXAMPLESDIR   | PETSC_DIR/share/petsc/examples/src                    |
55     +---------------------+-------------------------------------------------------+
56     | PETSC_TESTDIR       | PETSC_DIR/PETSC_ARCH/tests                            |
57     +---------------------+-------------------------------------------------------+
58     | PETSC_GMAKEFILETEST | PETSC_DIR/share/petsc/examples/gmakefile.test         |
59     +---------------------+-------------------------------------------------------+
60     | PETSC_GMAKEGENTEST  | PETSC_DIR/share/petsc/examples/config/gmakegentest.py |
61     +---------------------+-------------------------------------------------------+
62
63"""
64
65def install_files(source, destdir):
66  """Install file or directory 'source' to 'destdir'.  Does not preserve
67  mode (permissions).
68  """
69  if not os.path.isdir(destdir):
70    os.makedirs(destdir)
71  if os.path.isdir(source):
72    for name in os.listdir(source):
73      install_files(os.path.join(source, name), os.path.join(destdir, os.path.basename(source)))
74  else:
75    shutil.copyfile(source, os.path.join(destdir, os.path.basename(source)))
76
77def nameSpace(srcfile,srcdir):
78  """
79  Because the scripts have a non-unique naming, the pretty-printing
80  needs to convey the srcdir and srcfile.  There are two ways of doing this.
81  """
82  if srcfile.startswith('run'): srcfile=re.sub('^run','',srcfile)
83  prefix=srcdir.replace("/","_")+"-"
84  nameString=prefix+srcfile
85  return nameString
86
87class generateExamples(Petsc):
88  """
89    gmakegen.py has basic structure for finding the files, writing out
90      the dependencies, etc.
91  """
92  def __init__(self,petsc_dir=None, petsc_arch=None, pkg_dir=None, pkg_arch=None, pkg_name=None, pkg_pkgs=None, testdir='tests', verbose=False, single_ex=False, srcdir=None, check=False):
93    super(generateExamples, self).__init__(petsc_dir=petsc_dir, petsc_arch=petsc_arch, pkg_dir=pkg_dir, pkg_arch=pkg_arch, pkg_name=pkg_name, pkg_pkgs=pkg_pkgs, verbose=verbose)
94
95    self.single_ex=single_ex
96    self.srcdir=srcdir
97    self.check_output=check
98
99    # Set locations to handle movement
100    self.inInstallDir=self.getInInstallDir(thisscriptdir)
101
102    # Special configuration for CI testing
103    if self.petsc_arch.find('valgrind') >= 0:
104      self.conf['PETSCTEST_VALGRIND']=1
105
106    if self.inInstallDir:
107      # Case 2 discussed above
108      # set PETSC_ARCH to install directory to allow script to work in both
109      dirlist=thisscriptdir.split(os.path.sep)
110      installdir=os.path.sep.join(dirlist[0:len(dirlist)-4])
111      self.arch_dir=installdir
112      if self.srcdir is None:
113        self.srcdir=os.path.join(os.path.dirname(thisscriptdir),'src')
114    else:
115      if petsc_arch == '':
116        raise RuntimeError('PETSC_ARCH must be set when running from build directory')
117      # Case 1 discussed above
118      self.arch_dir=os.path.join(self.petsc_dir,self.petsc_arch)
119      if self.srcdir is None:
120        self.srcdir=os.path.join(self.petsc_dir,'src')
121
122    self.testroot_dir=os.path.abspath(testdir)
123
124    self.verbose=verbose
125    # Whether to write out a useful debugging
126    self.summarize=True if verbose else False
127
128    # For help in setting the requirements
129    self.precision_types="single double __float128 int32".split()
130    self.integer_types="int32 int64 long32 long64".split()
131    self.languages="fortran cuda hip sycl cxx cpp".split()    # Always requires C so do not list
132
133    # Things that are not test
134    self.buildkeys=testparse.buildkeys
135
136    # Adding a dictionary for storing sources, objects, and tests
137    # to make building the dependency tree easier
138    self.sources={}
139    self.objects={}
140    self.tests={}
141    for pkg in self.pkg_pkgs:
142      self.sources[pkg]={}
143      self.objects[pkg]=[]
144      self.tests[pkg]={}
145      for lang in LANGS:
146        self.sources[pkg][lang]={}
147        self.sources[pkg][lang]['srcs']=[]
148        self.tests[pkg][lang]={}
149
150    if not os.path.isdir(self.testroot_dir): os.makedirs(self.testroot_dir)
151
152    self.indent="   "
153    if self.verbose: print('Finishing the constructor')
154    return
155
156  def srcrelpath(self,rdir):
157    """
158    Get relative path to source directory
159    """
160    return os.path.relpath(rdir,self.srcdir)
161
162  def getInInstallDir(self,thisscriptdir):
163    """
164    When petsc is installed then this file in installed in:
165         <PREFIX>/share/petsc/examples/config/gmakegentest.py
166    otherwise the path is:
167         <PETSC_DIR>/config/gmakegentest.py
168    We use this difference to determine if we are in installdir
169    """
170    dirlist=thisscriptdir.split(os.path.sep)
171    if len(dirlist)>4:
172      lastfour=os.path.sep.join(dirlist[len(dirlist)-4:])
173      if lastfour==os.path.join('share','petsc','examples','config'):
174        return True
175      else:
176        return False
177    else:
178      return False
179
180  def getLanguage(self,srcfile):
181    """
182    Based on the source, determine associated language as found in gmakegen.LANGS
183    Can we just return srcext[1:\] now?
184    """
185    langReq=None
186    srcext = getlangext(srcfile)
187    if srcext in ".F90".split(): langReq="F90"
188    if srcext in ".F".split(): langReq="F"
189    if srcext in ".cxx".split(): langReq="cxx"
190    if srcext in ".kokkos.cxx".split(): langReq="kokkos_cxx"
191    if srcext in ".cpp".split(): langReq="cpp"
192    if srcext == ".cu": langReq="cu"
193    if srcext == ".c": langReq="c"
194    #if not langReq: print("ERROR: ", srcext, srcfile)
195    return langReq
196
197  def _getAltList(self,output_file,srcdir):
198    ''' Calculate AltList based on output file-- see
199       src/snes/tutorials/output/ex22*.out
200    '''
201    altlist=[output_file]
202    basefile = getlangsplit(output_file)
203    for i in range(1,9):
204      altroot=basefile+"_alt"
205      if i > 1: altroot=altroot+"_"+str(i)
206      af=altroot+".out"
207      srcaf=os.path.join(srcdir,af)
208      fullaf=os.path.join(self.petsc_dir,srcaf)
209      if os.path.isfile(fullaf): altlist.append(srcaf)
210
211    return altlist
212
213
214  def _getLoopVars(self,inDict,testname, isSubtest=False):
215    """
216    Given: 'args: -bs {{1 2 3 4 5}} -pc_type {{cholesky sor}} -ksp_monitor'
217    Return:
218      inDict['args']: -ksp_monitor
219      inDict['subargs']: -bs ${bs} -pc_type ${pc_type}
220      loopVars['subargs']['varlist']=['bs' 'pc_type']   # Don't worry about OrderedDict
221      loopVars['subargs']['bs']=[["bs"],["1 2 3 4 5"]]
222      loopVars['subargs']['pc_type']=[["pc_type"],["cholesky sor"]]
223    subst should be passed in instead of inDict
224    """
225    loopVars={}; newargs=[]
226    lsuffix='+'
227    argregex = re.compile(' (?=-[a-zA-Z])')
228    from testparse import parseLoopArgs
229    for key in inDict:
230      if key in ('SKIP', 'regexes'):
231        continue
232      akey=('subargs' if key=='args' else key)  # what to assign
233      if akey not in inDict: inDict[akey]=''
234      if akey == 'nsize' and not inDict['nsize'].startswith('{{'):
235        # Always generate a loop over nsize, even if there is only one value
236        inDict['nsize'] = '{{' + inDict['nsize'] + '}}'
237      keystr = str(inDict[key])
238      varlist = []
239      for varset in argregex.split(keystr):
240        if not varset.strip(): continue
241        if '{{' in varset:
242          keyvar,lvars,ftype=parseLoopArgs(varset)
243          if akey not in loopVars: loopVars[akey]={}
244          varlist.append(keyvar)
245          loopVars[akey][keyvar]=[keyvar,lvars]
246          if akey=='nsize':
247            if len(lvars.split()) > 1:
248              lsuffix += akey +'-${i' + keyvar + '}'
249          else:
250            inDict[akey] += ' -'+keyvar+' ${i' + keyvar + '}'
251            lsuffix+=keyvar+'-${i' + keyvar + '}_'
252        else:
253          if key=='args':
254            newargs.append(varset.strip())
255        if varlist:
256          loopVars[akey]['varlist']=varlist
257
258    # For subtests, args are always substituted in (not top level)
259    if isSubtest:
260      inDict['subargs'] += " "+" ".join(newargs)
261      inDict['args']=''
262      if 'label_suffix' in inDict:
263        inDict['label_suffix']+=lsuffix.rstrip('+').rstrip('_')
264      else:
265        inDict['label_suffix']=lsuffix.rstrip('+').rstrip('_')
266    else:
267      if loopVars:
268        inDict['args'] = ' '.join(newargs)
269        inDict['label_suffix']=lsuffix.rstrip('+').rstrip('_')
270    return loopVars
271
272  def getArgLabel(self,testDict):
273    """
274    In all of the arguments in the test dictionary, create a simple
275    string for searching within the makefile system.  For simplicity in
276    search, remove "-", for strings, etc.
277    Also, concatenate the arg commands
278    For now, ignore nsize -- seems hard to search for anyway
279    """
280    # Collect all of the args associated with a test
281    argStr=("" if 'args' not in testDict else testDict['args'])
282    if 'subtests' in testDict:
283      for stest in testDict["subtests"]:
284         sd=testDict[stest]
285         argStr=argStr+("" if 'args' not in sd else sd['args'])
286
287    # Now go through and cleanup
288    argStr=re.sub('{{(.*?)}}',"",argStr)
289    argStr=re.sub('-'," ",argStr)
290    for digit in string.digits: argStr=re.sub(digit," ",argStr)
291    argStr=re.sub("\.","",argStr)
292    argStr=re.sub(",","",argStr)
293    argStr=re.sub('\+',' ',argStr)
294    argStr=re.sub(' +',' ',argStr)  # Remove repeated white space
295    return argStr.strip()
296
297  def addToSources(self,exfile,rpath,srcDict):
298    """
299      Put into data structure that allows easy generation of makefile
300    """
301    pkg=rpath.split(os.path.sep)[0]
302    relpfile=os.path.join(rpath,exfile)
303    lang=self.getLanguage(exfile)
304    if not lang: return
305    if pkg not in self.sources: return
306    self.sources[pkg][lang]['srcs'].append(relpfile)
307    self.sources[pkg][lang][relpfile] = []
308    if 'depends' in srcDict:
309      depSrcList=srcDict['depends'].split()
310      for depSrc in depSrcList:
311        depObj = getlangsplit(depSrc)+'.o'
312        self.sources[pkg][lang][relpfile].append(os.path.join(rpath,depObj))
313
314    # In gmakefile, ${TESTDIR} var specifies the object compilation
315    testsdir=rpath+"/"
316    objfile="${TESTDIR}/"+testsdir+getlangsplit(exfile)+'.o'
317    self.objects[pkg].append(objfile)
318    return
319
320  def addToTests(self,test,rpath,exfile,execname,testDict):
321    """
322      Put into data structure that allows easy generation of makefile
323      Organized by languages to allow testing of languages
324    """
325    pkg=rpath.split("/")[0]
326    nmtest=os.path.join(rpath,test)
327    lang=self.getLanguage(exfile)
328    if not lang: return
329    if pkg not in self.tests: return
330    self.tests[pkg][lang][nmtest]={}
331    self.tests[pkg][lang][nmtest]['exfile']=os.path.join(rpath,exfile)
332    self.tests[pkg][lang][nmtest]['exec']=execname
333    self.tests[pkg][lang][nmtest]['argLabel']=self.getArgLabel(testDict)
334    return
335
336  def getExecname(self,exfile,rpath):
337    """
338      Generate bash script using template found next to this file.
339      This file is read in at constructor time to avoid file I/O
340    """
341    if self.single_ex:
342      execname=rpath.split("/")[1]+"-ex"
343    else:
344      execname=getlangsplit(exfile)
345    return execname
346
347  def getSubstVars(self,testDict,rpath,testname):
348    """
349      Create a dictionary with all of the variables that get substituted
350      into the template commands found in example_template.py
351    """
352    subst={}
353
354    # Handle defaults of testparse.acceptedkeys (e.g., ignores subtests)
355    if 'nsize' not in testDict: testDict['nsize'] = '1'
356    if 'timeoutfactor' not in testDict: testDict['timeoutfactor']="1"
357    for ak in testparse.acceptedkeys:
358      if ak=='test': continue
359      subst[ak]=(testDict[ak] if ak in testDict else '')
360
361    # Now do other variables
362    subst['execname']=testDict['execname']
363    subst['error']=''
364    if 'filter' in testDict:
365      if testDict['filter'].startswith("Error:"):
366        subst['error']="Error"
367        subst['filter']=testDict['filter'].lstrip("Error:")
368      else:
369        subst['filter']=testDict['filter']
370
371    # Others
372    subst['subargs']=''  # Default.  For variables override
373    subst['srcdir']=os.path.join(self.srcdir, rpath)
374    subst['label_suffix']=''
375    subst['comments']="\n#".join(subst['comments'].split("\n"))
376    if subst['comments']: subst['comments']="#"+subst['comments']
377    subst['exec']="../"+subst['execname']
378    subst['testroot']=self.testroot_dir
379    subst['testname']=testname
380    dp = self.conf.get('DATAFILESPATH','')
381    subst['datafilespath_line'] = 'DATAFILESPATH=${DATAFILESPATH:-"'+dp+'"}'
382
383    # This is used to label some matrices
384    subst['petsc_index_size']=str(self.conf['PETSC_INDEX_SIZE'])
385    subst['petsc_scalar_size']=str(self.conf['PETSC_SCALAR_SIZE'])
386
387    if 'PETSC_OPTIONS' in os.environ:
388      subst['petsc_options']=os.environ['PETSC_OPTIONS']
389    else:
390      subst['petsc_options']=''
391    subst['petsc_test_options']=self.conf['PETSC_TEST_OPTIONS']
392
393    #Conf vars
394    if self.petsc_arch.find('valgrind')>=0:
395      subst['mpiexec']='petsc_mpiexec_valgrind ' + self.conf['MPIEXEC']
396    else:
397      subst['mpiexec']=self.conf['MPIEXEC']
398    subst['pkg_name']=self.pkg_name
399    subst['pkg_dir']=self.pkg_dir
400    subst['pkg_arch']=self.petsc_arch
401    subst['CONFIG_DIR']=thisscriptdir
402    subst['PETSC_BINDIR']=os.path.join(self.petsc_dir,'lib','petsc','bin')
403    subst['diff']=self.conf['DIFF']
404    subst['rm']=self.conf['RM']
405    subst['grep']=self.conf['GREP']
406    subst['petsc_lib_dir']=self.conf['PETSC_LIB_DIR']
407    subst['wpetsc_dir']=self.conf['wPETSC_DIR']
408
409    # Output file is special because of subtests override
410    defroot = testparse.getDefaultOutputFileRoot(testname)
411    if 'output_file' not in testDict:
412      subst['output_file']="output/"+defroot+".out"
413    subst['redirect_file']=defroot+".tmp"
414    subst['label']=nameSpace(defroot,self.srcrelpath(subst['srcdir']))
415
416    # Add in the full path here.
417    subst['output_file']=os.path.join(subst['srcdir'],subst['output_file'])
418
419    subst['regexes']={}
420    for subkey in subst:
421      if subkey=='regexes': continue
422      if not isinstance(subst[subkey],str): continue
423      patt="@"+subkey.upper()+"@"
424      subst['regexes'][subkey]=re.compile(patt)
425
426    return subst
427
428  def _substVars(self,subst,origStr):
429    """
430      Substitute variables
431    """
432    Str=origStr
433    for subkey in subst:
434      if subkey=='regexes': continue
435      if not isinstance(subst[subkey],str): continue
436      if subkey.upper() not in Str: continue
437      Str=subst['regexes'][subkey].sub(lambda x: subst[subkey],Str)
438    return Str
439
440  def getCmds(self,subst,i, debug=False):
441    """
442      Generate bash script using template found next to this file.
443      This file is read in at constructor time to avoid file I/O
444    """
445    nindnt=i # the start and has to be consistent with below
446    cmdindnt=self.indent*nindnt
447    cmdLines=""
448
449    # MPI is the default -- but we have a few odd commands
450    if not subst['command']:
451      cmd=cmdindnt+self._substVars(subst,example_template.mpitest)
452    else:
453      cmd=cmdindnt+self._substVars(subst,example_template.commandtest)
454    cmdLines+=cmd+"\n"+cmdindnt+"res=$?\n\n"
455
456    cmdLines+=cmdindnt+'if test $res = 0; then\n'
457    diffindnt=self.indent*(nindnt+1)
458
459    # Do some checks on existence of output_file and alt files
460    if not os.path.isfile(os.path.join(self.petsc_dir,subst['output_file'])):
461      if not subst['TODO']:
462        print("Warning: "+subst['output_file']+" not found.")
463    altlist=self._getAltList(subst['output_file'], subst['srcdir'])
464
465    # altlist always has output_file
466    if len(altlist)==1:
467      cmd=diffindnt+self._substVars(subst,example_template.difftest)
468    else:
469      if debug: print("Found alt files: ",altlist)
470      # Have to do it by hand a bit because of variable number of alt files
471      rf=subst['redirect_file']
472      cmd=diffindnt+example_template.difftest.split('@')[0]
473      for i in range(len(altlist)):
474        af=altlist[i]
475        cmd+=af+' '+rf
476        if i!=len(altlist)-1:
477          cmd+=' > diff-${testname}-'+str(i)+'.out 2> diff-${testname}-'+str(i)+'.out'
478          cmd+=' || ${diff_exe} '
479        else:
480          cmd+='" diff-${testname}.out diff-${testname}.out diff-${label}'
481          cmd+=subst['label_suffix']+' ""'  # Quotes are painful
482    cmdLines+=cmd+"\n"
483    cmdLines+=cmdindnt+'else\n'
484    cmdLines+=diffindnt+'petsc_report_tapoutput "" ${label} "SKIP Command failed so no diff"\n'
485    cmdLines+=cmdindnt+'fi\n'
486    return cmdLines
487
488  def _writeTodoSkip(self,fh,tors,reasons,footer):
489    """
490    Write out the TODO and SKIP lines in the file
491    The TODO or SKIP variable, tors, should be lower case
492    """
493    TORS=tors.upper()
494    template=eval("example_template."+tors+"line")
495    tsStr=re.sub("@"+TORS+"COMMENT@",', '.join(reasons),template)
496    tab = ''
497    if reasons:
498      fh.write('if ! $force; then\n')
499      tab = tab + '    '
500    if reasons == ["Requires DATAFILESPATH"]:
501      # The only reason not to run is DATAFILESPATH, which we check at run-time
502      fh.write(tab + 'if test -z "${DATAFILESPATH}"; then\n')
503      tab = tab + '    '
504    if reasons:
505      fh.write(tab+tsStr+"\n" + tab + "total=1; "+tors+"=1\n")
506      fh.write(tab+footer+"\n")
507      fh.write(tab+"exit\n")
508    if reasons == ["Requires DATAFILESPATH"]:
509      fh.write('    fi\n')
510    if reasons:
511      fh.write('fi\n')
512    fh.write('\n\n')
513    return
514
515  def getLoopVarsHead(self,loopVars,i,usedVars={}):
516    """
517    Generate a nicely indented string with the format loops
518    Here is what the data structure looks like
519      loopVars['subargs']['varlist']=['bs' 'pc_type']   # Don't worry about OrderedDict
520      loopVars['subargs']['bs']=["i","1 2 3 4 5"]
521      loopVars['subargs']['pc_type']=["j","cholesky sor"]
522    """
523    outstr=''; indnt=self.indent
524
525    for key in loopVars:
526      for var in loopVars[key]['varlist']:
527        varval=loopVars[key][var]
528        outstr += "{0}_in=${{{0}:-{1}}}\n".format(*varval)
529    outstr += "\n\n"
530
531    for key in loopVars:
532      for var in loopVars[key]['varlist']:
533        varval=loopVars[key][var]
534        outstr += indnt * i + "for i{0} in ${{{0}_in}}; do\n".format(*varval)
535        i = i + 1
536    return (outstr,i)
537
538  def getLoopVarsFoot(self,loopVars,i):
539    outstr=''; indnt=self.indent
540    for key in loopVars:
541      for var in loopVars[key]['varlist']:
542        i = i - 1
543        outstr += indnt * i + "done\n"
544    return (outstr,i)
545
546  def genRunScript(self,testname,root,isRun,srcDict):
547    """
548      Generate bash script using template found next to this file.
549      This file is read in at constructor time to avoid file I/O
550    """
551    # runscript_dir directory has to be consistent with gmakefile
552    testDict=srcDict[testname]
553    rpath=self.srcrelpath(root)
554    runscript_dir=os.path.join(self.testroot_dir,rpath)
555    if not os.path.isdir(runscript_dir): os.makedirs(runscript_dir)
556    with open(os.path.join(runscript_dir,testname+".sh"),"w") as fh:
557
558      # Get variables to go into shell scripts.  last time testDict used
559      subst=self.getSubstVars(testDict,rpath,testname)
560      loopVars = self._getLoopVars(subst,testname)  # Alters subst as well
561      if 'subtests' in testDict:
562        # The subtests inherit inDict, so we don't need top-level loops.
563        loopVars = {}
564
565      #Handle runfiles
566      for lfile in subst.get('localrunfiles','').split():
567        install_files(os.path.join(root, lfile),
568                      os.path.join(runscript_dir, os.path.dirname(lfile)))
569      # Check subtests for local runfiles
570      for stest in subst.get("subtests",[]):
571        for lfile in testDict[stest].get('localrunfiles','').split():
572          install_files(os.path.join(root, lfile),
573                        os.path.join(runscript_dir, os.path.dirname(lfile)))
574
575      # Now substitute the key variables into the header and footer
576      header=self._substVars(subst,example_template.header)
577      # The header is done twice to enable @...@ in header
578      header=self._substVars(subst,header)
579      footer=re.sub('@TESTROOT@',subst['testroot'],example_template.footer)
580
581      # Start writing the file
582      fh.write(header+"\n")
583
584      # If there is a TODO or a SKIP then we do it before writing out the
585      # rest of the command (which is useful for working on the test)
586      # SKIP and TODO can be for the source file or for the runs
587      self._writeTodoSkip(fh,'todo',[s for s in [srcDict.get('TODO',''), testDict.get('TODO','')] if s],footer)
588      self._writeTodoSkip(fh,'skip',srcDict.get('SKIP',[]) + testDict.get('SKIP',[]),footer)
589
590      j=0  # for indentation
591
592      if loopVars:
593        (loopHead,j) = self.getLoopVarsHead(loopVars,j)
594        if (loopHead): fh.write(loopHead+"\n")
595
596      # Subtests are special
597      allLoopVars=list(loopVars.keys())
598      if 'subtests' in testDict:
599        substP=subst   # Subtests can inherit args but be careful
600        k=0  # for label suffixes
601        for stest in testDict["subtests"]:
602          subst=substP.copy()
603          subst.update(testDict[stest])
604          subst['label_suffix']='+'+string.ascii_letters[k]; k+=1
605          sLoopVars = self._getLoopVars(subst,testname,isSubtest=True)
606          if sLoopVars:
607            (sLoopHead,j) = self.getLoopVarsHead(sLoopVars,j,allLoopVars)
608            allLoopVars+=list(sLoopVars.keys())
609            fh.write(sLoopHead+"\n")
610          fh.write(self.getCmds(subst,j)+"\n")
611          if sLoopVars:
612            (sLoopFoot,j) = self.getLoopVarsFoot(sLoopVars,j)
613            fh.write(sLoopFoot+"\n")
614      else:
615        fh.write(self.getCmds(subst,j)+"\n")
616
617      if loopVars:
618        (loopFoot,j) = self.getLoopVarsFoot(loopVars,j)
619        fh.write(loopFoot+"\n")
620
621      fh.write(footer+"\n")
622
623    os.chmod(os.path.join(runscript_dir,testname+".sh"),0o755)
624    #if '10_9' in testname: sys.exit()
625    return
626
627  def  genScriptsAndInfo(self,exfile,root,srcDict):
628    """
629    Generate scripts from the source file, determine if built, etc.
630     For every test in the exfile with info in the srcDict:
631      1. Determine if it needs to be run for this arch
632      2. Generate the script
633      3. Generate the data needed to write out the makefile in a
634         convenient way
635     All tests are *always* run, but some may be SKIP'd per the TAP standard
636    """
637    debug=False
638    rpath=self.srcrelpath(root)
639    execname=self.getExecname(exfile,rpath)
640    isBuilt=self._isBuilt(exfile,srcDict)
641    for test in srcDict:
642      if test in self.buildkeys: continue
643      if debug: print(nameSpace(exfile,root), test)
644      srcDict[test]['execname']=execname   # Convenience in generating scripts
645      isRun=self._isRun(srcDict[test])
646      self.genRunScript(test,root,isRun,srcDict)
647      srcDict[test]['isrun']=isRun
648      self.addToTests(test,rpath,exfile,execname,srcDict[test])
649
650    # This adds to datastructure for building deps
651    if isBuilt: self.addToSources(exfile,rpath,srcDict)
652    return
653
654  def _isBuilt(self,exfile,srcDict):
655    """
656    Determine if this file should be built.
657    """
658    # Get the language based on file extension
659    srcDict['SKIP'] = []
660    lang=self.getLanguage(exfile)
661    if (lang=="F" or lang=="F90"):
662      if not self.have_fortran:
663        srcDict["SKIP"].append("Fortran required for this test")
664      elif lang=="F90" and 'PETSC_USING_F90FREEFORM' not in self.conf:
665        srcDict["SKIP"].append("Fortran f90freeform required for this test")
666    if lang=="cu" and 'PETSC_HAVE_CUDA' not in self.conf:
667      srcDict["SKIP"].append("CUDA required for this test")
668    if lang=="hip" and 'PETSC_HAVE_HIP' not in self.conf:
669      srcDict["SKIP"].append("HIP required for this test")
670    if lang=="sycl" and 'PETSC_HAVE_SYCL' not in self.conf:
671      srcDict["SKIP"].append("SYCL required for this test")
672    if lang=="kokkos_cxx" and 'PETSC_HAVE_KOKKOS' not in self.conf:
673      srcDict["SKIP"].append("KOKKOS required for this test")
674    if lang=="cxx" and 'PETSC_HAVE_CXX' not in self.conf:
675      srcDict["SKIP"].append("C++ required for this test")
676    if lang=="cpp" and 'PETSC_HAVE_CXX' not in self.conf:
677      srcDict["SKIP"].append("C++ required for this test")
678
679    # Deprecated source files
680    if srcDict.get("TODO"):
681      return False
682
683    # isRun can work with srcDict to handle the requires
684    if "requires" in srcDict:
685      if srcDict["requires"]:
686        return self._isRun(srcDict)
687
688    return srcDict['SKIP'] == []
689
690
691  def _isRun(self,testDict, debug=False):
692    """
693    Based on the requirements listed in the src file and the petscconf.h
694    info, determine whether this test should be run or not.
695    """
696    indent="  "
697
698    if 'SKIP' not in testDict:
699      testDict['SKIP'] = []
700    # MPI requirements
701    if 'MPI_IS_MPIUNI' in self.conf:
702      if testDict.get('nsize', '1') != '1':
703        testDict['SKIP'].append("Parallel test with serial build")
704
705      # The requirements for the test are the sum of all the run subtests
706      if 'subtests' in testDict:
707        if 'requires' not in testDict: testDict['requires']=""
708        for stest in testDict['subtests']:
709          if 'requires' in testDict[stest]:
710            testDict['requires']+=" "+testDict[stest]['requires']
711          if testDict[stest].get('nsize', '1') != '1':
712            testDict['SKIP'].append("Parallel test with serial build")
713            break
714
715    # Now go through all requirements
716    if 'requires' in testDict:
717      for requirement in testDict['requires'].split():
718        requirement=requirement.strip()
719        if not requirement: continue
720        if debug: print(indent+"Requirement: ", requirement)
721        isNull=False
722        if requirement.startswith("!"):
723          requirement=requirement[1:]; isNull=True
724        # Precision requirement for reals
725        if requirement in self.precision_types:
726          if self.conf['PETSC_PRECISION']==requirement:
727            if isNull:
728              testDict['SKIP'].append("not "+requirement+" required")
729              continue
730            continue  # Success
731          elif not isNull:
732            testDict['SKIP'].append(requirement+" required")
733            continue
734        # Precision requirement for ints
735        if requirement in self.integer_types:
736          if requirement=="int32":
737            if self.conf['PETSC_SIZEOF_INT']==4:
738              if isNull:
739                testDict['SKIP'].append("not int32 required")
740                continue
741              continue  # Success
742            elif not isNull:
743              testDict['SKIP'].append("int32 required")
744              continue
745          if requirement=="int64":
746            if self.conf['PETSC_SIZEOF_INT']==8:
747              if isNull:
748                testDict['SKIP'].append("NOT int64 required")
749                continue
750              continue  # Success
751            elif not isNull:
752              testDict['SKIP'].append("int64 required")
753              continue
754          if requirement.startswith("long"):
755            reqsize = int(requirement[4:])//8
756            longsize = int(self.conf['PETSC_SIZEOF_LONG'].strip())
757            if longsize==reqsize:
758              if isNull:
759                testDict['SKIP'].append("not %s required" % requirement)
760                continue
761              continue  # Success
762            elif not isNull:
763              testDict['SKIP'].append("%s required" % requirement)
764              continue
765        # Datafilespath
766        if requirement=="datafilespath" and not isNull:
767          testDict['SKIP'].append("Requires DATAFILESPATH")
768          continue
769        # Defines -- not sure I have comments matching
770        if "defined(" in requirement.lower():
771          reqdef=requirement.split("(")[1].split(")")[0]
772          if reqdef in self.conf:
773            if isNull:
774              testDict['SKIP'].append("Null requirement not met: "+requirement)
775              continue
776            continue  # Success
777          elif not isNull:
778            testDict['SKIP'].append("Required: "+requirement)
779            continue
780
781        # Rest should be packages that we can just get from conf
782        if requirement in ["complex","debug"]:
783          petscconfvar="PETSC_USE_"+requirement.upper()
784          pkgconfvar=self.pkg_name.upper()+"_USE_"+requirement.upper()
785        else:
786          petscconfvar="PETSC_HAVE_"+requirement.upper()
787          pkgconfvar=self.pkg_name.upper()+'_HAVE_'+requirement.upper()
788        petsccv = self.conf.get(petscconfvar)
789        pkgcv = self.conf.get(pkgconfvar)
790
791        if petsccv or pkgcv:
792          if isNull:
793            if petsccv:
794              testDict['SKIP'].append("Not "+petscconfvar+" requirement not met")
795              continue
796            else:
797              testDict['SKIP'].append("Not "+pkgconfvar+" requirement not met")
798              continue
799          continue  # Success
800        elif not isNull:
801          if not petsccv and not pkgcv:
802            if debug: print("requirement not found: ", requirement)
803            if self.pkg_name == 'petsc':
804              testDict['SKIP'].append(petscconfvar+" requirement not met")
805            else:
806              testDict['SKIP'].append(petscconfvar+" or "+pkgconfvar+" requirement not met")
807            continue
808    return testDict['SKIP'] == []
809
810  def  checkOutput(self,exfile,root,srcDict):
811    """
812     Check and make sure the output files are in the output directory
813    """
814    debug=False
815    rpath=self.srcrelpath(root)
816    for test in srcDict:
817      if test in self.buildkeys: continue
818      if debug: print(rpath, exfile, test)
819      if 'output_file' in srcDict[test]:
820        output_file=srcDict[test]['output_file']
821      else:
822        defroot = testparse.getDefaultOutputFileRoot(test)
823        if 'TODO' in srcDict[test]: continue
824        output_file="output/"+defroot+".out"
825
826      fullout=os.path.join(root,output_file)
827      if debug: print("---> ",fullout)
828      if not os.path.exists(fullout):
829        self.missing_files.append(fullout)
830
831    return
832
833  def genPetscTests_summarize(self,dataDict):
834    """
835    Required method to state what happened
836    """
837    if not self.summarize: return
838    indent="   "
839    fhname=os.path.join(self.testroot_dir,'GenPetscTests_summarize.txt')
840    with open(fhname, "w") as fh:
841      for root in dataDict:
842        relroot=self.srcrelpath(root)
843        pkg=relroot.split("/")[1]
844        fh.write(relroot+"\n")
845        allSrcs=[]
846        for lang in LANGS: allSrcs+=self.sources[pkg][lang]['srcs']
847        for exfile in dataDict[root]:
848          # Basic  information
849          rfile=os.path.join(relroot,exfile)
850          builtStatus=(" Is built" if rfile in allSrcs else " Is NOT built")
851          fh.write(indent+exfile+indent*4+builtStatus+"\n")
852          for test in dataDict[root][exfile]:
853            if test in self.buildkeys: continue
854            line=indent*2+test
855            fh.write(line+"\n")
856            # Looks nice to have the keys in order
857            #for key in dataDict[root][exfile][test]:
858            for key in "isrun abstracted nsize args requires script".split():
859              if key not in dataDict[root][exfile][test]: continue
860              line=indent*3+key+": "+str(dataDict[root][exfile][test][key])
861              fh.write(line+"\n")
862            fh.write("\n")
863          fh.write("\n")
864        fh.write("\n")
865    return
866
867  def genPetscTests(self,root,dirs,files,dataDict):
868    """
869     Go through and parse the source files in the directory to generate
870     the examples based on the metadata contained in the source files
871    """
872    debug=False
873    # Use examplesAnalyze to get what the makefles think are sources
874    #self.examplesAnalyze(root,dirs,files,anlzDict)
875
876    dataDict[root]={}
877
878    for exfile in files:
879      #TST: Until we replace files, still leaving the orginals as is
880      #if not exfile.startswith("new_"+"ex"): continue
881      #if not exfile.startswith("ex"): continue
882
883      # Ignore emacs and other temporary files
884      if exfile.startswith("."): continue
885      if exfile.startswith("#"): continue
886      if exfile.endswith("~"): continue
887      # Only parse source files
888      ext=getlangext(exfile).lstrip('.').replace('.','_')
889      if ext not in LANGS: continue
890
891      # Convenience
892      fullex=os.path.join(root,exfile)
893      if self.verbose: print('   --> '+fullex)
894      dataDict[root].update(testparse.parseTestFile(fullex,0))
895      if exfile in dataDict[root]:
896        if not self.check_output:
897          self.genScriptsAndInfo(exfile,root,dataDict[root][exfile])
898        else:
899          self.checkOutput(exfile,root,dataDict[root][exfile])
900
901    return
902
903  def walktree(self,top):
904    """
905    Walk a directory tree, starting from 'top'
906    """
907    if self.check_output:
908      print("Checking for missing output files")
909      self.missing_files=[]
910
911    # Goal of action is to fill this dictionary
912    dataDict={}
913    for root, dirs, files in os.walk(top, topdown=True):
914      dirs.sort()
915      files.sort()
916      if "/tests" not in root and "/tutorials" not in root: continue
917      if "dSYM" in root: continue
918      if "tutorials"+os.sep+"build" in root: continue
919      if os.path.basename(root.rstrip("/")) == 'output': continue
920      if self.verbose: print(root)
921      self.genPetscTests(root,dirs,files,dataDict)
922
923    # If checking output, report results
924    if self.check_output:
925      if self.missing_files:
926        for file in set(self.missing_files):  # set uniqifies
927          print(file)
928        sys.exit(1)
929
930    # Now summarize this dictionary
931    if self.verbose: self.genPetscTests_summarize(dataDict)
932    return dataDict
933
934  def gen_gnumake(self, fd):
935    """
936     Overwrite of the method in the base PETSc class
937    """
938    def write(stem, srcs):
939      for lang in LANGS:
940        if srcs[lang]['srcs']:
941          fd.write('%(stem)s.%(lang)s := %(srcs)s\n' % dict(stem=stem, lang=lang.replace('_','.'), srcs=' '.join(srcs[lang]['srcs'])))
942    for pkg in self.pkg_pkgs:
943        srcs = self.gen_pkg(pkg)
944        write('testsrcs-' + pkg, srcs)
945        # Handle dependencies
946        for lang in LANGS:
947            for exfile in srcs[lang]['srcs']:
948                if exfile in srcs[lang]:
949                    ex='$(TESTDIR)/'+getlangsplit(exfile)
950                    exfo=ex+'.o'
951                    deps = [os.path.join('$(TESTDIR)', dep) for dep in srcs[lang][exfile]]
952                    if deps:
953                        # The executable literally depends on the object file because it is linked
954                        fd.write(ex   +": " + " ".join(deps) +'\n')
955                        # The object file containing 'main' does not normally depend on other object
956                        # files, but it does when it includes their modules.  This dependency is
957                        # overly blunt and could be reduced to only depend on object files for
958                        # modules that are used, like "*f90aux.o".
959                        fd.write(exfo +": " + " ".join(deps) +'\n')
960
961    return self.gendeps
962
963  def gen_pkg(self, pkg):
964    """
965     Overwrite of the method in the base PETSc class
966    """
967    return self.sources[pkg]
968
969  def write_gnumake(self, dataDict, output=None):
970    """
971     Write out something similar to files from gmakegen.py
972
973     Test depends on script which also depends on source
974     file, but since I don't have a good way generating
975     acting on a single file (oops) just depend on
976     executable which in turn will depend on src file
977    """
978    # Different options for how to set up the targets
979    compileExecsFirst=False
980
981    # Open file
982    with open(output, 'w') as fd:
983      # Write out the sources
984      gendeps = self.gen_gnumake(fd)
985
986      # Write out the tests and execname targets
987      fd.write("\n#Tests and executables\n")    # Delimiter
988
989      for pkg in self.pkg_pkgs:
990        # These grab the ones that are built
991        for lang in LANGS:
992          testdeps=[]
993          for ftest in self.tests[pkg][lang]:
994            test=os.path.basename(ftest)
995            basedir=os.path.dirname(ftest)
996            testdeps.append(nameSpace(test,basedir))
997          fd.write("test-"+pkg+"."+lang.replace('_','.')+" := "+' '.join(testdeps)+"\n")
998          fd.write('test-%s.%s : $(test-%s.%s)\n' % (pkg, lang.replace('_','.'), pkg, lang.replace('_','.')))
999
1000          # test targets
1001          for ftest in self.tests[pkg][lang]:
1002            test=os.path.basename(ftest)
1003            basedir=os.path.dirname(ftest)
1004            testdir="${TESTDIR}/"+basedir+"/"
1005            nmtest=nameSpace(test,basedir)
1006            rundir=os.path.join(testdir,test)
1007            script=test+".sh"
1008
1009            # Deps
1010            exfile=self.tests[pkg][lang][ftest]['exfile']
1011            fullex=os.path.join(self.srcdir,exfile)
1012            localexec=self.tests[pkg][lang][ftest]['exec']
1013            execname=os.path.join(testdir,localexec)
1014            fullscript=os.path.join(testdir,script)
1015            tmpfile=os.path.join(testdir,test,test+".tmp")
1016
1017            # *.counts depends on the script and either executable (will
1018            # be run) or the example source file (SKIP or TODO)
1019            fd.write('%s.counts : %s %s'
1020                % (os.path.join('$(TESTDIR)/counts', nmtest),
1021                   fullscript,
1022                   execname if exfile in self.sources[pkg][lang]['srcs'] else fullex)
1023                )
1024            if exfile in self.sources[pkg][lang]:
1025              for dep in self.sources[pkg][lang][exfile]:
1026                fd.write(' %s' % os.path.join('$(TESTDIR)',dep))
1027            fd.write('\n')
1028
1029            # Now write the args:
1030            fd.write(nmtest+"_ARGS := '"+self.tests[pkg][lang][ftest]['argLabel']+"'\n")
1031
1032    return
1033
1034  def write_db(self, dataDict, testdir):
1035    """
1036     Write out the dataDict into a pickle file
1037    """
1038    with open(os.path.join(testdir,'datatest.pkl'), 'wb') as fd:
1039      pickle.dump(dataDict,fd)
1040    return
1041
1042def main(petsc_dir=None, petsc_arch=None, pkg_dir=None, pkg_arch=None,
1043         pkg_name=None, pkg_pkgs=None, verbose=False, single_ex=False,
1044         srcdir=None, testdir=None, check=False):
1045    # Allow petsc_arch to have both petsc_dir and petsc_arch for convenience
1046    testdir=os.path.normpath(testdir)
1047    if petsc_arch:
1048        petsc_arch=petsc_arch.rstrip(os.path.sep)
1049        if len(petsc_arch.split(os.path.sep))>1:
1050            petsc_dir,petsc_arch=os.path.split(petsc_arch)
1051    output = os.path.join(testdir, 'testfiles')
1052
1053    pEx=generateExamples(petsc_dir=petsc_dir, petsc_arch=petsc_arch,
1054                         pkg_dir=pkg_dir, pkg_arch=pkg_arch, pkg_name=pkg_name, pkg_pkgs=pkg_pkgs,
1055                         verbose=verbose, single_ex=single_ex, srcdir=srcdir,
1056                         testdir=testdir,check=check)
1057    dataDict=pEx.walktree(os.path.join(pEx.srcdir))
1058    if not pEx.check_output:
1059        pEx.write_gnumake(dataDict, output)
1060        pEx.write_db(dataDict, testdir)
1061
1062if __name__ == '__main__':
1063    import optparse
1064    parser = optparse.OptionParser()
1065    parser.add_option('--verbose', help='Show mismatches between makefiles and the filesystem', action='store_true', default=False)
1066    parser.add_option('--petsc-dir', help='Set PETSC_DIR different from environment', default=os.environ.get('PETSC_DIR'))
1067    parser.add_option('--petsc-arch', help='Set PETSC_ARCH different from environment', default=os.environ.get('PETSC_ARCH'))
1068    parser.add_option('--srcdir', help='Set location of sources different from PETSC_DIR/src', default=None)
1069    parser.add_option('-s', '--single_executable', dest='single_executable', action="store_false", help='Whether there should be single executable per src subdir.  Default is false')
1070    parser.add_option('-t', '--testdir', dest='testdir',  help='Test directory [$PETSC_ARCH/tests]')
1071    parser.add_option('-c', '--check-output', dest='check_output', action="store_true",
1072                      help='Check whether output files are in output director')
1073    parser.add_option('--pkg-dir', help='Set the directory of the package (different from PETSc) you want to generate the makefile rules for', default=None)
1074    parser.add_option('--pkg-name', help='Set the name of the package you want to generate the makefile rules for', default=None)
1075    parser.add_option('--pkg-arch', help='Set the package arch name you want to generate the makefile rules for', default=None)
1076    parser.add_option('--pkg-pkgs', help='Set the package folders (comma separated list, different from the usual sys,vec,mat etc) you want to generate the makefile rules for', default=None)
1077
1078    opts, extra_args = parser.parse_args()
1079    if extra_args:
1080        import sys
1081        sys.stderr.write('Unknown arguments: %s\n' % ' '.join(extra_args))
1082        exit(1)
1083    if opts.testdir is None:
1084      opts.testdir = os.path.join(opts.petsc_arch, 'tests')
1085
1086    main(petsc_dir=opts.petsc_dir, petsc_arch=opts.petsc_arch,
1087         pkg_dir=opts.pkg_dir,pkg_arch=opts.pkg_arch,pkg_name=opts.pkg_name,pkg_pkgs=opts.pkg_pkgs,
1088         verbose=opts.verbose,
1089         single_ex=opts.single_executable, srcdir=opts.srcdir,
1090         testdir=opts.testdir, check=opts.check_output)
1091