source: tests/pybin/tools.py@ 0497b6ba

Last change on this file since 0497b6ba was 195d1d8, checked in by Peter A. Buhr <pabuhr@…>, 16 months ago

update search strings to raw strings for python 3.12

  • 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(r".*: (.*)", 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(r"--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.