source: tests/pybin/tools.py @ a659b31

ADTast-experimental
Last change on this file since a659b31 was 0fc91db1, checked in by Thierry Delisle <tdelisle@…>, 21 months ago

Removed old ast from configure and tests.py

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