1 # Copyright 2002-2005 Vladimir Prus.
2 # Copyright 2002-2003 Dave Abrahams.
3 # Copyright 2006 Rene Ferdinand Rivera Morell.
4 # Distributed under the Boost Software License, Version 1.0.
5 # (See accompanying file LICENSE.txt or copy at
6 # https://www.bfgroup.xyz/b2/LICENSE.txt)
8 from __future__
import print_function
21 from StringIO
import StringIO
23 from io
import StringIO
32 from xml
.sax
.saxutils
import escape
35 from functools
import reduce
41 return isinstance(data
, (type(''), type(u
'')))
44 class TestEnvironmentError(Exception):
51 def print_annotation(name
, value
, xml
):
52 """Writes some named bits of information about the current test run."""
54 print(escape(name
) + " {{{")
63 def flush_annotations(xml
=0):
65 for ann
in annotations
:
66 print_annotation(ann
[0], ann
[1], xml
)
70 def clear_annotations():
77 def set_defer_annotations(n
):
78 global defer_annotations
82 def annotate_stack_trace(tb
=None):
84 trace
= TestCmd
.caller(traceback
.extract_tb(tb
), 0)
86 trace
= TestCmd
.caller(traceback
.extract_stack(), 1)
87 annotation("stacktrace", trace
)
90 def annotation(name
, value
):
91 """Records an annotation about the test run."""
92 annotations
.append((name
, value
))
93 if not defer_annotations
:
99 for arg
in sys
.argv
[1:]:
100 if not arg
.startswith("-"):
102 return toolset
or "gcc"
105 # Detect the host OS.
106 cygwin
= hasattr(os
, "uname") and os
.uname()[0].lower().startswith("cygwin")
107 windows
= cygwin
or os
.environ
.get("OS", "").lower().startswith("windows")
110 default_os
= "cygwin"
112 default_os
= "windows"
113 elif hasattr(os
, "uname"):
114 default_os
= os
.uname()[0].lower()
117 def expand_toolset(toolset
, target_os
=default_os
):
118 match
= re
.match(r
'^(clang|intel)(-[\d\.]+|)$', toolset
)
120 if match
.group(1) == "intel" and target_os
== "windows":
121 return match
.expand(r
'\1-win\2')
122 elif target_os
== "darwin":
123 return match
.expand(r
'\1-darwin\2')
125 return match
.expand(r
'\1-linux\2')
130 def prepare_prefixes_and_suffixes(toolset
, target_os
=default_os
):
131 ind
= toolset
.find('-')
135 rtoolset
= toolset
[:ind
]
136 prepare_suffix_map(rtoolset
, target_os
)
137 prepare_library_prefix(rtoolset
, target_os
)
140 def prepare_suffix_map(toolset
, target_os
=default_os
):
142 Set up suffix translation performed by the Boost Build testing framework
143 to accommodate different toolsets generating targets of the same type using
144 different filename extensions (suffixes).
149 if target_os
== "cygwin":
150 suffixes
[".lib"] = ".a"
151 suffixes
[".obj"] = ".o"
152 suffixes
[".implib"] = ".lib.a"
153 elif target_os
== "windows":
156 suffixes
[".lib"] = ".a"
157 suffixes
[".obj"] = ".o"
158 suffixes
[".implib"] = ".dll.a"
160 # Everything else Windows
161 suffixes
[".implib"] = ".lib"
163 suffixes
[".exe"] = ""
164 suffixes
[".dll"] = ".so"
165 suffixes
[".lib"] = ".a"
166 suffixes
[".obj"] = ".o"
167 suffixes
[".implib"] = ".no_implib_files_on_this_platform"
169 if target_os
== "darwin":
170 suffixes
[".dll"] = ".dylib"
173 def prepare_library_prefix(toolset
, target_os
=default_os
):
175 Setup whether Boost Build is expected to automatically prepend prefixes
176 to its built library targets.
183 if target_os
== "cygwin":
185 elif target_os
== "windows" and toolset
!= "gcc":
191 def re_remove(sequence
, regex
):
192 me
= re
.compile(regex
)
193 result
= list(filter(lambda x
: me
.match(x
), sequence
))
200 def glob_remove(sequence
, pattern
):
201 result
= list(fnmatch
.filter(sequence
, pattern
))
208 class Tester(TestCmd
.TestCmd
):
209 """Main tester class for Boost Build.
213 `arguments` - Arguments passed to the run executable.
214 `executable` - Name of the executable to invoke.
215 `match` - Function to use for compating actual and
216 expected file contents.
217 `boost_build_path` - Boost build path to be passed to the run
219 `translate_suffixes` - Whether to update suffixes on the the file
220 names passed from the test script so they
221 match those actually created by the current
222 toolset. For example, static library files
223 are specified by using the .lib suffix but
224 when the "gcc" toolset is used it actually
225 creates them using the .a suffix.
226 `pass_toolset` - Whether the test system should pass the
227 specified toolset to the run executable.
228 `use_test_config` - Whether the test system should tell the run
229 executable to read in the test_config.jam
231 `ignore_toolset_requirements` - Whether the test system should tell the run
232 executable to ignore toolset requirements.
233 `workdir` - Absolute directory where the test will be
235 `pass_d0` - If set, when tests are not explicitly run
236 in verbose mode, they are run as silent
237 (-d0 & --quiet Boost Jam options).
239 Optional arguments inherited from the base class:
241 `description` - Test description string displayed in case
243 `subdir` - List of subdirectories to automatically
244 create under the working directory. Each
245 subdirectory needs to be specified
246 separately, parent coming before its child.
247 `verbose` - Flag that may be used to enable more
248 verbose test system output. Note that it
249 does not also enable more verbose build
250 system output like the --verbose command
253 def __init__(self
, arguments
=None, executable
=None,
254 match
=TestCmd
.match_exact
, boost_build_path
=None,
255 translate_suffixes
=True, pass_toolset
=True, use_test_config
=True,
256 ignore_toolset_requirements
=False, workdir
="", pass_d0
=False,
260 executable
= os
.getenv('B2')
264 assert arguments
.__class
__ is not str
265 self
.original_workdir
= os
.path
.dirname(__file__
)
266 if workdir
and not os
.path
.isabs(workdir
):
267 raise ("Parameter workdir <%s> must point to an absolute "
268 "directory: " % workdir
)
270 self
.last_build_timestamp
= 0
271 self
.translate_suffixes
= translate_suffixes
272 self
.use_test_config
= use_test_config
274 self
.toolset
= get_toolset()
275 self
.expanded_toolset
= expand_toolset(self
.toolset
)
276 self
.pass_toolset
= pass_toolset
277 self
.ignore_toolset_requirements
= ignore_toolset_requirements
279 prepare_prefixes_and_suffixes(pass_toolset
and self
.toolset
or "gcc")
281 use_default_bjam
= "--default-bjam" in sys
.argv
283 if not use_default_bjam
:
286 # Find where jam_src is located. Try for the debug version if it is
288 srcdir
= os
.path
.join(os
.path
.dirname(__file__
), "..", "src")
289 dirs
= [os
.path
.join(srcdir
, "engine", jam_build_dir
+ ".debug"),
290 os
.path
.join(srcdir
, "engine", jam_build_dir
)]
292 if os
.path
.exists(d
):
296 print("Cannot find built Boost.Jam")
299 verbosity
= ["-d0", "--quiet"]
302 if "--verbose" in sys
.argv
:
303 keywords
["verbose"] = True
305 self
.verbosity
= verbosity
307 if boost_build_path
is None:
308 boost_build_path
= self
.original_workdir
+ "/.."
312 program_list
.append(executable
)
314 program_list
.append(os
.path
.join(jam_build_dir
, executable
))
315 program_list
.append('-sBOOST_BUILD_PATH="' + boost_build_path
+ '"')
317 program_list
+= arguments
319 TestCmd
.TestCmd
.__init
__(self
, program
=program_list
, match
=match
,
320 workdir
=workdir
, inpath
=use_default_bjam
, **keywords
)
322 os
.chdir(self
.workdir
)
326 TestCmd
.TestCmd
.cleanup(self
)
327 os
.chdir(self
.original_workdir
)
328 except AttributeError:
329 # When this is called during TestCmd.TestCmd.__del__ we can have
330 # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in
334 def set_toolset(self
, toolset
, target_os
=default_os
):
335 self
.toolset
= toolset
336 self
.expanded_toolset
= expand_toolset(toolset
, target_os
)
337 self
.pass_toolset
= True
338 prepare_prefixes_and_suffixes(toolset
, target_os
)
342 # Methods that change the working directory's content.
344 def set_tree(self
, tree_location
):
345 # It is not possible to remove the current directory.
347 os
.chdir(os
.path
.dirname(self
.workdir
))
348 shutil
.rmtree(self
.workdir
, ignore_errors
=False)
350 if not os
.path
.isabs(tree_location
):
351 tree_location
= os
.path
.join(self
.original_workdir
, tree_location
)
352 shutil
.copytree(tree_location
, self
.workdir
)
355 def make_writable(unused
, dir, entries
):
357 name
= os
.path
.join(dir, e
)
358 os
.chmod(name
, os
.stat(name
).st_mode |
0o222)
359 for root
, _
, files
in os
.walk("."):
360 make_writable(None, root
, files
)
362 def write(self
, file, content
, wait
=True):
363 nfile
= self
.native_file_name(file)
364 self
.__makedirs
(os
.path
.dirname(nfile
), wait
)
365 if not type(content
) == bytes
:
366 content
= content
.encode()
367 f
= open(nfile
, "wb")
372 self
.__ensure
_newer
_than
_last
_build
(nfile
)
374 def rename(self
, src
, dst
):
375 src_name
= self
.native_file_name(src
)
376 dst_name
= self
.native_file_name(dst
)
377 os
.rename(src_name
, dst_name
)
379 def copy(self
, src
, dst
):
381 self
.write(dst
, self
.read(src
, binary
=True))
385 def copy_timestamp(self
, src
, dst
):
386 src_name
= self
.native_file_name(src
)
387 dst_name
= self
.native_file_name(dst
)
388 shutil
.copystat(src_name
, dst_name
)
390 def copy_preserving_timestamp(self
, src
, dst
):
391 src_name
= self
.native_file_name(src
)
392 dst_name
= self
.native_file_name(dst
)
393 shutil
.copy2(src_name
, dst_name
)
395 def touch(self
, names
, wait
=True):
399 path
= self
.native_file_name(name
)
401 self
.__ensure
_newer
_than
_last
_build
(path
)
406 if not type(names
) == list:
410 # If we are deleting the entire workspace, there is no need to wait
412 self
.last_build_timestamp
= 0
414 # Avoid attempts to remove the current directory.
415 os
.chdir(self
.original_workdir
)
417 n
= glob
.glob(self
.native_file_name(name
))
420 n
= self
.glob_file(name
.replace("$toolset", self
.expanded_toolset
+ "*")
424 shutil
.rmtree(n
, ignore_errors
=False)
428 # Create working dir root again in case we removed it.
429 if not os
.path
.exists(self
.workdir
):
430 os
.mkdir(self
.workdir
)
431 os
.chdir(self
.workdir
)
433 def expand_toolset(self
, name
):
435 Expands $toolset placeholder in the given file to the name of the
436 toolset currently being tested.
439 self
.write(name
, self
.read(name
).replace("$toolset", self
.expanded_toolset
))
441 def dump_stdio(self
):
442 annotation("STDOUT", self
.stdout())
443 annotation("STDERR", self
.stderr())
445 def run_build_system(self
, extra_args
=None, subdir
="", stdout
=None,
446 stderr
="", status
=0, match
=None, pass_toolset
=None,
447 use_test_config
=None, ignore_toolset_requirements
=None,
448 expected_duration
=None, **kw
):
450 assert extra_args
.__class
__ is not str
452 if os
.path
.isabs(subdir
):
454 "You must pass a relative directory to subdir <%s>." % subdir
)
456 self
.previous_tree
, dummy
= tree
.build_tree(self
.workdir
)
457 self
.wait_for_time_change_since_last_build()
462 if pass_toolset
is None:
463 pass_toolset
= self
.pass_toolset
465 if use_test_config
is None:
466 use_test_config
= self
.use_test_config
468 if ignore_toolset_requirements
is None:
469 ignore_toolset_requirements
= self
.ignore_toolset_requirements
473 kw
["program"] += self
.program
475 kw
["program"] += extra_args
476 if not extra_args
or not any(a
.startswith("-j") for a
in extra_args
):
477 kw
["program"] += ["-j1"]
478 if stdout
is None and not any(a
.startswith("-d") for a
in kw
["program"]):
479 kw
["program"] += self
.verbosity
481 kw
["program"].append("toolset=" + self
.toolset
)
483 kw
["program"].append('--test-config="%s"' % os
.path
.join(
484 self
.original_workdir
, "test-config.jam"))
485 if ignore_toolset_requirements
:
486 kw
["program"].append("--ignore-toolset-requirements")
487 if "--python" in sys
.argv
:
488 # -z disables Python optimization mode.
489 # this enables type checking (all assert
490 # and if __debug__ statements).
491 kw
["program"].extend(["--python", "-z"])
492 if "--stacktrace" in sys
.argv
:
493 kw
["program"].append("--stacktrace")
495 self
.last_program_invocation
= kw
["program"]
496 build_time_start
= time
.time()
497 TestCmd
.TestCmd
.run(self
, **kw
)
498 build_time_finish
= time
.time()
503 old_last_build_timestamp
= self
.last_build_timestamp
504 self
.tree
, self
.last_build_timestamp
= tree
.build_tree(self
.workdir
)
505 self
.difference
= tree
.tree_difference(self
.previous_tree
, self
.tree
)
506 if self
.difference
.empty():
507 # If nothing has been changed by this build and sufficient time has
508 # passed since the last build that actually changed something,
509 # there is no need to wait for touched or newly created files to
510 # start getting newer timestamps than the currently existing ones.
511 self
.last_build_timestamp
= old_last_build_timestamp
513 self
.difference
.ignore_directories()
514 self
.unexpected_difference
= copy
.deepcopy(self
.difference
)
516 if (status
and self
.status
) is not None and self
.status
!= status
:
519 expect
= " (expected %d)" % status
521 annotation("failure", '"%s" returned %d%s' % (kw
["program"],
522 self
.status
, expect
))
524 annotation("reason", "unexpected status returned by bjam")
527 if stdout
is not None and not match(self
.stdout(), stdout
):
528 stdout_test
= match(self
.stdout(), stdout
)
529 annotation("failure", "Unexpected stdout")
530 annotation("Expected STDOUT", stdout
)
531 annotation("Actual STDOUT", self
.stdout())
532 stderr
= self
.stderr()
534 annotation("STDERR", stderr
)
535 self
.maybe_do_diff(self
.stdout(), stdout
, stdout_test
)
536 self
.fail_test(1, dump_stdio
=False)
538 # Intel tends to produce some messages to stderr which make tests fail.
539 intel_workaround
= re
.compile("^xi(link|lib): executing.*\n", re
.M
)
540 actual_stderr
= re
.sub(intel_workaround
, "", self
.stderr())
542 if stderr
is not None and not match(actual_stderr
, stderr
):
543 stderr_test
= match(actual_stderr
, stderr
)
544 annotation("failure", "Unexpected stderr")
545 annotation("Expected STDERR", stderr
)
546 annotation("Actual STDERR", self
.stderr())
547 annotation("STDOUT", self
.stdout())
548 self
.maybe_do_diff(actual_stderr
, stderr
, stderr_test
)
549 self
.fail_test(1, dump_stdio
=False)
551 if expected_duration
is not None:
552 actual_duration
= build_time_finish
- build_time_start
553 if actual_duration
> expected_duration
:
554 print("Test run lasted %f seconds while it was expected to "
555 "finish in under %f seconds." % (actual_duration
,
557 self
.fail_test(1, dump_stdio
=False)
561 def glob_file(self
, name
):
562 name
= self
.adjust_name(name
)
564 if hasattr(self
, "difference"):
565 for f
in (self
.difference
.added_files
+
566 self
.difference
.modified_files
+
567 self
.difference
.touched_files
):
568 if fnmatch
.fnmatch(f
, name
):
569 result
= self
.__native
_file
_name
(f
)
572 result
= glob
.glob(self
.__native
_file
_name
(name
))
577 def __read(self
, name
, binary
=False):
584 f
= open(name
, openMode
)
589 annotation("failure", "Could not open '%s'" % name
)
593 def read(self
, name
, binary
=False):
594 name
= self
.glob_file(name
)
595 return self
.__read
(name
, binary
=binary
)
597 def read_and_strip(self
, name
):
598 if not self
.glob_file(name
):
600 f
= open(self
.glob_file(name
), "rb")
601 lines
= f
.readlines()
603 result
= "\n".join(x
.decode().rstrip() for x
in lines
)
604 if lines
and lines
[-1][-1] != "\n":
608 def fail_test(self
, condition
, dump_difference
=True, dump_stdio
=True,
613 if dump_difference
and hasattr(self
, "difference"):
615 self
.difference
.pprint(f
)
616 annotation("changes caused by the last build command",
622 if "--preserve" in sys
.argv
:
624 print("*** Copying the state of working dir into 'failed_test' ***")
626 path
= os
.path
.join(self
.original_workdir
, "failed_test")
627 if os
.path
.isdir(path
):
628 shutil
.rmtree(path
, ignore_errors
=False)
629 elif os
.path
.exists(path
):
630 raise "Path " + path
+ " already exists and is not a directory"
631 shutil
.copytree(self
.workdir
, path
)
632 print("The failed command was:")
633 print(" ".join(self
.last_program_invocation
))
636 annotate_stack_trace()
639 # A number of methods below check expectations with actual difference
640 # between directory trees before and after a build. All the 'expect*'
641 # methods require exact names to be passed. All the 'ignore*' methods allow
644 # All names can be either a string or a list of strings.
645 def expect_addition(self
, names
):
646 for name
in self
.adjust_names(names
):
648 glob_remove(self
.unexpected_difference
.added_files
, name
)
650 annotation("failure", "File %s not added as expected" % name
)
653 def ignore_addition(self
, wildcard
):
654 self
.__ignore
_elements
(self
.unexpected_difference
.added_files
,
657 def expect_removal(self
, names
):
658 for name
in self
.adjust_names(names
):
660 glob_remove(self
.unexpected_difference
.removed_files
, name
)
662 annotation("failure", "File %s not removed as expected" % name
)
665 def ignore_removal(self
, wildcard
):
666 self
.__ignore
_elements
(self
.unexpected_difference
.removed_files
,
669 def expect_modification(self
, names
):
670 for name
in self
.adjust_names(names
):
672 glob_remove(self
.unexpected_difference
.modified_files
, name
)
674 annotation("failure", "File %s not modified as expected" %
678 def ignore_modification(self
, wildcard
):
679 self
.__ignore
_elements
(self
.unexpected_difference
.modified_files
,
682 def expect_touch(self
, names
):
683 d
= self
.unexpected_difference
684 for name
in self
.adjust_names(names
):
685 # We need to check both touched and modified files. The reason is
687 # (1) Windows binaries such as obj, exe or dll files have slight
688 # differences even with identical inputs due to Windows PE
689 # format headers containing an internal timestamp.
690 # (2) Intel's compiler for Linux has the same behaviour.
691 filesets
= [d
.modified_files
, d
.touched_files
]
695 glob_remove(filesets
[-1], name
)
701 annotation("failure", "File %s not touched as expected" % name
)
704 def ignore_touch(self
, wildcard
):
705 self
.__ignore
_elements
(self
.unexpected_difference
.touched_files
,
708 def ignore(self
, wildcard
):
709 self
.ignore_addition(wildcard
)
710 self
.ignore_removal(wildcard
)
711 self
.ignore_modification(wildcard
)
712 self
.ignore_touch(wildcard
)
714 def expect_nothing(self
, names
):
715 for name
in self
.adjust_names(names
):
716 if name
in self
.difference
.added_files
:
717 annotation("failure",
718 "File %s added, but no action was expected" % name
)
720 if name
in self
.difference
.removed_files
:
721 annotation("failure",
722 "File %s removed, but no action was expected" % name
)
725 if name
in self
.difference
.modified_files
:
726 annotation("failure",
727 "File %s modified, but no action was expected" % name
)
729 if name
in self
.difference
.touched_files
:
730 annotation("failure",
731 "File %s touched, but no action was expected" % name
)
734 def __ignore_junk(self
):
735 # Not totally sure about this change, but I do not see a good
738 self
.ignore("*.ilk") # MSVC incremental linking files.
739 self
.ignore("*.pdb") # MSVC program database files.
740 self
.ignore("*.rsp") # Response files.
741 self
.ignore("*.tds") # Borland debug symbols.
742 self
.ignore("*.manifest") # MSVC DLL manifests.
743 self
.ignore("bin/standalone/msvc/*/msvc-setup.bat")
745 # Debug builds of bjam built with gcc produce this profiling data.
746 self
.ignore("gmon.out")
747 self
.ignore("*/gmon.out")
749 # Boost Build's 'configure' functionality (unfinished at the time)
750 # produces this file.
751 self
.ignore("bin/config.log")
752 self
.ignore("bin/project-cache.jam")
754 # Compiled Python files created when running Python based Boost Build.
757 # OSX/Darwin files and dirs.
758 self
.ignore("*.dSYM/*")
760 def expect_nothing_more(self
):
761 if not self
.unexpected_difference
.empty():
762 annotation("failure", "Unexpected changes found")
764 self
.unexpected_difference
.pprint(output
)
765 annotation("unexpected changes", output
.getvalue())
768 def expect_output_lines(self
, lines
, expected
=True):
769 self
.__expect
_lines
(self
.stdout(), lines
, expected
)
771 def expect_content_lines(self
, filename
, line
, expected
=True):
772 self
.__expect
_lines
(self
.read_and_strip(filename
), line
, expected
)
774 def expect_content(self
, name
, content
, exact
=False):
775 actual
= self
.read(name
)
776 content
= content
.replace("$toolset", self
.expanded_toolset
+ "*")
780 matched
= fnmatch
.fnmatch(actual
, content
)
783 z
.sort(key
=lambda x
: x
.lower().replace("\\", "/"))
785 actual_
= list(map(lambda x
: sorted_(x
.split()), actual
.splitlines()))
786 content_
= list(map(lambda x
: sorted_(x
.split()), content
.splitlines()))
787 if len(actual_
) == len(content_
):
789 lambda x
, y
: map(lambda n
, p
: fnmatch
.fnmatch(n
, p
), x
, y
),
792 lambda x
, y
: x
and reduce(
793 lambda a
, b
: a
and b
,
804 def maybe_do_diff(self
, actual
, expected
, result
=None):
805 if os
.environ
.get("DO_DIFF"):
806 e
= tempfile
.mktemp("expected")
807 a
= tempfile
.mktemp("actual")
815 # Current diff should return 1 to indicate 'different input files'
816 # but some older diff versions may return 0 and depending on the
817 # exact Python/OS platform version, os.system() call may gobble up
818 # the external process's return code and return 0 itself.
819 if os
.system('diff -u "%s" "%s"' % (e
, a
)) not in [0, 1]:
820 print('Unable to compute difference: diff -u "%s" "%s"' % (e
, a
824 elif type(result
) is TestCmd
.MatchError
:
825 print(result
.message
)
827 print("Set environmental variable 'DO_DIFF' to examine the "
831 def adjust_lib_name(self
, name
):
836 pos
= name
.rfind(".")
840 (head
, tail
) = os
.path
.split(name
)
842 tail
= lib_prefix
+ tail
843 result
= os
.path
.join(head
, tail
)
844 elif suffix
== ".dll" or suffix
== ".implib":
845 (head
, tail
) = os
.path
.split(name
)
847 tail
= dll_prefix
+ tail
848 result
= os
.path
.join(head
, tail
)
849 # If we want to use this name in a Jamfile, we better convert \ to /,
850 # as otherwise we would have to quote \.
851 result
= result
.replace("\\", "/")
854 def adjust_suffix(self
, name
):
855 if not self
.translate_suffixes
:
857 pos
= name
.rfind(".")
861 return name
[:pos
] + suffixes
.get(suffix
, suffix
)
863 # Acceps either a string or a list of strings and returns a list of
864 # strings. Adjusts suffixes on all names.
865 def adjust_names(self
, names
):
868 r
= map(self
.adjust_lib_name
, names
)
869 r
= map(self
.adjust_suffix
, r
)
870 r
= map(lambda x
, t
=self
.expanded_toolset
: x
.replace("$toolset", t
+ "*"), r
)
873 def adjust_name(self
, name
):
874 return self
.adjust_names(name
)[0]
876 def __native_file_name(self
, name
):
877 return os
.path
.normpath(os
.path
.join(self
.workdir
, *name
.split("/")))
879 def native_file_name(self
, name
):
880 return self
.__native
_file
_name
(self
.adjust_name(name
))
882 def wait_for_time_change(self
, path
, touch
):
884 Wait for newly assigned file system modification timestamps for the
885 given path to become large enough for the timestamp difference to be
886 correctly recognized by both this Python based testing framework and
887 the Boost Jam executable being tested. May optionally touch the given
888 path to set its modification timestamp to the new value.
891 self
.__wait
_for
_time
_change
(path
, touch
, last_build_time
=False)
893 def wait_for_time_change_since_last_build(self
):
895 Wait for newly assigned file system modification timestamps to
896 become large enough for the timestamp difference to be
897 correctly recognized by the Python based testing framework.
898 Does not care about Jam's timestamp resolution, since we
899 only need this to detect touched files.
901 if self
.last_build_timestamp
:
902 timestamp_file
= "timestamp-3df2f2317e15e4a9"
903 open(timestamp_file
, "wb").close()
904 self
.__wait
_for
_time
_change
_impl
(timestamp_file
,
905 self
.last_build_timestamp
,
906 self
.__python
_timestamp
_resolution
(timestamp_file
, 0), 0)
907 os
.unlink(timestamp_file
)
909 def __build_timestamp_resolution(self
):
911 Returns the minimum path modification timestamp resolution supported
912 by the used Boost Jam executable.
915 dir = tempfile
.mkdtemp("bjam_version_info")
917 jam_script
= "timestamp_resolution.jam"
918 f
= open(os
.path
.join(dir, jam_script
), "w")
920 f
.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;")
923 p
= subprocess
.Popen([self
.program
[0], "-d0", "-f%s" % jam_script
],
924 stdout
=subprocess
.PIPE
, cwd
=dir, universal_newlines
=True)
925 out
, err
= p
.communicate()
927 shutil
.rmtree(dir, ignore_errors
=False)
929 if p
.returncode
!= 0:
930 raise TestEnvironmentError("Unexpected return code (%s) when "
931 "detecting Boost Jam's minimum supported path modification "
932 "timestamp resolution version information." % p
.returncode
)
934 raise TestEnvironmentError("Unexpected error output (%s) when "
935 "detecting Boost Jam's minimum supported path modification "
936 "timestamp resolution version information." % err
)
938 r
= re
.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out
)
940 # Older Boost Jam versions did not report their minimum supported
941 # path modification timestamp resolution and did not actually
942 # support path modification timestamp resolutions finer than 1
944 # TODO: Phase this support out to avoid such fallback code from
945 # possibly covering up other problems.
947 if r
.group(1) != "00" or r
.group(2) != "00": # hours, minutes
948 raise TestEnvironmentError("Boost Jam with too coarse minimum "
949 "supported path modification timestamp resolution (%s:%s:%s)."
950 % (r
.group(1), r
.group(2), r
.group(3)))
951 return float(r
.group(3)) # seconds.nanoseconds
953 def __ensure_newer_than_last_build(self
, path
):
955 Updates the given path's modification timestamp after waiting for the
956 newly assigned file system modification timestamp to become large
957 enough for the timestamp difference between it and the last build
958 timestamp to be correctly recognized by both this Python based testing
959 framework and the Boost Jam executable being tested. Does nothing if
960 there is no 'last build' information available.
963 if self
.last_build_timestamp
:
964 self
.__wait
_for
_time
_change
(path
, touch
=True, last_build_time
=True)
966 def __expect_lines(self
, data
, lines
, expected
):
968 Checks whether the given data contains the given lines.
970 Data may be specified as a single string containing text lines
971 separated by newline characters.
973 Lines may be specified in any of the following forms:
974 * Single string containing text lines separated by newlines - the
975 given lines are searched for in the given data without any extra
976 data lines between them.
977 * Container of strings containing text lines separated by newlines
978 - the given lines are searched for in the given data with extra
979 data lines allowed between lines belonging to different strings.
980 * Container of strings containing text lines separated by newlines
981 and containers containing strings - the same as above with the
982 internal containers containing strings being interpreted as if
983 all their content was joined together into a single string
984 separated by newlines.
986 A newline at the end of any multi-line lines string is interpreted as
987 an expected extra trailig empty line.
989 # str.splitlines() trims at most one trailing newline while we want the
990 # trailing newline to indicate that there should be an extra empty line
993 return (x
+ "\n").splitlines()
998 data
= splitlines(data
)
1001 lines
= [splitlines(lines
)]
1010 if _contains_lines(data
, lines
) != bool(expected
):
1013 output
= ["Did not find expected lines:"]
1015 output
= ["Found unexpected lines:"]
1017 for line_sequence
in lines
:
1022 output
.append("...")
1023 output
.extend(" > " + line
for line
in line_sequence
)
1024 output
.append("in output:")
1025 output
.extend(" > " + line
for line
in data
)
1026 annotation("failure", "\n".join(output
))
1029 def __ignore_elements(self
, things
, wildcard
):
1030 """Removes in-place 'things' elements matching the given 'wildcard'."""
1031 things
[:] = list(filter(lambda x
: not fnmatch
.fnmatch(x
, wildcard
), things
))
1033 def __makedirs(self
, path
, wait
):
1035 Creates a folder with the given path, together with any missing
1036 parent folders. If WAIT is set, makes sure any newly created folders
1037 have modification timestamps newer than the ones left behind by the
1044 while path
and path
not in stack
and not os
.path
.isdir(path
):
1046 path
= os
.path
.dirname(path
)
1050 self
.__ensure
_newer
_than
_last
_build
(path
)
1056 def __python_timestamp_resolution(self
, path
, minimum_resolution
):
1058 Returns the modification timestamp resolution for the given path
1059 supported by the used Python interpreter/OS/filesystem combination.
1060 Will not check for resolutions less than the given minimum value. Will
1061 change the path's modification timestamp in the process.
1064 0 - nanosecond resolution supported
1065 positive decimal - timestamp resolution in seconds
1068 # Note on Python's floating point timestamp support:
1069 # Python interpreter versions prior to Python 2.3 did not support
1070 # floating point timestamps. Versions 2.3 through 3.3 may or may not
1071 # support it depending on the configuration (may be toggled by calling
1072 # os.stat_float_times(True/False) at program startup, disabled by
1073 # default prior to Python 2.5 and enabled by default since). Python 3.3
1074 # deprecated this configuration and 3.4 removed support for it after
1075 # which floating point timestamps are always supported.
1076 ver
= sys
.version_info
[0:2]
1077 python_nanosecond_support
= ver
>= (3, 4) or (ver
>= (2, 3) and
1078 os
.stat_float_times())
1080 # Minimal expected floating point difference used to account for
1081 # possible imprecise floating point number representations. We want
1082 # this number to be small (at least smaller than 0.0001) but still
1083 # large enough that we can be sure that increasing a floating point
1084 # value by 2 * eta guarantees the value read back will be increased by
1088 stats_orig
= os
.stat(path
)
1089 def test_time(diff
):
1090 """Returns whether a timestamp difference is detectable."""
1091 os
.utime(path
, (stats_orig
.st_atime
, stats_orig
.st_mtime
+ diff
))
1092 return os
.stat(path
).st_mtime
> stats_orig
.st_mtime
+ eta
1094 # Test for nanosecond timestamp resolution support.
1095 if not minimum_resolution
and python_nanosecond_support
:
1096 if test_time(2 * eta
):
1099 # Detect the filesystem timestamp resolution. Note that there is no
1100 # need to make this code 'as fast as possible' as, this function gets
1101 # called before having to sleep until the next detectable modification
1102 # timestamp value and that, since we already know nanosecond resolution
1103 # is not supported, will surely take longer than whatever we do here to
1104 # detect this minimal detectable modification timestamp resolution.
1106 if not python_nanosecond_support
:
1107 # If Python does not support nanosecond timestamp resolution we
1108 # know the minimum possible supported timestamp resolution is 1
1110 minimum_resolution
= max(1, minimum_resolution
)
1111 index
= max(1, int(minimum_resolution
/ step
))
1112 while step
* index
< minimum_resolution
:
1113 # Floating point number representation errors may cause our
1114 # initially calculated start index to be too small if calculated
1118 # Do not simply add up the steps to avoid cumulative floating point
1119 # number representation errors.
1122 raise TestEnvironmentError("File systems with too coarse "
1123 "modification timestamp resolutions not supported.")
1128 def __wait_for_time_change(self
, path
, touch
, last_build_time
):
1130 Wait until a newly assigned file system modification timestamp for
1131 the given path is large enough for the timestamp difference between it
1132 and the last build timestamp or the path's original file system
1133 modification timestamp (depending on the last_build_time flag) to be
1134 correctly recognized by both this Python based testing framework and
1135 the Boost Jam executable being tested. May optionally touch the given
1136 path to set its modification timestamp to the new value.
1139 assert self
.last_build_timestamp
or not last_build_time
1140 stats_orig
= os
.stat(path
)
1143 start_time
= self
.last_build_timestamp
1145 start_time
= stats_orig
.st_mtime
1147 build_resolution
= self
.__build
_timestamp
_resolution
()
1148 assert build_resolution
>= 0
1150 # Check whether the current timestamp is already new enough.
1151 if stats_orig
.st_mtime
> start_time
and (not build_resolution
or
1152 stats_orig
.st_mtime
>= start_time
+ build_resolution
):
1155 resolution
= self
.__python
_timestamp
_resolution
(path
, build_resolution
)
1156 assert resolution
>= build_resolution
1157 self
.__wait
_for
_time
_change
_impl
(path
, start_time
, resolution
, build_resolution
)
1160 os
.utime(path
, (stats_orig
.st_atime
, stats_orig
.st_mtime
))
1162 def __wait_for_time_change_impl(self
, path
, start_time
, resolution
, build_resolution
):
1163 # Implementation notes:
1164 # * Theoretically time.sleep() API might get interrupted too soon
1165 # (never actually encountered).
1166 # * We encountered cases where we sleep just long enough for the
1167 # filesystem's modifiction timestamp to change to the desired value,
1168 # but after waking up, the read timestamp is still just a tiny bit
1169 # too small (encountered on Windows). This is most likely caused by
1170 # imprecise floating point timestamp & sleep interval representation
1171 # used by Python. Note though that we never encountered a case where
1172 # more than one additional tiny sleep() call was needed to remedy
1174 # * We try to wait long enough for the timestamp to change, but do not
1175 # want to waste processing time by waiting too long. The main
1176 # problem is that when we have a coarse resolution, the actual times
1177 # get rounded and we do not know the exact sleep time needed for the
1178 # difference between two such times to pass. E.g. if we have a 1
1179 # second resolution and the original and the current file timestamps
1180 # are both 10 seconds then it could be that the current time is
1181 # 10.99 seconds and that we can wait for just one hundredth of a
1182 # second for the current file timestamp to reach its next value, and
1183 # using a longer sleep interval than that would just be wasting
1186 os
.utime(path
, None)
1187 c
= os
.stat(path
).st_mtime
1189 if c
> start_time
and (not build_resolution
or c
>= start_time
1190 + build_resolution
):
1192 if c
<= start_time
- resolution
:
1193 # Move close to the desired timestamp in one sleep, but not
1194 # close enough for timestamp rounding to potentially cause
1195 # us to wait too long.
1196 if start_time
- c
> 5:
1198 error_message
= ("Last build time recorded as "
1199 "being a future event, causing a too long "
1200 "wait period. Something must have played "
1201 "around with the system clock.")
1203 error_message
= ("Original path modification "
1204 "timestamp set to far into the future or "
1205 "something must have played around with the "
1206 "system clock, causing a too long wait "
1207 "period.\nPath: '%s'" % path
)
1208 raise TestEnvironmentError(message
)
1209 _sleep(start_time
- c
)
1211 # We are close to the desired timestamp so take baby sleeps
1212 # to avoid sleeping too long.
1213 _sleep(max(0.01, resolution
/ 10))
1217 _sleep(max(0.01, start_time
- c
))
1221 def __init__(self
, s
=""):
1224 # Have to handle escaped spaces correctly.
1225 elements
= s
.replace("\ ", "\001").split()
1228 self
.l
= [e
.replace("\001", " ") for e
in elements
]
1233 def __getitem__(self
, key
):
1236 def __setitem__(self
, key
, value
):
1239 def __delitem__(self
, key
):
1246 return "%s.List(%r)" % (self
.__module
__, " ".join(self
.l
))
1248 def __mul__(self
, other
):
1250 if not isinstance(other
, List
):
1254 result
.l
.append(f
+ s
)
1257 def __rmul__(self
, other
):
1258 if not isinstance(other
, List
):
1260 return List
.__mul
__(other
, self
)
1262 def __add__(self
, other
):
1264 result
.l
= self
.l
[:] + other
.l
[:]
1268 def _contains_lines(data
, lines
):
1269 data_line_count
= len(data
)
1270 expected_line_count
= reduce(lambda x
, y
: x
+ len(y
), lines
, 0)
1272 for expected
in lines
:
1273 if expected_line_count
> data_line_count
- index
:
1275 expected_line_count
-= len(expected
)
1276 index
= _match_line_sequence(data
, index
, data_line_count
-
1277 expected_line_count
, expected
)
1283 def _match_line_sequence(data
, start
, end
, lines
):
1286 for index
in range(start
, end
- len(lines
) + 1):
1288 for expected
in lines
:
1289 if not fnmatch
.fnmatch(data
[data_index
], expected
):
1299 raise TestEnvironmentError("Test environment error: sleep period of "
1300 "more than 5 seconds requested. Most likely caused by a file with "
1301 "its modification timestamp set to sometime in the future.")
1305 ###############################################################################
1309 ###############################################################################
1311 # Make os.stat() return file modification times as floats instead of integers
1312 # to get the best possible file timestamp resolution available. The exact
1313 # resolution depends on the underlying file system and the Python os.stat()
1314 # implementation. The better the resolution we achieve, the shorter we need to
1315 # wait for files we create to start getting new timestamps.
1318 # * os.stat_float_times() function first introduced in Python 2.3. and
1319 # suggested for deprecation in Python 3.3.
1320 # * On Python versions 2.5+ we do not need to do this as there os.stat()
1321 # returns floating point file modification times by default.
1322 # * Windows CPython implementations prior to version 2.5 do not support file
1323 # modification timestamp resolutions of less than 1 second no matter whether
1324 # these timestamps are returned as integer or floating point values.
1325 # * Python documentation states that this should be set in a program's
1326 # __main__ module to avoid affecting other libraries that might not be ready
1327 # to support floating point timestamps. Since we use no such external
1328 # libraries, we ignore this warning to make it easier to enable this feature
1329 # in both our single & multiple-test scripts.
1330 if (2, 3) <= sys
.version_info
< (2, 5) and not os
.stat_float_times():
1331 os
.stat_float_times(True)
1334 # Quickie tests. Should use doctest instead.
1335 if __name__
== "__main__":
1336 assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']"
1337 assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')"
1339 assert _contains_lines([], [])
1340 assert _contains_lines([], [[]])
1341 assert _contains_lines([], [[], []])
1342 assert _contains_lines([], [[], [], []])
1343 assert not _contains_lines([], [[""]])
1344 assert not _contains_lines([], [["a"]])
1346 assert _contains_lines([""], [])
1347 assert _contains_lines(["a"], [])
1348 assert _contains_lines(["a", "b"], [])
1349 assert _contains_lines(["a", "b"], [[], [], []])
1351 assert _contains_lines([""], [[""]])
1352 assert not _contains_lines([""], [["a"]])
1353 assert not _contains_lines(["a"], [[""]])
1354 assert _contains_lines(["a", "", "b", ""], [["a"]])
1355 assert _contains_lines(["a", "", "b", ""], [[""]])
1356 assert _contains_lines(["a", "", "b"], [["b"]])
1357 assert not _contains_lines(["a", "b"], [[""]])
1358 assert not _contains_lines(["a", "", "b", ""], [["c"]])
1359 assert _contains_lines(["a", "", "b", "x"], [["x"]])
1361 data
= ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
1362 assert _contains_lines(data
, [["1", "2"]])
1363 assert not _contains_lines(data
, [["2", "1"]])
1364 assert not _contains_lines(data
, [["1", "3"]])
1365 assert not _contains_lines(data
, [["1", "3"]])
1366 assert _contains_lines(data
, [["1"], ["2"]])
1367 assert _contains_lines(data
, [["1"], [], [], [], ["2"]])
1368 assert _contains_lines(data
, [["1"], ["3"]])
1369 assert not _contains_lines(data
, [["3"], ["1"]])
1370 assert _contains_lines(data
, [["3"], ["7"], ["8"]])
1371 assert not _contains_lines(data
, [["1"], ["3", "5"]])
1372 assert not _contains_lines(data
, [["1"], [""], ["5"]])
1373 assert not _contains_lines(data
, [["1"], ["5"], ["3"]])
1374 assert not _contains_lines(data
, [["1"], ["5", "3"]])
1376 assert not _contains_lines(data
, [[" 3"]])
1377 assert not _contains_lines(data
, [["3 "]])
1378 assert not _contains_lines(data
, [["3", ""]])
1379 assert not _contains_lines(data
, [["", "3"]])
1381 print("tests passed")