source: tests/pybin/tools.py@ 5baa33c

ADT ast-experimental enum pthread-emulation qualifiedEnum
Last change on this file since 5baa33c was 5baa33c, checked in by Thierry Delisle <tdelisle@…>, 4 years ago

Removed debug print and drop spurious 'w' in make flags.

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