source: tests/pybin/tools.py @ d796be70

Last change on this file since d796be70 was 195d1d8, checked in by Peter A. Buhr <pabuhr@…>, 6 months ago

update search strings to raw strings for python 3.12

  • Property mode set to 100644
File size: 15.3 KB
RevLine 
[c07d724]1import __main__
2import argparse
[a45fc7b]3import contextlib
[dcfedca]4import datetime
[0c13238]5import fileinput
[bacc36c]6import multiprocessing
[c07d724]7import os
8import re
[0c13238]9import resource
[bacc36c]10import signal
[c07d724]11import stat
[1bb2488]12import subprocess
[bacc36c]13import sys
[f806b61]14import tempfile
[0c13238]15import time
[5b993e0]16import types
[c07d724]17
[bacc36c]18from pybin import settings
[c07d724]19
[bacc36c]20################################################################################
21#               shell helpers
22################################################################################
23
[c07d724]24# helper functions to run terminal commands
[f58522b0]25def sh(*cmd, timeout = False, output_file = None, input_file = None, input_text = None, error = subprocess.STDOUT, ignore_dry_run = False, pass_fds = [], nice = False):
[8364209]26        try:
27                cmd = list(cmd)
[1bb2488]28
[8364209]29                if input_file and input_text:
30                        return 401, "Cannot use both text and file inputs"
[d65f92c]31
[8364209]32                # if this is a dry_run, only print the commands that would be ran
33                if settings.dry_run and not ignore_dry_run:
34                        cmd = "{} cmd: {}".format(os.getcwd(), ' '.join(cmd))
35                        if output_file and not isinstance(output_file, int):
36                                cmd += " > "
37                                cmd += output_file
[ea62265]38
[8364209]39                        if error and not isinstance(error, int):
40                                cmd += " 2> "
41                                cmd += error
[ea62265]42
[8364209]43                        if input_file and not isinstance(input_file, int) and os.path.isfile(input_file):
44                                cmd += " < "
45                                cmd += input_file
[ea62265]46
[8364209]47                        print(cmd)
[5a96b36]48                        return 0, None, None
[bacc36c]49
[8364209]50                with contextlib.ExitStack() as onexit:
51                        # add input redirection if needed
52                        input_file = openfd(input_file, 'r', onexit, True)
[a45fc7b]53
[8364209]54                        # add output redirection if needed
55                        output_file = openfd(output_file, 'w', onexit, False)
[f806b61]56
[8364209]57                        # add error redirection if needed
58                        error = openfd(error, 'w', onexit, False)
[a45fc7b]59
[f58522b0]60                        # prepare the parameters to the call
61                        popen_kwargs = {
62                                'stdout' : output_file,
63                                'stderr' : error,
64                                'pass_fds': pass_fds,
65                        }
66
67                        # depending on how we are passing inputs we need to set a different argument to popen
68                        if input_text:
69                                popen_kwargs['input'] = bytes(input_text, encoding='utf-8')
70                        else:
71                                popen_kwargs['stdin'] = input_file
72
73                        # we might want to nice this so it's not to obnixious to users
74                        if nice:
75                                popen_kwargs['preexec_fn'] = lambda: os.nice(5)
76
[8364209]77                        # run the desired command
78                        # use with statement to make sure proc is cleaned
79                        # don't use subprocess.run because we want to send SIGABRT on exit
[f58522b0]80                        with subprocess.Popen( cmd, **popen_kwargs ) as proc:
[8364209]81                                try:
[cc9b520]82                                        out, errout = proc.communicate(
[8364209]83                                                timeout = settings.timeout.single if timeout else None
84                                        )
85
[cc9b520]86                                        return proc.returncode, out.decode("latin-1") if out else None, errout.decode("latin-1") if errout else None
[8364209]87                                except subprocess.TimeoutExpired:
[d658183]88                                        if settings.timeout2gdb:
89                                                print("Process {} timeout".format(proc.pid))
90                                                proc.communicate()
[cc9b520]91                                                return 124, str(None), "Subprocess Timeout 2 gdb"
[d658183]92                                        else:
93                                                proc.send_signal(signal.SIGABRT)
94                                                proc.communicate()
[cc9b520]95                                                return 124, str(None), "Subprocess Timeout 2 gdb"
[8364209]96
97        except Exception as ex:
98                print ("Unexpected error: %s" % ex)
99                raise
[c07d724]100
[f866d15]101def is_empty(fname):
102        if not os.path.isfile(fname):
103                return True
104
105        if os.stat(fname).st_size == 0:
106                return True
107
108        return False
109
[f85bc15]110def is_ascii(fname):
[202ad72]111        if settings.dry_run:
112                print("is_ascii: %s" % fname)
[f866d15]113                return (True, "")
[202ad72]114
[f85bc15]115        if not os.path.isfile(fname):
[f866d15]116                return (False, "No file")
[f85bc15]117
[cc9b520]118        code, out, err = sh("file", fname, output_file=subprocess.PIPE)
[f85bc15]119        if code != 0:
[cc9b520]120                return (False, "'file EXPECT' failed with code {} '{}'".format(code, err))
[f85bc15]121
[195d1d8]122        match = re.search(r".*: (.*)", out)
[f85bc15]123
124        if not match:
[f866d15]125                return (False, "Unreadable file type: '{}'".format(out))
126
127        if "ASCII text" in match.group(1):
128                return (True, "")
[f85bc15]129
[f866d15]130        return (False, "File type should be 'ASCII text', was '{}'".format(match.group(1)))
[f85bc15]131
[5bf1f3e]132def is_exe(fname):
133        return os.path.isfile(fname) and os.access(fname, os.X_OK)
134
[f806b61]135def openfd(file, mode, exitstack, checkfile):
136        if not file:
137                return file
138
139        if isinstance(file, int):
140                return file
141
142        if checkfile and not os.path.isfile(file):
143                return None
144
[09bbe78]145        file = open(file, mode, encoding="latin-1") # use latin-1 so all chars mean something.
[f806b61]146        exitstack.push(file)
147        return file
148
[c07d724]149# Remove 1 or more files silently
[bacc36c]150def rm( files ):
[5b993e0]151        if isinstance(files, str ): files = [ files ]
152        for file in files:
[d65f92c]153                sh( 'rm', '-f', file, output_file=subprocess.DEVNULL, error=subprocess.DEVNULL )
[c07d724]154
[a95c117]155# Create 1 or more directory
156def mkdir( files ):
[5b993e0]157        if isinstance(files, str ): files = [ files ]
158        for file in files:
159                p = os.path.normpath( file )
160                d = os.path.dirname ( p )
[d65f92c]161                sh( 'mkdir', '-p', d, output_file=subprocess.DEVNULL, error=subprocess.DEVNULL )
[28582b2]162
[a95c117]163
[c07d724]164def chdir( dest = __main__.__file__ ):
165        abspath = os.path.abspath(dest)
166        dname = os.path.dirname(abspath)
167        os.chdir(dname)
168
[bacc36c]169# diff two files
170def diff( lhs, rhs ):
171        # fetch return code and error from the diff command
[a45fc7b]172        return sh(
173                '''diff''',
174                '''--text''',
175                '''--old-group-format=\t\tmissing lines :\n%<''',
176                '''--new-line-format=\t\t%dn\t%L''',
177                '''--new-group-format=\t\tnew lines : \n%>''',
178                '''--old-line-format=\t\t%dn\t%L''',
179                '''--unchanged-group-format=%=''',
180                '''--changed-group-format=\t\texpected :\n%<\t\tgot :\n%>''',
181                '''--unchanged-line-format=''',
182                lhs,
183                rhs,
[d65f92c]184                output_file=subprocess.PIPE
[a45fc7b]185        )
[bacc36c]186
187# call make
[d65f92c]188def make(target, *, flags = '', output_file = None, error = None, error_file = None, silent = False):
[a45fc7b]189        test_param = """test="%s" """ % (error_file) if error_file else None
190        cmd = [
191                *settings.make,
192                '-s' if silent else None,
[bacc36c]193                test_param,
[575a6e5]194                settings.arch.flags,
[f3b9efc]195                settings.debug.flags,
[a5121bf]196                settings.install.flags,
[d65f92c]197                settings.distcc if settings.distribute else None,
[bacc36c]198                flags,
[a45fc7b]199                target
200        ]
201        cmd = [s for s in cmd if s]
[fc01219]202        return sh(*cmd, output_file=output_file, error=error, pass_fds=settings.make_jobfds)
[bacc36c]203
[a468e1e9]204def make_recon(target):
205        cmd = [
206                *settings.make,
207                '-W',
208                os.path.abspath(os.path.join(settings.BUILDDIR, '../driver/cfa')),
209                '--recon',
210                target
211        ]
212        cmd = [s for s in cmd if s]
213        return sh(*cmd, output_file=subprocess.PIPE)
214
[ed45af6]215def which(program):
[0f5da65]216        fpath, fname = os.path.split(program)
217        if fpath:
218                if is_exe(program):
219                        return program
220        else:
221                for path in os.environ["PATH"].split(os.pathsep):
222                        exe_file = os.path.join(path, program)
223                        if is_exe(exe_file):
224                                return exe_file
225        return None
[0c13238]226
[f806b61]227@contextlib.contextmanager
228def tempdir():
229        cwd = os.getcwd()
230        with tempfile.TemporaryDirectory() as temp:
231                os.chdir(temp)
232                try:
233                        yield temp
234                finally:
235                        os.chdir(cwd)
236
[35a408b7]237def killgroup():
238        try:
239                os.killpg(os.getpgrp(), signal.SIGINT)
240        except KeyboardInterrupt:
241                pass # expected
242        except Exception as exc:
243                print("Unexpected exception", file=sys.stderr)
244                print(exc, file=sys.stderr)
245                sys.stderr.flush()
246                sys.exit(2)
247
[bacc36c]248################################################################################
249#               file handling
250################################################################################
[0c13238]251# move a file
252def mv(source, dest):
[cc9b520]253        ret, _, _ = sh("mv", source, dest)
[0c13238]254        return ret
255
256# cat one file into the other
257def cat(source, dest):
[cc9b520]258        ret, _, _ = sh("cat", source, output_file=dest)
[0c13238]259        return ret
[bacc36c]260
[c07d724]261# helper function to replace patterns in a file
262def file_replace(fname, pat, s_after):
[202ad72]263        if settings.dry_run:
264                print("replacing '%s' with '%s' in %s" % (pat, s_after, fname))
265                return
266
[f85bc15]267        file = fileinput.FileInput(fname, inplace=True, backup='.bak')
268        for line in file:
269                print(line.replace(pat, s_after), end='')
270        file.close()
[c07d724]271
[0ad0c55]272# helper function to check if a files contains only a specific string
[5bf1f3e]273def file_contains_only(file, text) :
[eb67b47]274        with open(file, encoding="latin-1") as f: # use latin-1 so all chars mean something.
[c07d724]275                ff = f.read().strip()
276                result = ff == text.strip()
277
[5bf1f3e]278                return result
[c07d724]279
[bacc36c]280# transform path to canonical form
[5bf1f3e]281def canonical_path(path):
[8e516fd]282        abspath = os.path.abspath(os.path.realpath(__main__.__file__))
[f85bc15]283        dname = os.path.dirname(abspath)
284        return os.path.join(dname, os.path.normpath(path) )
[c07d724]285
[bacc36c]286# compare path even if form is different
[5bf1f3e]287def path_cmp(lhs, rhs):
288        return canonical_path( lhs ) == canonical_path( rhs )
[c07d724]289
[bacc36c]290# walk all files in a path
[5bf1f3e]291def path_walk( op ):
[56de5932]292        dname = settings.SRCDIR
[5b993e0]293        for dirname, _, names in os.walk(dname):
294                for name in names:
295                        path = os.path.join(dirname, name)
296                        op( path )
[bacc36c]297
298################################################################################
299#               system
300################################################################################
[fc01219]301def jobserver_version():
[5a96b36]302        make_ret, out, err = sh('make', '.test_makeflags', '-j2', ignore_dry_run = True, output_file=subprocess.PIPE, error=subprocess.PIPE)
[fc01219]303        if make_ret != 0:
304                print("ERROR: cannot find Makefile jobserver version", file=sys.stderr)
[cc9b520]305                print("       test returned : {} '{}'".format(make_ret, err), file=sys.stderr)
[fc01219]306                sys.exit(1)
307
[195d1d8]308        re_jobs = re.search(r"--jobserver-(auth|fds)", out)
[fc01219]309        if not re_jobs:
310                print("ERROR: cannot find Makefile jobserver version", file=sys.stderr)
[cc9b520]311                print("       MAKEFLAGS are : '{}'".format(out), file=sys.stderr)
[fc01219]312                sys.exit(1)
313
314        return "--jobserver-{}".format(re_jobs.group(1))
315
316def prep_recursive_make(N):
317        if N < 2:
318                return []
319
320        # create the pipe
321        (r, w) = os.pipe()
322
323        # feel it with N-1 tokens, (Why N-1 and not N, I don't know it's in the manpage for make)
324        os.write(w, b'+' * (N - 1));
325
326        # prep the flags for make
327        make_flags = ["-j{}".format(N), "--jobserver-auth={},{}".format(r, w)]
328
329        # tell make about the pipes
330        os.environ["MAKEFLAGS"] = os.environ["MFLAGS"] = " ".join(make_flags)
331
332        # make sure pass the pipes to our children
333        settings.update_make_fds(r, w)
334
335        return make_flags
336
[ef56087]337def prep_unlimited_recursive_make():
338        # prep the flags for make
339        make_flags = ["-j"]
340
341        # tell make about the pipes
342        os.environ["MAKEFLAGS"] = os.environ["MFLAGS"] = "-j"
343
344        return make_flags
345
346
347def eval_hardware():
348        # we can create as many things as we want
349        # how much hardware do we have?
350        if settings.distribute:
351                # remote hardware is allowed
352                # how much do we have?
[cc9b520]353                ret, jstr, _ = sh("distcc", "-j", output_file=subprocess.PIPE, ignore_dry_run=True)
[ef56087]354                return int(jstr.strip()) if ret == 0 else multiprocessing.cpu_count()
355        else:
356                # remote isn't allowed, use local cpus
357                return multiprocessing.cpu_count()
358
[bacc36c]359# count number of jobs to create
[9cd44ba]360def job_count( options ):
[bacc36c]361        # check if the user already passed in a number of jobs for multi-threading
[fc01219]362        make_env = os.environ.get('MAKEFLAGS')
363        make_flags = make_env.split() if make_env else None
364        jobstr = jobserver_version()
365
366        if options.jobs and make_flags:
367                print('WARNING: -j options should not be specified when called form Make', file=sys.stderr)
368
369        # Top level make is calling the shots, just follow
370        if make_flags:
371                # do we have -j and --jobserver-...
372                jobopt = None
373                exists_fds = None
374                for f in make_flags:
375                        jobopt = f if f.startswith("-j") else jobopt
376                        exists_fds = f if f.startswith(jobstr) else exists_fds
377
378                # do we have limited parallelism?
379                if exists_fds :
380                        try:
381                                rfd, wfd = tuple(exists_fds.split('=')[1].split(','))
382                        except:
383                                print("ERROR: jobserver has unrecoginzable format, was '{}'".format(exists_fds), file=sys.stderr)
384                                sys.exit(1)
385
386                        # read the token pipe to count number of available tokens and restore the pipe
387                        # this assumes the test suite script isn't invoked in parellel with something else
388                        tokens = os.read(int(rfd), 65536)
389                        os.write(int(wfd), tokens)
390
391                        # the number of tokens is off by one for obscure but well documented reason
392                        # see man make for more details
393                        options.jobs = len(tokens) + 1
394
395                # do we have unlimited parallelism?
396                elif jobopt and jobopt != "-j1":
397                        # check that this actually make sense
398                        if jobopt != "-j":
399                                print("ERROR: -j option passed by make but no {}, was '{}'".format(jobstr, jobopt), file=sys.stderr)
400                                sys.exit(1)
401
[ef56087]402                        options.jobs = eval_hardware()
403                        flags = prep_unlimited_recursive_make()
[fc01219]404
405
406                # then no parallelism
407                else:
408                        options.jobs = 1
409
[5baa33c]410                # keep all flags make passed along, except the weird 'w' which is about subdirectories
411                flags = [f for f in make_flags if f != 'w']
[fc01219]412
413        # Arguments are calling the shots, fake the top level make
[ef56087]414        elif options.jobs :
415
[fc01219]416                # make sure we have a valid number of jobs that corresponds to user input
[ef56087]417                if options.jobs < 0 :
[fc01219]418                        print('ERROR: Invalid number of jobs', file=sys.stderr)
419                        sys.exit(1)
420
421                flags = prep_recursive_make(options.jobs)
422
[ef56087]423        # Arguments are calling the shots, fake the top level make, but 0 is a special case
424        elif options.jobs == 0:
425                options.jobs = eval_hardware()
426                flags = prep_unlimited_recursive_make()
427
[fc01219]428        # No one says to run in parallel, then don't
[bacc36c]429        else :
[fc01219]430                options.jobs = 1
431                flags = []
[bacc36c]432
[fc01219]433        # Make sure we call make as expected
434        settings.update_make_cmd( flags )
[bacc36c]435
[fc01219]436        # return the job count
437        return options.jobs
[bacc36c]438
[0c13238]439# enable core dumps for all the test children
440resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
441
[bacc36c]442################################################################################
443#               misc
444################################################################################
445
[d65f92c]446# get hash for given configuration
447def config_hash():
448        path = os.path.normpath(os.path.join(
449                settings.SRCDIR,
450        ))
451
452        distcc_hash = os.path.join(settings.SRCDIR, '../tools/build/distcc_hash')
453        config = "%s-%s" % (settings.arch.target, settings.debug.path)
[cc9b520]454        _, out, _ = sh(distcc_hash, config, output_file=subprocess.PIPE, ignore_dry_run=True)
[d65f92c]455        return out.strip()
456
[5c4a473]457# get pretty string for time of day
[dcfedca]458def pretty_now():
459        ts = time.time()
460        print(ts, file=sys.stderr)
461        return datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d_%H:%M:%S')
462
[bacc36c]463# check if arguments is yes or no
464def yes_no(string):
465        if string == "yes" :
466                return True
467        if string == "no" :
468                return False
469        raise argparse.ArgumentTypeError(msg)
[f3b9efc]470
[136f86b]471# Convert a function that converts a string to one that converts comma separated string.
472def comma_separated(elements):
473    return lambda string: [elements(part) for part in string.split(',')]
474
[ed45af6]475def fancy_print(text):
476        column = which('column')
477        if column:
[1bb2488]478                subprocess.run(column, input=bytes(text + "\n", "UTF-8"))
[ed45af6]479        else:
480                print(text)
[0c13238]481
482
[5bf1f3e]483def core_info(path):
[f806b61]484        if not os.path.isfile(path):
485                return 1, "ERR Executable path is wrong"
486
[0c13238]487        cmd   = os.path.join(settings.SRCDIR, "pybin/print-core.gdb")
488        if not os.path.isfile(cmd):
489                return 1, "ERR Printing format for core dumps not found"
490
[f806b61]491        core  = os.path.join(os.getcwd(), "core" )
[0c13238]492
493        if not os.path.isfile(core):
[630c4bb]494                return 1, "ERR No core dump, expected '{}' (limit soft: {} hard: {})".format(core, *resource.getrlimit(resource.RLIMIT_CORE))
[0c13238]495
[22a4292]496        try:
[b053083]497                ret, out, err = sh('gdb', '-n', path, core, '-batch', '-x', cmd, output_file=subprocess.PIPE)
498                if ret == 0:
499                        return 0, out
500                else:
501                        return 1, err
[22a4292]502        except:
503                return 1, "ERR Could not read core with gdb"
[0c13238]504
[dcfedca]505def core_archive(dst, name, exe):
[143e6f3]506        # Get the core dump
[dcfedca]507        core = os.path.join(os.getcwd(), "core" )
508
[143e6f3]509        # update the path for this test
510        dst  = os.path.join(dst, name)
[dcfedca]511
512        # make a directory for this test
[143e6f3]513        # mkdir makes the parent directory only so add a dummy
[8c28967]514        mkdir( os.path.join(dst, "core") )
[dcfedca]515
516        # moves the files
517        mv( core, os.path.join(dst, "core" ) )
[8c28967]518        mv( exe , os.path.join(dst, "exe"  ) )
[dcfedca]519
520        # return explanatory test
521        return "Archiving %s (executable and core) to %s" % (os.path.relpath(exe, settings.BUILDDIR), os.path.relpath(dst, settings.original_path))
522
[0c13238]523class Timed:
[0f5da65]524        def __enter__(self):
525                self.start = time.time()
526                return self
[0c13238]527
[0f5da65]528        def __exit__(self, *args):
529                self.end = time.time()
530                self.duration = self.end - self.start
[35a408b7]531
532def timed(src, timeout):
533        expire = time.time() + timeout
534        i = iter(src)
[0f5da65]535        with contextlib.suppress(StopIteration):
536                while True:
537                        yield i.next(max(expire - time.time(), 0))
[76de075]538
539def fmtDur( duration ):
540        if duration :
541                hours, rem = divmod(duration, 3600)
542                minutes, rem = divmod(rem, 60)
543                seconds, millis = divmod(rem, 1)
544                return "%2d:%02d.%03d" % (minutes, seconds, millis * 1000)
[195d1d8]545        return " n/a"
Note: See TracBrowser for help on using the repository browser.