source: tests/pybin/tools.py @ 60f4919

Last change on this file since 60f4919 was f58522b0, checked in by Thierry Delisle <tdelisle@…>, 2 years ago

python test script now nice +5 the executables before running them.

  • Property mode set to 100644
File size: 15.3 KB
Line 
1import __main__
2import argparse
3import contextlib
4import datetime
5import fileinput
6import multiprocessing
7import os
8import re
9import resource
10import signal
11import stat
12import subprocess
13import sys
14import tempfile
15import time
16import types
17
18from pybin import settings
19
20################################################################################
21#               shell helpers
22################################################################################
23
24# helper functions to run terminal commands
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):
26        try:
27                cmd = list(cmd)
28
29                if input_file and input_text:
30                        return 401, "Cannot use both text and file inputs"
31
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
38
39                        if error and not isinstance(error, int):
40                                cmd += " 2> "
41                                cmd += error
42
43                        if input_file and not isinstance(input_file, int) and os.path.isfile(input_file):
44                                cmd += " < "
45                                cmd += input_file
46
47                        print(cmd)
48                        return 0, None, None
49
50                with contextlib.ExitStack() as onexit:
51                        # add input redirection if needed
52                        input_file = openfd(input_file, 'r', onexit, True)
53
54                        # add output redirection if needed
55                        output_file = openfd(output_file, 'w', onexit, False)
56
57                        # add error redirection if needed
58                        error = openfd(error, 'w', onexit, False)
59
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
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
80                        with subprocess.Popen( cmd, **popen_kwargs ) as proc:
81                                try:
82                                        out, errout = proc.communicate(
83                                                timeout = settings.timeout.single if timeout else None
84                                        )
85
86                                        return proc.returncode, out.decode("latin-1") if out else None, errout.decode("latin-1") if errout else None
87                                except subprocess.TimeoutExpired:
88                                        if settings.timeout2gdb:
89                                                print("Process {} timeout".format(proc.pid))
90                                                proc.communicate()
91                                                return 124, str(None), "Subprocess Timeout 2 gdb"
92                                        else:
93                                                proc.send_signal(signal.SIGABRT)
94                                                proc.communicate()
95                                                return 124, str(None), "Subprocess Timeout 2 gdb"
96
97        except Exception as ex:
98                print ("Unexpected error: %s" % ex)
99                raise
100
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
110def is_ascii(fname):
111        if settings.dry_run:
112                print("is_ascii: %s" % fname)
113                return (True, "")
114
115        if not os.path.isfile(fname):
116                return (False, "No file")
117
118        code, out, err = sh("file", fname, output_file=subprocess.PIPE)
119        if code != 0:
120                return (False, "'file EXPECT' failed with code {} '{}'".format(code, err))
121
122        match = re.search(".*: (.*)", out)
123
124        if not match:
125                return (False, "Unreadable file type: '{}'".format(out))
126
127        if "ASCII text" in match.group(1):
128                return (True, "")
129
130        return (False, "File type should be 'ASCII text', was '{}'".format(match.group(1)))
131
132def is_exe(fname):
133        return os.path.isfile(fname) and os.access(fname, os.X_OK)
134
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
145        file = open(file, mode, encoding="latin-1") # use latin-1 so all chars mean something.
146        exitstack.push(file)
147        return file
148
149# Remove 1 or more files silently
150def rm( files ):
151        if isinstance(files, str ): files = [ files ]
152        for file in files:
153                sh( 'rm', '-f', file, output_file=subprocess.DEVNULL, error=subprocess.DEVNULL )
154
155# Create 1 or more directory
156def mkdir( files ):
157        if isinstance(files, str ): files = [ files ]
158        for file in files:
159                p = os.path.normpath( file )
160                d = os.path.dirname ( p )
161                sh( 'mkdir', '-p', d, output_file=subprocess.DEVNULL, error=subprocess.DEVNULL )
162
163
164def chdir( dest = __main__.__file__ ):
165        abspath = os.path.abspath(dest)
166        dname = os.path.dirname(abspath)
167        os.chdir(dname)
168
169# diff two files
170def diff( lhs, rhs ):
171        # fetch return code and error from the diff command
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,
184                output_file=subprocess.PIPE
185        )
186
187# call make
188def make(target, *, flags = '', output_file = None, error = None, error_file = None, silent = False):
189        test_param = """test="%s" """ % (error_file) if error_file else None
190        cmd = [
191                *settings.make,
192                '-s' if silent else None,
193                test_param,
194                settings.arch.flags,
195                settings.debug.flags,
196                settings.install.flags,
197                settings.distcc if settings.distribute else None,
198                flags,
199                target
200        ]
201        cmd = [s for s in cmd if s]
202        return sh(*cmd, output_file=output_file, error=error, pass_fds=settings.make_jobfds)
203
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
215def which(program):
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
226
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
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
248################################################################################
249#               file handling
250################################################################################
251# move a file
252def mv(source, dest):
253        ret, _, _ = sh("mv", source, dest)
254        return ret
255
256# cat one file into the other
257def cat(source, dest):
258        ret, _, _ = sh("cat", source, output_file=dest)
259        return ret
260
261# helper function to replace patterns in a file
262def file_replace(fname, pat, s_after):
263        if settings.dry_run:
264                print("replacing '%s' with '%s' in %s" % (pat, s_after, fname))
265                return
266
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()
271
272# helper function to check if a files contains only a specific string
273def file_contains_only(file, text) :
274        with open(file, encoding="latin-1") as f: # use latin-1 so all chars mean something.
275                ff = f.read().strip()
276                result = ff == text.strip()
277
278                return result
279
280# transform path to canonical form
281def canonical_path(path):
282        abspath = os.path.abspath(os.path.realpath(__main__.__file__))
283        dname = os.path.dirname(abspath)
284        return os.path.join(dname, os.path.normpath(path) )
285
286# compare path even if form is different
287def path_cmp(lhs, rhs):
288        return canonical_path( lhs ) == canonical_path( rhs )
289
290# walk all files in a path
291def path_walk( op ):
292        dname = settings.SRCDIR
293        for dirname, _, names in os.walk(dname):
294                for name in names:
295                        path = os.path.join(dirname, name)
296                        op( path )
297
298################################################################################
299#               system
300################################################################################
301def jobserver_version():
302        make_ret, out, err = sh('make', '.test_makeflags', '-j2', ignore_dry_run = True, output_file=subprocess.PIPE, error=subprocess.PIPE)
303        if make_ret != 0:
304                print("ERROR: cannot find Makefile jobserver version", file=sys.stderr)
305                print("       test returned : {} '{}'".format(make_ret, err), file=sys.stderr)
306                sys.exit(1)
307
308        re_jobs = re.search("--jobserver-(auth|fds)", out)
309        if not re_jobs:
310                print("ERROR: cannot find Makefile jobserver version", file=sys.stderr)
311                print("       MAKEFLAGS are : '{}'".format(out), file=sys.stderr)
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
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?
353                ret, jstr, _ = sh("distcc", "-j", output_file=subprocess.PIPE, ignore_dry_run=True)
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
359# count number of jobs to create
360def job_count( options ):
361        # check if the user already passed in a number of jobs for multi-threading
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
402                        options.jobs = eval_hardware()
403                        flags = prep_unlimited_recursive_make()
404
405
406                # then no parallelism
407                else:
408                        options.jobs = 1
409
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']
412
413        # Arguments are calling the shots, fake the top level make
414        elif options.jobs :
415
416                # make sure we have a valid number of jobs that corresponds to user input
417                if options.jobs < 0 :
418                        print('ERROR: Invalid number of jobs', file=sys.stderr)
419                        sys.exit(1)
420
421                flags = prep_recursive_make(options.jobs)
422
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
428        # No one says to run in parallel, then don't
429        else :
430                options.jobs = 1
431                flags = []
432
433        # Make sure we call make as expected
434        settings.update_make_cmd( flags )
435
436        # return the job count
437        return options.jobs
438
439# enable core dumps for all the test children
440resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
441
442################################################################################
443#               misc
444################################################################################
445
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)
454        _, out, _ = sh(distcc_hash, config, output_file=subprocess.PIPE, ignore_dry_run=True)
455        return out.strip()
456
457# get pretty string for time of day
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
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)
470
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
475def fancy_print(text):
476        column = which('column')
477        if column:
478                subprocess.run(column, input=bytes(text + "\n", "UTF-8"))
479        else:
480                print(text)
481
482
483def core_info(path):
484        if not os.path.isfile(path):
485                return 1, "ERR Executable path is wrong"
486
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
491        core  = os.path.join(os.getcwd(), "core" )
492
493        if not os.path.isfile(core):
494                return 1, "ERR No core dump, expected '{}' (limit soft: {} hard: {})".format(core, *resource.getrlimit(resource.RLIMIT_CORE))
495
496        try:
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
502        except:
503                return 1, "ERR Could not read core with gdb"
504
505def core_archive(dst, name, exe):
506        # Get the core dump
507        core = os.path.join(os.getcwd(), "core" )
508
509        # update the path for this test
510        dst  = os.path.join(dst, name)
511
512        # make a directory for this test
513        # mkdir makes the parent directory only so add a dummy
514        mkdir( os.path.join(dst, "core") )
515
516        # moves the files
517        mv( core, os.path.join(dst, "core" ) )
518        mv( exe , os.path.join(dst, "exe"  ) )
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
523class Timed:
524        def __enter__(self):
525                self.start = time.time()
526                return self
527
528        def __exit__(self, *args):
529                self.end = time.time()
530                self.duration = self.end - self.start
531
532def timed(src, timeout):
533        expire = time.time() + timeout
534        i = iter(src)
535        with contextlib.suppress(StopIteration):
536                while True:
537                        yield i.next(max(expire - time.time(), 0))
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)
545        return " n/a"
Note: See TracBrowser for help on using the repository browser.