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.
25 A testing framework for the SCons software construction tool.
27 A TestSCons environment object is created via the usual invocation:
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.
42 import subprocess
as sp
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
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.
96 machine
= machine_map
.get(machine
, machine
)
100 _shobj
= shobj_suffix
101 shobj_
= shobj_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:
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.
122 for c
in '\\.[]()*+?': # Not an exhaustive list.
123 str = str.replace(c
, f
"\\{c}")
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
)
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
147 for idx
, o
in enumerate(out
):
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():
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()))
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')
197 sconsflags
= [save_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']
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:
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
:])
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
258 scons_version
= SConsVersion
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,
269 interpreter = 'python'
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
279 self
.orig_cwd
= os
.getcwd()
280 self
.external
= os
.environ
.get('SCONS_EXTERNAL_TEST', 0)
282 if not self
.external
:
284 script_dir
= os
.environ
['SCONS_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'
296 kw
['program'] = 'scons.py'
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
:
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
:
322 if SCons
.Node
.FS
.default_fs
is None:
323 SCons
.Node
.FS
.default_fs
= SCons
.Node
.FS
.FS()
326 self
.fixture_dirs
= (os
.environ
['FIXTURE_DIRS']).split(os
.pathsep
)
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
341 return SCons
.Environment
.Environment(*args
, **kw
)
342 except (SCons
.Errors
.UserError
, SCons
.Errors
.InternalError
):
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
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
)
367 v
= env
.subst(f
"${var}")
374 result
= env
.WhereIs(prog
)
375 if result
and norm
and os
.sep
!= '/':
376 result
= result
.replace(os
.sep
, '/')
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.
393 env
= self
.Environment(ENV
, tools
=[tool
])
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.
404 path
= os
.environ
['PATH']
406 if isinstance(prog
, str):
409 result
= TestCmd
.where_is(self
, p
, path
, pathext
)
411 return os
.path
.normpath(result
)
413 import SCons
.Environment
414 env
= SCons
.Environment
.Environment()
415 return env
.WhereIs(prog
, path
, pathext
)
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.
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
]
440 term
= f
"scons: {lc}ing terminated because of errors.\n"
442 term
= f
"scons: done {lc}ing targets.\n"
444 return "scons: Reading SConscript files ...\n" + \
446 "scons: done reading SConscript files.\n" + \
447 f
"scons: {cap}ing targets ...\n" + \
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
)
458 super().run(*args
, **kw
)
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')
468 # options = [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')
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.
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
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.
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
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"
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
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')
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()
567 # most common case done first
568 if match_re_dotall(stderr
, warning
):
571 elif match_re_dotall(stderr
, err_out()):
572 # now a fatal error; skip the rest of the tests
575 # test failed; have to do this by hand...
576 print(self
.banner('STDOUT '))
578 print(self
.diff(warning
, stderr
, 'STDERR '))
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}",
601 match
=match_re_dotall
)
602 self
.run(options
=f
"WARN={option}",
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
)
619 def diff_substr(self
, expect
, actual
, prelen
: int=20, postlen
: int=40) -> str:
621 for x
, y
in zip(expect
, actual
):
623 return "Actual did not match expect at char %d:\n" \
626 % (i
, repr(expect
[i
- prelen
:i
+ postlen
]),
627 repr(actual
[i
- prelen
:i
+ postlen
]))
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]')
650 # x = traceback.format_stack()
651 # x = # XXX: .lstrip()
652 # x = x.replace('<string>', file)
653 # x = x.replace('line 1,', 'line %s,' % line)
655 x
= f
'File "{file}", line {line}, in <module>\n'
658 def normalize_ps(self
, s
):
659 s
= re
.sub(r
'(Creation|Mod)Date: .*',
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}',
665 s
= re
.sub(r
'BeginFont: [A-Z0-9]{6}',
666 r
'BeginFont: XXXXXX', s
)
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
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}',
687 s
= self
.to_bytes_re_sub(r
'/Length \d+ *\n/Filter /FlateDecode\n',
688 r
'/Length XXXX\n/Filter /FlateDecode\n', s
)
695 begin_marker
= to_bytes('/FlateDecode\n>>\nstream\n')
696 end_marker
= to_bytes('endstream\nendobj')
699 b
= s
.find(begin_marker
, 0)
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
))
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}',
720 s
= to_bytes('').join(r
)
724 def paths(self
, patterns
):
728 result
.extend(sorted(glob
.glob(p
)))
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.
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':
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.
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.
767 version: if set, match only that version
772 if not self
.external
:
774 return self
._java
_env
[version
]['ENV']
775 except AttributeError:
780 import SCons
.Environment
781 env
= SCons
.Environment
.Environment()
782 self
._java
_env
[version
] = env
785 if sys
.platform
== 'win32':
787 f
'C:/Program Files*/Java/jdk*{version}*/bin',
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']]
797 if sys
.platform
== 'win32':
799 'C:/Program Files*/Java/jdk*/bin',
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
)
814 def java_where_includes(self
, version
=None):
815 """ Find include path needed for compiling java jni code.
818 version: if set, match only that version
821 path to java headers or None
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
))
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']
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
)
845 d
= os
.path
.dirname(self
.paths(jni_dirs
)[0])
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'))
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.
860 version: if set, match only that version
863 path where JDK components live
864 Bails out of the entire test (skip) if not found.
866 if sys
.platform
[:6] == 'darwin':
868 home_tool
= '/usr/libexec/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()
879 '/System/Library/Frameworks/JavaVM.framework/Home',
881 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home'
883 if os
.path
.exists(home
):
886 if java_home
.find(f
'jdk{version}') != -1:
889 f
'/System/Library/Frameworks/JavaVM.framework/Versions/{version}/Home',
891 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/'
893 if os
.path
.exists(home
):
895 # if we fell through, make sure flagged as not found
898 jar
= self
.java_where_jar(version
)
899 home
= os
.path
.normpath(f
'{jar}/..')
901 if home
and os
.path
.isdir(home
):
905 "Could not run Java: unable to detect valid JAVA_HOME, skipping test.\n",
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
)
920 b
"No Java runtime" in cp
.stdout
921 or b
"Unable to locate a Java Runtime" in cp
.stdout
924 f
"Could not find Java {java_bin_name}, skipping test.\n",
928 def java_where_jar(self
, version
=None) -> str:
929 """ Find java archiver jar.
932 version: if set, match only that version
937 ENV
= self
.java_ENV(version
)
938 if self
.detect_tool('jar', ENV
=ENV
):
939 where_jar
= self
.detect('JAR', 'jar', ENV
=ENV
)
941 where_jar
= self
.where_is('jar', ENV
['PATH'])
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')
949 def java_where_java(self
, version
=None) -> str:
950 """ Find java executable.
953 version: if set, match only that version
956 path to the java rutime
958 ENV
= self
.java_ENV(version
)
959 where_java
= self
.where_is('java', ENV
['PATH'])
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')
968 def java_where_javac(self
, version
=None) -> str:
969 """ Find java compiler.
972 version: if set, match only that version
977 ENV
= self
.java_ENV(version
)
978 if self
.detect_tool('javac'):
979 where_javac
= self
.detect('JAVAC', 'javac', ENV
=ENV
)
981 where_javac
= self
.where_is('javac', ENV
['PATH'])
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',
991 # Note recent versions output version info to stdout instead of stderr
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)
998 version_re
= r
'javac (\d*\.*\d)'
999 m
= re
.search(version_re
, self
.stderr())
1001 m
= re
.search(version_re
, self
.stdout())
1004 version
= m
.group(1)
1005 self
.javac_is_gcj
= False
1006 elif self
.stderr().find('gcj') != -1:
1008 self
.javac_is_gcj
= True
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?
1022 version: if set, match only that version
1027 ENV
= self
.java_ENV(version
)
1028 if self
.detect_tool('javah'):
1029 where_javah
= self
.detect('JAVAH', 'javah', ENV
=ENV
)
1031 where_javah
= self
.where_is('javah', ENV
['PATH'])
1033 self
.skip_test("Could not find Java javah, skipping test(s).\n", from_fw
=True)
1036 def java_where_rmic(self
, version
=None) -> str:
1037 """ Find java rmic tool.
1040 version: if set, match only that version
1045 ENV
= self
.java_ENV(version
)
1046 if self
.detect_tool('rmic'):
1047 where_rmic
= self
.detect('RMIC', 'rmic', ENV
=ENV
)
1049 where_rmic
= self
.where_is('rmic', ENV
['PATH'])
1051 self
.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n", from_fw
=True)
1054 def java_get_class_files(self
, dir):
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'], """\
1071 # -w and -z are fake options used in test/QT/QTFLAGS.py
1072 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
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)
1083 with open(a, 'r') as ifp:
1084 contents = ifp.read()
1085 a = a.replace('\\\\', '\\\\\\\\')
1086 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
1088 contents = re.sub(r'#include.*', '', contents)
1089 ofp.write(contents.replace('Q_OBJECT', subst))
1093 self
.write([dir, 'bin', 'myuic.py'], """\
1102 for arg in sys.argv[1:]:
1111 elif arg == "-impl":
1113 elif arg[0:1] == "-":
1114 opt_string = opt_string + ' ' + arg
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)
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')
1130 ofp.write('#include "my_qobject.h"\\n' + ifp.read() + " Q_OBJECT \\n")
1134 self
.write([dir, 'include', 'my_qobject.h'], r
"""
1136 void my_qt_symbol(const char *arg);
1139 self
.write([dir, 'lib', 'my_qobject.cpp'], r
"""
1140 #include "../include/my_qobject.h"
1142 void my_qt_symbol(const char *arg) {
1147 self
.write([dir, 'lib', 'SConstruct'], r
"""
1149 DefaultEnvironment(tools=[]) # test speedup
1151 if sys.platform == 'win32':
1152 env.StaticLibrary('myqt', 'my_qobject.cpp')
1154 env.SharedLibrary('myqt', 'my_qobject.cpp')
1157 self
.run(chdir
=self
.workpath(dir, 'lib'),
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
1177 {var_prefix}DIR = r'{self.QT}'
1178 DefaultEnvironment(tools=[]) # test speedup
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}']
1184 if ARGUMENTS.get('variant_dir', 0):
1185 if ARGUMENTS.get('chdir', 0):
1189 dup = int(ARGUMENTS.get('dup', 1))
1191 builddir = 'build_dup0'
1195 VariantDir(builddir, '.', duplicate=dup)
1196 print(builddir, dup)
1197 sconscript = Dir(builddir).File('SConscript')
1199 sconscript = File('SConscript')
1201 SConscript(sconscript)
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'
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).
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)
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)
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?
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
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))
1281 logfile
.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1284 log
= r
'file \S*%s\,line \d+:' % re
.escape(sconstruct
) + ls
1286 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1288 log
= f
"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1290 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1294 for check_info
in checks
:
1295 log
= re
.escape(f
"scons: Configure: {check_info.check_string}") + ls
1298 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
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
1309 if ext
in ['.c', '.cpp']:
1310 log
= log
+ conf_filename
+ re
.escape(" <-") + ls
1311 log
= f
"{log}( \\|{nols}*{ls})+?"
1313 log
= f
"{log}({nols}*{ls})*?"
1316 # CR = cached rebuild (up to date)s
1319 re
.escape("scons: Configure: \"") + \
1321 re
.escape("\" is up to date.") + ls
1322 log
= log
+ re
.escape("scons: Configure: The original builder "
1324 log
= f
"{log}( \\|.*{ls})+"
1325 if flag
== self
.NCF
:
1326 # non-cached rebuild failure
1327 log
= f
"{log}({nols}*{ls})*?"
1330 # cached rebuild failure
1332 re
.escape("scons: Configure: Building \"") + \
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})+"
1338 result
= f
"(cached) {check_info.result}"
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
1346 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
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.")
1358 print("------------------------------------------------------")
1359 print(logfile
[m
.pos
:])
1360 print("------------------------------------------------------")
1361 print("log regexp: ")
1362 print("------------------------------------------------------")
1364 print("------------------------------------------------------")
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("-----------------------------------------------------")
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?
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
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))
1416 logfile
.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1419 sconf_dir
= sconf_dir
1420 sconstruct
= sconstruct
1422 log
= r
'file \S*%s\,line \d+:' % re
.escape(sconstruct
) + ls
1424 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1426 log
= f
"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1428 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1433 for check
, result
, cache_desc
in zip(checks
, results
, cached
):
1434 log
= re
.escape(f
"scons: Configure: {check}") + ls
1437 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
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
)
1447 conf_filename
= re
.escape(os
.path
.join(sconf_dir
, "conftest")) +\
1448 r
'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?'
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
1465 if ext
in ['.c', '.cpp']:
1466 log
= log
+ conf_filename
+ re
.escape(" <-") + ls
1467 log
= f
"{log}( \\|{nols}*{ls})+?"
1469 log
= f
"{log}({nols}*{ls})*?"
1472 # CR = cached rebuild (up to date)s
1475 re
.escape("scons: Configure: \"") + \
1477 re
.escape("\" is up to date.") + ls
1478 log
= log
+ re
.escape("scons: Configure: The original builder "
1480 log
= f
"{log}( \\|.*{ls})+"
1481 if flag
== self
.NCF
:
1482 # non-cached rebuild failure
1483 log
= f
"{log}({nols}*{ls})*?"
1486 # cached rebuild failure
1488 re
.escape("scons: Configure: Building \"") + \
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})+"
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
1502 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
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.")
1514 print("------------------------------------------------------")
1515 print(logfile
[m
.pos
:])
1516 print("------------------------------------------------------")
1517 print("log regexp: ")
1518 print("------------------------------------------------------")
1520 print("------------------------------------------------------")
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("-----------------------------------------------------")
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
1550 If the Python executable or Python header (if required)
1551 is not found, the test is skipped.
1554 tuple: path to python, include path, library path, library name
1556 python
= os
.environ
.get('python_executable', self
.where_is('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]
1568 exec_prefix = sysconfig.get_config_var("exec_prefix")
1569 include = sysconfig.get_config_var("INCLUDEPY")
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.
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')
1586 include = os.path.join(sys.prefix, 'include', py_ver)
1588 lib_path = os.path.join(sys.prefix, 'lib', py_ver, 'config')
1591 Python_h = os.path.join(include, "Python.h")
1592 if os.path.exists(Python_h):
1598 self
.run(program
=python
, stdin
="""\
1599 import sys, sysconfig, os.path
1600 include = sysconfig.get_config_var("INCLUDEPY")
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):
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
:
1629 sconsflags
= initialize_sconsflags(self
.ignore_python_version
)
1631 p
= super().start(*args
, **kw
)
1633 restore_sconsflags(sconsflags
)
1636 def wait_for(self
, fname
, timeout
: float=20.0, popen
=None) -> None:
1638 Waits for the specified file name to exist.
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')
1649 stdout
= self
.stdout()
1651 sys
.stdout
.write(f
"{self.banner('STDOUT ')}\n")
1652 sys
.stdout
.write(stdout
)
1653 stderr
= self
.stderr()
1655 sys
.stderr
.write(f
"{self.banner('STDERR ')}\n")
1656 sys
.stderr
.write(stderr
)
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'
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':
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()
1698 def __init__(self
, name
, units
, expression
, convert
=None) -> None:
1700 convert
= lambda x
: x
1703 self
.expression
= re
.compile(expression
)
1704 self
.convert
= convert
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
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
)
1749 value
= float(value
)
1753 default_calibrate_variables
.append(variable
)
1755 default_calibrate_variables
.append(variable
)
1756 self
.variables
[variable
] = value
1758 calibrate_keyword_arg
= kw
.get('calibrate')
1759 if calibrate_keyword_arg
is None:
1760 self
.calibrate_variables
= default_calibrate_variables
1762 self
.calibrate_variables
= calibrate_keyword_arg
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
1789 Once as a full build of all targets.
1791 Once again as a (presumably) null or up-to-date build of
1794 The elapsed time to execute each build is printed after
1797 if 'options' not in kw
and self
.variables
:
1799 for variable
, value
in self
.variables
.items():
1800 options
.append(f
'{variable}={value}')
1801 kw
['options'] = ' '.join(options
)
1803 self
.calibration(*args
, **kw
)
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}"
1816 sys
.stdout
.write(line
)
1819 def report_traces(self
, trace
, stats
) -> None:
1820 self
.trace('TimeSCons-elapsed',
1822 self
.elapsed_time(),
1825 for name
, args
in stats
.items():
1826 self
.trace(name
, trace
, **args
)
1828 def uptime(self
) -> None:
1830 fp
= open('/proc/loadavg')
1834 avg1
, avg5
, avg15
= fp
.readline().split(" ")[:3]
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):
1842 for stat
in StatList
:
1843 m
= stat
.expression
.search(input)
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
}
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.
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
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()
1951 result
= TestSCons
.run(self
, *args
, **kw
)
1953 self
.endTime
= time
.perf_counter()
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
):
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
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
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?)*'
2016 # indent-tabs-mode:nil
2018 # vim: set expandtab tabstop=4 shiftwidth=4: