source: tests/pybin/tools.py@ a552a8c

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

Added more information when we expect a core dump and didn't get one.

  • 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, expected '{}' (limit soft: {} hard: {})".format(core, *resource.getrlimit(resource.RLIMIT_CORE))
486
487 try:
488 ret, out, err = sh('gdb', '-n', path, core, '-batch', '-x', cmd, output_file=subprocess.PIPE)
489 if ret == 0:
490 return 0, out
491 else:
492 return 1, err
493 except:
494 return 1, "ERR Could not read core with gdb"
495
496def core_archive(dst, name, exe):
497 # Get the core dump
498 core = os.path.join(os.getcwd(), "core" )
499
500 # update the path for this test
501 dst = os.path.join(dst, name)
502
503 # make a directory for this test
504 # mkdir makes the parent directory only so add a dummy
505 mkdir( os.path.join(dst, "core") )
506
507 # moves the files
508 mv( core, os.path.join(dst, "core" ) )
509 mv( exe , os.path.join(dst, "exe" ) )
510
511 # return explanatory test
512 return "Archiving %s (executable and core) to %s" % (os.path.relpath(exe, settings.BUILDDIR), os.path.relpath(dst, settings.original_path))
513
514class Timed:
515 def __enter__(self):
516 self.start = time.time()
517 return self
518
519 def __exit__(self, *args):
520 self.end = time.time()
521 self.duration = self.end - self.start
522
523def timed(src, timeout):
524 expire = time.time() + timeout
525 i = iter(src)
526 with contextlib.suppress(StopIteration):
527 while True:
528 yield i.next(max(expire - time.time(), 0))
529
530def fmtDur( duration ):
531 if duration :
532 hours, rem = divmod(duration, 3600)
533 minutes, rem = divmod(rem, 60)
534 seconds, millis = divmod(rem, 1)
535 return "%2d:%02d.%03d" % (minutes, seconds, millis * 1000)
536 return " n/a"
Note: See TracBrowser for help on using the repository browser.