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
45 from typing
import Optional
, Tuple
47 from TestCommon
import *
48 from TestCommon
import __all__
, _python_
49 from SCons
.Util
import get_hash_format
, get_current_hash_algorithm_used
51 from TestCmd
import Popen
52 from TestCmd
import PIPE
54 # Some tests which verify that SCons has been packaged properly need to
55 # look for specific version file names. Replicating the version number
56 # here provides some independent verification that what we packaged
57 # conforms to what we expect.
59 default_version
= '4.8.2ayyyymmdd'
61 # TODO: these need to be hand-edited when there are changes
62 python_version_unsupported
= (3, 6, 0)
63 python_version_deprecated
= (3, 7, 0)
64 python_version_supported_str
= "3.7.0" # str of lowest non-deprecated Python
66 SConsVersion
= default_version
90 except AttributeError:
91 # Windows doesn't have a uname() function. We could use something like
92 # sys.platform as a fallback, but that's not really a "machine," so
93 # just leave it as None.
97 machine
= machine_map
.get(machine
, machine
)
101 _shobj
= shobj_suffix
102 shobj_
= shobj_prefix
108 if sys
.platform
== 'cygwin':
109 # On Cygwin, os.path.normcase() lies, so just report back the
110 # fact that the underlying Win32 OS is case-insensitive.
111 def case_sensitive_suffixes(s1
, s2
) -> int:
114 def case_sensitive_suffixes(s1
, s2
):
115 return (os
.path
.normcase(s1
) != os
.path
.normcase(s2
))
117 file_expr
= r
"""File "[^"]*", line \d+, in [^\n]+
121 # re.escape escapes too much.
123 for c
in '\\.[]()*+?': # Not an exhaustive list.
124 str = str.replace(c
, f
"\\{c}")
129 # Helper functions that we use as a replacement to the default re.match
130 # when searching for special strings in stdout/stderr.
132 def search_re(out
, l
):
133 """ Search the regular expression 'l' in the output 'out'
134 and return the start index when successful.
136 m
= re
.search(l
, out
)
143 def search_re_in_list(out
, l
):
144 """ Search the regular expression 'l' in each line of
145 the given string list 'out' and return the line's index
148 for idx
, o
in enumerate(out
):
157 # Helpers for handling Python version numbers
159 def python_version_string():
160 return sys
.version
.split()[0]
163 def python_minor_version_string():
164 return sys
.version
[:3]
167 def unsupported_python_version(version
=sys
.version_info
):
168 return version
< python_version_unsupported
171 def deprecated_python_version(version
=sys
.version_info
):
172 return version
< python_version_deprecated
175 if deprecated_python_version():
177 scons: warning: Support for Python older than %s is deprecated (%s detected).
178 If this will cause hardship, contact scons-dev@scons.org
180 deprecated_python_expr
= (
181 re_escape(msg
% (python_version_supported_str
, python_version_string()))
186 deprecated_python_expr
= ""
189 def initialize_sconsflags(ignore_python_version
):
191 Add the --warn=no-python-version option to SCONSFLAGS for every
192 command so test scripts don't have to filter out Python version
193 deprecation warnings.
194 Same for --warn=no-visual-c-missing.
196 save_sconsflags
= os
.environ
.get('SCONSFLAGS')
198 sconsflags
= [save_sconsflags
]
201 if ignore_python_version
and deprecated_python_version():
202 sconsflags
.append('--warn=no-python-version')
203 # Provide a way to suppress or provide alternate flags for
204 # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
205 # (The intended use case is to set it to null when running
206 # timing tests of earlier versions of SCons which don't
207 # support the --warn=no-visual-c-missing warning.)
208 visual_c
= os
.environ
.get('TESTSCONS_SCONSFLAGS',
209 '--warn=no-visual-c-missing')
210 if visual_c
and visual_c
not in sconsflags
:
211 sconsflags
.append(visual_c
)
212 os
.environ
['SCONSFLAGS'] = ' '.join(sconsflags
)
213 return save_sconsflags
216 def restore_sconsflags(sconsflags
) -> None:
217 if sconsflags
is None:
218 del os
.environ
['SCONSFLAGS']
220 os
.environ
['SCONSFLAGS'] = sconsflags
223 # Helpers for Configure()'s config.log processing
224 ConfigCheckInfo
= namedtuple('ConfigCheckInfo',
225 ['check_string', 'result', 'cached', 'temp_filename'])
226 # check_string: the string output to for this checker
227 # results : The expected results for each check
228 # cached : If the corresponding check is expected to be cached
229 # temp_filename : The name of the generated tempfile for this check
232 class NoMatch(Exception):
234 Exception for matchPart to indicate there was no match found in the passed logfile
236 def __init__(self
, p
) -> None:
240 def match_part_of_configlog(log
, logfile
, lastEnd
, NoMatch
=NoMatch
):
242 Match part of the logfile
244 # print("Match:\n%s\n==============\n%s" % (log , logfile[lastEnd:]))
245 m
= re
.match(log
, logfile
[lastEnd
:])
247 raise NoMatch(lastEnd
)
248 return m
.end() + lastEnd
251 class TestSCons(TestCommon
):
252 """Class for testing SCons.
254 This provides a common place for initializing SCons tests,
255 eliminating the need to begin every test with the same repeated
259 scons_version
= SConsVersion
262 def __init__(self
, **kw
) -> None:
263 """Initialize an SCons testing object.
265 If they're not overridden by keyword arguments, this
266 initializes the object with the following default values:
268 program = 'scons' if it exists,
270 interpreter = 'python'
274 The workdir value means that, by default, a temporary workspace
275 directory is created for a TestSCons environment. In addition,
276 this method changes directory (chdir) to the workspace directory,
277 so an explicit "chdir = '.'" on all of the run() method calls
280 self
.orig_cwd
= os
.getcwd()
281 self
.external
= os
.environ
.get('SCONS_EXTERNAL_TEST', 0)
283 if not self
.external
:
285 script_dir
= os
.environ
['SCONS_SCRIPT_DIR']
290 if 'program' not in kw
:
291 kw
['program'] = os
.environ
.get('SCONS')
292 if not kw
['program']:
293 if not self
.external
:
294 if os
.path
.exists('scons'):
295 kw
['program'] = 'scons'
297 kw
['program'] = 'scons.py'
299 kw
['program'] = 'scons'
300 kw
['interpreter'] = ''
301 elif not self
.external
and not os
.path
.isabs(kw
['program']):
302 kw
['program'] = os
.path
.join(self
.orig_cwd
, kw
['program'])
303 if 'interpreter' not in kw
and not os
.environ
.get('SCONS_EXEC'):
304 kw
['interpreter'] = [python
, ]
305 if 'match' not in kw
:
306 kw
['match'] = match_exact
307 if 'workdir' not in kw
:
310 # Term causing test failures due to bogus readline init
311 # control character output on FC8
312 # TERM can cause test failures due to control chars in prompts etc.
313 os
.environ
['TERM'] = 'dumb'
315 self
.ignore_python_version
= kw
.get('ignore_python_version', 1)
316 if kw
.get('ignore_python_version', -1) != -1:
317 del kw
['ignore_python_version']
319 super().__init
__(**kw
)
321 if not self
.external
:
323 if SCons
.Node
.FS
.default_fs
is None:
324 SCons
.Node
.FS
.default_fs
= SCons
.Node
.FS
.FS()
327 self
.fixture_dirs
= (os
.environ
['FIXTURE_DIRS']).split(os
.pathsep
)
331 def Environment(self
, ENV
=None, *args
, **kw
):
333 Return a construction Environment that optionally overrides
334 the default external environment with the specified ENV.
336 if not self
.external
:
337 import SCons
.Environment
342 return SCons
.Environment
.Environment(*args
, **kw
)
343 except (SCons
.Errors
.UserError
, SCons
.Errors
.InternalError
):
348 def detect(self
, var
, prog
=None, ENV
=None, norm
=None):
350 Return the detected path to a tool program.
352 Searches first the named construction variable, then
356 var: name of construction variable to check for tool name.
357 prog: tool program to check for.
358 ENV: if present, kwargs to initialize an environment that
359 will be created to perform the lookup.
360 norm: if true, normalize any returned path looked up in
361 the environment to use UNIX-style path separators.
363 Returns: full path to the tool, or None.
366 env
= self
.Environment(ENV
)
368 v
= env
.subst(f
"${var}")
375 result
= env
.WhereIs(prog
)
376 if result
and norm
and os
.sep
!= '/':
377 result
= result
.replace(os
.sep
, '/')
380 return self
.where_is(prog
)
382 def detect_tool(self
, tool
, prog
=None, ENV
=None):
384 Given a tool (i.e., tool specification that would be passed
385 to the "tools=" parameter of Environment()) and a program that
386 corresponds to that tool, return true if and only if we can find
387 that tool using Environment.Detect().
389 By default, prog is set to the value passed into the tools parameter.
394 env
= self
.Environment(ENV
, tools
=[tool
])
397 return env
.Detect([prog
])
399 def where_is(self
, prog
, path
=None, pathext
=None):
401 Given a program, search for it in the specified external PATH,
402 or in the actual external PATH if none is specified.
405 path
= os
.environ
['PATH']
407 if isinstance(prog
, str):
410 result
= TestCmd
.where_is(self
, p
, path
, pathext
)
412 return os
.path
.normpath(result
)
414 import SCons
.Environment
415 env
= SCons
.Environment
.Environment()
416 return env
.WhereIs(prog
, path
, pathext
)
420 def wrap_stdout(self
, build_str
: str="", read_str
: str="", error
: int=0, cleaning
: int=0) -> str:
421 """Wraps "expect" strings in SCons boilerplate.
423 Given strings of expected output specific to a test,
424 returns a string which includes the SCons wrapping such as
425 "Reading ... done", etc.: that is, adds the text that would
426 be left out by running SCons in quiet mode;
427 Makes a complete message to match against.
430 build_str: the message for the execution part of the output.
431 If non-empty, needs to be newline-terminated.
432 read_str: the message for the reading-sconscript part of
433 the output. If non-empty, needs to be newline-terminated.
434 error: if true, expect a fail message rather than a done message.
435 cleaning: index into type messages, if 0 selects
436 build messages, if 1 selects clean messages.
438 cap
, lc
= [('Build', 'build'),
439 ('Clean', 'clean')][cleaning
]
441 term
= f
"scons: {lc}ing terminated because of errors.\n"
443 term
= f
"scons: done {lc}ing targets.\n"
445 return "scons: Reading SConscript files ...\n" + \
447 "scons: done reading SConscript files.\n" + \
448 f
"scons: {cap}ing targets ...\n" + \
452 def run(self
, *args
, **kw
) -> None:
454 Set up SCONSFLAGS for every command so test scripts don't need
455 to worry about unexpected warnings in their output.
457 sconsflags
= initialize_sconsflags(self
.ignore_python_version
)
459 super().run(*args
, **kw
)
461 restore_sconsflags(sconsflags
)
463 # Modifying the options should work and ought to be simpler, but this
464 # class is used for more than just running 'scons' itself. If there's
465 # an automated way of determining whether it's running 'scons' or
466 # something else, this code should be resurected.
467 # options = kw.get('options')
469 # options = [options]
472 # if self.ignore_python_version and deprecated_python_version():
473 # options.append('--warn=no-python-version')
474 # # Provide a way to suppress or provide alternate flags for
475 # # TestSCons purposes by setting TESTSCONS_SCONSFLAGS.
476 # # (The intended use case is to set it to null when running
477 # # timing tests of earlier versions of SCons which don't
478 # # support the --warn=no-visual-c-missing warning.)
479 # visual_c = os.environ.get('TESTSCONS_SCONSFLAGS',
480 # '--warn=no-visual-c-missing')
482 # options.append(visual_c)
483 # kw['options'] = ' '.join(options)
484 # TestCommon.run(self, *args, **kw)
486 def up_to_date(self
, arguments
: str='.', read_str
: str="", **kw
) -> None:
487 """Asserts that all of the targets listed in arguments is
488 up to date, but does not make any assumptions on other targets.
489 This function is most useful in conjunction with the -n option.
492 for arg
in arguments
.split():
493 s
= f
"{s}scons: `{arg}' is up to date.\n"
494 kw
['arguments'] = arguments
495 stdout
= self
.wrap_stdout(read_str
=read_str
, build_str
=s
)
496 # Append '.*' so that timing output that comes after the
497 # up-to-date output is okay.
498 kw
['stdout'] = f
"{re.escape(stdout)}.*"
499 kw
['match'] = self
.match_re_dotall
502 def not_up_to_date(self
, arguments
: str='.', read_str
: str="", **kw
) -> None:
503 """Asserts that none of the targets listed in arguments is
504 up to date, but does not make any assumptions on other targets.
505 This function is most useful in conjunction with the -n option.
508 for arg
in arguments
.split():
509 s
= f
"{s}(?!scons: `{re.escape(arg)}' is up to date.)"
510 s
= f
"({s}[^\n]*\n)*"
511 kw
['arguments'] = arguments
512 stdout
= re
.escape(self
.wrap_stdout(read_str
=read_str
, build_str
='ARGUMENTSGOHERE'))
513 kw
['stdout'] = stdout
.replace('ARGUMENTSGOHERE', s
)
514 kw
['match'] = self
.match_re_dotall
517 def option_not_yet_implemented(self
, option
, arguments
=None, **kw
):
519 Verifies expected behavior for options that are not yet implemented:
520 a warning message, and exit status 1.
522 msg
= f
"Warning: the {option} option is not yet implemented\n"
525 # If it's a long option and the argument string begins with '=',
526 # it's of the form --foo=bar and needs no separating space.
527 if option
[:2] == '--' and arguments
[0] == '=':
528 kw
['arguments'] = option
+ arguments
530 kw
['arguments'] = f
"{option} {arguments}"
531 return self
.run(**kw
)
533 def deprecated_wrap(self
, msg
) -> str:
535 Calculate the pattern that matches a deprecation warning.
537 return f
"\nscons: warning: {re_escape(msg)}\n{file_expr}"
539 def deprecated_fatal(self
, warn
, msg
):
541 Determines if the warning has turned into a fatal error. If so,
542 passes the test, as any remaining runs are now moot.
544 This method expects a SConscript to be present that will causes
545 the warning. The method writes a SConstruct that calls the
546 SConsscript and looks to see what type of result occurs.
548 The pattern that matches the warning is returned.
550 TODO: Actually detect that it's now an error. We don't have any
551 cases yet, so there's no way to test it.
553 self
.write('SConstruct', """if True:
554 WARN = ARGUMENTS.get('WARN')
555 if WARN: SetOption('warn', WARN)
556 SConscript('SConscript')
560 # TODO calculate stderr for fatal error
561 return re_escape('put something here')
563 # no option, should get one of nothing, warning, or error
564 warning
= self
.deprecated_wrap(msg
)
565 self
.run(arguments
='.', stderr
=None)
566 stderr
= self
.stderr()
568 # most common case done first
569 if match_re_dotall(stderr
, warning
):
572 elif match_re_dotall(stderr
, err_out()):
573 # now a fatal error; skip the rest of the tests
576 # test failed; have to do this by hand...
577 stdout
= self
.stdout() or ""
578 print(self
.banner('STDOUT '))
580 print(self
.diff(warning
, stderr
, 'STDERR '))
585 def deprecated_warning(self
, warn
, msg
):
587 Verifies the expected behavior occurs for deprecation warnings.
588 This method expects a SConscript to be present that will causes
589 the warning. The method writes a SConstruct and exercises various
590 combinations of command-line options and SetOption parameters to
591 validate that it performs correctly.
593 The pattern that matches the warning is returned.
595 warning
= self
.deprecated_fatal(warn
, msg
)
597 def RunPair(option
, expected
) -> None:
598 # run the same test with the option on the command line and
599 # then with the option passed via SetOption().
600 self
.run(options
=f
"--warn={option}",
603 match
=match_re_dotall
)
604 self
.run(options
=f
"WARN={option}",
607 match
=match_re_dotall
)
609 # all warnings off, should get no output
610 RunPair('no-deprecated', '')
612 # warning enabled, should get expected output
613 RunPair(warn
, warning
)
615 # warning disabled, should get either nothing or mandatory message
616 expect
= f
"""()|(Can not disable mandataory warning: 'no-{warn}'\n\n{warning})"""
617 RunPair(f
"no-{warn}", expect
)
621 def diff_substr(self
, expect
, actual
, prelen
: int=20, postlen
: int=40) -> str:
623 for x
, y
in zip(expect
, actual
):
625 return "Actual did not match expect at char %d:\n" \
628 % (i
, repr(expect
[i
- prelen
:i
+ postlen
]),
629 repr(actual
[i
- prelen
:i
+ postlen
]))
631 return "Actual matched the expected output???"
633 def python_file_line(self
, file, line
):
635 Returns a Python error line for output comparisons.
637 The exec of the traceback line gives us the correct format for
638 this version of Python.
640 File "<string>", line 1, <module>
642 We stick the requested file name and line number in the right
643 places, abstracting out the version difference.
645 # This routine used to use traceback to get the proper format
646 # that doesn't work well with py3. And the format of the
647 # traceback seems to be stable, so let's just format
648 # an appropriate string
650 # exec('import traceback; x = traceback.format_stack()[-1]')
652 # x = traceback.format_stack()
653 # x = # XXX: .lstrip()
654 # x = x.replace('<string>', file)
655 # x = x.replace('line 1,', 'line %s,' % line)
657 x
= f
'File "{file}", line {line}, in <module>\n'
660 def normalize_ps(self
, s
):
661 s
= re
.sub(r
'(Creation|Mod)Date: .*',
663 s
= re
.sub(r
'%DVIPSSource:\s+TeX output\s.*',
664 r
'%DVIPSSource: TeX output XXXX', s
)
665 s
= re
.sub(r
'/(BaseFont|FontName) /[A-Z0-9]{6}',
667 s
= re
.sub(r
'BeginFont: [A-Z0-9]{6}',
668 r
'BeginFont: XXXXXX', s
)
673 def to_bytes_re_sub(pattern
, repl
, string
, count
: int=0, flags
: int=0):
675 Wrapper around re.sub to change pattern and repl to bytes to work with
678 pattern
= to_bytes(pattern
)
679 repl
= to_bytes(repl
)
680 return re
.sub(pattern
, repl
, string
, count
=count
, flags
=flags
)
682 def normalize_pdf(self
, s
):
683 s
= self
.to_bytes_re_sub(r
'/(Creation|Mod)Date \(D:[^)]*\)',
684 r
'/\1Date (D:XXXX)', s
)
685 s
= self
.to_bytes_re_sub(r
'/ID \[<[0-9a-fA-F]*> <[0-9a-fA-F]*>\]',
686 r
'/ID [<XXXX> <XXXX>]', s
)
687 s
= self
.to_bytes_re_sub(r
'/(BaseFont|FontName) /[A-Z]{6}',
689 s
= self
.to_bytes_re_sub(r
'/Length \d+ *\n/Filter /FlateDecode\n',
690 r
'/Length XXXX\n/Filter /FlateDecode\n', s
)
697 begin_marker
= to_bytes('/FlateDecode\n>>\nstream\n')
698 end_marker
= to_bytes('endstream\nendobj')
701 b
= s
.find(begin_marker
, 0)
703 b
= b
+ len(begin_marker
)
704 e
= s
.find(end_marker
, b
)
705 encoded
.append((b
, e
))
706 b
= s
.find(begin_marker
, e
+ len(end_marker
))
712 d
= zlib
.decompress(s
[b
:e
])
713 d
= self
.to_bytes_re_sub(r
'%%CreationDate: [^\n]*\n',
714 r
'%%CreationDate: 1970 Jan 01 00:00:00\n', d
)
715 d
= self
.to_bytes_re_sub(r
'%DVIPSSource: TeX output \d\d\d\d\.\d\d\.\d\d:\d\d\d\d',
716 r
'%DVIPSSource: TeX output 1970.01.01:0000', d
)
717 d
= self
.to_bytes_re_sub(r
'/(BaseFont|FontName) /[A-Z]{6}',
722 s
= to_bytes('').join(r
)
726 def paths(self
, patterns
):
730 result
.extend(sorted(glob
.glob(p
)))
733 def get_sconsignname(self
) -> str:
734 """Get the scons database name used, and return both the prefix and full filename.
736 if the user left the options defaulted AND the default algorithm set by
737 SCons is md5, then set the database name to be the special default name
739 otherwise, if it defaults to something like 'sha1' or the user explicitly
740 set 'md5' as the hash format, set the database name to .sconsign_<algorithm>
741 eg .sconsign_sha1, etc.
744 a pair containing: the current dbname, the dbname.dblite filename
746 TODO: docstring is not truthful about returning "both" - but which to fix?
747 Say it returns just basename, or return both?
748 TODO: has no way to account for an ``SConsignFile()`` call which might assign
749 a different dbname. Document that it's only useful for hash testing?
751 hash_format
= get_hash_format()
752 current_hash_algorithm
= get_current_hash_algorithm_used()
753 if hash_format
is None and current_hash_algorithm
== 'md5':
756 database_prefix
=f
".sconsign_{current_hash_algorithm}"
757 return database_prefix
760 def unlink_sconsignfile(self
, name
: str = '.sconsign.dblite') -> None:
761 """Delete the sconsign file.
763 Provides a hook to do special things for the sconsign DB,
764 although currently it just calls unlink.
767 name: expected name of sconsign file
769 TODO: deal with suffix if :meth:`getsconsignname` does not provide it.
770 How do we know, since multiple formats are allowed?
772 return self
.unlink(name
)
774 def java_ENV(self
, version
=None):
775 """ Initialize JAVA SDK environment.
777 Initialize with a default external environment that uses a local
778 Java SDK in preference to whatever's found in the default PATH.
781 version: if set, match only that version
786 if not self
.external
:
788 return self
._java
_env
[version
]['ENV']
789 except AttributeError:
794 import SCons
.Environment
795 env
= SCons
.Environment
.Environment()
796 self
._java
_env
[version
] = env
799 if sys
.platform
== 'win32':
801 f
'C:/Program Files*/Java/jdk*{version}*/bin',
805 f
'/usr/java/jdk{version}*/bin',
806 f
'/usr/lib/jvm/*-{version}*/bin',
807 f
'/usr/local/j2sdk{version}*/bin',
809 java_path
= self
.paths(patterns
) + [env
['ENV']['PATH']]
811 if sys
.platform
== 'win32':
813 'C:/Program Files*/Java/jdk*/bin',
817 '/usr/java/latest/bin',
818 '/usr/lib/jvm/*/bin',
819 '/usr/local/j2sdk*/bin',
821 java_path
= self
.paths(patterns
) + [env
['ENV']['PATH']]
823 env
['ENV']['PATH'] = os
.pathsep
.join(java_path
)
828 def java_where_includes(self
, version
=None):
829 """ Find include path needed for compiling java jni code.
832 version: if set, match only that version
835 path to java headers or None
840 if sys
.platform
[:6] == 'darwin':
841 java_home
= self
.java_where_java_home(version
)
842 jni_path
= os
.path
.join(java_home
, 'include', 'jni.h')
843 if os
.path
.exists(jni_path
):
844 result
.append(os
.path
.dirname(jni_path
))
848 jni_dirs
= ['/System/Library/Frameworks/JavaVM.framework/Headers/jni.h',
849 '/usr/lib/jvm/default-java/include/jni.h',
850 '/usr/lib/jvm/java-*-oracle/include/jni.h']
852 jni_dirs
= [f
'/System/Library/Frameworks/JavaVM.framework/Versions/{version}*/Headers/jni.h']
853 jni_dirs
.extend([f
'/usr/lib/jvm/java-*-sun-{version}*/include/jni.h',
854 f
'/usr/lib/jvm/java-{version}*-openjdk*/include/jni.h',
855 f
'/usr/java/jdk{version}*/include/jni.h'])
856 dirs
= self
.paths(jni_dirs
)
859 d
= os
.path
.dirname(self
.paths(jni_dirs
)[0])
862 if sys
.platform
== 'win32':
863 result
.append(os
.path
.join(d
, 'win32'))
864 elif sys
.platform
.startswith('linux'):
865 result
.append(os
.path
.join(d
, 'linux'))
868 def java_where_java_home(self
, version
=None) -> Optional
[str]:
869 """ Find path to what would be JAVA_HOME.
871 SCons does not read JAVA_HOME from the environment, so deduce it.
874 version: if set, match only that version
877 path where JDK components live
878 Bails out of the entire test (skip) if not found.
880 if sys
.platform
[:6] == 'darwin':
882 home_tool
= '/usr/libexec/java_home'
884 if os
.path
.exists(home_tool
):
885 cp
= sp
.run(home_tool
, stdout
=sp
.PIPE
, stderr
=sp
.STDOUT
)
886 if cp
.returncode
== 0:
887 java_home
= cp
.stdout
.decode().strip()
893 '/System/Library/Frameworks/JavaVM.framework/Home',
895 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/Home'
897 if os
.path
.exists(home
):
900 if java_home
.find(f
'jdk{version}') != -1:
903 f
'/System/Library/Frameworks/JavaVM.framework/Versions/{version}/Home',
905 '/System/Library/Frameworks/JavaVM.framework/Versions/Current/'
907 if os
.path
.exists(home
):
909 # if we fell through, make sure flagged as not found
912 jar
= self
.java_where_jar(version
)
913 home
= os
.path
.normpath(f
'{jar}/..')
915 if home
and os
.path
.isdir(home
):
919 "Could not run Java: unable to detect valid JAVA_HOME, skipping test.\n",
924 def java_mac_check(self
, where_java_bin
, java_bin_name
) -> None:
925 """Extra check for Java on MacOS.
927 MacOS has a place holder java/javac, which fails with a detectable
928 error if Java is not actually installed, and works normally if it is.
929 Note msg has changed over time.
931 Bails out of the entire test (skip) if not found.
933 cp
= sp
.run([where_java_bin
, "-version"], stdout
=sp
.PIPE
, stderr
=sp
.STDOUT
)
935 b
"No Java runtime" in cp
.stdout
936 or b
"Unable to locate a Java Runtime" in cp
.stdout
939 f
"Could not find Java {java_bin_name}, skipping test.\n",
943 def java_where_jar(self
, version
=None) -> str:
944 """ Find java archiver jar.
947 version: if set, match only that version
952 ENV
= self
.java_ENV(version
)
953 if self
.detect_tool('jar', ENV
=ENV
):
954 where_jar
= self
.detect('JAR', 'jar', ENV
=ENV
)
956 where_jar
= self
.where_is('jar', ENV
['PATH'])
958 self
.skip_test("Could not find Java jar, skipping test(s).\n", from_fw
=True)
959 elif sys
.platform
== "darwin":
960 self
.java_mac_check(where_jar
, 'jar')
964 def java_where_java(self
, version
=None) -> str:
965 """ Find java executable.
968 version: if set, match only that version
971 path to the java rutime
973 ENV
= self
.java_ENV(version
)
974 where_java
= self
.where_is('java', ENV
['PATH'])
977 self
.skip_test("Could not find Java java, skipping test(s).\n", from_fw
=True)
978 elif sys
.platform
== "darwin":
979 self
.java_mac_check(where_java
, 'java')
983 def java_where_javac(self
, version
=None) -> Tuple
[str, str]:
984 """ Find java compiler.
987 version: if set, match only that version
992 ENV
= self
.java_ENV(version
)
993 if self
.detect_tool('javac'):
994 where_javac
= self
.detect('JAVAC', 'javac', ENV
=ENV
)
996 where_javac
= self
.where_is('javac', ENV
['PATH'])
998 self
.skip_test("Could not find Java javac, skipping test(s).\n", from_fw
=True)
999 elif sys
.platform
== "darwin":
1000 self
.java_mac_check(where_javac
, 'javac')
1002 self
.run(program
=where_javac
,
1003 arguments
='-version',
1006 # Note recent versions output version info to stdout instead of stderr
1007 stdout
= self
.stdout() or ""
1008 stderr
= self
.stderr() or ""
1010 verf
= f
'javac {version}'
1011 if stderr
.find(verf
) == -1 and stdout
.find(verf
) == -1:
1012 fmt
= "Could not find javac for Java version %s, skipping test(s).\n"
1013 self
.skip_test(fmt
% version
, from_fw
=True)
1015 version_re
= r
'javac (\d*\.*\d)'
1016 m
= re
.search(version_re
, stderr
)
1018 m
= re
.search(version_re
, stdout
)
1021 version
= m
.group(1)
1022 self
.javac_is_gcj
= False
1023 return where_javac
, version
1025 if stderr
.find('gcj') != -1:
1027 self
.javac_is_gcj
= True
1030 self
.javac_is_gcj
= False
1031 return where_javac
, version
1033 def java_where_javah(self
, version
=None) -> str:
1034 """ Find java header generation tool.
1036 TODO issue #3347 since JDK10, there is no separate javah command,
1037 'javac -h' is used. We should not return a javah from a different
1038 installed JDK - how to detect and what to return in this case?
1041 version: if set, match only that version
1046 ENV
= self
.java_ENV(version
)
1047 if self
.detect_tool('javah'):
1048 where_javah
= self
.detect('JAVAH', 'javah', ENV
=ENV
)
1050 where_javah
= self
.where_is('javah', ENV
['PATH'])
1052 self
.skip_test("Could not find Java javah, skipping test(s).\n", from_fw
=True)
1055 def java_where_rmic(self
, version
=None) -> str:
1056 """ Find java rmic tool.
1059 version: if set, match only that version
1064 ENV
= self
.java_ENV(version
)
1065 if self
.detect_tool('rmic'):
1066 where_rmic
= self
.detect('RMIC', 'rmic', ENV
=ENV
)
1068 where_rmic
= self
.where_is('rmic', ENV
['PATH'])
1070 self
.skip_test("Could not find Java rmic, skipping non-simulated test(s).\n", from_fw
=True)
1073 def java_get_class_files(self
, dir):
1075 for dirpath
, dirnames
, filenames
in os
.walk(dir):
1076 for fname
in filenames
:
1077 if fname
.endswith('.class'):
1078 result
.append(os
.path
.join(dirpath
, fname
))
1079 return sorted(result
)
1081 def Qt_dummy_installation(self
, dir: str='qt') -> None:
1082 # create a dummy qt installation
1084 self
.subdir(dir, [dir, 'bin'], [dir, 'include'], [dir, 'lib'])
1086 self
.write([dir, 'bin', 'mymoc.py'], """\
1090 # -w and -z are fake options used in test/QT/QTFLAGS.py
1091 cmd_opts, args = getopt.getopt(sys.argv[1:], 'io:wz', [])
1094 for opt, arg in cmd_opts:
1095 if opt == '-o': outfile = arg
1096 elif opt == '-i': impl = 1
1097 else: opt_string = opt_string + ' ' + opt
1099 with open(outfile, 'w') as ofp:
1100 ofp.write("/* mymoc.py%s */\\n" % opt_string)
1102 with open(a, 'r') as ifp:
1103 contents = ifp.read()
1104 a = a.replace('\\\\', '\\\\\\\\')
1105 subst = r'{ my_qt_symbol( "' + a + '\\\\n" ); }'
1107 contents = re.sub(r'#include.*', '', contents)
1108 ofp.write(contents.replace('Q_OBJECT', subst))
1112 self
.write([dir, 'bin', 'myuic.py'], """\
1121 for arg in sys.argv[1:]:
1130 elif arg == "-impl":
1132 elif arg[0:1] == "-":
1133 opt_string = opt_string + ' ' + arg
1137 source = sourceFile = arg
1139 with open(outfile, 'w') as ofp, open(source, 'r') as ifp:
1140 ofp.write("/* myuic.py%s */\\n" % opt_string)
1142 ofp.write('#include "' + impl + '"\\n')
1143 includes = re.findall('<include.*?>(.*?)</include>', ifp.read())
1144 for incFile in includes:
1145 # this is valid for ui.h files, at least
1146 if os.path.exists(incFile):
1147 ofp.write('#include "' + incFile + '"\\n')
1149 ofp.write('#include "my_qobject.h"\\n' + ifp.read() + " Q_OBJECT \\n")
1153 self
.write([dir, 'include', 'my_qobject.h'], r
"""
1155 void my_qt_symbol(const char *arg);
1158 self
.write([dir, 'lib', 'my_qobject.cpp'], r
"""
1159 #include "../include/my_qobject.h"
1161 void my_qt_symbol(const char *arg) {
1166 self
.write([dir, 'lib', 'SConstruct'], r
"""
1168 DefaultEnvironment(tools=[]) # test speedup
1170 if sys.platform == 'win32':
1171 env.StaticLibrary('myqt', 'my_qobject.cpp')
1173 env.SharedLibrary('myqt', 'my_qobject.cpp')
1176 self
.run(chdir
=self
.workpath(dir, 'lib'),
1179 match
=self
.match_re_dotall
)
1181 self
.QT
= self
.workpath(dir)
1182 self
.QT_LIB
= 'myqt'
1183 self
.QT_MOC
= f
"{_python_} {self.workpath(dir, 'bin', 'mymoc.py')}"
1184 self
.QT_UIC
= f
"{_python_} {self.workpath(dir, 'bin', 'myuic.py')}"
1185 self
.QT_LIB_DIR
= self
.workpath(dir, 'lib')
1187 def Qt_create_SConstruct(self
, place
, qt_tool
: str='qt3') -> None:
1188 if isinstance(place
, list):
1189 place
= self
.workpath(*place
)
1191 var_prefix
=qt_tool
.upper()
1192 self
.write(place
, f
"""\
1193 if ARGUMENTS.get('noqtdir', 0):
1194 {var_prefix}DIR = None
1196 {var_prefix}DIR = r'{self.QT}'
1197 DefaultEnvironment(tools=[]) # test speedup
1199 {var_prefix}DIR={var_prefix}DIR, {var_prefix}_LIB=r'{self.QT_LIB}', {var_prefix}_MOC=r'{self.QT_MOC}',
1200 {var_prefix}_UIC=r'{self.QT_UIC}', tools=['default', '{qt_tool}']
1203 if ARGUMENTS.get('variant_dir', 0):
1204 if ARGUMENTS.get('chdir', 0):
1208 dup = int(ARGUMENTS.get('dup', 1))
1210 builddir = 'build_dup0'
1214 VariantDir(builddir, '.', duplicate=dup)
1215 print(builddir, dup)
1216 sconscript = Dir(builddir).File('SConscript')
1218 sconscript = File('SConscript')
1220 SConscript(sconscript)
1223 NCR
= 0 # non-cached rebuild
1224 CR
= 1 # cached rebuild (up to date)
1225 NCF
= 2 # non-cached build failure
1226 CF
= 3 # cached build failure
1228 if sys
.platform
== 'win32':
1229 Configure_lib
= 'msvcrt'
1233 # to use cygwin compilers on cmd.exe -> uncomment following line
1234 # Configure_lib = 'm'
1236 def coverage_run(self
) -> bool:
1237 """ Check if the the tests are being run under coverage.
1239 return 'COVERAGE_PROCESS_START' in os
.environ
or 'COVERAGE_FILE' in os
.environ
1241 def skip_if_not_msvc(self
, check_platform
: bool=True) -> None:
1242 """ Skip test if MSVC is not available.
1244 Check whether we are on a Windows platform and skip the test if
1245 not. This check can be omitted by setting check_platform to False.
1247 Then, for a win32 platform, additionally check whether we have
1248 an MSVC toolchain installed in the system, and skip the test if
1249 none can be found (e.g. MinGW is the only compiler available).
1252 if sys
.platform
!= 'win32':
1253 msg
= f
"Skipping Visual C/C++ test on non-Windows platform '{sys.platform}'\n"
1254 self
.skip_test(msg
, from_fw
=True)
1258 import SCons
.Tool
.MSCommon
as msc
1259 if not msc
.msvc_exists():
1260 msg
= "No MSVC toolchain found...skipping test\n"
1261 self
.skip_test(msg
, from_fw
=True)
1265 def checkConfigureLogAndStdout(self
, checks
,
1266 logfile
: str='config.log',
1267 sconf_dir
: str='.sconf_temp',
1268 sconstruct
: str="SConstruct",
1269 doCheckLog
: bool=True, doCheckStdout
: bool=True):
1270 """ Verify expected output from Configure.
1272 Used to verify the expected output from using Configure()
1273 via the contents of one or both of stdout or config.log file.
1274 If the algorithm does not succeed, the test is marked a fail
1275 and this function does not return.
1277 TODO: Perhaps a better API makes sense?
1280 checks: list of ConfigCheckInfo tuples which specify
1281 logfile: Name of the config log
1282 sconf_dir: Name of the sconf dir
1283 sconstruct: SConstruct file name
1284 doCheckLog: check specified log file, defaults to true
1285 doCheckStdout: Check stdout, defaults to true
1293 # Read the whole logfile
1294 logfile
= self
.read(self
.workpath(logfile
), mode
='r')
1296 # Some debug code to keep around..
1297 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1300 logfile
.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1303 log
= r
'file \S*%s\,line \d+:' % re
.escape(sconstruct
) + ls
1305 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1307 log
= f
"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1309 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1313 for check_info
in checks
:
1314 log
= re
.escape(f
"scons: Configure: {check_info.check_string}") + ls
1317 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1321 for bld_desc
in check_info
.cached
: # each TryXXX
1322 for ext
, flag
in bld_desc
: # each file in TryBuild
1323 conf_filename
= re
.escape(check_info
.temp_filename
%ext
)
1325 if flag
== self
.NCR
:
1326 # NCR = Non Cached Rebuild
1328 if ext
in ['.c', '.cpp']:
1329 log
= log
+ conf_filename
+ re
.escape(" <-") + ls
1330 log
= f
"{log}( \\|{nols}*{ls})+?"
1332 log
= f
"{log}({nols}*{ls})*?"
1335 # CR = cached rebuild (up to date)s
1338 re
.escape("scons: Configure: \"") + \
1340 re
.escape("\" is up to date.") + ls
1341 log
= log
+ re
.escape("scons: Configure: The original builder "
1343 log
= f
"{log}( \\|.*{ls})+"
1344 if flag
== self
.NCF
:
1345 # non-cached rebuild failure
1346 log
= f
"{log}({nols}*{ls})*?"
1349 # cached rebuild failure
1351 re
.escape("scons: Configure: Building \"") + \
1353 re
.escape("\" failed in a previous run and all its sources are up to date.") + ls
1354 log
= log
+ re
.escape("scons: Configure: The original builder output was:") + ls
1355 log
= f
"{log}( \\|.*{ls})+"
1357 result
= f
"(cached) {check_info.result}"
1359 result
= check_info
.result
1360 rdstr
= f
"{rdstr + re.escape(check_info.check_string) + re.escape(result)}\n"
1362 log
= log
+ re
.escape(f
"scons: Configure: {result}") + ls
+ ls
1365 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1369 lastEnd
= match_part_of_configlog(ls
, logfile
, lastEnd
)
1371 if doCheckLog
and lastEnd
!= len(logfile
):
1372 raise NoMatch(lastEnd
)
1374 except NoMatch
as m
:
1375 print("Cannot match log file against log regexp.")
1377 print("------------------------------------------------------")
1378 print(logfile
[m
.pos
:])
1379 print("------------------------------------------------------")
1380 print("log regexp: ")
1381 print("------------------------------------------------------")
1383 print("------------------------------------------------------")
1387 exp_stdout
= self
.wrap_stdout(".*", rdstr
)
1388 stdout
= self
.stdout() or ""
1389 if not self
.match_re_dotall(stdout
, exp_stdout
):
1390 print("Unexpected stdout: ")
1391 print("-----------------------------------------------------")
1393 print("-----------------------------------------------------")
1394 print(repr(exp_stdout
))
1395 print("-----------------------------------------------------")
1400 def checkLogAndStdout(self
, checks
, results
, cached
,
1401 logfile
, sconf_dir
, sconstruct
,
1402 doCheckLog
: bool=True, doCheckStdout
: bool=True):
1403 """ Verify expected output from Configure.
1405 Used to verify the expected output from using Configure()
1406 via the contents of one or both of stdout or config.log file.
1407 The checks, results, cached parameters all are zipped together
1408 for use in comparing results. If the algorithm does not
1409 succeed, the test is marked a fail and this function does not return.
1411 TODO: Perhaps a better API makes sense?
1414 checks: The Configure checks being run
1415 results: The expected results for each check
1416 cached: If the corresponding check is expected to be cached
1417 logfile: Name of the config log
1418 sconf_dir: Name of the sconf dir
1419 sconstruct: SConstruct file name
1420 doCheckLog: check specified log file, defaults to true
1421 doCheckStdout: Check stdout, defaults to true
1429 # Read the whole logfile
1430 logfile
= self
.read(self
.workpath(logfile
), mode
='r')
1432 # Some debug code to keep around..
1433 # sys.stderr.write("LOGFILE[%s]:%s"%(type(logfile),logfile))
1436 logfile
.find("scons: warning: The stored build information has an unexpected class.") >= 0):
1439 sconf_dir
= sconf_dir
1440 sconstruct
= sconstruct
1442 log
= r
'file \S*%s\,line \d+:' % re
.escape(sconstruct
) + ls
1444 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1446 log
= f
"\t{re.escape(f'Configure(confdir = {sconf_dir})')}" + ls
1448 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1453 for check
, result
, cache_desc
in zip(checks
, results
, cached
):
1454 log
= re
.escape(f
"scons: Configure: {check}") + ls
1457 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1461 for bld_desc
in cache_desc
: # each TryXXX
1462 for ext
, flag
in bld_desc
: # each file in TryBuild
1463 if ext
in ['.c', '.cpp']:
1464 conf_filename
= re
.escape(os
.path
.join(sconf_dir
, "conftest")) +\
1465 r
'_[a-z0-9]{32,64}_\d+%s' % re
.escape(ext
)
1467 conf_filename
= re
.escape(os
.path
.join(sconf_dir
, "conftest")) +\
1468 r
'_[a-z0-9]{32,64}(_\d+_[a-z0-9]{32,64})?'
1471 # We allow the second hash group to be optional because
1472 # TryLink() will create a c file, then compile to obj, then link that
1473 # The intermediate object file will not get the action hash
1474 # But TryCompile()'s where the product is the .o will get the
1475 # action hash. Rather than add a ton of complications to this logic
1476 # this shortcut should be sufficient.
1477 # TODO: perhaps revisit and/or fix file naming for intermediate files in
1478 # Configure context logic
1479 conf_filename
= re
.escape(os
.path
.join(sconf_dir
, "conftest")) +\
1480 r
'_[a-z0-9]{32,64}_\d+(_[a-z0-9]{32,64})?%s' % re
.escape(ext
)
1482 if flag
== self
.NCR
:
1483 # NCR = Non Cached Rebuild
1485 if ext
in ['.c', '.cpp']:
1486 log
= log
+ conf_filename
+ re
.escape(" <-") + ls
1487 log
= f
"{log}( \\|{nols}*{ls})+?"
1489 log
= f
"{log}({nols}*{ls})*?"
1492 # CR = cached rebuild (up to date)s
1495 re
.escape("scons: Configure: \"") + \
1497 re
.escape("\" is up to date.") + ls
1498 log
= log
+ re
.escape("scons: Configure: The original builder "
1500 log
= f
"{log}( \\|.*{ls})+"
1501 if flag
== self
.NCF
:
1502 # non-cached rebuild failure
1503 log
= f
"{log}({nols}*{ls})*?"
1506 # cached rebuild failure
1508 re
.escape("scons: Configure: Building \"") + \
1510 re
.escape("\" failed in a previous run and all its sources are up to date.") + ls
1511 log
= log
+ re
.escape("scons: Configure: The original builder output was:") + ls
1512 log
= f
"{log}( \\|.*{ls})+"
1515 result
= f
"(cached) {result}"
1517 rdstr
= f
"{rdstr + re.escape(check) + re.escape(result)}\n"
1519 log
= log
+ re
.escape(f
"scons: Configure: {result}") + ls
+ ls
1522 lastEnd
= match_part_of_configlog(log
, logfile
, lastEnd
)
1526 lastEnd
= match_part_of_configlog(ls
, logfile
, lastEnd
)
1528 if doCheckLog
and lastEnd
!= len(logfile
):
1529 raise NoMatch(lastEnd
)
1531 except NoMatch
as m
:
1532 print("Cannot match log file against log regexp.")
1534 print("------------------------------------------------------")
1535 print(logfile
[m
.pos
:])
1536 print("------------------------------------------------------")
1537 print("log regexp: ")
1538 print("------------------------------------------------------")
1540 print("------------------------------------------------------")
1544 exp_stdout
= self
.wrap_stdout(".*", rdstr
)
1545 stdout
= self
.stdout() or ""
1546 if not self
.match_re_dotall(stdout
, exp_stdout
):
1547 print("Unexpected stdout: ")
1548 print("----Actual-------------------------------------------")
1550 print("----Expected-----------------------------------------")
1551 print(repr(exp_stdout
))
1552 print("-----------------------------------------------------")
1555 def get_python_version(self
) -> str:
1556 """ Returns the Python version.
1558 Convenience function so everyone doesn't have to
1559 hand-code slicing the right number of characters
1561 # see also sys.prefix documentation
1562 return python_minor_version_string()
1564 def get_platform_python_info(self
, python_h_required
: bool=False):
1565 """Return information about Python.
1567 Returns a path to a Python executable suitable for testing on
1568 this platform and its associated include path, library path and
1571 If the Python executable or Python header (if required)
1572 is not found, the test is skipped.
1575 tuple: path to python, include path, library path, library name
1577 python
= os
.environ
.get('python_executable', self
.where_is('python'))
1579 self
.skip_test('Can not find installed "python", skipping test.\n', from_fw
=True)
1581 # construct a program to run in the intended environment
1582 # in order to fetch the characteristics of that Python.
1583 # Windows Python doesn't store all the info in config vars.
1584 if sys
.platform
== 'win32':
1585 self
.run(program
=python
, stdin
="""\
1586 import sysconfig, sys, os.path
1587 py_ver = 'python%d%d' % sys.version_info[:2]
1589 exec_prefix = sysconfig.get_config_var("exec_prefix")
1590 include = sysconfig.get_config_var("INCLUDEPY")
1592 lib_path = os.path.join(exec_prefix, 'libs')
1593 if not os.path.exists(lib_path):
1594 # check for virtualenv path.
1595 # this might not build anything different than first try.
1597 if hasattr(sys, 'real_prefix'):
1598 return sys.real_prefix
1599 if hasattr(sys, 'base_prefix'):
1600 return sys.base_prefix
1601 lib_path = os.path.join(venv_path(), 'libs')
1602 if not os.path.exists(lib_path):
1603 # not clear this is useful: 'lib' does not contain linkable libs
1604 lib_path = os.path.join(exec_prefix, 'lib')
1607 include = os.path.join(sys.prefix, 'include', py_ver)
1609 lib_path = os.path.join(sys.prefix, 'lib', py_ver, 'config')
1612 Python_h = os.path.join(include, "Python.h")
1613 if os.path.exists(Python_h):
1619 self
.run(program
=python
, stdin
="""\
1620 import sys, sysconfig, os.path
1621 include = sysconfig.get_config_var("INCLUDEPY")
1623 print(sysconfig.get_config_var("LIBDIR"))
1624 py_library_ver = sysconfig.get_config_var("LDVERSION")
1625 if not py_library_ver:
1626 py_library_ver = '%d.%d' % sys.version_info[:2]
1627 print("python"+py_library_ver)
1628 Python_h = os.path.join(include, "Python.h")
1629 if os.path.exists(Python_h):
1634 stdout
= self
.stdout() or ""
1635 incpath
, libpath
, libname
, python_h
= stdout
.strip().split('\n')
1636 if python_h
== "False" and python_h_required
:
1637 self
.skip_test('Can not find required "Python.h", skipping test.\n', from_fw
=True)
1639 return (python
, incpath
, libpath
, libname
+ _lib
)
1641 def start(self
, *args
, **kw
):
1643 Starts SCons in the test environment.
1645 This method exists to tell Test{Cmd,Common} that we're going to
1646 use standard input without forcing every .start() call in the
1647 individual tests to do so explicitly.
1649 if 'stdin' not in kw
:
1651 sconsflags
= initialize_sconsflags(self
.ignore_python_version
)
1653 p
= super().start(*args
, **kw
)
1655 restore_sconsflags(sconsflags
)
1658 def wait_for(self
, fname
, timeout
: float=20.0, popen
=None) -> None:
1660 Waits for the specified file name to exist.
1663 while not os
.path
.exists(fname
):
1664 if timeout
and waited
>= timeout
:
1665 sys
.stderr
.write(f
'timed out waiting for {fname} to exist\n')
1671 stdout
= self
.stdout()
1673 sys
.stdout
.write(f
"{self.banner('STDOUT ')}\n")
1674 sys
.stdout
.write(stdout
)
1675 stderr
= self
.stderr()
1677 sys
.stderr
.write(f
"{self.banner('STDERR ')}\n")
1678 sys
.stderr
.write(stderr
)
1681 waited
= waited
+ 1.0
1683 def get_alt_cpp_suffix(self
):
1684 """Return alternate C++ file suffix.
1686 Many CXX tests have this same logic.
1687 They all needed to determine if the current os supports
1688 files with .C and .c as different files or not
1689 in which case they are instructed to use .cpp instead of .C
1691 if not case_sensitive_suffixes('.c', '.C'):
1692 alt_cpp_suffix
= '.cpp'
1694 alt_cpp_suffix
= '.C'
1695 return alt_cpp_suffix
1697 def platform_has_symlink(self
) -> bool:
1698 """Retun an indication of whether symlink tests should be run.
1700 Despite the name, we really mean "are they reliably usable"
1701 rather than "do they exist" - basically the Windows case.
1703 if not hasattr(os
, 'symlink') or sys
.platform
== 'win32':
1708 def zipfile_contains(self
, zipfilename
, names
):
1709 """Returns True if zipfilename contains all the names, False otherwise."""
1710 with zipfile
.ZipFile(zipfilename
, 'r') as zf
:
1711 return all(elem
in zf
.namelist() for elem
in names
)
1713 def zipfile_files(self
, fname
):
1714 """Returns all the filenames in zip file fname."""
1715 with zipfile
.ZipFile(fname
, 'r') as zf
:
1716 return zf
.namelist()
1720 def __init__(self
, name
, units
, expression
, convert
=None) -> None:
1722 convert
= lambda x
: x
1725 self
.expression
= re
.compile(expression
)
1726 self
.convert
= convert
1730 Stat('memory-initial', 'kbytes',
1731 r
'Memory before reading SConscript files:\s+(\d+)',
1732 convert
=lambda s
: int(s
) // 1024),
1733 Stat('memory-prebuild', 'kbytes',
1734 r
'Memory before building targets:\s+(\d+)',
1735 convert
=lambda s
: int(s
) // 1024),
1736 Stat('memory-final', 'kbytes',
1737 r
'Memory after building targets:\s+(\d+)',
1738 convert
=lambda s
: int(s
) // 1024),
1740 Stat('time-sconscript', 'seconds',
1741 r
'Total SConscript file execution time:\s+([\d.]+) seconds'),
1742 Stat('time-scons', 'seconds',
1743 r
'Total SCons execution time:\s+([\d.]+) seconds'),
1744 Stat('time-commands', 'seconds',
1745 r
'Total command execution time:\s+([\d.]+) seconds'),
1746 Stat('time-total', 'seconds',
1747 r
'Total build time:\s+([\d.]+) seconds'),
1751 class TimeSCons(TestSCons
):
1752 """Class for timing SCons."""
1754 def __init__(self
, *args
, **kw
) -> None:
1756 In addition to normal TestSCons.TestSCons intialization,
1757 this enables verbose mode (which causes the command lines to
1758 be displayed in the output) and copies the contents of the
1759 directory containing the executing script to the temporary
1762 self
.variables
: dict = kw
.get('variables')
1763 default_calibrate_variables
= []
1764 if self
.variables
is not None:
1765 for variable
, value
in self
.variables
.items():
1766 value
= os
.environ
.get(variable
, value
)
1771 value
= float(value
)
1775 default_calibrate_variables
.append(variable
)
1777 default_calibrate_variables
.append(variable
)
1778 self
.variables
[variable
] = value
1780 calibrate_keyword_arg
= kw
.get('calibrate')
1781 if calibrate_keyword_arg
is None:
1782 self
.calibrate_variables
= default_calibrate_variables
1784 self
.calibrate_variables
= calibrate_keyword_arg
1787 self
.calibrate
= os
.environ
.get('TIMESCONS_CALIBRATE', '0') != '0'
1789 if 'verbose' not in kw
and not self
.calibrate
:
1790 kw
['verbose'] = True
1792 super().__init
__(*args
, **kw
)
1794 # TODO(sgk): better way to get the script dir than sys.argv[0]
1795 self
.test_dir
= os
.path
.dirname(sys
.argv
[0])
1796 test_name
= os
.path
.basename(self
.test_dir
)
1798 if not os
.path
.isabs(self
.test_dir
):
1799 self
.test_dir
= os
.path
.join(self
.orig_cwd
, self
.test_dir
)
1800 self
.copy_timing_configuration(self
.test_dir
, self
.workpath())
1802 def main(self
, *args
, **kw
) -> None:
1804 The main entry point for standard execution of timings.
1806 This method run SCons three times:
1808 Once with the --help option, to have it exit after just reading
1811 Once as a full build of all targets.
1813 Once again as a (presumably) null or up-to-date build of
1816 The elapsed time to execute each build is printed after
1819 if 'options' not in kw
and self
.variables
:
1821 for variable
, value
in self
.variables
.items():
1822 options
.append(f
'{variable}={value}')
1823 kw
['options'] = ' '.join(options
)
1825 self
.calibration(*args
, **kw
)
1828 self
.startup(*args
, **kw
)
1829 self
.full(*args
, **kw
)
1830 self
.null(*args
, **kw
)
1832 def trace(self
, graph
, name
, value
, units
, sort
=None) -> None:
1833 fmt
= "TRACE: graph=%s name=%s value=%s units=%s"
1834 line
= fmt
% (graph
, name
, value
, units
)
1835 if sort
is not None:
1836 line
= f
"{line} sort={sort}"
1838 sys
.stdout
.write(line
)
1841 def report_traces(self
, trace
, stats
) -> None:
1842 self
.trace('TimeSCons-elapsed',
1844 self
.elapsed_time(),
1847 for name
, args
in stats
.items():
1848 self
.trace(name
, trace
, **args
)
1850 def uptime(self
) -> None:
1852 fp
= open('/proc/loadavg')
1856 avg1
, avg5
, avg15
= fp
.readline().split(" ")[:3]
1858 self
.trace('load-average', 'average1', avg1
, 'processes')
1859 self
.trace('load-average', 'average5', avg5
, 'processes')
1860 self
.trace('load-average', 'average15', avg15
, 'processes')
1862 def collect_stats(self
, input):
1864 for stat
in StatList
:
1865 m
= stat
.expression
.search(input)
1867 value
= stat
.convert(m
.group(1))
1868 # The dict keys match the keyword= arguments
1869 # of the trace() method above so they can be
1870 # applied directly to that call.
1871 result
[stat
.name
] = {'value': value
, 'units': stat
.units
}
1874 def add_timing_options(self
, kw
, additional
=None) -> None:
1876 Add the necessary timings options to the kw['options'] value.
1878 options
= kw
.get('options', '')
1879 if additional
is not None:
1880 options
+= additional
1881 kw
['options'] = f
"{options} --debug=memory,time"
1883 def startup(self
, *args
, **kw
) -> None:
1885 Runs scons with the --help option.
1887 This serves as a way to isolate just the amount of startup time
1888 spent reading up the configuration, since --help exits before any
1889 "real work" is done.
1891 self
.add_timing_options(kw
, ' --help')
1892 # Ignore the exit status. If the --help run dies, we just
1893 # won't report any statistics for it, but we can still execute
1894 # the full and null builds.
1896 self
.run(*args
, **kw
)
1897 stdout
= self
.stdout() or ""
1898 sys
.stdout
.write(stdout
)
1899 stats
= self
.collect_stats(stdout
)
1900 # Delete the time-commands, since no commands are ever
1901 # executed on the help run and it is (or should be) always 0.0.
1902 del stats
['time-commands']
1903 self
.report_traces('startup', stats
)
1905 def full(self
, *args
, **kw
) -> None:
1907 Runs a full build of SCons.
1909 self
.add_timing_options(kw
)
1910 self
.run(*args
, **kw
)
1911 stdout
= self
.stdout() or ""
1912 sys
.stdout
.write(stdout
)
1913 stats
= self
.collect_stats(stdout
)
1914 self
.report_traces('full', stats
)
1915 self
.trace('full-memory', 'initial', **stats
['memory-initial'])
1916 self
.trace('full-memory', 'prebuild', **stats
['memory-prebuild'])
1917 self
.trace('full-memory', 'final', **stats
['memory-final'])
1919 def calibration(self
, *args
, **kw
) -> None:
1921 Runs a full build of SCons, but only reports calibration
1922 information (the variable(s) that were set for this configuration,
1923 and the elapsed time to run.
1925 self
.add_timing_options(kw
)
1926 self
.run(*args
, **kw
)
1927 for variable
in self
.calibrate_variables
:
1928 value
= self
.variables
[variable
]
1929 sys
.stdout
.write(f
'VARIABLE: {variable}={value}\n')
1930 sys
.stdout
.write(f
'ELAPSED: {self.elapsed_time()}\n')
1932 def null(self
, *args
, **kw
) -> None:
1934 Runs an up-to-date null build of SCons.
1936 # TODO(sgk): allow the caller to specify the target (argument)
1937 # that must be up-to-date.
1938 self
.add_timing_options(kw
)
1940 # Build up regex for
1941 # SConscript:/private/var/folders/ng/48pttrpj239fw5rmm3x65pxr0000gn/T/testcmd.12081.pk1bv5i5/SConstruct took 533.646 ms
1942 read_str
= 'SConscript:.*\n'
1943 self
.up_to_date(arguments
='.', read_str
=read_str
, **kw
)
1944 stdout
= self
.stdout() or ""
1945 sys
.stdout
.write(stdout
)
1946 stats
= self
.collect_stats(stdout
)
1947 # time-commands should always be 0.0 on a null build, because
1948 # no commands should be executed. Remove it from the stats
1949 # so we don't trace it, but only if it *is* 0 so that we'll
1950 # get some indication if a supposedly-null build actually does
1952 if float(stats
['time-commands']['value']) == 0.0:
1953 del stats
['time-commands']
1954 self
.report_traces('null', stats
)
1955 self
.trace('null-memory', 'initial', **stats
['memory-initial'])
1956 self
.trace('null-memory', 'prebuild', **stats
['memory-prebuild'])
1957 self
.trace('null-memory', 'final', **stats
['memory-final'])
1959 def elapsed_time(self
):
1961 Returns the elapsed time of the most recent command execution.
1963 return self
.endTime
- self
.startTime
1965 def run(self
, *args
, **kw
):
1967 Runs a single build command, capturing output in the specified file.
1969 Because this class is about timing SCons, we record the start
1970 and end times of the elapsed execution, and also add the
1971 --debug=memory and --debug=time options to have SCons report
1972 its own memory and timing statistics.
1974 self
.startTime
= time
.perf_counter()
1976 result
= TestSCons
.run(self
, *args
, **kw
)
1978 self
.endTime
= time
.perf_counter()
1981 def copy_timing_configuration(self
, source_dir
, dest_dir
) -> None:
1983 Copies the timing configuration from the specified source_dir (the
1984 directory in which the controlling script lives) to the specified
1985 dest_dir (a temporary working directory).
1987 This ignores all files and directories that begin with the string
1988 'TimeSCons-', and all '.svn' subdirectories.
1990 for root
, dirs
, files
in os
.walk(source_dir
):
1993 dirs
= [d
for d
in dirs
if not d
.startswith('TimeSCons-')]
1994 files
= [f
for f
in files
if not f
.startswith('TimeSCons-')]
1995 for dirname
in dirs
:
1996 source
= os
.path
.join(root
, dirname
)
1997 destination
= source
.replace(source_dir
, dest_dir
)
1998 os
.mkdir(destination
)
1999 if sys
.platform
!= 'win32':
2000 shutil
.copystat(source
, destination
)
2001 for filename
in files
:
2002 source
= os
.path
.join(root
, filename
)
2003 destination
= source
.replace(source_dir
, dest_dir
)
2004 shutil
.copy2(source
, destination
)
2006 def up_to_date(self
, arguments
: str='.', read_str
: str="", **kw
) -> None:
2007 """Asserts that all of the targets listed in arguments is
2008 up to date, but does not make any assumptions on other targets.
2009 This function is most useful in conjunction with the -n option.
2010 Note: This custom version for timings tests does NOT escape
2014 for arg
in arguments
.split():
2015 s
= f
"{s}scons: `{arg}' is up to date.\n"
2016 kw
['arguments'] = arguments
2017 stdout
= self
.wrap_stdout(read_str
="REPLACEME", build_str
=s
)
2018 # Append '.*' so that timing output that comes after the
2019 # up-to-date output is okay.
2020 stdout
= f
"{re.escape(stdout)}.*"
2021 stdout
= stdout
.replace('REPLACEME', read_str
)
2022 kw
['stdout'] = stdout
2023 kw
['match'] = self
.match_re_dotall
2028 # In some environments, $AR will generate a warning message to stderr
2029 # if the library doesn't previously exist and is being created. One
2030 # way to fix this is to tell AR to be quiet (sometimes the 'c' flag),
2031 # but this is difficult to do in a platform-/implementation-specific
2032 # method. Instead, we will use the following as a stderr match for
2033 # tests that use AR so that we will view zero or more "ar: creating
2034 # <file>" messages to be successful executions of the test (see
2035 # test/AR.py for sample usage).
2037 noisy_ar
= r
'(ar: creating( archive)? \S+\n?)*'
2041 # indent-tabs-mode:nil
2043 # vim: set expandtab tabstop=4 shiftwidth=4: