change look-up -> look up
[scons.git] / testing / framework / TestSCons.py
blob243be753a4e444e50b9c60aeb8ea7ff240b51559
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """
25 A testing framework for the SCons software construction tool.
27 A TestSCons environment object is created via the usual invocation:
29 test = TestSCons()
31 TestScons is a subclass of TestCommon, which in turn is a subclass
32 of TestCmd), and hence has available all of the methods and attributes
33 from those classes, as well as any overridden or additional methods or
34 attributes defined in this subclass.
35 """
37 import os
38 import re
39 import shutil
40 import sys
41 import time
42 import subprocess as sp
43 import zipfile
44 from collections import namedtuple
45 from typing import Optional, Tuple
47 from TestCommon import *
48 from TestCommon import __all__, _python_
49 from SCons.Util import get_hash_format, get_current_hash_algorithm_used
51 from TestCmd import Popen
52 from TestCmd import PIPE
54 # Some tests which verify that SCons has been packaged properly need to
55 # look for specific version file names. Replicating the version number
56 # here provides some independent verification that what we packaged
57 # conforms to what we expect.
59 default_version = '4.8.2ayyyymmdd'
61 # TODO: these need to be hand-edited when there are changes
62 python_version_unsupported = (3, 6, 0)
63 python_version_deprecated = (3, 7, 0)
64 python_version_supported_str = "3.7.0" # str of lowest non-deprecated Python
66 SConsVersion = default_version
68 __all__.extend([
69 'TestSCons',
70 'machine',
71 'python',
72 '_exe',
73 '_obj',
74 '_shobj',
75 'shobj_',
76 'lib_',
77 '_lib',
78 'dll_',
79 '_dll'
82 machine_map = {
83 'i686': 'i386',
84 'i586': 'i386',
85 'i486': 'i386',
88 try:
89 uname = os.uname
90 except AttributeError:
91 # Windows doesn't have a uname() function. We could use something like
92 # sys.platform as a fallback, but that's not really a "machine," so
93 # just leave it as None.
94 machine = None
95 else:
96 machine = uname()[4]
97 machine = machine_map.get(machine, machine)
99 _exe = exe_suffix
100 _obj = obj_suffix
101 _shobj = shobj_suffix
102 shobj_ = shobj_prefix
103 _lib = lib_suffix
104 lib_ = lib_prefix
105 _dll = dll_suffix
106 dll_ = dll_prefix
108 if sys.platform == 'cygwin':
109 # On Cygwin, os.path.normcase() lies, so just report back the
110 # fact that the underlying Win32 OS is case-insensitive.
111 def case_sensitive_suffixes(s1, s2) -> int:
112 return 0
113 else:
114 def case_sensitive_suffixes(s1, s2):
115 return (os.path.normcase(s1) != os.path.normcase(s2))
117 file_expr = r"""File "[^"]*", line \d+, in [^\n]+
121 # re.escape escapes too much.
122 def re_escape(str):
123 for c in '\\.[]()*+?': # Not an exhaustive list.
124 str = str.replace(c, f"\\{c}")
125 return str
129 # Helper functions that we use as a replacement to the default re.match
130 # when searching for special strings in stdout/stderr.
132 def search_re(out, l):
133 """ Search the regular expression 'l' in the output 'out'
134 and return the start index when successful.
136 m = re.search(l, out)
137 if m:
138 return m.start()
140 return None
143 def search_re_in_list(out, l):
144 """ Search the regular expression 'l' in each line of
145 the given string list 'out' and return the line's index
146 when successful.
148 for idx, o in enumerate(out):
149 m = re.search(l, o)
150 if m:
151 return idx
153 return None
157 # Helpers for handling Python version numbers
159 def python_version_string():
160 return sys.version.split()[0]
163 def python_minor_version_string():
164 return sys.version[:3]
167 def unsupported_python_version(version=sys.version_info):
168 return version < python_version_unsupported
171 def deprecated_python_version(version=sys.version_info):
172 return version < python_version_deprecated
175 if deprecated_python_version():
176 msg = r"""
177 scons: warning: Support for Python older than %s is deprecated (%s detected).
178 If this will cause hardship, contact scons-dev@scons.org
180 deprecated_python_expr = (
181 re_escape(msg % (python_version_supported_str, python_version_string()))
182 + file_expr
184 del msg
185 else:
186 deprecated_python_expr = ""
189 def initialize_sconsflags(ignore_python_version):
191 Add the --warn=no-python-version option to SCONSFLAGS for every
192 command so test scripts don't have to filter out Python version
193 deprecation warnings.
194 Same for --warn=no-visual-c-missing.
196 save_sconsflags = os.environ.get('SCONSFLAGS')
197 if save_sconsflags:
198 sconsflags = [save_sconsflags]
199 else:
200 sconsflags = []
201 if ignore_python_version and deprecated_python_version():
202 sconsflags.append('--warn=no-python-version')
203 # Provide a way to suppress or provide alternate flags for
204 # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
205 # (The intended use case is to set it to null when running
206 # timing tests of earlier versions of SCons which don't
207 # support the --warn=no-visual-c-missing warning.)
208 visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
209 '--warn=no-visual-c-missing')
210 if visual_c and visual_c not in sconsflags:
211 sconsflags.append(visual_c)
212 os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
213 return save_sconsflags
216 def restore_sconsflags(sconsflags) -> None:
217 if sconsflags is None:
218 del os.environ['SCONSFLAGS']
219 else:
220 os.environ['SCONSFLAGS'] = sconsflags
223 # Helpers for Configure()'s config.log processing
224 ConfigCheckInfo = namedtuple('ConfigCheckInfo',
225 ['check_string', 'result', 'cached', 'temp_filename'])
226 # check_string: the string output to for this checker
227 # results : The expected results for each check
228 # cached : If the corresponding check is expected to be cached
229 # temp_filename : The name of the generated tempfile for this check
232 class NoMatch(Exception):
234 Exception for matchPart to indicate there was no match found in the passed logfile
236 def __init__(self, p) -> None:
237 self.pos = p
240 def match_part_of_configlog(log, logfile, lastEnd, NoMatch=NoMatch):
242 Match part of the logfile
244 # print("Match:\n%s\n==============\n%s" % (log , logfile[lastEnd:]))
245 m = re.match(log, logfile[lastEnd:])
246 if not m:
247 raise NoMatch(lastEnd)
248 return m.end() + lastEnd
251 class TestSCons(TestCommon):
252 """Class for testing SCons.
254 This provides a common place for initializing SCons tests,
255 eliminating the need to begin every test with the same repeated
256 initializations.
259 scons_version = SConsVersion
260 javac_is_gcj = False
262 def __init__(self, **kw) -> None:
263 """Initialize an SCons testing object.
265 If they're not overridden by keyword arguments, this
266 initializes the object with the following default values:
268 program = 'scons' if it exists,
269 else 'scons.py'
270 interpreter = 'python'
271 match = match_exact
272 workdir = ''
274 The workdir value means that, by default, a temporary workspace
275 directory is created for a TestSCons environment. In addition,
276 this method changes directory (chdir) to the workspace directory,
277 so an explicit "chdir = '.'" on all of the run() method calls
278 is not necessary.
280 self.orig_cwd = os.getcwd()
281 self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0)
283 if not self.external:
284 try:
285 script_dir = os.environ['SCONS_SCRIPT_DIR']
286 except KeyError:
287 pass
288 else:
289 os.chdir(script_dir)
290 if 'program' not in kw:
291 kw['program'] = os.environ.get('SCONS')
292 if not kw['program']:
293 if not self.external:
294 if os.path.exists('scons'):
295 kw['program'] = 'scons'
296 else:
297 kw['program'] = 'scons.py'
298 else:
299 kw['program'] = 'scons'
300 kw['interpreter'] = ''
301 elif not self.external and not os.path.isabs(kw['program']):
302 kw['program'] = os.path.join(self.orig_cwd, kw['program'])
303 if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
304 kw['interpreter'] = [python, ]
305 if 'match' not in kw:
306 kw['match'] = match_exact
307 if 'workdir' not in kw:
308 kw['workdir'] = ''
310 # Term causing test failures due to bogus readline init
311 # control character output on FC8
312 # TERM can cause test failures due to control chars in prompts etc.
313 os.environ['TERM'] = 'dumb'
315 self.ignore_python_version = kw.get('ignore_python_version', 1)
316 if kw.get('ignore_python_version', -1) != -1:
317 del kw['ignore_python_version']
319 super().__init__(**kw)
321 if not self.external:
322 import SCons.Node.FS
323 if SCons.Node.FS.default_fs is None:
324 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
326 try:
327 self.fixture_dirs = (os.environ['FIXTURE_DIRS']).split(os.pathsep)
328 except KeyError:
329 pass
331 def Environment(self, ENV=None, *args, **kw):
333 Return a construction Environment that optionally overrides
334 the default external environment with the specified ENV.
336 if not self.external:
337 import SCons.Environment
338 import SCons.Errors
339 if ENV is not None:
340 kw['ENV'] = ENV
341 try:
342 return SCons.Environment.Environment(*args, **kw)
343 except (SCons.Errors.UserError, SCons.Errors.InternalError):
344 return None
346 return None
348 def detect(self, var, prog=None, ENV=None, norm=None):
350 Return the detected path to a tool program.
352 Searches first the named construction variable, then
353 the SCons path.
355 Args:
356 var: name of construction variable to check for tool name.
357 prog: tool program to check for.
358 ENV: if present, kwargs to initialize an environment that
359 will be created to perform the lookup.
360 norm: if true, normalize any returned path looked up in
361 the environment to use UNIX-style path separators.
363 Returns: full path to the tool, or None.
366 env = self.Environment(ENV)
367 if env:
368 v = env.subst(f"${var}")
369 if not v:
370 return None
371 if prog is None:
372 prog = v
373 if v != prog:
374 return None
375 result = env.WhereIs(prog)
376 if result and norm and os.sep != '/':
377 result = result.replace(os.sep, '/')
378 return result
380 return self.where_is(prog)
382 def detect_tool(self, tool, prog=None, ENV=None):
384 Given a tool (i.e., tool specification that would be passed
385 to the "tools=" parameter of Environment()) and a program that
386 corresponds to that tool, return true if and only if we can find
387 that tool using Environment.Detect().
389 By default, prog is set to the value passed into the tools parameter.
392 if not prog:
393 prog = tool
394 env = self.Environment(ENV, tools=[tool])
395 if env is None:
396 return None
397 return env.Detect([prog])
399 def where_is(self, prog, path=None, pathext=None):
401 Given a program, search for it in the specified external PATH,
402 or in the actual external PATH if none is specified.
404 if path is None:
405 path = os.environ['PATH']
406 if self.external:
407 if isinstance(prog, str):
408 prog = [prog]
409 for p in prog:
410 result = TestCmd.where_is(self, p, path, pathext)
411 if result:
412 return os.path.normpath(result)
413 else:
414 import SCons.Environment
415 env = SCons.Environment.Environment()
416 return env.WhereIs(prog, path, pathext)
418 return None
420 def wrap_stdout(self, build_str: str="", read_str: str="", error: int=0, cleaning: int=0) -> str:
421 """Wraps "expect" strings in SCons boilerplate.
423 Given strings of expected output specific to a test,
424 returns a string which includes the SCons wrapping such as
425 "Reading ... done", etc.: that is, adds the text that would
426 be left out by running SCons in quiet mode;
427 Makes a complete message to match against.
429 Args:
430 build_str: the message for the execution part of the output.
431 If non-empty, needs to be newline-terminated.
432 read_str: the message for the reading-sconscript part of
433 the output. If non-empty, needs to be newline-terminated.
434 error: if true, expect a fail message rather than a done message.
435 cleaning: index into type messages, if 0 selects
436 build messages, if 1 selects clean messages.
438 cap, lc = [('Build', 'build'),
439 ('Clean', 'clean')][cleaning]
440 if error:
441 term = f"scons: {lc}ing terminated because of errors.\n"
442 else:
443 term = f"scons: done {lc}ing targets.\n"
445 return "scons: Reading SConscript files ...\n" + \
446 read_str + \
447 "scons: done reading SConscript files.\n" + \
448 f"scons: {cap}ing targets ...\n" + \
449 build_str + \
450 term
452 def run(self, *args, **kw) -> None:
454 Set up SCONSFLAGS for every command so test scripts don't need
455 to worry about unexpected warnings in their output.
457 sconsflags = initialize_sconsflags(self.ignore_python_version)
458 try:
459 super().run(*args, **kw)
460 finally:
461 restore_sconsflags(sconsflags)
463 # Modifying the options should work and ought to be simpler, but this
464 # class is used for more than just running 'scons' itself. If there's
465 # an automated way of determining whether it's running 'scons' or
466 # something else, this code should be resurected.
467 # options = kw.get('options')
468 # if options:
469 # options = [options]
470 # else:
471 # options = []
472 # if self.ignore_python_version and deprecated_python_version():
473 # options.append('--warn=no-python-version')
474 # # Provide a way to suppress or provide alternate flags for
475 # # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
476 # # (The intended use case is to set it to null when running
477 # # timing tests of earlier versions of SCons which don't
478 # # support the --warn=no-visual-c-missing warning.)
479 # visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
480 # '--warn=no-visual-c-missing')
481 # if visual_c:
482 # options.append(visual_c)
483 # kw['options'] = ' '.join(options)
484 # TestCommon.run(self, *args, **kw)
486 def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
487 """Asserts that all of the targets listed in arguments is
488 up to date, but does not make any assumptions on other targets.
489 This function is most useful in conjunction with the -n option.
491 s = ""
492 for arg in arguments.split():
493 s = f"{s}scons: `{arg}' is up to date.\n"
494 kw['arguments'] = arguments
495 stdout = self.wrap_stdout(read_str=read_str, build_str=s)
496 # Append '.*' so that timing output that comes after the
497 # up-to-date output is okay.
498 kw['stdout'] = f"{re.escape(stdout)}.*"
499 kw['match'] = self.match_re_dotall
500 self.run(**kw)
502 def not_up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
503 """Asserts that none of the targets listed in arguments is
504 up to date, but does not make any assumptions on other targets.
505 This function is most useful in conjunction with the -n option.
507 s = ""
508 for arg in arguments.split():
509 s = f"{s}(?!scons: `{re.escape(arg)}' is up to date.)"
510 s = f"({s}[^\n]*\n)*"
511 kw['arguments'] = arguments
512 stdout = re.escape(self.wrap_stdout(read_str=read_str, build_str='ARGUMENTSGOHERE'))
513 kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s)
514 kw['match'] = self.match_re_dotall
515 self.run(**kw)
517 def option_not_yet_implemented(self, option, arguments=None, **kw):
519 Verifies expected behavior for options that are not yet implemented:
520 a warning message, and exit status 1.
522 msg = f"Warning: the {option} option is not yet implemented\n"
523 kw['stderr'] = msg
524 if arguments:
525 # If it's a long option and the argument string begins with '=',
526 # it's of the form --foo=bar and needs no separating space.
527 if option[:2] == '--' and arguments[0] == '=':
528 kw['arguments'] = option + arguments
529 else:
530 kw['arguments'] = f"{option} {arguments}"
531 return self.run(**kw)
533 def deprecated_wrap(self, msg) -> str:
535 Calculate the pattern that matches a deprecation warning.
537 return f"\nscons: warning: {re_escape(msg)}\n{file_expr}"
539 def deprecated_fatal(self, warn, msg):
541 Determines if the warning has turned into a fatal error. If so,
542 passes the test, as any remaining runs are now moot.
544 This method expects a SConscript to be present that will causes
545 the warning. The method writes a SConstruct that calls the
546 SConsscript and looks to see what type of result occurs.
548 The pattern that matches the warning is returned.
550 TODO: Actually detect that it's now an error. We don't have any
551 cases yet, so there's no way to test it.
553 self.write('SConstruct', """if True:
554 WARN = ARGUMENTS.get('WARN')
555 if WARN: SetOption('warn', WARN)
556 SConscript('SConscript')
557 """)
559 def err_out():
560 # TODO calculate stderr for fatal error
561 return re_escape('put something here')
563 # no option, should get one of nothing, warning, or error
564 warning = self.deprecated_wrap(msg)
565 self.run(arguments='.', stderr=None)
566 stderr = self.stderr()
567 if stderr:
568 # most common case done first
569 if match_re_dotall(stderr, warning):
570 # expected output
571 pass
572 elif match_re_dotall(stderr, err_out()):
573 # now a fatal error; skip the rest of the tests
574 self.pass_test()
575 else:
576 # test failed; have to do this by hand...
577 stdout = self.stdout() or ""
578 print(self.banner('STDOUT '))
579 print(stdout)
580 print(self.diff(warning, stderr, 'STDERR '))
581 self.fail_test()
583 return warning
585 def deprecated_warning(self, warn, msg):
587 Verifies the expected behavior occurs for deprecation warnings.
588 This method expects a SConscript to be present that will causes
589 the warning. The method writes a SConstruct and exercises various
590 combinations of command-line options and SetOption parameters to
591 validate that it performs correctly.
593 The pattern that matches the warning is returned.
595 warning = self.deprecated_fatal(warn, msg)
597 def RunPair(option, expected) -> None:
598 # run the same test with the option on the command line and
599 # then with the option passed via SetOption().
600 self.run(options=f"--warn={option}",
601 arguments='.',
602 stderr=expected,
603 match=match_re_dotall)
604 self.run(options=f"WARN={option}",
605 arguments='.',
606 stderr=expected,
607 match=match_re_dotall)
609 # all warnings off, should get no output
610 RunPair('no-deprecated', '')
612 # warning enabled, should get expected output
613 RunPair(warn, warning)
615 # warning disabled, should get either nothing or mandatory message
616 expect = f"""()|(Can not disable mandataory warning: 'no-{warn}'\n\n{warning})"""
617 RunPair(f"no-{warn}", expect)
619 return warning
621 def diff_substr(self, expect, actual, prelen: int=20, postlen: int=40) -> str:
622 i = 0
623 for x, y in zip(expect, actual):
624 if x != y:
625 return "Actual did not match expect at char %d:\n" \
626 " Expect: %s\n" \
627 " Actual: %s\n" \
628 % (i, repr(expect[i - prelen:i + postlen]),
629 repr(actual[i - prelen:i + postlen]))
630 i = i + 1
631 return "Actual matched the expected output???"
633 def python_file_line(self, file, line):
635 Returns a Python error line for output comparisons.
637 The exec of the traceback line gives us the correct format for
638 this version of Python.
640 File "<string>", line 1, <module>
642 We stick the requested file name and line number in the right
643 places, abstracting out the version difference.
645 # This routine used to use traceback to get the proper format
646 # that doesn't work well with py3. And the format of the
647 # traceback seems to be stable, so let's just format
648 # an appropriate string
650 # exec('import traceback; x = traceback.format_stack()[-1]')
651 # import traceback
652 # x = traceback.format_stack()
653 # x = # XXX: .lstrip()
654 # x = x.replace('<string>', file)
655 # x = x.replace('line 1,', 'line %s,' % line)
656 # x="\n".join(x)
657 x = f'File "{file}", line {line}, in <module>\n'
658 return x
660 def normalize_ps(self, s):
661 s = re.sub(r'(Creation|Mod)Date: .*',
662 r'\1Date XXXX', s)
663 s = re.sub(r'%DVIPSSource:\s+TeX output\s.*',
664 r'%DVIPSSource: TeX output XXXX', s)
665 s = re.sub(r'/(BaseFont|FontName) /[A-Z0-9]{6}',
666 r'/\1 /XXXXXX', s)
667 s = re.sub(r'BeginFont: [A-Z0-9]{6}',
668 r'BeginFont: XXXXXX', s)
670 return s
672 @staticmethod
673 def to_bytes_re_sub(pattern, repl, string, count: int=0, flags: int=0):
675 Wrapper around re.sub to change pattern and repl to bytes to work with
676 both python 2 & 3
678 pattern = to_bytes(pattern)
679 repl = to_bytes(repl)
680 return re.sub(pattern, repl, string, count=count, flags=flags)
682 def normalize_pdf(self, s):
683 s = self.to_bytes_re_sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
684 r'/\1Date (D:XXXX)', s)
685 s = self.to_bytes_re_sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
686 r'/ID [<XXXX> <XXXX>]', s)
687 s = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
688 r'/\1 /XXXXXX', s)
689 s = self.to_bytes_re_sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
690 r'/Length XXXX\n/Filter /FlateDecode\n', s)
692 try:
693 import zlib
694 except ImportError:
695 pass
696 else:
697 begin_marker = to_bytes('/FlateDecode\n>>\nstream\n')
698 end_marker = to_bytes('endstream\nendobj')
700 encoded = []
701 b = s.find(begin_marker, 0)
702 while b != -1:
703 b = b + len(begin_marker)
704 e = s.find(end_marker, b)
705 encoded.append((b, e))
706 b = s.find(begin_marker, e + len(end_marker))
708 x = 0
709 r = []
710 for b, e in encoded:
711 r.append(s[x:b])
712 d = zlib.decompress(s[b:e])
713 d = self.to_bytes_re_sub(r'%%CreationDate: [^\n]*\n',
714 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
715 d = self.to_bytes_re_sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
716 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
717 d = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
718 r'/\1 /XXXXXX', d)
719 r.append(d)
720 x = e
721 r.append(s[x:])
722 s = to_bytes('').join(r)
724 return s
726 def paths(self, patterns):
727 import glob
728 result = []
729 for p in patterns:
730 result.extend(sorted(glob.glob(p)))
731 return result
733 def get_sconsignname(self) -> str:
734 """Get the scons database name used, and return both the prefix and full filename.
736 if the user left the options defaulted AND the default algorithm set by
737 SCons is md5, then set the database name to be the special default name
739 otherwise, if it defaults to something like 'sha1' or the user explicitly
740 set 'md5' as the hash format, set the database name to .sconsign_<algorithm>
741 eg .sconsign_sha1, etc.
743 Returns:
744 a pair containing: the current dbname, the dbname.dblite filename
746 TODO: docstring is not truthful about returning "both" - but which to fix?
747 Say it returns just basename, or return both?
748 TODO: has no way to account for an ``SConsignFile()`` call which might assign
749 a different dbname. Document that it's only useful for hash testing?
751 hash_format = get_hash_format()
752 current_hash_algorithm = get_current_hash_algorithm_used()
753 if hash_format is None and current_hash_algorithm == 'md5':
754 return ".sconsign"
755 else:
756 database_prefix=f".sconsign_{current_hash_algorithm}"
757 return database_prefix
760 def unlink_sconsignfile(self, name: str = '.sconsign.dblite') -> None:
761 """Delete the sconsign file.
763 Provides a hook to do special things for the sconsign DB,
764 although currently it just calls unlink.
766 Args:
767 name: expected name of sconsign file
769 TODO: deal with suffix if :meth:`getsconsignname` does not provide it.
770 How do we know, since multiple formats are allowed?
772 return self.unlink(name)
774 def java_ENV(self, version=None):
775 """ Initialize JAVA SDK environment.
777 Initialize with a default external environment that uses a local
778 Java SDK in preference to whatever's found in the default PATH.
780 Args:
781 version: if set, match only that version
783 Returns:
784 the new env.
786 if not self.external:
787 try:
788 return self._java_env[version]['ENV']
789 except AttributeError:
790 self._java_env = {}
791 except KeyError:
792 pass
794 import SCons.Environment
795 env = SCons.Environment.Environment()
796 self._java_env[version] = env
798 if version:
799 if sys.platform == 'win32':
800 patterns = [
801 f'C:/Program Files*/Java/jdk*{version}*/bin',
803 else:
804 patterns = [
805 f'/usr/java/jdk{version}*/bin',
806 f'/usr/lib/jvm/*-{version}*/bin',
807 f'/usr/local/j2sdk{version}*/bin',
809 java_path = self.paths(patterns) + [env['ENV']['PATH']]
810 else:
811 if sys.platform == 'win32':
812 patterns = [
813 'C:/Program Files*/Java/jdk*/bin',
815 else:
816 patterns = [
817 '/usr/java/latest/bin',
818 '/usr/lib/jvm/*/bin',
819 '/usr/local/j2sdk*/bin',
821 java_path = self.paths(patterns) + [env['ENV']['PATH']]
823 env['ENV']['PATH'] = os.pathsep.join(java_path)
824 return env['ENV']
826 return None
828 def java_where_includes(self, version=None):
829 """ Find include path needed for compiling java jni code.
831 Args:
832 version: if set, match only that version
834 Returns:
835 path to java headers or None
837 import sys
839 result = []
840 if sys.platform[:6] == 'darwin':
841 java_home = self.java_where_java_home(version)
842 jni_path = os.path.join(java_home, 'include', 'jni.h')
843 if os.path.exists(jni_path):
844 result.append(os.path.dirname(jni_path))
846 if not version:
847 version = ''
848 jni_dirs = ['/System/Library/Frameworks/JavaVM.framework/Headers/jni.h',
849 '/usr/lib/jvm/default-java/include/jni.h',
850 '/usr/lib/jvm/java-*-oracle/include/jni.h']
851 else:
852 jni_dirs = [f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}*/Headers/jni.h']
853 jni_dirs.extend([f'/usr/lib/jvm/java-*-sun-{version}*/include/jni.h',
854 f'/usr/lib/jvm/java-{version}*-openjdk*/include/jni.h',
855 f'/usr/java/jdk{version}*/include/jni.h'])
856 dirs = self.paths(jni_dirs)
857 if not dirs:
858 return None
859 d = os.path.dirname(self.paths(jni_dirs)[0])
860 result.append(d)
862 if sys.platform == 'win32':
863 result.append(os.path.join(d, 'win32'))
864 elif sys.platform.startswith('linux'):
865 result.append(os.path.join(d, 'linux'))
866 return result
868 def java_where_java_home(self, version=None) -> Optional[str]:
869 """ Find path to what would be JAVA_HOME.
871 SCons does not read JAVA_HOME from the environment, so deduce it.
873 Args:
874 version: if set, match only that version
876 Returns:
877 path where JDK components live
878 Bails out of the entire test (skip) if not found.
880 if sys.platform[:6] == 'darwin':
881 # osx 10.11+
882 home_tool = '/usr/libexec/java_home'
883 java_home = ''
884 if os.path.exists(home_tool):
885 cp = sp.run(home_tool, stdout=sp.PIPE, stderr=sp.STDOUT)
886 if cp.returncode == 0:
887 java_home = cp.stdout.decode().strip()
889 if version is None:
890 if java_home:
891 return java_home
892 for home in [
893 '/System/Library/Frameworks/JavaVM.framework/Home',
894 # osx 10.10
895 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home'
897 if os.path.exists(home):
898 return home
899 else:
900 if java_home.find(f'jdk{version}') != -1:
901 return java_home
902 for home in [
903 f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}/Home',
904 # osx 10.10
905 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/'
907 if os.path.exists(home):
908 return home
909 # if we fell through, make sure flagged as not found
910 home = ''
911 else:
912 jar = self.java_where_jar(version)
913 home = os.path.normpath(f'{jar}/..')
915 if home and os.path.isdir(home):
916 return home
918 self.skip_test(
919 "Could not run Java: unable to detect valid JAVA_HOME, skipping test.\n",
920 from_fw=True,
922 return None
924 def java_mac_check(self, where_java_bin, java_bin_name) -> None:
925 """Extra check for Java on MacOS.
927 MacOS has a place holder java/javac, which fails with a detectable
928 error if Java is not actually installed, and works normally if it is.
929 Note msg has changed over time.
931 Bails out of the entire test (skip) if not found.
933 cp = sp.run([where_java_bin, "-version"], stdout=sp.PIPE, stderr=sp.STDOUT)
934 if (
935 b"No Java runtime" in cp.stdout
936 or b"Unable to locate a Java Runtime" in cp.stdout
938 self.skip_test(
939 f"Could not find Java {java_bin_name}, skipping test.\n",
940 from_fw=True,
943 def java_where_jar(self, version=None) -> str:
944 """ Find java archiver jar.
946 Args:
947 version: if set, match only that version
949 Returns:
950 path to jar
952 ENV = self.java_ENV(version)
953 if self.detect_tool('jar', ENV=ENV):
954 where_jar = self.detect('JAR', 'jar', ENV=ENV)
955 else:
956 where_jar = self.where_is('jar', ENV['PATH'])
957 if not where_jar:
958 self.skip_test("Could not find Java jar, skipping test(s).\n", from_fw=True)
959 elif sys.platform == "darwin":
960 self.java_mac_check(where_jar, 'jar')
962 return where_jar
964 def java_where_java(self, version=None) -> str:
965 """ Find java executable.
967 Args:
968 version: if set, match only that version
970 Returns:
971 path to the java rutime
973 ENV = self.java_ENV(version)
974 where_java = self.where_is('java', ENV['PATH'])
976 if not where_java:
977 self.skip_test("Could not find Java java, skipping test(s).\n", from_fw=True)
978 elif sys.platform == "darwin":
979 self.java_mac_check(where_java, 'java')
981 return where_java
983 def java_where_javac(self, version=None) -> Tuple[str, str]:
984 """ Find java compiler.
986 Args:
987 version: if set, match only that version
989 Returns:
990 path to javac
992 ENV = self.java_ENV(version)
993 if self.detect_tool('javac'):
994 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
995 else:
996 where_javac = self.where_is('javac', ENV['PATH'])
997 if not where_javac:
998 self.skip_test("Could not find Java javac, skipping test(s).\n", from_fw=True)
999 elif sys.platform == "darwin":
1000 self.java_mac_check(where_javac, 'javac')
1002 self.run(program=where_javac,
1003 arguments='-version',
1004 stderr=None,
1005 status=None)
1006 # Note recent versions output version info to stdout instead of stderr
1007 stdout = self.stdout() or ""
1008 stderr = self.stderr() or ""
1009 if version:
1010 verf = f'javac {version}'
1011 if stderr.find(verf) == -1 and stdout.find(verf) == -1:
1012 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
1013 self.skip_test(fmt % version, from_fw=True)
1014 else:
1015 version_re = r'javac (\d*\.*\d)'
1016 m = re.search(version_re, stderr)
1017 if not m:
1018 m = re.search(version_re, stdout)
1020 if m:
1021 version = m.group(1)
1022 self.javac_is_gcj = False
1023 return where_javac, version
1025 if stderr.find('gcj') != -1:
1026 version = '1.2'
1027 self.javac_is_gcj = True
1028 else:
1029 version = None
1030 self.javac_is_gcj = False
1031 return where_javac, version
1033 def java_where_javah(self, version=None) -> str:
1034 """ Find java header generation tool.
1036 TODO issue #3347 since JDK10, there is no separate javah command,
1037 'javac -h' is used. We should not return a javah from a different
1038 installed JDK - how to detect and what to return in this case?
1040 Args:
1041 version: if set, match only that version
1043 Returns:
1044 path to javah
1046 ENV = self.java_ENV(version)
1047 if self.detect_tool('javah'):
1048 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
1049 else:
1050 where_javah = self.where_is('javah', ENV['PATH'])
1051 if not where_javah:
1052 self.skip_test("Could not find Java javah, skipping test(s).\n", from_fw=True)
1053 return where_javah
1055 def java_where_rmic(self, version=None) -> str:
1056 """ Find java rmic tool.
1058 Args:
1059 version: if set, match only that version
1061 Returns:
1062 path to rmic
1064 ENV = self.java_ENV(version)
1065 if self.detect_tool('rmic'):
1066 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
1067 else:
1068 where_rmic = self.where_is('rmic', ENV['PATH'])
1069 if not where_rmic:
1070 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n", from_fw=True)
1071 return where_rmic
1073 def java_get_class_files(self, dir):
1074 result = []
1075 for dirpath, dirnames, filenames in os.walk(dir):
1076 for fname in filenames:
1077 if fname.endswith('.class'):
1078 result.append(os.path.join(dirpath, fname))
1079 return sorted(result)
1081 def Qt_dummy_installation(self, dir: str='qt') -> None:
1082 # create a dummy qt installation
1084 self.subdir(dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'])
1086 self.write([dir, 'bin', 'mymoc.py'], """\
1087 import getopt
1088 import sys
1089 import re
1090 # -w and -z are fake options used in test/QT/QTFLAGS.py
1091 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
1092 impl = 0
1093 opt_string = ''
1094 for opt, arg in cmd_opts:
1095 if opt == '-o': outfile = arg
1096 elif opt == '-i': impl = 1
1097 else: opt_string = opt_string + ' ' + opt
1099 with open(outfile, 'w') as ofp:
1100 ofp.write("/* mymoc.py%s */\\n" % opt_string)
1101 for a in args:
1102 with open(a, 'r') as ifp:
1103 contents = ifp.read()
1104 a = a.replace('\\\\', '\\\\\\\\')
1105 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
1106 if impl:
1107 contents = re.sub(r'#include.*', '', contents)
1108 ofp.write(contents.replace('Q_OBJECT', subst))
1109 sys.exit(0)
1110 """)
1112 self.write([dir, 'bin', 'myuic.py'], """\
1113 import os.path
1114 import re
1115 import sys
1116 output_arg = 0
1117 impl_arg = 0
1118 impl = None
1119 source = None
1120 opt_string = ''
1121 for arg in sys.argv[1:]:
1122 if output_arg:
1123 outfile = arg
1124 output_arg = 0
1125 elif impl_arg:
1126 impl = arg
1127 impl_arg = 0
1128 elif arg == "-o":
1129 output_arg = 1
1130 elif arg == "-impl":
1131 impl_arg = 1
1132 elif arg[0:1] == "-":
1133 opt_string = opt_string + ' ' + arg
1134 else:
1135 if source:
1136 sys.exit(1)
1137 source = sourceFile = arg
1139 with open(outfile, 'w') as ofp, open(source, 'r') as ifp:
1140 ofp.write("/* myuic.py%s */\\n" % opt_string)
1141 if impl:
1142 ofp.write('#include "' + impl + '"\\n')
1143 includes = re.findall('<include.*?>(.*?)</include>', ifp.read())
1144 for incFile in includes:
1145 # this is valid for ui.h files, at least
1146 if os.path.exists(incFile):
1147 ofp.write('#include "' + incFile + '"\\n')
1148 else:
1149 ofp.write('#include "my_qobject.h"\\n' + ifp.read() + " Q_OBJECT \\n")
1150 sys.exit(0)
1151 """)
1153 self.write([dir, 'include', 'my_qobject.h'], r"""
1154 #define Q_OBJECT ;
1155 void my_qt_symbol(const char *arg);
1156 """)
1158 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
1159 #include "../include/my_qobject.h"
1160 #include <stdio.h>
1161 void my_qt_symbol(const char *arg) {
1162 fputs(arg, stdout);
1164 """)
1166 self.write([dir, 'lib', 'SConstruct'], r"""
1167 import sys
1168 DefaultEnvironment(tools=[]) # test speedup
1169 env = Environment()
1170 if sys.platform == 'win32':
1171 env.StaticLibrary('myqt', 'my_qobject.cpp')
1172 else:
1173 env.SharedLibrary('myqt', 'my_qobject.cpp')
1174 """)
1176 self.run(chdir=self.workpath(dir, 'lib'),
1177 arguments='.',
1178 stderr=noisy_ar,
1179 match=self.match_re_dotall)
1181 self.QT = self.workpath(dir)
1182 self.QT_LIB = 'myqt'
1183 self.QT_MOC = f"{_python_} {self.workpath(dir, 'bin', 'mymoc.py')}"
1184 self.QT_UIC = f"{_python_} {self.workpath(dir, 'bin', 'myuic.py')}"
1185 self.QT_LIB_DIR = self.workpath(dir, 'lib')
1187 def Qt_create_SConstruct(self, place, qt_tool: str='qt3') -> None:
1188 if isinstance(place, list):
1189 place = self.workpath(*place)
1191 var_prefix=qt_tool.upper()
1192 self.write(place, f"""\
1193 if ARGUMENTS.get('noqtdir', 0):
1194 {var_prefix}DIR = None
1195 else:
1196 {var_prefix}DIR = r'{self.QT}'
1197 DefaultEnvironment(tools=[]) # test speedup
1198 env = Environment(
1199 {var_prefix}DIR={var_prefix}DIR, {var_prefix}_LIB=r'{self.QT_LIB}', {var_prefix}_MOC=r'{self.QT_MOC}',
1200 {var_prefix}_UIC=r'{self.QT_UIC}', tools=['default', '{qt_tool}']
1202 dup = 1
1203 if ARGUMENTS.get('variant_dir', 0):
1204 if ARGUMENTS.get('chdir', 0):
1205 SConscriptChdir(1)
1206 else:
1207 SConscriptChdir(0)
1208 dup = int(ARGUMENTS.get('dup', 1))
1209 if dup == 0:
1210 builddir = 'build_dup0'
1211 env['QT_DEBUG'] = 1
1212 else:
1213 builddir = 'build'
1214 VariantDir(builddir, '.', duplicate=dup)
1215 print(builddir, dup)
1216 sconscript = Dir(builddir).File('SConscript')
1217 else:
1218 sconscript = File('SConscript')
1219 Export("env dup")
1220 SConscript(sconscript)
1221 """)
1223 NCR = 0 # non-cached rebuild
1224 CR = 1 # cached rebuild (up to date)
1225 NCF = 2 # non-cached build failure
1226 CF = 3 # cached build failure
1228 if sys.platform == 'win32':
1229 Configure_lib = 'msvcrt'
1230 else:
1231 Configure_lib = 'm'
1233 # to use cygwin compilers on cmd.exe -> uncomment following line
1234 # Configure_lib = 'm'
1236 def coverage_run(self) -> bool:
1237 """ Check if the the tests are being run under coverage.
1239 return 'COVERAGE_PROCESS_START' in os.environ or 'COVERAGE_FILE' in os.environ
1241 def skip_if_not_msvc(self, check_platform: bool=True) -> None:
1242 """ Skip test if MSVC is not available.
1244 Check whether we are on a Windows platform and skip the test if
1245 not. This check can be omitted by setting check_platform to False.
1247 Then, for a win32 platform, additionally check whether we have
1248 an MSVC toolchain installed in the system, and skip the test if
1249 none can be found (e.g. MinGW is the only compiler available).
1251 if check_platform:
1252 if sys.platform != 'win32':
1253 msg = f"Skipping Visual C/C++ test on non-Windows platform '{sys.platform}'\n"
1254 self.skip_test(msg, from_fw=True)
1255 return
1257 try:
1258 import SCons.Tool.MSCommon as msc
1259 if not msc.msvc_exists():
1260 msg = "No MSVC toolchain found...skipping test\n"
1261 self.skip_test(msg, from_fw=True)
1262 except Exception:
1263 pass
1265 def checkConfigureLogAndStdout(self, checks,
1266 logfile: str='config.log',
1267 sconf_dir: str='.sconf_temp',
1268 sconstruct: str="SConstruct",
1269 doCheckLog: bool=True, doCheckStdout: bool=True):
1270 """ Verify expected output from Configure.
1272 Used to verify the expected output from using Configure()
1273 via the contents of one or both of stdout or config.log file.
1274 If the algorithm does not succeed, the test is marked a fail
1275 and this function does not return.
1277 TODO: Perhaps a better API makes sense?
1279 Args:
1280 checks: list of ConfigCheckInfo tuples which specify
1281 logfile: Name of the config log
1282 sconf_dir: Name of the sconf dir
1283 sconstruct: SConstruct file name
1284 doCheckLog: check specified log file, defaults to true
1285 doCheckStdout: Check stdout, defaults to true
1288 try:
1289 ls = '\n'
1290 nols = '([^\n])'
1291 lastEnd = 0
1293 # Read the whole logfile
1294 logfile = self.read(self.workpath(logfile), mode='r')
1296 # Some debug code to keep around..
1297 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1299 if (doCheckLog and
1300 logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1301 self.fail_test()
1303 log = r'file \S*%s\,line \d+:' % re.escape(sconstruct) + ls
1304 if doCheckLog:
1305 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1307 log = f"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1308 if doCheckLog:
1309 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1311 rdstr = ""
1313 for check_info in checks:
1314 log = re.escape(f"scons: Configure: {check_info.check_string}") + ls
1316 if doCheckLog:
1317 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1319 log = ""
1320 result_cached = 1
1321 for bld_desc in check_info.cached: # each TryXXX
1322 for ext, flag in bld_desc: # each file in TryBuild
1323 conf_filename = re.escape(check_info.temp_filename%ext)
1325 if flag == self.NCR:
1326 # NCR = Non Cached Rebuild
1327 # rebuild will pass
1328 if ext in ['.c', '.cpp']:
1329 log = log + conf_filename + re.escape(" <-") + ls
1330 log = f"{log}( \\|{nols}*{ls})+?"
1331 else:
1332 log = f"{log}({nols}*{ls})*?"
1333 result_cached = 0
1334 if flag == self.CR:
1335 # CR = cached rebuild (up to date)s
1336 # up to date
1337 log = log + \
1338 re.escape("scons: Configure: \"") + \
1339 conf_filename + \
1340 re.escape("\" is up to date.") + ls
1341 log = log + re.escape("scons: Configure: The original builder "
1342 "output was:") + ls
1343 log = f"{log}( \\|.*{ls})+"
1344 if flag == self.NCF:
1345 # non-cached rebuild failure
1346 log = f"{log}({nols}*{ls})*?"
1347 result_cached = 0
1348 if flag == self.CF:
1349 # cached rebuild failure
1350 log = log + \
1351 re.escape("scons: Configure: Building \"") + \
1352 conf_filename + \
1353 re.escape("\" failed in a previous run and all its sources are up to date.") + ls
1354 log = log + re.escape("scons: Configure: The original builder output was:") + ls
1355 log = f"{log}( \\|.*{ls})+"
1356 if result_cached:
1357 result = f"(cached) {check_info.result}"
1358 else:
1359 result = check_info.result
1360 rdstr = f"{rdstr + re.escape(check_info.check_string) + re.escape(result)}\n"
1362 log = log + re.escape(f"scons: Configure: {result}") + ls + ls
1364 if doCheckLog:
1365 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1367 log = ""
1368 if doCheckLog:
1369 lastEnd = match_part_of_configlog(ls, logfile, lastEnd)
1371 if doCheckLog and lastEnd != len(logfile):
1372 raise NoMatch(lastEnd)
1374 except NoMatch as m:
1375 print("Cannot match log file against log regexp.")
1376 print("log file: ")
1377 print("------------------------------------------------------")
1378 print(logfile[m.pos:])
1379 print("------------------------------------------------------")
1380 print("log regexp: ")
1381 print("------------------------------------------------------")
1382 print(log)
1383 print("------------------------------------------------------")
1384 self.fail_test()
1386 if doCheckStdout:
1387 exp_stdout = self.wrap_stdout(".*", rdstr)
1388 stdout = self.stdout() or ""
1389 if not self.match_re_dotall(stdout, exp_stdout):
1390 print("Unexpected stdout: ")
1391 print("-----------------------------------------------------")
1392 print(repr(stdout))
1393 print("-----------------------------------------------------")
1394 print(repr(exp_stdout))
1395 print("-----------------------------------------------------")
1396 self.fail_test()
1400 def checkLogAndStdout(self, checks, results, cached,
1401 logfile, sconf_dir, sconstruct,
1402 doCheckLog: bool=True, doCheckStdout: bool=True):
1403 """ Verify expected output from Configure.
1405 Used to verify the expected output from using Configure()
1406 via the contents of one or both of stdout or config.log file.
1407 The checks, results, cached parameters all are zipped together
1408 for use in comparing results. If the algorithm does not
1409 succeed, the test is marked a fail and this function does not return.
1411 TODO: Perhaps a better API makes sense?
1413 Args:
1414 checks: The Configure checks being run
1415 results: The expected results for each check
1416 cached: If the corresponding check is expected to be cached
1417 logfile: Name of the config log
1418 sconf_dir: Name of the sconf dir
1419 sconstruct: SConstruct file name
1420 doCheckLog: check specified log file, defaults to true
1421 doCheckStdout: Check stdout, defaults to true
1423 try:
1425 ls = '\n'
1426 nols = '([^\n])'
1427 lastEnd = 0
1429 # Read the whole logfile
1430 logfile = self.read(self.workpath(logfile), mode='r')
1432 # Some debug code to keep around..
1433 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1435 if (doCheckLog and
1436 logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1437 self.fail_test()
1439 sconf_dir = sconf_dir
1440 sconstruct = sconstruct
1442 log = r'file \S*%s\,line \d+:' % re.escape(sconstruct) + ls
1443 if doCheckLog:
1444 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1446 log = f"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1447 if doCheckLog:
1448 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1450 rdstr = ""
1452 cnt = 0
1453 for check, result, cache_desc in zip(checks, results, cached):
1454 log = re.escape(f"scons: Configure: {check}") + ls
1456 if doCheckLog:
1457 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1459 log = ""
1460 result_cached = 1
1461 for bld_desc in cache_desc: # each TryXXX
1462 for ext, flag in bld_desc: # each file in TryBuild
1463 if ext in ['.c', '.cpp']:
1464 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1465 r'_[a-z0-9]{32,64}_\d+%s' % re.escape(ext)
1466 elif ext == '':
1467 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1468 r'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?'
1470 else:
1471 # We allow the second hash group to be optional because
1472 # TryLink() will create a c file, then compile to obj, then link that
1473 # The intermediate object file will not get the action hash
1474 # But TryCompile()'s where the product is the .o will get the
1475 # action hash. Rather than add a ton of complications to this logic
1476 # this shortcut should be sufficient.
1477 # TODO: perhaps revisit and/or fix file naming for intermediate files in
1478 # Configure context logic
1479 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1480 r'_[a-z0-9]{32,64}_\d+(_[a-z0-9]{32,64})?%s' % re.escape(ext)
1482 if flag == self.NCR:
1483 # NCR = Non Cached Rebuild
1484 # rebuild will pass
1485 if ext in ['.c', '.cpp']:
1486 log = log + conf_filename + re.escape(" <-") + ls
1487 log = f"{log}( \\|{nols}*{ls})+?"
1488 else:
1489 log = f"{log}({nols}*{ls})*?"
1490 result_cached = 0
1491 if flag == self.CR:
1492 # CR = cached rebuild (up to date)s
1493 # up to date
1494 log = log + \
1495 re.escape("scons: Configure: \"") + \
1496 conf_filename + \
1497 re.escape("\" is up to date.") + ls
1498 log = log + re.escape("scons: Configure: The original builder "
1499 "output was:") + ls
1500 log = f"{log}( \\|.*{ls})+"
1501 if flag == self.NCF:
1502 # non-cached rebuild failure
1503 log = f"{log}({nols}*{ls})*?"
1504 result_cached = 0
1505 if flag == self.CF:
1506 # cached rebuild failure
1507 log = log + \
1508 re.escape("scons: Configure: Building \"") + \
1509 conf_filename + \
1510 re.escape("\" failed in a previous run and all its sources are up to date.") + ls
1511 log = log + re.escape("scons: Configure: The original builder output was:") + ls
1512 log = f"{log}( \\|.*{ls})+"
1513 # cnt = cnt + 1
1514 if result_cached:
1515 result = f"(cached) {result}"
1517 rdstr = f"{rdstr + re.escape(check) + re.escape(result)}\n"
1519 log = log + re.escape(f"scons: Configure: {result}") + ls + ls
1521 if doCheckLog:
1522 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1524 log = ""
1525 if doCheckLog:
1526 lastEnd = match_part_of_configlog(ls, logfile, lastEnd)
1528 if doCheckLog and lastEnd != len(logfile):
1529 raise NoMatch(lastEnd)
1531 except NoMatch as m:
1532 print("Cannot match log file against log regexp.")
1533 print("log file: ")
1534 print("------------------------------------------------------")
1535 print(logfile[m.pos:])
1536 print("------------------------------------------------------")
1537 print("log regexp: ")
1538 print("------------------------------------------------------")
1539 print(log)
1540 print("------------------------------------------------------")
1541 self.fail_test()
1543 if doCheckStdout:
1544 exp_stdout = self.wrap_stdout(".*", rdstr)
1545 stdout = self.stdout() or ""
1546 if not self.match_re_dotall(stdout, exp_stdout):
1547 print("Unexpected stdout: ")
1548 print("----Actual-------------------------------------------")
1549 print(repr(stdout))
1550 print("----Expected-----------------------------------------")
1551 print(repr(exp_stdout))
1552 print("-----------------------------------------------------")
1553 self.fail_test()
1555 def get_python_version(self) -> str:
1556 """ Returns the Python version.
1558 Convenience function so everyone doesn't have to
1559 hand-code slicing the right number of characters
1561 # see also sys.prefix documentation
1562 return python_minor_version_string()
1564 def get_platform_python_info(self, python_h_required: bool=False):
1565 """Return information about Python.
1567 Returns a path to a Python executable suitable for testing on
1568 this platform and its associated include path, library path and
1569 library name.
1571 If the Python executable or Python header (if required)
1572 is not found, the test is skipped.
1574 Returns:
1575 tuple: path to python, include path, library path, library name
1577 python = os.environ.get('python_executable', self.where_is('python'))
1578 if not python:
1579 self.skip_test('Can not find installed "python", skipping test.\n', from_fw=True)
1581 # construct a program to run in the intended environment
1582 # in order to fetch the characteristics of that Python.
1583 # Windows Python doesn't store all the info in config vars.
1584 if sys.platform == 'win32':
1585 self.run(program=python, stdin="""\
1586 import sysconfig, sys, os.path
1587 py_ver = 'python%d%d' % sys.version_info[:2]
1588 try:
1589 exec_prefix = sysconfig.get_config_var("exec_prefix")
1590 include = sysconfig.get_config_var("INCLUDEPY")
1591 print(include)
1592 lib_path = os.path.join(exec_prefix, 'libs')
1593 if not os.path.exists(lib_path):
1594 # check for virtualenv path.
1595 # this might not build anything different than first try.
1596 def venv_path():
1597 if hasattr(sys, 'real_prefix'):
1598 return sys.real_prefix
1599 if hasattr(sys, 'base_prefix'):
1600 return sys.base_prefix
1601 lib_path = os.path.join(venv_path(), 'libs')
1602 if not os.path.exists(lib_path):
1603 # not clear this is useful: 'lib' does not contain linkable libs
1604 lib_path = os.path.join(exec_prefix, 'lib')
1605 print(lib_path)
1606 except:
1607 include = os.path.join(sys.prefix, 'include', py_ver)
1608 print(include)
1609 lib_path = os.path.join(sys.prefix, 'lib', py_ver, 'config')
1610 print(lib_path)
1611 print(py_ver)
1612 Python_h = os.path.join(include, "Python.h")
1613 if os.path.exists(Python_h):
1614 print(Python_h)
1615 else:
1616 print("False")
1617 """)
1618 else:
1619 self.run(program=python, stdin="""\
1620 import sys, sysconfig, os.path
1621 include = sysconfig.get_config_var("INCLUDEPY")
1622 print(include)
1623 print(sysconfig.get_config_var("LIBDIR"))
1624 py_library_ver = sysconfig.get_config_var("LDVERSION")
1625 if not py_library_ver:
1626 py_library_ver = '%d.%d' % sys.version_info[:2]
1627 print("python"+py_library_ver)
1628 Python_h = os.path.join(include, "Python.h")
1629 if os.path.exists(Python_h):
1630 print(Python_h)
1631 else:
1632 print("False")
1633 """)
1634 stdout = self.stdout() or ""
1635 incpath, libpath, libname, python_h = stdout.strip().split('\n')
1636 if python_h == "False" and python_h_required:
1637 self.skip_test('Can not find required "Python.h", skipping test.\n', from_fw=True)
1639 return (python, incpath, libpath, libname + _lib)
1641 def start(self, *args, **kw):
1643 Starts SCons in the test environment.
1645 This method exists to tell Test{Cmd,Common} that we're going to
1646 use standard input without forcing every .start() call in the
1647 individual tests to do so explicitly.
1649 if 'stdin' not in kw:
1650 kw['stdin'] = True
1651 sconsflags = initialize_sconsflags(self.ignore_python_version)
1652 try:
1653 p = super().start(*args, **kw)
1654 finally:
1655 restore_sconsflags(sconsflags)
1656 return p
1658 def wait_for(self, fname, timeout: float=20.0, popen=None) -> None:
1660 Waits for the specified file name to exist.
1662 waited = 0.0
1663 while not os.path.exists(fname):
1664 if timeout and waited >= timeout:
1665 sys.stderr.write(f'timed out waiting for {fname} to exist\n')
1666 if popen:
1667 popen.stdin.close()
1668 popen.stdin = None
1669 self.status = 1
1670 self.finish(popen)
1671 stdout = self.stdout()
1672 if stdout:
1673 sys.stdout.write(f"{self.banner('STDOUT ')}\n")
1674 sys.stdout.write(stdout)
1675 stderr = self.stderr()
1676 if stderr:
1677 sys.stderr.write(f"{self.banner('STDERR ')}\n")
1678 sys.stderr.write(stderr)
1679 self.fail_test()
1680 time.sleep(1.0)
1681 waited = waited + 1.0
1683 def get_alt_cpp_suffix(self):
1684 """Return alternate C++ file suffix.
1686 Many CXX tests have this same logic.
1687 They all needed to determine if the current os supports
1688 files with .C and .c as different files or not
1689 in which case they are instructed to use .cpp instead of .C
1691 if not case_sensitive_suffixes('.c', '.C'):
1692 alt_cpp_suffix = '.cpp'
1693 else:
1694 alt_cpp_suffix = '.C'
1695 return alt_cpp_suffix
1697 def platform_has_symlink(self) -> bool:
1698 """Retun an indication of whether symlink tests should be run.
1700 Despite the name, we really mean "are they reliably usable"
1701 rather than "do they exist" - basically the Windows case.
1703 if not hasattr(os, 'symlink') or sys.platform == 'win32':
1704 return False
1705 else:
1706 return True
1708 def zipfile_contains(self, zipfilename, names):
1709 """Returns True if zipfilename contains all the names, False otherwise."""
1710 with zipfile.ZipFile(zipfilename, 'r') as zf:
1711 return all(elem in zf.namelist() for elem in names)
1713 def zipfile_files(self, fname):
1714 """Returns all the filenames in zip file fname."""
1715 with zipfile.ZipFile(fname, 'r') as zf:
1716 return zf.namelist()
1719 class Stat:
1720 def __init__(self, name, units, expression, convert=None) -> None:
1721 if convert is None:
1722 convert = lambda x: x
1723 self.name = name
1724 self.units = units
1725 self.expression = re.compile(expression)
1726 self.convert = convert
1729 StatList = [
1730 Stat('memory-initial', 'kbytes',
1731 r'Memory before reading SConscript files:\s+(\d+)',
1732 convert=lambda s: int(s) // 1024),
1733 Stat('memory-prebuild', 'kbytes',
1734 r'Memory before building targets:\s+(\d+)',
1735 convert=lambda s: int(s) // 1024),
1736 Stat('memory-final', 'kbytes',
1737 r'Memory after building targets:\s+(\d+)',
1738 convert=lambda s: int(s) // 1024),
1740 Stat('time-sconscript', 'seconds',
1741 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1742 Stat('time-scons', 'seconds',
1743 r'Total SCons execution time:\s+([\d.]+) seconds'),
1744 Stat('time-commands', 'seconds',
1745 r'Total command execution time:\s+([\d.]+) seconds'),
1746 Stat('time-total', 'seconds',
1747 r'Total build time:\s+([\d.]+) seconds'),
1751 class TimeSCons(TestSCons):
1752 """Class for timing SCons."""
1754 def __init__(self, *args, **kw) -> None:
1756 In addition to normal TestSCons.TestSCons intialization,
1757 this enables verbose mode (which causes the command lines to
1758 be displayed in the output) and copies the contents of the
1759 directory containing the executing script to the temporary
1760 working directory.
1762 self.variables: dict = kw.get('variables')
1763 default_calibrate_variables = []
1764 if self.variables is not None:
1765 for variable, value in self.variables.items():
1766 value = os.environ.get(variable, value)
1767 try:
1768 value = int(value)
1769 except ValueError:
1770 try:
1771 value = float(value)
1772 except ValueError:
1773 pass
1774 else:
1775 default_calibrate_variables.append(variable)
1776 else:
1777 default_calibrate_variables.append(variable)
1778 self.variables[variable] = value
1779 del kw['variables']
1780 calibrate_keyword_arg = kw.get('calibrate')
1781 if calibrate_keyword_arg is None:
1782 self.calibrate_variables = default_calibrate_variables
1783 else:
1784 self.calibrate_variables = calibrate_keyword_arg
1785 del kw['calibrate']
1787 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1789 if 'verbose' not in kw and not self.calibrate:
1790 kw['verbose'] = True
1792 super().__init__(*args, **kw)
1794 # TODO(sgk): better way to get the script dir than sys.argv[0]
1795 self.test_dir = os.path.dirname(sys.argv[0])
1796 test_name = os.path.basename(self.test_dir)
1798 if not os.path.isabs(self.test_dir):
1799 self.test_dir = os.path.join(self.orig_cwd, self.test_dir)
1800 self.copy_timing_configuration(self.test_dir, self.workpath())
1802 def main(self, *args, **kw) -> None:
1804 The main entry point for standard execution of timings.
1806 This method run SCons three times:
1808 Once with the --help option, to have it exit after just reading
1809 the configuration.
1811 Once as a full build of all targets.
1813 Once again as a (presumably) null or up-to-date build of
1814 all targets.
1816 The elapsed time to execute each build is printed after
1817 it has finished.
1819 if 'options' not in kw and self.variables:
1820 options = []
1821 for variable, value in self.variables.items():
1822 options.append(f'{variable}={value}')
1823 kw['options'] = ' '.join(options)
1824 if self.calibrate:
1825 self.calibration(*args, **kw)
1826 else:
1827 self.uptime()
1828 self.startup(*args, **kw)
1829 self.full(*args, **kw)
1830 self.null(*args, **kw)
1832 def trace(self, graph, name, value, units, sort=None) -> None:
1833 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1834 line = fmt % (graph, name, value, units)
1835 if sort is not None:
1836 line = f"{line} sort={sort}"
1837 line = f"{line}\n"
1838 sys.stdout.write(line)
1839 sys.stdout.flush()
1841 def report_traces(self, trace, stats) -> None:
1842 self.trace('TimeSCons-elapsed',
1843 trace,
1844 self.elapsed_time(),
1845 "seconds",
1846 sort=0)
1847 for name, args in stats.items():
1848 self.trace(name, trace, **args)
1850 def uptime(self) -> None:
1851 try:
1852 fp = open('/proc/loadavg')
1853 except OSError:
1854 pass
1855 else:
1856 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1857 fp.close()
1858 self.trace('load-average', 'average1', avg1, 'processes')
1859 self.trace('load-average', 'average5', avg5, 'processes')
1860 self.trace('load-average', 'average15', avg15, 'processes')
1862 def collect_stats(self, input):
1863 result = {}
1864 for stat in StatList:
1865 m = stat.expression.search(input)
1866 if m:
1867 value = stat.convert(m.group(1))
1868 # The dict keys match the keyword= arguments
1869 # of the trace() method above so they can be
1870 # applied directly to that call.
1871 result[stat.name] = {'value': value, 'units': stat.units}
1872 return result
1874 def add_timing_options(self, kw, additional=None) -> None:
1876 Add the necessary timings options to the kw['options'] value.
1878 options = kw.get('options', '')
1879 if additional is not None:
1880 options += additional
1881 kw['options'] = f"{options} --debug=memory,time"
1883 def startup(self, *args, **kw) -> None:
1885 Runs scons with the --help option.
1887 This serves as a way to isolate just the amount of startup time
1888 spent reading up the configuration, since --help exits before any
1889 "real work" is done.
1891 self.add_timing_options(kw, ' --help')
1892 # Ignore the exit status. If the --help run dies, we just
1893 # won't report any statistics for it, but we can still execute
1894 # the full and null builds.
1895 kw['status'] = None
1896 self.run(*args, **kw)
1897 stdout = self.stdout() or ""
1898 sys.stdout.write(stdout)
1899 stats = self.collect_stats(stdout)
1900 # Delete the time-commands, since no commands are ever
1901 # executed on the help run and it is (or should be) always 0.0.
1902 del stats['time-commands']
1903 self.report_traces('startup', stats)
1905 def full(self, *args, **kw) -> None:
1907 Runs a full build of SCons.
1909 self.add_timing_options(kw)
1910 self.run(*args, **kw)
1911 stdout = self.stdout() or ""
1912 sys.stdout.write(stdout)
1913 stats = self.collect_stats(stdout)
1914 self.report_traces('full', stats)
1915 self.trace('full-memory', 'initial', **stats['memory-initial'])
1916 self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1917 self.trace('full-memory', 'final', **stats['memory-final'])
1919 def calibration(self, *args, **kw) -> None:
1921 Runs a full build of SCons, but only reports calibration
1922 information (the variable(s) that were set for this configuration,
1923 and the elapsed time to run.
1925 self.add_timing_options(kw)
1926 self.run(*args, **kw)
1927 for variable in self.calibrate_variables:
1928 value = self.variables[variable]
1929 sys.stdout.write(f'VARIABLE: {variable}={value}\n')
1930 sys.stdout.write(f'ELAPSED: {self.elapsed_time()}\n')
1932 def null(self, *args, **kw) -> None:
1934 Runs an up-to-date null build of SCons.
1936 # TODO(sgk): allow the caller to specify the target (argument)
1937 # that must be up-to-date.
1938 self.add_timing_options(kw)
1940 # Build up regex for
1941 # SConscript:/private/var/folders/ng/48pttrpj239fw5rmm3x65pxr0000gn/T/testcmd.12081.pk1bv5i5/SConstruct took 533.646 ms
1942 read_str = 'SConscript:.*\n'
1943 self.up_to_date(arguments='.', read_str=read_str, **kw)
1944 stdout = self.stdout() or ""
1945 sys.stdout.write(stdout)
1946 stats = self.collect_stats(stdout)
1947 # time-commands should always be 0.0 on a null build, because
1948 # no commands should be executed. Remove it from the stats
1949 # so we don't trace it, but only if it *is* 0 so that we'll
1950 # get some indication if a supposedly-null build actually does
1951 # build something.
1952 if float(stats['time-commands']['value']) == 0.0:
1953 del stats['time-commands']
1954 self.report_traces('null', stats)
1955 self.trace('null-memory', 'initial', **stats['memory-initial'])
1956 self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1957 self.trace('null-memory', 'final', **stats['memory-final'])
1959 def elapsed_time(self):
1961 Returns the elapsed time of the most recent command execution.
1963 return self.endTime - self.startTime
1965 def run(self, *args, **kw):
1967 Runs a single build command, capturing output in the specified file.
1969 Because this class is about timing SCons, we record the start
1970 and end times of the elapsed execution, and also add the
1971 --debug=memory and --debug=time options to have SCons report
1972 its own memory and timing statistics.
1974 self.startTime = time.perf_counter()
1975 try:
1976 result = TestSCons.run(self, *args, **kw)
1977 finally:
1978 self.endTime = time.perf_counter()
1979 return result
1981 def copy_timing_configuration(self, source_dir, dest_dir) -> None:
1983 Copies the timing configuration from the specified source_dir (the
1984 directory in which the controlling script lives) to the specified
1985 dest_dir (a temporary working directory).
1987 This ignores all files and directories that begin with the string
1988 'TimeSCons-', and all '.svn' subdirectories.
1990 for root, dirs, files in os.walk(source_dir):
1991 if '.svn' in dirs:
1992 dirs.remove('.svn')
1993 dirs = [d for d in dirs if not d.startswith('TimeSCons-')]
1994 files = [f for f in files if not f.startswith('TimeSCons-')]
1995 for dirname in dirs:
1996 source = os.path.join(root, dirname)
1997 destination = source.replace(source_dir, dest_dir)
1998 os.mkdir(destination)
1999 if sys.platform != 'win32':
2000 shutil.copystat(source, destination)
2001 for filename in files:
2002 source = os.path.join(root, filename)
2003 destination = source.replace(source_dir, dest_dir)
2004 shutil.copy2(source, destination)
2006 def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
2007 """Asserts that all of the targets listed in arguments is
2008 up to date, but does not make any assumptions on other targets.
2009 This function is most useful in conjunction with the -n option.
2010 Note: This custom version for timings tests does NOT escape
2011 read_str.
2013 s = ""
2014 for arg in arguments.split():
2015 s = f"{s}scons: `{arg}' is up to date.\n"
2016 kw['arguments'] = arguments
2017 stdout = self.wrap_stdout(read_str="REPLACEME", build_str=s)
2018 # Append '.*' so that timing output that comes after the
2019 # up-to-date output is okay.
2020 stdout = f"{re.escape(stdout)}.*"
2021 stdout = stdout.replace('REPLACEME', read_str)
2022 kw['stdout'] = stdout
2023 kw['match'] = self.match_re_dotall
2024 self.run(**kw)
2028 # In some environments, $AR will generate a warning message to stderr
2029 # if the library doesn't previously exist and is being created. One
2030 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
2031 # but this is difficult to do in a platform-/implementation-specific
2032 # method. Instead, we will use the following as a stderr match for
2033 # tests that use AR so that we will view zero or more "ar: creating
2034 # <file>" messages to be successful executions of the test (see
2035 # test/AR.py for sample usage).
2037 noisy_ar = r'(ar: creating( archive)? \S+\n?)*'
2039 # Local Variables:
2040 # tab-width:4
2041 # indent-tabs-mode:nil
2042 # End:
2043 # vim: set expandtab tabstop=4 shiftwidth=4: