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