Some minor test tweaking
[scons.git] / testing / framework / TestSCons.py
blobe0b45377b1c38a1727aad4a0cf566643c28f9a28
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
46 from TestCommon import *
47 from TestCommon import __all__, _python_
48 from SCons.Util import get_hash_format, get_current_hash_algorithm_used
50 from TestCmd import Popen
51 from TestCmd import PIPE
53 # Some tests which verify that SCons has been packaged properly need to
54 # look for specific version file names. Replicating the version number
55 # here provides some independent verification that what we packaged
56 # conforms to what we expect.
58 default_version = '4.7.1ayyyymmdd'
60 # TODO: these need to be hand-edited when there are changes
61 python_version_unsupported = (3, 6, 0)
62 python_version_deprecated = (3, 7, 0) # lowest non-deprecated Python
63 python_version_supported_str = "3.7.0" # str of lowest non-deprecated Python
65 SConsVersion = default_version
67 __all__.extend([
68 'TestSCons',
69 'machine',
70 'python',
71 '_exe',
72 '_obj',
73 '_shobj',
74 'shobj_',
75 'lib_',
76 '_lib',
77 'dll_',
78 '_dll'
81 machine_map = {
82 'i686': 'i386',
83 'i586': 'i386',
84 'i486': 'i386',
87 try:
88 uname = os.uname
89 except AttributeError:
90 # Windows doesn't have a uname() function. We could use something like
91 # sys.platform as a fallback, but that's not really a "machine," so
92 # just leave it as None.
93 machine = None
94 else:
95 machine = uname()[4]
96 machine = machine_map.get(machine, machine)
98 _exe = exe_suffix
99 _obj = obj_suffix
100 _shobj = shobj_suffix
101 shobj_ = shobj_prefix
102 _lib = lib_suffix
103 lib_ = lib_prefix
104 _dll = dll_suffix
105 dll_ = dll_prefix
107 if sys.platform == 'cygwin':
108 # On Cygwin, os.path.normcase() lies, so just report back the
109 # fact that the underlying Win32 OS is case-insensitive.
110 def case_sensitive_suffixes(s1, s2) -> int:
111 return 0
112 else:
113 def case_sensitive_suffixes(s1, s2):
114 return (os.path.normcase(s1) != os.path.normcase(s2))
116 file_expr = r"""File "[^"]*", line \d+, in [^\n]+
120 # re.escape escapes too much.
121 def re_escape(str):
122 for c in '\\.[]()*+?': # Not an exhaustive list.
123 str = str.replace(c, f"\\{c}")
124 return str
128 # Helper functions that we use as a replacement to the default re.match
129 # when searching for special strings in stdout/stderr.
131 def search_re(out, l):
132 """ Search the regular expression 'l' in the output 'out'
133 and return the start index when successful.
135 m = re.search(l, out)
136 if m:
137 return m.start()
139 return None
142 def search_re_in_list(out, l):
143 """ Search the regular expression 'l' in each line of
144 the given string list 'out' and return the line's index
145 when successful.
147 for idx, o in enumerate(out):
148 m = re.search(l, o)
149 if m:
150 return idx
152 return None
156 # Helpers for handling Python version numbers
158 def python_version_string():
159 return sys.version.split()[0]
162 def python_minor_version_string():
163 return sys.version[:3]
166 def unsupported_python_version(version=sys.version_info):
167 return version < python_version_unsupported
170 def deprecated_python_version(version=sys.version_info):
171 return version < python_version_deprecated
174 if deprecated_python_version():
175 msg = r"""
176 scons: warning: Support for Python older than %s is deprecated (%s detected).
177 If this will cause hardship, contact scons-dev@scons.org
179 deprecated_python_expr = (
180 re_escape(msg % (python_version_supported_str, python_version_string()))
181 + file_expr
183 del msg
184 else:
185 deprecated_python_expr = ""
188 def initialize_sconsflags(ignore_python_version):
190 Add the --warn=no-python-version option to SCONSFLAGS for every
191 command so test scripts don't have to filter out Python version
192 deprecation warnings.
193 Same for --warn=no-visual-c-missing.
195 save_sconsflags = os.environ.get('SCONSFLAGS')
196 if save_sconsflags:
197 sconsflags = [save_sconsflags]
198 else:
199 sconsflags = []
200 if ignore_python_version and deprecated_python_version():
201 sconsflags.append('--warn=no-python-version')
202 # Provide a way to suppress or provide alternate flags for
203 # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
204 # (The intended use case is to set it to null when running
205 # timing tests of earlier versions of SCons which don't
206 # support the --warn=no-visual-c-missing warning.)
207 visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
208 '--warn=no-visual-c-missing')
209 if visual_c and visual_c not in sconsflags:
210 sconsflags.append(visual_c)
211 os.environ['SCONSFLAGS'] = ' '.join(sconsflags)
212 return save_sconsflags
215 def restore_sconsflags(sconsflags) -> None:
216 if sconsflags is None:
217 del os.environ['SCONSFLAGS']
218 else:
219 os.environ['SCONSFLAGS'] = sconsflags
222 # Helpers for Configure()'s config.log processing
223 ConfigCheckInfo = namedtuple('ConfigCheckInfo',
224 ['check_string', 'result', 'cached', 'temp_filename'])
225 # check_string: the string output to for this checker
226 # results : The expected results for each check
227 # cached : If the corresponding check is expected to be cached
228 # temp_filename : The name of the generated tempfile for this check
231 class NoMatch(Exception):
233 Exception for matchPart to indicate there was no match found in the passed logfile
235 def __init__(self, p) -> None:
236 self.pos = p
239 def match_part_of_configlog(log, logfile, lastEnd, NoMatch=NoMatch):
241 Match part of the logfile
243 # print("Match:\n%s\n==============\n%s" % (log , logfile[lastEnd:]))
244 m = re.match(log, logfile[lastEnd:])
245 if not m:
246 raise NoMatch(lastEnd)
247 return m.end() + lastEnd
250 class TestSCons(TestCommon):
251 """Class for testing SCons.
253 This provides a common place for initializing SCons tests,
254 eliminating the need to begin every test with the same repeated
255 initializations.
258 scons_version = SConsVersion
259 javac_is_gcj = False
261 def __init__(self, **kw) -> None:
262 """Initialize an SCons testing object.
264 If they're not overridden by keyword arguments, this
265 initializes the object with the following default values:
267 program = 'scons' if it exists,
268 else 'scons.py'
269 interpreter = 'python'
270 match = match_exact
271 workdir = ''
273 The workdir value means that, by default, a temporary workspace
274 directory is created for a TestSCons environment. In addition,
275 this method changes directory (chdir) to the workspace directory,
276 so an explicit "chdir = '.'" on all of the run() method calls
277 is not necessary.
279 self.orig_cwd = os.getcwd()
280 self.external = os.environ.get('SCONS_EXTERNAL_TEST', 0)
282 if not self.external:
283 try:
284 script_dir = os.environ['SCONS_SCRIPT_DIR']
285 except KeyError:
286 pass
287 else:
288 os.chdir(script_dir)
289 if 'program' not in kw:
290 kw['program'] = os.environ.get('SCONS')
291 if not kw['program']:
292 if not self.external:
293 if os.path.exists('scons'):
294 kw['program'] = 'scons'
295 else:
296 kw['program'] = 'scons.py'
297 else:
298 kw['program'] = 'scons'
299 kw['interpreter'] = ''
300 elif not self.external and not os.path.isabs(kw['program']):
301 kw['program'] = os.path.join(self.orig_cwd, kw['program'])
302 if 'interpreter' not in kw and not os.environ.get('SCONS_EXEC'):
303 kw['interpreter'] = [python, ]
304 if 'match' not in kw:
305 kw['match'] = match_exact
306 if 'workdir' not in kw:
307 kw['workdir'] = ''
309 # Term causing test failures due to bogus readline init
310 # control character output on FC8
311 # TERM can cause test failures due to control chars in prompts etc.
312 os.environ['TERM'] = 'dumb'
314 self.ignore_python_version = kw.get('ignore_python_version', 1)
315 if kw.get('ignore_python_version', -1) != -1:
316 del kw['ignore_python_version']
318 super().__init__(**kw)
320 if not self.external:
321 import SCons.Node.FS
322 if SCons.Node.FS.default_fs is None:
323 SCons.Node.FS.default_fs = SCons.Node.FS.FS()
325 try:
326 self.fixture_dirs = (os.environ['FIXTURE_DIRS']).split(os.pathsep)
327 except KeyError:
328 pass
330 def Environment(self, ENV=None, *args, **kw):
332 Return a construction Environment that optionally overrides
333 the default external environment with the specified ENV.
335 if not self.external:
336 import SCons.Environment
337 import SCons.Errors
338 if ENV is not None:
339 kw['ENV'] = ENV
340 try:
341 return SCons.Environment.Environment(*args, **kw)
342 except (SCons.Errors.UserError, SCons.Errors.InternalError):
343 return None
345 return None
347 def detect(self, var, prog=None, ENV=None, norm=None):
349 Return the detected path to a tool program.
351 Searches first the named construction variable, then
352 the SCons path.
354 Args:
355 var: name of construction variable to check for tool name.
356 prog: tool program to check for.
357 ENV: if present, kwargs to initialize an environment that
358 will be created to perform the lookup.
359 norm: if true, normalize any returned path looked up in
360 the environment to use UNIX-style path separators.
362 Returns: full path to the tool, or None.
365 env = self.Environment(ENV)
366 if env:
367 v = env.subst(f"${var}")
368 if not v:
369 return None
370 if prog is None:
371 prog = v
372 if v != prog:
373 return None
374 result = env.WhereIs(prog)
375 if result and norm and os.sep != '/':
376 result = result.replace(os.sep, '/')
377 return result
379 return self.where_is(prog)
381 def detect_tool(self, tool, prog=None, ENV=None):
383 Given a tool (i.e., tool specification that would be passed
384 to the "tools=" parameter of Environment()) and a program that
385 corresponds to that tool, return true if and only if we can find
386 that tool using Environment.Detect().
388 By default, prog is set to the value passed into the tools parameter.
391 if not prog:
392 prog = tool
393 env = self.Environment(ENV, tools=[tool])
394 if env is None:
395 return None
396 return env.Detect([prog])
398 def where_is(self, prog, path=None, pathext=None):
400 Given a program, search for it in the specified external PATH,
401 or in the actual external PATH if none is specified.
403 if path is None:
404 path = os.environ['PATH']
405 if self.external:
406 if isinstance(prog, str):
407 prog = [prog]
408 for p in prog:
409 result = TestCmd.where_is(self, p, path, pathext)
410 if result:
411 return os.path.normpath(result)
412 else:
413 import SCons.Environment
414 env = SCons.Environment.Environment()
415 return env.WhereIs(prog, path, pathext)
417 return None
419 def wrap_stdout(self, build_str: str="", read_str: str="", error: int=0, cleaning: int=0) -> str:
420 """Wraps "expect" strings in SCons boilerplate.
422 Given strings of expected output specific to a test,
423 returns a string which includes the SCons wrapping such as
424 "Reading ... done", etc.: that is, adds the text that would
425 be left out by running SCons in quiet mode;
426 Makes a complete message to match against.
428 Args:
429 build_str: the message for the execution part of the output.
430 If non-empty, needs to be newline-terminated.
431 read_str: the message for the reading-sconscript part of
432 the output. If non-empty, needs to be newline-terminated.
433 error: if true, expect a fail message rather than a done message.
434 cleaning: index into type messages, if 0 selects
435 build messages, if 1 selects clean messages.
437 cap, lc = [('Build', 'build'),
438 ('Clean', 'clean')][cleaning]
439 if error:
440 term = f"scons: {lc}ing terminated because of errors.\n"
441 else:
442 term = f"scons: done {lc}ing targets.\n"
444 return "scons: Reading SConscript files ...\n" + \
445 read_str + \
446 "scons: done reading SConscript files.\n" + \
447 f"scons: {cap}ing targets ...\n" + \
448 build_str + \
449 term
451 def run(self, *args, **kw) -> None:
453 Set up SCONSFLAGS for every command so test scripts don't need
454 to worry about unexpected warnings in their output.
456 sconsflags = initialize_sconsflags(self.ignore_python_version)
457 try:
458 super().run(*args, **kw)
459 finally:
460 restore_sconsflags(sconsflags)
462 # Modifying the options should work and ought to be simpler, but this
463 # class is used for more than just running 'scons' itself. If there's
464 # an automated way of determining whether it's running 'scons' or
465 # something else, this code should be resurected.
466 # options = kw.get('options')
467 # if options:
468 # options = [options]
469 # else:
470 # options = []
471 # if self.ignore_python_version and deprecated_python_version():
472 # options.append('--warn=no-python-version')
473 # # Provide a way to suppress or provide alternate flags for
474 # # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
475 # # (The intended use case is to set it to null when running
476 # # timing tests of earlier versions of SCons which don't
477 # # support the --warn=no-visual-c-missing warning.)
478 # visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
479 # '--warn=no-visual-c-missing')
480 # if visual_c:
481 # options.append(visual_c)
482 # kw['options'] = ' '.join(options)
483 # TestCommon.run(self, *args, **kw)
485 def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
486 """Asserts that all of the targets listed in arguments is
487 up to date, but does not make any assumptions on other targets.
488 This function is most useful in conjunction with the -n option.
490 s = ""
491 for arg in arguments.split():
492 s = f"{s}scons: `{arg}' is up to date.\n"
493 kw['arguments'] = arguments
494 stdout = self.wrap_stdout(read_str=read_str, build_str=s)
495 # Append '.*' so that timing output that comes after the
496 # up-to-date output is okay.
497 kw['stdout'] = f"{re.escape(stdout)}.*"
498 kw['match'] = self.match_re_dotall
499 self.run(**kw)
501 def not_up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
502 """Asserts that none of the targets listed in arguments is
503 up to date, but does not make any assumptions on other targets.
504 This function is most useful in conjunction with the -n option.
506 s = ""
507 for arg in arguments.split():
508 s = f"{s}(?!scons: `{re.escape(arg)}' is up to date.)"
509 s = f"({s}[^\n]*\n)*"
510 kw['arguments'] = arguments
511 stdout = re.escape(self.wrap_stdout(read_str=read_str, build_str='ARGUMENTSGOHERE'))
512 kw['stdout'] = stdout.replace('ARGUMENTSGOHERE', s)
513 kw['match'] = self.match_re_dotall
514 self.run(**kw)
516 def option_not_yet_implemented(self, option, arguments=None, **kw):
518 Verifies expected behavior for options that are not yet implemented:
519 a warning message, and exit status 1.
521 msg = f"Warning: the {option} option is not yet implemented\n"
522 kw['stderr'] = msg
523 if arguments:
524 # If it's a long option and the argument string begins with '=',
525 # it's of the form --foo=bar and needs no separating space.
526 if option[:2] == '--' and arguments[0] == '=':
527 kw['arguments'] = option + arguments
528 else:
529 kw['arguments'] = f"{option} {arguments}"
530 return self.run(**kw)
532 def deprecated_wrap(self, msg) -> str:
534 Calculate the pattern that matches a deprecation warning.
536 return f"\nscons: warning: {re_escape(msg)}\n{file_expr}"
538 def deprecated_fatal(self, warn, msg):
540 Determines if the warning has turned into a fatal error. If so,
541 passes the test, as any remaining runs are now moot.
543 This method expects a SConscript to be present that will causes
544 the warning. The method writes a SConstruct that calls the
545 SConsscript and looks to see what type of result occurs.
547 The pattern that matches the warning is returned.
549 TODO: Actually detect that it's now an error. We don't have any
550 cases yet, so there's no way to test it.
552 self.write('SConstruct', """if True:
553 WARN = ARGUMENTS.get('WARN')
554 if WARN: SetOption('warn', WARN)
555 SConscript('SConscript')
556 """)
558 def err_out():
559 # TODO calculate stderr for fatal error
560 return re_escape('put something here')
562 # no option, should get one of nothing, warning, or error
563 warning = self.deprecated_wrap(msg)
564 self.run(arguments='.', stderr=None)
565 stderr = self.stderr()
566 if stderr:
567 # most common case done first
568 if match_re_dotall(stderr, warning):
569 # expected output
570 pass
571 elif match_re_dotall(stderr, err_out()):
572 # now a fatal error; skip the rest of the tests
573 self.pass_test()
574 else:
575 # test failed; have to do this by hand...
576 print(self.banner('STDOUT '))
577 print(self.stdout())
578 print(self.diff(warning, stderr, 'STDERR '))
579 self.fail_test()
581 return warning
583 def deprecated_warning(self, warn, msg):
585 Verifies the expected behavior occurs for deprecation warnings.
586 This method expects a SConscript to be present that will causes
587 the warning. The method writes a SConstruct and exercises various
588 combinations of command-line options and SetOption parameters to
589 validate that it performs correctly.
591 The pattern that matches the warning is returned.
593 warning = self.deprecated_fatal(warn, msg)
595 def RunPair(option, expected) -> None:
596 # run the same test with the option on the command line and
597 # then with the option passed via SetOption().
598 self.run(options=f"--warn={option}",
599 arguments='.',
600 stderr=expected,
601 match=match_re_dotall)
602 self.run(options=f"WARN={option}",
603 arguments='.',
604 stderr=expected,
605 match=match_re_dotall)
607 # all warnings off, should get no output
608 RunPair('no-deprecated', '')
610 # warning enabled, should get expected output
611 RunPair(warn, warning)
613 # warning disabled, should get either nothing or mandatory message
614 expect = f"""()|(Can not disable mandataory warning: 'no-{warn}'\n\n{warning})"""
615 RunPair(f"no-{warn}", expect)
617 return warning
619 def diff_substr(self, expect, actual, prelen: int=20, postlen: int=40) -> str:
620 i = 0
621 for x, y in zip(expect, actual):
622 if x != y:
623 return "Actual did not match expect at char %d:\n" \
624 " Expect: %s\n" \
625 " Actual: %s\n" \
626 % (i, repr(expect[i - prelen:i + postlen]),
627 repr(actual[i - prelen:i + postlen]))
628 i = i + 1
629 return "Actual matched the expected output???"
631 def python_file_line(self, file, line):
633 Returns a Python error line for output comparisons.
635 The exec of the traceback line gives us the correct format for
636 this version of Python.
638 File "<string>", line 1, <module>
640 We stick the requested file name and line number in the right
641 places, abstracting out the version difference.
643 # This routine used to use traceback to get the proper format
644 # that doesn't work well with py3. And the format of the
645 # traceback seems to be stable, so let's just format
646 # an appropriate string
648 # exec('import traceback; x = traceback.format_stack()[-1]')
649 # import traceback
650 # x = traceback.format_stack()
651 # x = # XXX: .lstrip()
652 # x = x.replace('<string>', file)
653 # x = x.replace('line 1,', 'line %s,' % line)
654 # x="\n".join(x)
655 x = f'File "{file}", line {line}, in <module>\n'
656 return x
658 def normalize_ps(self, s):
659 s = re.sub(r'(Creation|Mod)Date: .*',
660 r'\1Date XXXX', s)
661 s = re.sub(r'%DVIPSSource:\s+TeX output\s.*',
662 r'%DVIPSSource: TeX output XXXX', s)
663 s = re.sub(r'/(BaseFont|FontName) /[A-Z0-9]{6}',
664 r'/\1 /XXXXXX', s)
665 s = re.sub(r'BeginFont: [A-Z0-9]{6}',
666 r'BeginFont: XXXXXX', s)
668 return s
670 @staticmethod
671 def to_bytes_re_sub(pattern, repl, string, count: int=0, flags: int=0):
673 Wrapper around re.sub to change pattern and repl to bytes to work with
674 both python 2 & 3
676 pattern = to_bytes(pattern)
677 repl = to_bytes(repl)
678 return re.sub(pattern, repl, string, count=count, flags=flags)
680 def normalize_pdf(self, s):
681 s = self.to_bytes_re_sub(r'/(Creation|Mod)Date \(D:[^)]*\)',
682 r'/\1Date (D:XXXX)', s)
683 s = self.to_bytes_re_sub(r'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
684 r'/ID [<XXXX> <XXXX>]', s)
685 s = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
686 r'/\1 /XXXXXX', s)
687 s = self.to_bytes_re_sub(r'/Length \d+ *\n/Filter /FlateDecode\n',
688 r'/Length XXXX\n/Filter /FlateDecode\n', s)
690 try:
691 import zlib
692 except ImportError:
693 pass
694 else:
695 begin_marker = to_bytes('/FlateDecode\n>>\nstream\n')
696 end_marker = to_bytes('endstream\nendobj')
698 encoded = []
699 b = s.find(begin_marker, 0)
700 while b != -1:
701 b = b + len(begin_marker)
702 e = s.find(end_marker, b)
703 encoded.append((b, e))
704 b = s.find(begin_marker, e + len(end_marker))
706 x = 0
707 r = []
708 for b, e in encoded:
709 r.append(s[x:b])
710 d = zlib.decompress(s[b:e])
711 d = self.to_bytes_re_sub(r'%%CreationDate: [^\n]*\n',
712 r'%%CreationDate: 1970 Jan 01 00:00:00\n', d)
713 d = self.to_bytes_re_sub(r'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
714 r'%DVIPSSource: TeX output 1970.01.01:0000', d)
715 d = self.to_bytes_re_sub(r'/(BaseFont|FontName) /[A-Z]{6}',
716 r'/\1 /XXXXXX', d)
717 r.append(d)
718 x = e
719 r.append(s[x:])
720 s = to_bytes('').join(r)
722 return s
724 def paths(self, patterns):
725 import glob
726 result = []
727 for p in patterns:
728 result.extend(sorted(glob.glob(p)))
729 return result
731 def get_sconsignname(self):
732 """Get the scons database name used, and return both the prefix and full filename.
733 if the user left the options defaulted AND the default algorithm set by
734 SCons is md5, then set the database name to be the special default name
736 otherwise, if it defaults to something like 'sha1' or the user explicitly
737 set 'md5' as the hash format, set the database name to .sconsign_<algorithm>
738 eg .sconsign_sha1, etc.
740 Returns:
741 a pair containing: the current dbname, the dbname.dblite filename
743 hash_format = get_hash_format()
744 current_hash_algorithm = get_current_hash_algorithm_used()
745 if hash_format is None and current_hash_algorithm == 'md5':
746 return ".sconsign"
747 else:
748 database_prefix=f".sconsign_{current_hash_algorithm}"
749 return database_prefix
752 def unlink_sconsignfile(self, name: str='.sconsign.dblite') -> None:
753 """Delete the sconsign file.
755 Args:
756 name: expected name of sconsign file
758 return self.unlink(name)
760 def java_ENV(self, version=None):
761 """ Initialize JAVA SDK environment.
763 Initialize with a default external environment that uses a local
764 Java SDK in preference to whatever's found in the default PATH.
766 Args:
767 version: if set, match only that version
769 Returns:
770 the new env.
772 if not self.external:
773 try:
774 return self._java_env[version]['ENV']
775 except AttributeError:
776 self._java_env = {}
777 except KeyError:
778 pass
780 import SCons.Environment
781 env = SCons.Environment.Environment()
782 self._java_env[version] = env
784 if version:
785 if sys.platform == 'win32':
786 patterns = [
787 f'C:/Program Files*/Java/jdk*{version}*/bin',
789 else:
790 patterns = [
791 f'/usr/java/jdk{version}*/bin',
792 f'/usr/lib/jvm/*-{version}*/bin',
793 f'/usr/local/j2sdk{version}*/bin',
795 java_path = self.paths(patterns) + [env['ENV']['PATH']]
796 else:
797 if sys.platform == 'win32':
798 patterns = [
799 'C:/Program Files*/Java/jdk*/bin',
801 else:
802 patterns = [
803 '/usr/java/latest/bin',
804 '/usr/lib/jvm/*/bin',
805 '/usr/local/j2sdk*/bin',
807 java_path = self.paths(patterns) + [env['ENV']['PATH']]
809 env['ENV']['PATH'] = os.pathsep.join(java_path)
810 return env['ENV']
812 return None
814 def java_where_includes(self, version=None):
815 """ Find include path needed for compiling java jni code.
817 Args:
818 version: if set, match only that version
820 Returns:
821 path to java headers or None
823 import sys
825 result = []
826 if sys.platform[:6] == 'darwin':
827 java_home = self.java_where_java_home(version)
828 jni_path = os.path.join(java_home, 'include', 'jni.h')
829 if os.path.exists(jni_path):
830 result.append(os.path.dirname(jni_path))
832 if not version:
833 version = ''
834 jni_dirs = ['/System/Library/Frameworks/JavaVM.framework/Headers/jni.h',
835 '/usr/lib/jvm/default-java/include/jni.h',
836 '/usr/lib/jvm/java-*-oracle/include/jni.h']
837 else:
838 jni_dirs = [f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}*/Headers/jni.h']
839 jni_dirs.extend([f'/usr/lib/jvm/java-*-sun-{version}*/include/jni.h',
840 f'/usr/lib/jvm/java-{version}*-openjdk*/include/jni.h',
841 f'/usr/java/jdk{version}*/include/jni.h'])
842 dirs = self.paths(jni_dirs)
843 if not dirs:
844 return None
845 d = os.path.dirname(self.paths(jni_dirs)[0])
846 result.append(d)
848 if sys.platform == 'win32':
849 result.append(os.path.join(d, 'win32'))
850 elif sys.platform.startswith('linux'):
851 result.append(os.path.join(d, 'linux'))
852 return result
854 def java_where_java_home(self, version=None) -> str:
855 """ Find path to what would be JAVA_HOME.
857 SCons does not read JAVA_HOME from the environment, so deduce it.
859 Args:
860 version: if set, match only that version
862 Returns:
863 path where JDK components live
864 Bails out of the entire test (skip) if not found.
866 if sys.platform[:6] == 'darwin':
867 # osx 10.11+
868 home_tool = '/usr/libexec/java_home'
869 java_home = ''
870 if os.path.exists(home_tool):
871 cp = sp.run(home_tool, stdout=sp.PIPE, stderr=sp.STDOUT)
872 if cp.returncode == 0:
873 java_home = cp.stdout.decode().strip()
875 if version is None:
876 if java_home:
877 return java_home
878 for home in [
879 '/System/Library/Frameworks/JavaVM.framework/Home',
880 # osx 10.10
881 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home'
883 if os.path.exists(home):
884 return home
885 else:
886 if java_home.find(f'jdk{version}') != -1:
887 return java_home
888 for home in [
889 f'/System/Library/Frameworks/JavaVM.framework/Versions/{version}/Home',
890 # osx 10.10
891 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/'
893 if os.path.exists(home):
894 return home
895 # if we fell through, make sure flagged as not found
896 home = ''
897 else:
898 jar = self.java_where_jar(version)
899 home = os.path.normpath(f'{jar}/..')
901 if home and os.path.isdir(home):
902 return home
904 self.skip_test(
905 "Could not run Java: unable to detect valid JAVA_HOME, skipping test.\n",
906 from_fw=True,
909 def java_mac_check(self, where_java_bin, java_bin_name) -> None:
910 """Extra check for Java on MacOS.
912 MacOS has a place holder java/javac, which fails with a detectable
913 error if Java is not actually installed, and works normally if it is.
914 Note msg has changed over time.
916 Bails out of the entire test (skip) if not found.
918 cp = sp.run([where_java_bin, "-version"], stdout=sp.PIPE, stderr=sp.STDOUT)
919 if (
920 b"No Java runtime" in cp.stdout
921 or b"Unable to locate a Java Runtime" in cp.stdout
923 self.skip_test(
924 f"Could not find Java {java_bin_name}, skipping test.\n",
925 from_fw=True,
928 def java_where_jar(self, version=None) -> str:
929 """ Find java archiver jar.
931 Args:
932 version: if set, match only that version
934 Returns:
935 path to jar
937 ENV = self.java_ENV(version)
938 if self.detect_tool('jar', ENV=ENV):
939 where_jar = self.detect('JAR', 'jar', ENV=ENV)
940 else:
941 where_jar = self.where_is('jar', ENV['PATH'])
942 if not where_jar:
943 self.skip_test("Could not find Java jar, skipping test(s).\n", from_fw=True)
944 elif sys.platform == "darwin":
945 self.java_mac_check(where_jar, 'jar')
947 return where_jar
949 def java_where_java(self, version=None) -> str:
950 """ Find java executable.
952 Args:
953 version: if set, match only that version
955 Returns:
956 path to the java rutime
958 ENV = self.java_ENV(version)
959 where_java = self.where_is('java', ENV['PATH'])
961 if not where_java:
962 self.skip_test("Could not find Java java, skipping test(s).\n", from_fw=True)
963 elif sys.platform == "darwin":
964 self.java_mac_check(where_java, 'java')
966 return where_java
968 def java_where_javac(self, version=None) -> str:
969 """ Find java compiler.
971 Args:
972 version: if set, match only that version
974 Returns:
975 path to javac
977 ENV = self.java_ENV(version)
978 if self.detect_tool('javac'):
979 where_javac = self.detect('JAVAC', 'javac', ENV=ENV)
980 else:
981 where_javac = self.where_is('javac', ENV['PATH'])
982 if not where_javac:
983 self.skip_test("Could not find Java javac, skipping test(s).\n", from_fw=True)
984 elif sys.platform == "darwin":
985 self.java_mac_check(where_javac, 'javac')
987 self.run(program=where_javac,
988 arguments='-version',
989 stderr=None,
990 status=None)
991 # Note recent versions output version info to stdout instead of stderr
992 if version:
993 verf = f'javac {version}'
994 if self.stderr().find(verf) == -1 and self.stdout().find(verf) == -1:
995 fmt = "Could not find javac for Java version %s, skipping test(s).\n"
996 self.skip_test(fmt % version, from_fw=True)
997 else:
998 version_re = r'javac (\d*\.*\d)'
999 m = re.search(version_re, self.stderr())
1000 if not m:
1001 m = re.search(version_re, self.stdout())
1003 if m:
1004 version = m.group(1)
1005 self.javac_is_gcj = False
1006 elif self.stderr().find('gcj') != -1:
1007 version = '1.2'
1008 self.javac_is_gcj = True
1009 else:
1010 version = None
1011 self.javac_is_gcj = False
1012 return where_javac, version
1014 def java_where_javah(self, version=None) -> str:
1015 """ Find java header generation tool.
1017 TODO issue #3347 since JDK10, there is no separate javah command,
1018 'javac -h' is used. We should not return a javah from a different
1019 installed JDK - how to detect and what to return in this case?
1021 Args:
1022 version: if set, match only that version
1024 Returns:
1025 path to javah
1027 ENV = self.java_ENV(version)
1028 if self.detect_tool('javah'):
1029 where_javah = self.detect('JAVAH', 'javah', ENV=ENV)
1030 else:
1031 where_javah = self.where_is('javah', ENV['PATH'])
1032 if not where_javah:
1033 self.skip_test("Could not find Java javah, skipping test(s).\n", from_fw=True)
1034 return where_javah
1036 def java_where_rmic(self, version=None) -> str:
1037 """ Find java rmic tool.
1039 Args:
1040 version: if set, match only that version
1042 Returns:
1043 path to rmic
1045 ENV = self.java_ENV(version)
1046 if self.detect_tool('rmic'):
1047 where_rmic = self.detect('RMIC', 'rmic', ENV=ENV)
1048 else:
1049 where_rmic = self.where_is('rmic', ENV['PATH'])
1050 if not where_rmic:
1051 self.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n", from_fw=True)
1052 return where_rmic
1054 def java_get_class_files(self, dir):
1055 result = []
1056 for dirpath, dirnames, filenames in os.walk(dir):
1057 for fname in filenames:
1058 if fname.endswith('.class'):
1059 result.append(os.path.join(dirpath, fname))
1060 return sorted(result)
1062 def Qt_dummy_installation(self, dir: str='qt') -> None:
1063 # create a dummy qt installation
1065 self.subdir(dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'])
1067 self.write([dir, 'bin', 'mymoc.py'], """\
1068 import getopt
1069 import sys
1070 import re
1071 # -w and -z are fake options used in test/QT/QTFLAGS.py
1072 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
1073 impl = 0
1074 opt_string = ''
1075 for opt, arg in cmd_opts:
1076 if opt == '-o': outfile = arg
1077 elif opt == '-i': impl = 1
1078 else: opt_string = opt_string + ' ' + opt
1080 with open(outfile, 'w') as ofp:
1081 ofp.write("/* mymoc.py%s */\\n" % opt_string)
1082 for a in args:
1083 with open(a, 'r') as ifp:
1084 contents = ifp.read()
1085 a = a.replace('\\\\', '\\\\\\\\')
1086 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
1087 if impl:
1088 contents = re.sub(r'#include.*', '', contents)
1089 ofp.write(contents.replace('Q_OBJECT', subst))
1090 sys.exit(0)
1091 """)
1093 self.write([dir, 'bin', 'myuic.py'], """\
1094 import os.path
1095 import re
1096 import sys
1097 output_arg = 0
1098 impl_arg = 0
1099 impl = None
1100 source = None
1101 opt_string = ''
1102 for arg in sys.argv[1:]:
1103 if output_arg:
1104 outfile = arg
1105 output_arg = 0
1106 elif impl_arg:
1107 impl = arg
1108 impl_arg = 0
1109 elif arg == "-o":
1110 output_arg = 1
1111 elif arg == "-impl":
1112 impl_arg = 1
1113 elif arg[0:1] == "-":
1114 opt_string = opt_string + ' ' + arg
1115 else:
1116 if source:
1117 sys.exit(1)
1118 source = sourceFile = arg
1120 with open(outfile, 'w') as ofp, open(source, 'r') as ifp:
1121 ofp.write("/* myuic.py%s */\\n" % opt_string)
1122 if impl:
1123 ofp.write('#include "' + impl + '"\\n')
1124 includes = re.findall('<include.*?>(.*?)</include>', ifp.read())
1125 for incFile in includes:
1126 # this is valid for ui.h files, at least
1127 if os.path.exists(incFile):
1128 ofp.write('#include "' + incFile + '"\\n')
1129 else:
1130 ofp.write('#include "my_qobject.h"\\n' + ifp.read() + " Q_OBJECT \\n")
1131 sys.exit(0)
1132 """)
1134 self.write([dir, 'include', 'my_qobject.h'], r"""
1135 #define Q_OBJECT ;
1136 void my_qt_symbol(const char *arg);
1137 """)
1139 self.write([dir, 'lib', 'my_qobject.cpp'], r"""
1140 #include "../include/my_qobject.h"
1141 #include <stdio.h>
1142 void my_qt_symbol(const char *arg) {
1143 fputs(arg, stdout);
1145 """)
1147 self.write([dir, 'lib', 'SConstruct'], r"""
1148 import sys
1149 DefaultEnvironment(tools=[]) # test speedup
1150 env = Environment()
1151 if sys.platform == 'win32':
1152 env.StaticLibrary('myqt', 'my_qobject.cpp')
1153 else:
1154 env.SharedLibrary('myqt', 'my_qobject.cpp')
1155 """)
1157 self.run(chdir=self.workpath(dir, 'lib'),
1158 arguments='.',
1159 stderr=noisy_ar,
1160 match=self.match_re_dotall)
1162 self.QT = self.workpath(dir)
1163 self.QT_LIB = 'myqt'
1164 self.QT_MOC = f"{_python_} {self.workpath(dir, 'bin', 'mymoc.py')}"
1165 self.QT_UIC = f"{_python_} {self.workpath(dir, 'bin', 'myuic.py')}"
1166 self.QT_LIB_DIR = self.workpath(dir, 'lib')
1168 def Qt_create_SConstruct(self, place, qt_tool: str='qt3') -> None:
1169 if isinstance(place, list):
1170 place = test.workpath(*place)
1172 var_prefix=qt_tool.upper()
1173 self.write(place, f"""\
1174 if ARGUMENTS.get('noqtdir', 0):
1175 {var_prefix}DIR = None
1176 else:
1177 {var_prefix}DIR = r'{self.QT}'
1178 DefaultEnvironment(tools=[]) # test speedup
1179 env = Environment(
1180 {var_prefix}DIR={var_prefix}DIR, {var_prefix}_LIB=r'{self.QT_LIB}', {var_prefix}_MOC=r'{self.QT_MOC}',
1181 {var_prefix}_UIC=r'{self.QT_UIC}', tools=['default', '{qt_tool}']
1183 dup = 1
1184 if ARGUMENTS.get('variant_dir', 0):
1185 if ARGUMENTS.get('chdir', 0):
1186 SConscriptChdir(1)
1187 else:
1188 SConscriptChdir(0)
1189 dup = int(ARGUMENTS.get('dup', 1))
1190 if dup == 0:
1191 builddir = 'build_dup0'
1192 env['QT_DEBUG'] = 1
1193 else:
1194 builddir = 'build'
1195 VariantDir(builddir, '.', duplicate=dup)
1196 print(builddir, dup)
1197 sconscript = Dir(builddir).File('SConscript')
1198 else:
1199 sconscript = File('SConscript')
1200 Export("env dup")
1201 SConscript(sconscript)
1202 """)
1204 NCR = 0 # non-cached rebuild
1205 CR = 1 # cached rebuild (up to date)
1206 NCF = 2 # non-cached build failure
1207 CF = 3 # cached build failure
1209 if sys.platform == 'win32':
1210 Configure_lib = 'msvcrt'
1211 else:
1212 Configure_lib = 'm'
1214 # to use cygwin compilers on cmd.exe -> uncomment following line
1215 # Configure_lib = 'm'
1217 def coverage_run(self) -> bool:
1218 """ Check if the the tests are being run under coverage.
1220 return 'COVERAGE_PROCESS_START' in os.environ or 'COVERAGE_FILE' in os.environ
1222 def skip_if_not_msvc(self, check_platform: bool=True) -> None:
1223 """ Skip test if MSVC is not available.
1225 Check whether we are on a Windows platform and skip the test if
1226 not. This check can be omitted by setting check_platform to False.
1228 Then, for a win32 platform, additionally check whether we have
1229 an MSVC toolchain installed in the system, and skip the test if
1230 none can be found (e.g. MinGW is the only compiler available).
1232 if check_platform:
1233 if sys.platform != 'win32':
1234 msg = f"Skipping Visual C/C++ test on non-Windows platform '{sys.platform}'\n"
1235 self.skip_test(msg, from_fw=True)
1236 return
1238 try:
1239 import SCons.Tool.MSCommon as msc
1240 if not msc.msvc_exists():
1241 msg = "No MSVC toolchain found...skipping test\n"
1242 self.skip_test(msg, from_fw=True)
1243 except Exception:
1244 pass
1246 def checkConfigureLogAndStdout(self, checks,
1247 logfile: str='config.log',
1248 sconf_dir: str='.sconf_temp',
1249 sconstruct: str="SConstruct",
1250 doCheckLog: bool=True, doCheckStdout: bool=True):
1251 """ Verify expected output from Configure.
1253 Used to verify the expected output from using Configure()
1254 via the contents of one or both of stdout or config.log file.
1255 If the algorithm does not succeed, the test is marked a fail
1256 and this function does not return.
1258 TODO: Perhaps a better API makes sense?
1260 Args:
1261 checks: list of ConfigCheckInfo tuples which specify
1262 logfile: Name of the config log
1263 sconf_dir: Name of the sconf dir
1264 sconstruct: SConstruct file name
1265 doCheckLog: check specified log file, defaults to true
1266 doCheckStdout: Check stdout, defaults to true
1269 try:
1270 ls = '\n'
1271 nols = '([^\n])'
1272 lastEnd = 0
1274 # Read the whole logfile
1275 logfile = self.read(self.workpath(logfile), mode='r')
1277 # Some debug code to keep around..
1278 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1280 if (doCheckLog and
1281 logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1282 self.fail_test()
1284 log = r'file \S*%s\,line \d+:' % re.escape(sconstruct) + ls
1285 if doCheckLog:
1286 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1288 log = f"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1289 if doCheckLog:
1290 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1292 rdstr = ""
1294 for check_info in checks:
1295 log = re.escape(f"scons: Configure: {check_info.check_string}") + ls
1297 if doCheckLog:
1298 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1300 log = ""
1301 result_cached = 1
1302 for bld_desc in check_info.cached: # each TryXXX
1303 for ext, flag in bld_desc: # each file in TryBuild
1304 conf_filename = re.escape(check_info.temp_filename%ext)
1306 if flag == self.NCR:
1307 # NCR = Non Cached Rebuild
1308 # rebuild will pass
1309 if ext in ['.c', '.cpp']:
1310 log = log + conf_filename + re.escape(" <-") + ls
1311 log = f"{log}( \\|{nols}*{ls})+?"
1312 else:
1313 log = f"{log}({nols}*{ls})*?"
1314 result_cached = 0
1315 if flag == self.CR:
1316 # CR = cached rebuild (up to date)s
1317 # up to date
1318 log = log + \
1319 re.escape("scons: Configure: \"") + \
1320 conf_filename + \
1321 re.escape("\" is up to date.") + ls
1322 log = log + re.escape("scons: Configure: The original builder "
1323 "output was:") + ls
1324 log = f"{log}( \\|.*{ls})+"
1325 if flag == self.NCF:
1326 # non-cached rebuild failure
1327 log = f"{log}({nols}*{ls})*?"
1328 result_cached = 0
1329 if flag == self.CF:
1330 # cached rebuild failure
1331 log = log + \
1332 re.escape("scons: Configure: Building \"") + \
1333 conf_filename + \
1334 re.escape("\" failed in a previous run and all its sources are up to date.") + ls
1335 log = log + re.escape("scons: Configure: The original builder output was:") + ls
1336 log = f"{log}( \\|.*{ls})+"
1337 if result_cached:
1338 result = f"(cached) {check_info.result}"
1339 else:
1340 result = check_info.result
1341 rdstr = f"{rdstr + re.escape(check_info.check_string) + re.escape(result)}\n"
1343 log = log + re.escape(f"scons: Configure: {result}") + ls + ls
1345 if doCheckLog:
1346 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1348 log = ""
1349 if doCheckLog:
1350 lastEnd = match_part_of_configlog(ls, logfile, lastEnd)
1352 if doCheckLog and lastEnd != len(logfile):
1353 raise NoMatch(lastEnd)
1355 except NoMatch as m:
1356 print("Cannot match log file against log regexp.")
1357 print("log file: ")
1358 print("------------------------------------------------------")
1359 print(logfile[m.pos:])
1360 print("------------------------------------------------------")
1361 print("log regexp: ")
1362 print("------------------------------------------------------")
1363 print(log)
1364 print("------------------------------------------------------")
1365 self.fail_test()
1367 if doCheckStdout:
1368 exp_stdout = self.wrap_stdout(".*", rdstr)
1369 if not self.match_re_dotall(self.stdout(), exp_stdout):
1370 print("Unexpected stdout: ")
1371 print("-----------------------------------------------------")
1372 print(repr(self.stdout()))
1373 print("-----------------------------------------------------")
1374 print(repr(exp_stdout))
1375 print("-----------------------------------------------------")
1376 self.fail_test()
1380 def checkLogAndStdout(self, checks, results, cached,
1381 logfile, sconf_dir, sconstruct,
1382 doCheckLog: bool=True, doCheckStdout: bool=True):
1383 """ Verify expected output from Configure.
1385 Used to verify the expected output from using Configure()
1386 via the contents of one or both of stdout or config.log file.
1387 The checks, results, cached parameters all are zipped together
1388 for use in comparing results. If the algorithm does not
1389 succeed, the test is marked a fail and this function does not return.
1391 TODO: Perhaps a better API makes sense?
1393 Args:
1394 checks: The Configure checks being run
1395 results: The expected results for each check
1396 cached: If the corresponding check is expected to be cached
1397 logfile: Name of the config log
1398 sconf_dir: Name of the sconf dir
1399 sconstruct: SConstruct file name
1400 doCheckLog: check specified log file, defaults to true
1401 doCheckStdout: Check stdout, defaults to true
1403 try:
1405 ls = '\n'
1406 nols = '([^\n])'
1407 lastEnd = 0
1409 # Read the whole logfile
1410 logfile = self.read(self.workpath(logfile), mode='r')
1412 # Some debug code to keep around..
1413 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1415 if (doCheckLog and
1416 logfile.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1417 self.fail_test()
1419 sconf_dir = sconf_dir
1420 sconstruct = sconstruct
1422 log = r'file \S*%s\,line \d+:' % re.escape(sconstruct) + ls
1423 if doCheckLog:
1424 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1426 log = f"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1427 if doCheckLog:
1428 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1430 rdstr = ""
1432 cnt = 0
1433 for check, result, cache_desc in zip(checks, results, cached):
1434 log = re.escape(f"scons: Configure: {check}") + ls
1436 if doCheckLog:
1437 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1439 log = ""
1440 result_cached = 1
1441 for bld_desc in cache_desc: # each TryXXX
1442 for ext, flag in bld_desc: # each file in TryBuild
1443 if ext in ['.c', '.cpp']:
1444 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1445 r'_[a-z0-9]{32,64}_\d+%s' % re.escape(ext)
1446 elif ext == '':
1447 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1448 r'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?'
1450 else:
1451 # We allow the second hash group to be optional because
1452 # TryLink() will create a c file, then compile to obj, then link that
1453 # The intermediate object file will not get the action hash
1454 # But TryCompile()'s where the product is the .o will get the
1455 # action hash. Rather than add a ton of complications to this logic
1456 # this shortcut should be sufficient.
1457 # TODO: perhaps revisit and/or fix file naming for intermediate files in
1458 # Configure context logic
1459 conf_filename = re.escape(os.path.join(sconf_dir, "conftest")) +\
1460 r'_[a-z0-9]{32,64}_\d+(_[a-z0-9]{32,64})?%s' % re.escape(ext)
1462 if flag == self.NCR:
1463 # NCR = Non Cached Rebuild
1464 # rebuild will pass
1465 if ext in ['.c', '.cpp']:
1466 log = log + conf_filename + re.escape(" <-") + ls
1467 log = f"{log}( \\|{nols}*{ls})+?"
1468 else:
1469 log = f"{log}({nols}*{ls})*?"
1470 result_cached = 0
1471 if flag == self.CR:
1472 # CR = cached rebuild (up to date)s
1473 # up to date
1474 log = log + \
1475 re.escape("scons: Configure: \"") + \
1476 conf_filename + \
1477 re.escape("\" is up to date.") + ls
1478 log = log + re.escape("scons: Configure: The original builder "
1479 "output was:") + ls
1480 log = f"{log}( \\|.*{ls})+"
1481 if flag == self.NCF:
1482 # non-cached rebuild failure
1483 log = f"{log}({nols}*{ls})*?"
1484 result_cached = 0
1485 if flag == self.CF:
1486 # cached rebuild failure
1487 log = log + \
1488 re.escape("scons: Configure: Building \"") + \
1489 conf_filename + \
1490 re.escape("\" failed in a previous run and all its sources are up to date.") + ls
1491 log = log + re.escape("scons: Configure: The original builder output was:") + ls
1492 log = f"{log}( \\|.*{ls})+"
1493 # cnt = cnt + 1
1494 if result_cached:
1495 result = f"(cached) {result}"
1497 rdstr = f"{rdstr + re.escape(check) + re.escape(result)}\n"
1499 log = log + re.escape(f"scons: Configure: {result}") + ls + ls
1501 if doCheckLog:
1502 lastEnd = match_part_of_configlog(log, logfile, lastEnd)
1504 log = ""
1505 if doCheckLog:
1506 lastEnd = match_part_of_configlog(ls, logfile, lastEnd)
1508 if doCheckLog and lastEnd != len(logfile):
1509 raise NoMatch(lastEnd)
1511 except NoMatch as m:
1512 print("Cannot match log file against log regexp.")
1513 print("log file: ")
1514 print("------------------------------------------------------")
1515 print(logfile[m.pos:])
1516 print("------------------------------------------------------")
1517 print("log regexp: ")
1518 print("------------------------------------------------------")
1519 print(log)
1520 print("------------------------------------------------------")
1521 self.fail_test()
1523 if doCheckStdout:
1524 exp_stdout = self.wrap_stdout(".*", rdstr)
1525 if not self.match_re_dotall(self.stdout(), exp_stdout):
1526 print("Unexpected stdout: ")
1527 print("----Actual-------------------------------------------")
1528 print(repr(self.stdout()))
1529 print("----Expected-----------------------------------------")
1530 print(repr(exp_stdout))
1531 print("-----------------------------------------------------")
1532 self.fail_test()
1534 def get_python_version(self) -> str:
1535 """ Returns the Python version.
1537 Convenience function so everyone doesn't have to
1538 hand-code slicing the right number of characters
1540 # see also sys.prefix documentation
1541 return python_minor_version_string()
1543 def get_platform_python_info(self, python_h_required: bool=False):
1544 """Return information about Python.
1546 Returns a path to a Python executable suitable for testing on
1547 this platform and its associated include path, library path and
1548 library name.
1550 If the Python executable or Python header (if required)
1551 is not found, the test is skipped.
1553 Returns:
1554 tuple: path to python, include path, library path, library name
1556 python = os.environ.get('python_executable', self.where_is('python'))
1557 if not python:
1558 self.skip_test('Can not find installed "python", skipping test.\n', from_fw=True)
1560 # construct a program to run in the intended environment
1561 # in order to fetch the characteristics of that Python.
1562 # Windows Python doesn't store all the info in config vars.
1563 if sys.platform == 'win32':
1564 self.run(program=python, stdin="""\
1565 import sysconfig, sys, os.path
1566 py_ver = 'python%d%d' % sys.version_info[:2]
1567 try:
1568 exec_prefix = sysconfig.get_config_var("exec_prefix")
1569 include = sysconfig.get_config_var("INCLUDEPY")
1570 print(include)
1571 lib_path = os.path.join(exec_prefix, 'libs')
1572 if not os.path.exists(lib_path):
1573 # check for virtualenv path.
1574 # this might not build anything different than first try.
1575 def venv_path():
1576 if hasattr(sys, 'real_prefix'):
1577 return sys.real_prefix
1578 if hasattr(sys, 'base_prefix'):
1579 return sys.base_prefix
1580 lib_path = os.path.join(venv_path(), 'libs')
1581 if not os.path.exists(lib_path):
1582 # not clear this is useful: 'lib' does not contain linkable libs
1583 lib_path = os.path.join(exec_prefix, 'lib')
1584 print(lib_path)
1585 except:
1586 include = os.path.join(sys.prefix, 'include', py_ver)
1587 print(include)
1588 lib_path = os.path.join(sys.prefix, 'lib', py_ver, 'config')
1589 print(lib_path)
1590 print(py_ver)
1591 Python_h = os.path.join(include, "Python.h")
1592 if os.path.exists(Python_h):
1593 print(Python_h)
1594 else:
1595 print("False")
1596 """)
1597 else:
1598 self.run(program=python, stdin="""\
1599 import sys, sysconfig, os.path
1600 include = sysconfig.get_config_var("INCLUDEPY")
1601 print(include)
1602 print(sysconfig.get_config_var("LIBDIR"))
1603 py_library_ver = sysconfig.get_config_var("LDVERSION")
1604 if not py_library_ver:
1605 py_library_ver = '%d.%d' % sys.version_info[:2]
1606 print("python"+py_library_ver)
1607 Python_h = os.path.join(include, "Python.h")
1608 if os.path.exists(Python_h):
1609 print(Python_h)
1610 else:
1611 print("False")
1612 """)
1613 incpath, libpath, libname, python_h = self.stdout().strip().split('\n')
1614 if python_h == "False" and python_h_required:
1615 self.skip_test('Can not find required "Python.h", skipping test.\n', from_fw=True)
1617 return (python, incpath, libpath, libname + _lib)
1619 def start(self, *args, **kw):
1621 Starts SCons in the test environment.
1623 This method exists to tell Test{Cmd,Common} that we're going to
1624 use standard input without forcing every .start() call in the
1625 individual tests to do so explicitly.
1627 if 'stdin' not in kw:
1628 kw['stdin'] = True
1629 sconsflags = initialize_sconsflags(self.ignore_python_version)
1630 try:
1631 p = super().start(*args, **kw)
1632 finally:
1633 restore_sconsflags(sconsflags)
1634 return p
1636 def wait_for(self, fname, timeout: float=20.0, popen=None) -> None:
1638 Waits for the specified file name to exist.
1640 waited = 0.0
1641 while not os.path.exists(fname):
1642 if timeout and waited >= timeout:
1643 sys.stderr.write(f'timed out waiting for {fname} to exist\n')
1644 if popen:
1645 popen.stdin.close()
1646 popen.stdin = None
1647 self.status = 1
1648 self.finish(popen)
1649 stdout = self.stdout()
1650 if stdout:
1651 sys.stdout.write(f"{self.banner('STDOUT ')}\n")
1652 sys.stdout.write(stdout)
1653 stderr = self.stderr()
1654 if stderr:
1655 sys.stderr.write(f"{self.banner('STDERR ')}\n")
1656 sys.stderr.write(stderr)
1657 self.fail_test()
1658 time.sleep(1.0)
1659 waited = waited + 1.0
1661 def get_alt_cpp_suffix(self):
1662 """Return alternate C++ file suffix.
1664 Many CXX tests have this same logic.
1665 They all needed to determine if the current os supports
1666 files with .C and .c as different files or not
1667 in which case they are instructed to use .cpp instead of .C
1669 if not case_sensitive_suffixes('.c', '.C'):
1670 alt_cpp_suffix = '.cpp'
1671 else:
1672 alt_cpp_suffix = '.C'
1673 return alt_cpp_suffix
1675 def platform_has_symlink(self) -> bool:
1676 """Retun an indication of whether symlink tests should be run.
1678 Despite the name, we really mean "are they reliably usable"
1679 rather than "do they exist" - basically the Windows case.
1681 if not hasattr(os, 'symlink') or sys.platform == 'win32':
1682 return False
1683 else:
1684 return True
1686 def zipfile_contains(self, zipfilename, names):
1687 """Returns True if zipfilename contains all the names, False otherwise."""
1688 with zipfile.ZipFile(zipfilename, 'r') as zf:
1689 return all(elem in zf.namelist() for elem in names)
1691 def zipfile_files(self, fname):
1692 """Returns all the filenames in zip file fname."""
1693 with zipfile.ZipFile(fname, 'r') as zf:
1694 return zf.namelist()
1697 class Stat:
1698 def __init__(self, name, units, expression, convert=None) -> None:
1699 if convert is None:
1700 convert = lambda x: x
1701 self.name = name
1702 self.units = units
1703 self.expression = re.compile(expression)
1704 self.convert = convert
1707 StatList = [
1708 Stat('memory-initial', 'kbytes',
1709 r'Memory before reading SConscript files:\s+(\d+)',
1710 convert=lambda s: int(s) // 1024),
1711 Stat('memory-prebuild', 'kbytes',
1712 r'Memory before building targets:\s+(\d+)',
1713 convert=lambda s: int(s) // 1024),
1714 Stat('memory-final', 'kbytes',
1715 r'Memory after building targets:\s+(\d+)',
1716 convert=lambda s: int(s) // 1024),
1718 Stat('time-sconscript', 'seconds',
1719 r'Total SConscript file execution time:\s+([\d.]+) seconds'),
1720 Stat('time-scons', 'seconds',
1721 r'Total SCons execution time:\s+([\d.]+) seconds'),
1722 Stat('time-commands', 'seconds',
1723 r'Total command execution time:\s+([\d.]+) seconds'),
1724 Stat('time-total', 'seconds',
1725 r'Total build time:\s+([\d.]+) seconds'),
1729 class TimeSCons(TestSCons):
1730 """Class for timing SCons."""
1732 def __init__(self, *args, **kw) -> None:
1734 In addition to normal TestSCons.TestSCons intialization,
1735 this enables verbose mode (which causes the command lines to
1736 be displayed in the output) and copies the contents of the
1737 directory containing the executing script to the temporary
1738 working directory.
1740 self.variables = kw.get('variables')
1741 default_calibrate_variables = []
1742 if self.variables is not None:
1743 for variable, value in self.variables.items():
1744 value = os.environ.get(variable, value)
1745 try:
1746 value = int(value)
1747 except ValueError:
1748 try:
1749 value = float(value)
1750 except ValueError:
1751 pass
1752 else:
1753 default_calibrate_variables.append(variable)
1754 else:
1755 default_calibrate_variables.append(variable)
1756 self.variables[variable] = value
1757 del kw['variables']
1758 calibrate_keyword_arg = kw.get('calibrate')
1759 if calibrate_keyword_arg is None:
1760 self.calibrate_variables = default_calibrate_variables
1761 else:
1762 self.calibrate_variables = calibrate_keyword_arg
1763 del kw['calibrate']
1765 self.calibrate = os.environ.get('TIMESCONS_CALIBRATE', '0') != '0'
1767 if 'verbose' not in kw and not self.calibrate:
1768 kw['verbose'] = True
1770 super().__init__(*args, **kw)
1772 # TODO(sgk): better way to get the script dir than sys.argv[0]
1773 self.test_dir = os.path.dirname(sys.argv[0])
1774 test_name = os.path.basename(self.test_dir)
1776 if not os.path.isabs(self.test_dir):
1777 self.test_dir = os.path.join(self.orig_cwd, self.test_dir)
1778 self.copy_timing_configuration(self.test_dir, self.workpath())
1780 def main(self, *args, **kw) -> None:
1782 The main entry point for standard execution of timings.
1784 This method run SCons three times:
1786 Once with the --help option, to have it exit after just reading
1787 the configuration.
1789 Once as a full build of all targets.
1791 Once again as a (presumably) null or up-to-date build of
1792 all targets.
1794 The elapsed time to execute each build is printed after
1795 it has finished.
1797 if 'options' not in kw and self.variables:
1798 options = []
1799 for variable, value in self.variables.items():
1800 options.append(f'{variable}={value}')
1801 kw['options'] = ' '.join(options)
1802 if self.calibrate:
1803 self.calibration(*args, **kw)
1804 else:
1805 self.uptime()
1806 self.startup(*args, **kw)
1807 self.full(*args, **kw)
1808 self.null(*args, **kw)
1810 def trace(self, graph, name, value, units, sort=None) -> None:
1811 fmt = "TRACE: graph=%s name=%s value=%s units=%s"
1812 line = fmt % (graph, name, value, units)
1813 if sort is not None:
1814 line = f"{line} sort={sort}"
1815 line = f"{line}\n"
1816 sys.stdout.write(line)
1817 sys.stdout.flush()
1819 def report_traces(self, trace, stats) -> None:
1820 self.trace('TimeSCons-elapsed',
1821 trace,
1822 self.elapsed_time(),
1823 "seconds",
1824 sort=0)
1825 for name, args in stats.items():
1826 self.trace(name, trace, **args)
1828 def uptime(self) -> None:
1829 try:
1830 fp = open('/proc/loadavg')
1831 except OSError:
1832 pass
1833 else:
1834 avg1, avg5, avg15 = fp.readline().split(" ")[:3]
1835 fp.close()
1836 self.trace('load-average', 'average1', avg1, 'processes')
1837 self.trace('load-average', 'average5', avg5, 'processes')
1838 self.trace('load-average', 'average15', avg15, 'processes')
1840 def collect_stats(self, input):
1841 result = {}
1842 for stat in StatList:
1843 m = stat.expression.search(input)
1844 if m:
1845 value = stat.convert(m.group(1))
1846 # The dict keys match the keyword= arguments
1847 # of the trace() method above so they can be
1848 # applied directly to that call.
1849 result[stat.name] = {'value': value, 'units': stat.units}
1850 return result
1852 def add_timing_options(self, kw, additional=None) -> None:
1854 Add the necessary timings options to the kw['options'] value.
1856 options = kw.get('options', '')
1857 if additional is not None:
1858 options += additional
1859 kw['options'] = f"{options} --debug=memory,time"
1861 def startup(self, *args, **kw) -> None:
1863 Runs scons with the --help option.
1865 This serves as a way to isolate just the amount of startup time
1866 spent reading up the configuration, since --help exits before any
1867 "real work" is done.
1869 self.add_timing_options(kw, ' --help')
1870 # Ignore the exit status. If the --help run dies, we just
1871 # won't report any statistics for it, but we can still execute
1872 # the full and null builds.
1873 kw['status'] = None
1874 self.run(*args, **kw)
1875 sys.stdout.write(self.stdout())
1876 stats = self.collect_stats(self.stdout())
1877 # Delete the time-commands, since no commands are ever
1878 # executed on the help run and it is (or should be) always 0.0.
1879 del stats['time-commands']
1880 self.report_traces('startup', stats)
1882 def full(self, *args, **kw) -> None:
1884 Runs a full build of SCons.
1886 self.add_timing_options(kw)
1887 self.run(*args, **kw)
1888 sys.stdout.write(self.stdout())
1889 stats = self.collect_stats(self.stdout())
1890 self.report_traces('full', stats)
1891 self.trace('full-memory', 'initial', **stats['memory-initial'])
1892 self.trace('full-memory', 'prebuild', **stats['memory-prebuild'])
1893 self.trace('full-memory', 'final', **stats['memory-final'])
1895 def calibration(self, *args, **kw) -> None:
1897 Runs a full build of SCons, but only reports calibration
1898 information (the variable(s) that were set for this configuration,
1899 and the elapsed time to run.
1901 self.add_timing_options(kw)
1902 self.run(*args, **kw)
1903 for variable in self.calibrate_variables:
1904 value = self.variables[variable]
1905 sys.stdout.write(f'VARIABLE: {variable}={value}\n')
1906 sys.stdout.write(f'ELAPSED: {self.elapsed_time()}\n')
1908 def null(self, *args, **kw) -> None:
1910 Runs an up-to-date null build of SCons.
1912 # TODO(sgk): allow the caller to specify the target (argument)
1913 # that must be up-to-date.
1914 self.add_timing_options(kw)
1916 # Build up regex for
1917 # SConscript:/private/var/folders/ng/48pttrpj239fw5rmm3x65pxr0000gn/T/testcmd.12081.pk1bv5i5/SConstruct took 533.646 ms
1918 read_str = 'SConscript:.*\n'
1919 self.up_to_date(arguments='.', read_str=read_str, **kw)
1920 sys.stdout.write(self.stdout())
1921 stats = self.collect_stats(self.stdout())
1922 # time-commands should always be 0.0 on a null build, because
1923 # no commands should be executed. Remove it from the stats
1924 # so we don't trace it, but only if it *is* 0 so that we'll
1925 # get some indication if a supposedly-null build actually does
1926 # build something.
1927 if float(stats['time-commands']['value']) == 0.0:
1928 del stats['time-commands']
1929 self.report_traces('null', stats)
1930 self.trace('null-memory', 'initial', **stats['memory-initial'])
1931 self.trace('null-memory', 'prebuild', **stats['memory-prebuild'])
1932 self.trace('null-memory', 'final', **stats['memory-final'])
1934 def elapsed_time(self):
1936 Returns the elapsed time of the most recent command execution.
1938 return self.endTime - self.startTime
1940 def run(self, *args, **kw):
1942 Runs a single build command, capturing output in the specified file.
1944 Because this class is about timing SCons, we record the start
1945 and end times of the elapsed execution, and also add the
1946 --debug=memory and --debug=time options to have SCons report
1947 its own memory and timing statistics.
1949 self.startTime = time.perf_counter()
1950 try:
1951 result = TestSCons.run(self, *args, **kw)
1952 finally:
1953 self.endTime = time.perf_counter()
1954 return result
1956 def copy_timing_configuration(self, source_dir, dest_dir) -> None:
1958 Copies the timing configuration from the specified source_dir (the
1959 directory in which the controlling script lives) to the specified
1960 dest_dir (a temporary working directory).
1962 This ignores all files and directories that begin with the string
1963 'TimeSCons-', and all '.svn' subdirectories.
1965 for root, dirs, files in os.walk(source_dir):
1966 if '.svn' in dirs:
1967 dirs.remove('.svn')
1968 dirs = [d for d in dirs if not d.startswith('TimeSCons-')]
1969 files = [f for f in files if not f.startswith('TimeSCons-')]
1970 for dirname in dirs:
1971 source = os.path.join(root, dirname)
1972 destination = source.replace(source_dir, dest_dir)
1973 os.mkdir(destination)
1974 if sys.platform != 'win32':
1975 shutil.copystat(source, destination)
1976 for filename in files:
1977 source = os.path.join(root, filename)
1978 destination = source.replace(source_dir, dest_dir)
1979 shutil.copy2(source, destination)
1981 def up_to_date(self, arguments: str='.', read_str: str="", **kw) -> None:
1982 """Asserts that all of the targets listed in arguments is
1983 up to date, but does not make any assumptions on other targets.
1984 This function is most useful in conjunction with the -n option.
1985 Note: This custom version for timings tests does NOT escape
1986 read_str.
1988 s = ""
1989 for arg in arguments.split():
1990 s = f"{s}scons: `{arg}' is up to date.\n"
1991 kw['arguments'] = arguments
1992 stdout = self.wrap_stdout(read_str="REPLACEME", build_str=s)
1993 # Append '.*' so that timing output that comes after the
1994 # up-to-date output is okay.
1995 stdout = f"{re.escape(stdout)}.*"
1996 stdout = stdout.replace('REPLACEME', read_str)
1997 kw['stdout'] = stdout
1998 kw['match'] = self.match_re_dotall
1999 self.run(**kw)
2003 # In some environments, $AR will generate a warning message to stderr
2004 # if the library doesn't previously exist and is being created. One
2005 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
2006 # but this is difficult to do in a platform-/implementation-specific
2007 # method. Instead, we will use the following as a stderr match for
2008 # tests that use AR so that we will view zero or more "ar: creating
2009 # <file>" messages to be successful executions of the test (see
2010 # test/AR.py for sample usage).
2012 noisy_ar = r'(ar: creating( archive)? \S+\n?)*'
2014 # Local Variables:
2015 # tab-width:4
2016 # indent-tabs-mode:nil
2017 # End:
2018 # vim: set expandtab tabstop=4 shiftwidth=4: