source: tests/pybin/tools.py @ cc9b520

ADTast-experimentalenumpthread-emulationqualifiedEnum
Last change on this file since cc9b520 was cc9b520, checked in by Thierry Delisle <tdelisle@…>, 3 years ago

Clean-up error handling in test scripts

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