source: tests/pybin/tools.py@ e01eb4a

ADT ast-experimental
Last change on this file since e01eb4a was 0fc91db1, checked in by Thierry Delisle <tdelisle@…>, 3 years 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.