1 # Copyright 2002-2005 Vladimir Prus.
2 # Copyright 2002-2003 Dave Abrahams.
3 # Copyright 2006 Rene Rivera.
4 # Distributed under the Boost Software License, Version 1.0.
5 # (See accompanying file LICENSE_1_0.txt or copy at
6 # http://www.boost.org/LICENSE_1_0.txt)
27 from xml
.sax
.saxutils
import escape
30 class TestEnvironmentError(Exception):
37 def print_annotation(name
, value
, xml
):
38 """Writes some named bits of information about the current test run."""
40 print escape(name
) + " {{{"
49 def flush_annotations(xml
=0):
51 for ann
in annotations
:
52 print_annotation(ann
[0], ann
[1], xml
)
56 def clear_annotations():
63 def set_defer_annotations(n
):
64 global defer_annotations
68 def annotate_stack_trace(tb
=None):
70 trace
= TestCmd
.caller(traceback
.extract_tb(tb
), 0)
72 trace
= TestCmd
.caller(traceback
.extract_stack(), 1)
73 annotation("stacktrace", trace
)
76 def annotation(name
, value
):
77 """Records an annotation about the test run."""
78 annotations
.append((name
, value
))
79 if not defer_annotations
:
85 for arg
in sys
.argv
[1:]:
86 if not arg
.startswith("-"):
88 return toolset
or "gcc"
92 cygwin
= hasattr(os
, "uname") and os
.uname()[0].lower().startswith("cygwin")
93 windows
= cygwin
or os
.environ
.get("OS", "").lower().startswith("windows")
96 def prepare_prefixes_and_suffixes(toolset
):
97 prepare_suffix_map(toolset
)
98 prepare_library_prefix(toolset
)
101 def prepare_suffix_map(toolset
):
103 Set up suffix translation performed by the Boost Build testing framework
104 to accomodate different toolsets generating targets of the same type using
105 different filename extensions (suffixes).
112 suffixes
[".lib"] = ".a" # mingw static libs use suffix ".a".
113 suffixes
[".obj"] = ".o"
115 suffixes
[".implib"] = ".lib.a"
117 suffixes
[".implib"] = ".lib"
119 suffixes
[".exe"] = ""
120 suffixes
[".dll"] = ".so"
121 suffixes
[".lib"] = ".a"
122 suffixes
[".obj"] = ".o"
123 suffixes
[".implib"] = ".no_implib_files_on_this_platform"
125 if hasattr(os
, "uname") and os
.uname()[0] == "Darwin":
126 suffixes
[".dll"] = ".dylib"
129 def prepare_library_prefix(toolset
):
131 Setup whether Boost Build is expected to automatically prepend prefixes
132 to its built library targets.
141 elif windows
and toolset
!= "gcc":
147 def re_remove(sequence
, regex
):
148 me
= re
.compile(regex
)
149 result
= filter(lambda x
: me
.match(x
), sequence
)
156 def glob_remove(sequence
, pattern
):
157 result
= fnmatch
.filter(sequence
, pattern
)
164 class Tester(TestCmd
.TestCmd
):
165 """Main tester class for Boost Build.
169 `arguments` - Arguments passed to the run executable.
170 `executable` - Name of the executable to invoke.
171 `match` - Function to use for compating actual and
172 expected file contents.
173 `boost_build_path` - Boost build path to be passed to the run
175 `translate_suffixes` - Whether to update suffixes on the the file
176 names passed from the test script so they
177 match those actually created by the current
178 toolset. For example, static library files
179 are specified by using the .lib suffix but
180 when the "gcc" toolset is used it actually
181 creates them using the .a suffix.
182 `pass_toolset` - Whether the test system should pass the
183 specified toolset to the run executable.
184 `use_test_config` - Whether the test system should tell the run
185 executable to read in the test_config.jam
187 `ignore_toolset_requirements` - Whether the test system should tell the run
188 executable to ignore toolset requirements.
189 `workdir` - Absolute directory where the test will be
191 `pass_d0` - If set, when tests are not explicitly run
192 in verbose mode, they are run as silent
193 (-d0 & --quiet Boost Jam options).
195 Optional arguments inherited from the base class:
197 `description` - Test description string displayed in case
199 `subdir` - List of subdirectories to automatically
200 create under the working directory. Each
201 subdirectory needs to be specified
202 separately, parent coming before its child.
203 `verbose` - Flag that may be used to enable more
204 verbose test system output. Note that it
205 does not also enable more verbose build
206 system output like the --verbose command
209 def __init__(self
, arguments
=None, executable
="bjam",
210 match
=TestCmd
.match_exact
, boost_build_path
=None,
211 translate_suffixes
=True, pass_toolset
=True, use_test_config
=True,
212 ignore_toolset_requirements
=False, workdir
="", pass_d0
=True,
215 assert arguments
.__class
__ is not str
216 self
.original_workdir
= os
.getcwd()
217 if workdir
and not os
.path
.isabs(workdir
):
218 raise ("Parameter workdir <%s> must point to an absolute "
219 "directory: " % workdir
)
221 self
.last_build_timestamp
= 0
222 self
.translate_suffixes
= translate_suffixes
223 self
.use_test_config
= use_test_config
225 self
.toolset
= get_toolset()
226 self
.pass_toolset
= pass_toolset
227 self
.ignore_toolset_requirements
= ignore_toolset_requirements
229 prepare_prefixes_and_suffixes(pass_toolset
and self
.toolset
or "gcc")
231 use_default_bjam
= "--default-bjam" in sys
.argv
233 if not use_default_bjam
:
236 jam_build_dir
= "bin.ntx86"
237 elif (os
.name
== "posix") and os
.__dict
__.has_key("uname"):
238 if os
.uname()[0].lower().startswith("cygwin"):
239 jam_build_dir
= "bin.cygwinx86"
240 if ("TMP" in os
.environ
and
241 os
.environ
["TMP"].find("~") != -1):
242 print("Setting $TMP to /tmp to get around problem "
243 "with short path names")
244 os
.environ
["TMP"] = "/tmp"
245 elif os
.uname()[0] == "Linux":
247 if re
.match("i.86", cpu
):
248 jam_build_dir
= "bin.linuxx86"
250 jam_build_dir
= "bin.linux" + os
.uname()[4]
251 elif os
.uname()[0] == "SunOS":
252 jam_build_dir
= "bin.solaris"
253 elif os
.uname()[0] == "Darwin":
254 if os
.uname()[4] == "i386":
255 jam_build_dir
= "bin.macosxx86"
256 elif os
.uname()[4] == "x86_64":
257 jam_build_dir
= "bin.macosxx86_64"
259 jam_build_dir
= "bin.macosxppc"
260 elif os
.uname()[0] == "AIX":
261 jam_build_dir
= "bin.aix"
262 elif os
.uname()[0] == "IRIX64":
263 jam_build_dir
= "bin.irix"
264 elif os
.uname()[0] == "FreeBSD":
265 jam_build_dir
= "bin.freebsd"
266 elif os
.uname()[0] == "OSF1":
267 jam_build_dir
= "bin.osf"
269 raise ("Do not know directory where Jam is built for this "
270 "system: %s/%s" % (os
.name
, os
.uname()[0]))
272 raise ("Do not know directory where Jam is built for this "
273 "system: %s" % os
.name
)
275 # Find where jam_src is located. Try for the debug version if it is
277 dirs
= [os
.path
.join("..", "src", "engine", jam_build_dir
+ ".debug"),
278 os
.path
.join("..", "src", "engine", jam_build_dir
)]
280 if os
.path
.exists(d
):
284 print("Cannot find built Boost.Jam")
287 verbosity
= ["-d0", "--quiet"]
290 if "--verbose" in sys
.argv
:
291 keywords
["verbose"] = True
294 if boost_build_path
is None:
295 boost_build_path
= self
.original_workdir
+ "/.."
299 program_list
.append(executable
)
301 program_list
.append(os
.path
.join(jam_build_dir
, executable
))
302 program_list
.append('-sBOOST_BUILD_PATH="' + boost_build_path
+ '"')
304 program_list
+= verbosity
306 program_list
+= arguments
308 TestCmd
.TestCmd
.__init
__(self
, program
=program_list
, match
=match
,
309 workdir
=workdir
, inpath
=use_default_bjam
, **keywords
)
311 os
.chdir(self
.workdir
)
315 TestCmd
.TestCmd
.cleanup(self
)
316 os
.chdir(self
.original_workdir
)
317 except AttributeError:
318 # When this is called during TestCmd.TestCmd.__del__ we can have
319 # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in
324 # Methods that change the working directory's content.
326 def set_tree(self
, tree_location
):
327 # It is not possible to remove the current directory.
329 os
.chdir(os
.path
.dirname(self
.workdir
))
330 shutil
.rmtree(self
.workdir
, ignore_errors
=False)
332 if not os
.path
.isabs(tree_location
):
333 tree_location
= os
.path
.join(self
.original_workdir
, tree_location
)
334 shutil
.copytree(tree_location
, self
.workdir
)
337 def make_writable(unused
, dir, entries
):
339 name
= os
.path
.join(dir, e
)
340 os
.chmod(name
, os
.stat(name
).st_mode |
0222)
341 os
.path
.walk(".", make_writable
, None)
343 def write(self
, file, content
, wait
=True):
344 nfile
= self
.native_file_name(file)
345 self
.__makedirs
(os
.path
.dirname(nfile
), wait
)
346 f
= open(nfile
, "wb")
351 self
.__ensure
_newer
_than
_last
_build
(nfile
)
353 def copy(self
, src
, dst
):
355 self
.write(dst
, self
.read(src
, binary
=True))
359 def copy_preserving_timestamp(self
, src
, dst
):
360 src_name
= self
.native_file_name(src
)
361 dst_name
= self
.native_file_name(dst
)
362 stats
= os
.stat(src_name
)
363 self
.write(dst
, self
.__read
(src
, binary
=True))
364 os
.utime(dst_name
, (stats
.st_atime
, stats
.st_mtime
))
366 def touch(self
, names
, wait
=True):
367 if names
.__class
__ is str:
370 path
= self
.native_file_name(name
)
372 self
.__ensure
_newer
_than
_last
_build
(path
)
377 if not type(names
) == types
.ListType
:
381 # If we are deleting the entire workspace, there is no need to wait
383 self
.last_build_timestamp
= 0
385 # Avoid attempts to remove the current directory.
386 os
.chdir(self
.original_workdir
)
388 n
= glob
.glob(self
.native_file_name(name
))
391 n
= self
.glob_file(name
.replace("$toolset", self
.toolset
+ "*")
395 shutil
.rmtree(n
, ignore_errors
=False)
399 # Create working dir root again in case we removed it.
400 if not os
.path
.exists(self
.workdir
):
401 os
.mkdir(self
.workdir
)
402 os
.chdir(self
.workdir
)
404 def expand_toolset(self
, name
):
406 Expands $toolset placeholder in the given file to the name of the
407 toolset currently being tested.
410 self
.write(name
, self
.read(name
).replace("$toolset", self
.toolset
))
412 def dump_stdio(self
):
413 annotation("STDOUT", self
.stdout())
414 annotation("STDERR", self
.stderr())
416 def run_build_system(self
, extra_args
=None, subdir
="", stdout
=None,
417 stderr
="", status
=0, match
=None, pass_toolset
=None,
418 use_test_config
=None, ignore_toolset_requirements
=None,
419 expected_duration
=None, **kw
):
421 assert extra_args
.__class
__ is not str
423 if os
.path
.isabs(subdir
):
424 print("You must pass a relative directory to subdir <%s>." % subdir
428 self
.previous_tree
, dummy
= tree
.build_tree(self
.workdir
)
433 if pass_toolset
is None:
434 pass_toolset
= self
.pass_toolset
436 if use_test_config
is None:
437 use_test_config
= self
.use_test_config
439 if ignore_toolset_requirements
is None:
440 ignore_toolset_requirements
= self
.ignore_toolset_requirements
444 kw
["program"] += self
.program
446 kw
["program"] += extra_args
448 kw
["program"].append("toolset=" + self
.toolset
)
450 kw
["program"].append('--test-config="%s"' % os
.path
.join(
451 self
.original_workdir
, "test-config.jam"))
452 if ignore_toolset_requirements
:
453 kw
["program"].append("--ignore-toolset-requirements")
454 if "--python" in sys
.argv
:
455 # -z disables Python optimization mode.
456 # this enables type checking (all assert
457 # and if __debug__ statements).
458 kw
["program"].extend(["--python", "-z"])
459 if "--stacktrace" in sys
.argv
:
460 kw
["program"].append("--stacktrace")
462 self
.last_program_invocation
= kw
["program"]
463 build_time_start
= time
.time()
464 apply(TestCmd
.TestCmd
.run
, [self
], kw
)
465 build_time_finish
= time
.time()
470 old_last_build_timestamp
= self
.last_build_timestamp
471 self
.tree
, self
.last_build_timestamp
= tree
.build_tree(self
.workdir
)
472 self
.difference
= tree
.tree_difference(self
.previous_tree
, self
.tree
)
473 if self
.difference
.empty():
474 # If nothing has been changed by this build and sufficient time has
475 # passed since the last build that actually changed something,
476 # there is no need to wait for touched or newly created files to
477 # start getting newer timestamps than the currently existing ones.
478 self
.last_build_timestamp
= old_last_build_timestamp
480 self
.difference
.ignore_directories()
481 self
.unexpected_difference
= copy
.deepcopy(self
.difference
)
483 if (status
and self
.status
) is not None and self
.status
!= status
:
486 expect
= " (expected %d)" % status
488 annotation("failure", '"%s" returned %d%s' % (kw
["program"],
489 self
.status
, expect
))
491 annotation("reason", "unexpected status returned by bjam")
494 if stdout
is not None and not match(self
.stdout(), stdout
):
495 stdout_test
= match(self
.stdout(), stdout
)
496 annotation("failure", "Unexpected stdout")
497 annotation("Expected STDOUT", stdout
)
498 annotation("Actual STDOUT", self
.stdout())
499 stderr
= self
.stderr()
501 annotation("STDERR", stderr
)
502 self
.maybe_do_diff(self
.stdout(), stdout
, stdout_test
)
503 self
.fail_test(1, dump_stdio
=False)
505 # Intel tends to produce some messages to stderr which make tests fail.
506 intel_workaround
= re
.compile("^xi(link|lib): executing.*\n", re
.M
)
507 actual_stderr
= re
.sub(intel_workaround
, "", self
.stderr())
509 if stderr
is not None and not match(actual_stderr
, stderr
):
510 stderr_test
= match(actual_stderr
, stderr
)
511 annotation("failure", "Unexpected stderr")
512 annotation("Expected STDERR", stderr
)
513 annotation("Actual STDERR", self
.stderr())
514 annotation("STDOUT", self
.stdout())
515 self
.maybe_do_diff(actual_stderr
, stderr
, stderr_test
)
516 self
.fail_test(1, dump_stdio
=False)
518 if expected_duration
is not None:
519 actual_duration
= build_time_finish
- build_time_start
520 if actual_duration
> expected_duration
:
521 print("Test run lasted %f seconds while it was expected to "
522 "finish in under %f seconds." % (actual_duration
,
524 self
.fail_test(1, dump_stdio
=False)
528 def glob_file(self
, name
):
529 name
= self
.adjust_name(name
)
531 if hasattr(self
, "difference"):
532 for f
in (self
.difference
.added_files
+
533 self
.difference
.modified_files
+
534 self
.difference
.touched_files
):
535 if fnmatch
.fnmatch(f
, name
):
536 result
= self
.__native
_file
_name
(f
)
539 result
= glob
.glob(self
.__native
_file
_name
(name
))
544 def __read(self
, name
, binary
=False):
551 f
= open(name
, openMode
)
556 annotation("failure", "Could not open '%s'" % name
)
560 def read(self
, name
, binary
=False):
561 name
= self
.glob_file(name
)
562 return self
.__read
(name
, binary
=binary
)
564 def read_and_strip(self
, name
):
565 if not self
.glob_file(name
):
567 f
= open(self
.glob_file(name
), "rb")
568 lines
= f
.readlines()
570 result
= "\n".join(x
.rstrip() for x
in lines
)
571 if lines
and lines
[-1][-1] != "\n":
575 def fail_test(self
, condition
, dump_difference
=True, dump_stdio
=True,
580 if dump_difference
and hasattr(self
, "difference"):
581 f
= StringIO
.StringIO()
582 self
.difference
.pprint(f
)
583 annotation("changes caused by the last build command",
589 if "--preserve" in sys
.argv
:
591 print "*** Copying the state of working dir into 'failed_test' ***"
593 path
= os
.path
.join(self
.original_workdir
, "failed_test")
594 if os
.path
.isdir(path
):
595 shutil
.rmtree(path
, ignore_errors
=False)
596 elif os
.path
.exists(path
):
597 raise "Path " + path
+ " already exists and is not a directory"
598 shutil
.copytree(self
.workdir
, path
)
599 print "The failed command was:"
600 print " ".join(self
.last_program_invocation
)
603 annotate_stack_trace()
606 # A number of methods below check expectations with actual difference
607 # between directory trees before and after a build. All the 'expect*'
608 # methods require exact names to be passed. All the 'ignore*' methods allow
611 # All names can be either a string or a list of strings.
612 def expect_addition(self
, names
):
613 for name
in self
.adjust_names(names
):
615 glob_remove(self
.unexpected_difference
.added_files
, name
)
617 annotation("failure", "File %s not added as expected" % name
)
620 def ignore_addition(self
, wildcard
):
621 self
.__ignore
_elements
(self
.unexpected_difference
.added_files
,
624 def expect_removal(self
, names
):
625 for name
in self
.adjust_names(names
):
627 glob_remove(self
.unexpected_difference
.removed_files
, name
)
629 annotation("failure", "File %s not removed as expected" % name
)
632 def ignore_removal(self
, wildcard
):
633 self
.__ignore
_elements
(self
.unexpected_difference
.removed_files
,
636 def expect_modification(self
, names
):
637 for name
in self
.adjust_names(names
):
639 glob_remove(self
.unexpected_difference
.modified_files
, name
)
641 annotation("failure", "File %s not modified as expected" %
645 def ignore_modification(self
, wildcard
):
646 self
.__ignore
_elements
(self
.unexpected_difference
.modified_files
,
649 def expect_touch(self
, names
):
650 d
= self
.unexpected_difference
651 for name
in self
.adjust_names(names
):
652 # We need to check both touched and modified files. The reason is
654 # (1) Windows binaries such as obj, exe or dll files have slight
655 # differences even with identical inputs due to Windows PE
656 # format headers containing an internal timestamp.
657 # (2) Intel's compiler for Linux has the same behaviour.
658 filesets
= [d
.modified_files
, d
.touched_files
]
662 glob_remove(filesets
[-1], name
)
668 annotation("failure", "File %s not touched as expected" % name
)
671 def ignore_touch(self
, wildcard
):
672 self
.__ignore
_elements
(self
.unexpected_difference
.touched_files
,
675 def ignore(self
, wildcard
):
676 self
.ignore_addition(wildcard
)
677 self
.ignore_removal(wildcard
)
678 self
.ignore_modification(wildcard
)
679 self
.ignore_touch(wildcard
)
681 def expect_nothing(self
, names
):
682 for name
in self
.adjust_names(names
):
683 if name
in self
.difference
.added_files
:
684 annotation("failure",
685 "File %s added, but no action was expected" % name
)
687 if name
in self
.difference
.removed_files
:
688 annotation("failure",
689 "File %s removed, but no action was expected" % name
)
692 if name
in self
.difference
.modified_files
:
693 annotation("failure",
694 "File %s modified, but no action was expected" % name
)
696 if name
in self
.difference
.touched_files
:
697 annotation("failure",
698 "File %s touched, but no action was expected" % name
)
701 def __ignore_junk(self
):
702 # Not totally sure about this change, but I do not see a good
705 self
.ignore("*.ilk") # MSVC incremental linking files.
706 self
.ignore("*.pdb") # MSVC program database files.
707 self
.ignore("*.rsp") # Response files.
708 self
.ignore("*.tds") # Borland debug symbols.
709 self
.ignore("*.manifest") # MSVC DLL manifests.
711 # Debug builds of bjam built with gcc produce this profiling data.
712 self
.ignore("gmon.out")
713 self
.ignore("*/gmon.out")
715 # Boost Build's 'configure' functionality (unfinished at the time)
716 # produces this file.
717 self
.ignore("bin/config.log")
718 self
.ignore("bin/project-cache.jam")
720 # Compiled Python files created when running Python based Boost Build.
723 # OSX/Darwin files and dirs.
724 self
.ignore("*.dSYM/*")
726 def expect_nothing_more(self
):
727 if not self
.unexpected_difference
.empty():
728 annotation("failure", "Unexpected changes found")
729 output
= StringIO
.StringIO()
730 self
.unexpected_difference
.pprint(output
)
731 annotation("unexpected changes", output
.getvalue())
734 def expect_output_lines(self
, lines
, expected
=True):
735 self
.__expect
_lines
(self
.stdout(), lines
, expected
)
737 def expect_content_lines(self
, filename
, line
, expected
=True):
738 self
.__expect
_lines
(self
.read_and_strip(filename
), line
, expected
)
740 def expect_content(self
, name
, content
, exact
=False):
741 actual
= self
.read(name
)
742 content
= content
.replace("$toolset", self
.toolset
+ "*")
746 matched
= fnmatch
.fnmatch(actual
, content
)
749 x
.sort(lambda x
, y
: cmp(x
.lower().replace("\\","/"), y
.lower().replace("\\","/")))
751 actual_
= map(lambda x
: sorted_(x
.split()), actual
.splitlines())
752 content_
= map(lambda x
: sorted_(x
.split()), content
.splitlines())
753 if len(actual_
) == len(content_
):
755 lambda x
, y
: map(lambda n
, p
: fnmatch
.fnmatch(n
, p
), x
, y
),
758 lambda x
, y
: x
and reduce(
759 lambda a
, b
: a
and b
,
770 def maybe_do_diff(self
, actual
, expected
, result
=None):
771 if os
.environ
.get("DO_DIFF"):
772 e
= tempfile
.mktemp("expected")
773 a
= tempfile
.mktemp("actual")
781 # Current diff should return 1 to indicate 'different input files'
782 # but some older diff versions may return 0 and depending on the
783 # exact Python/OS platform version, os.system() call may gobble up
784 # the external process's return code and return 0 itself.
785 if os
.system('diff -u "%s" "%s"' % (e
, a
)) not in [0, 1]:
786 print('Unable to compute difference: diff -u "%s" "%s"' % (e
, a
790 elif type(result
) is TestCmd
.MatchError
:
791 print(result
.message
)
793 print("Set environmental variable 'DO_DIFF' to examine the "
797 def adjust_lib_name(self
, name
):
802 pos
= name
.rfind(".")
806 (head
, tail
) = os
.path
.split(name
)
808 tail
= lib_prefix
+ tail
809 result
= os
.path
.join(head
, tail
)
810 elif suffix
== ".dll":
811 (head
, tail
) = os
.path
.split(name
)
813 tail
= dll_prefix
+ tail
814 result
= os
.path
.join(head
, tail
)
815 # If we want to use this name in a Jamfile, we better convert \ to /,
816 # as otherwise we would have to quote \.
817 result
= result
.replace("\\", "/")
820 def adjust_suffix(self
, name
):
821 if not self
.translate_suffixes
:
823 pos
= name
.rfind(".")
827 return name
[:pos
] + suffixes
.get(suffix
, suffix
)
829 # Acceps either a string or a list of strings and returns a list of
830 # strings. Adjusts suffixes on all names.
831 def adjust_names(self
, names
):
832 if names
.__class
__ is str:
834 r
= map(self
.adjust_lib_name
, names
)
835 r
= map(self
.adjust_suffix
, r
)
836 r
= map(lambda x
, t
=self
.toolset
: x
.replace("$toolset", t
+ "*"), r
)
839 def adjust_name(self
, name
):
840 return self
.adjust_names(name
)[0]
842 def __native_file_name(self
, name
):
843 return os
.path
.normpath(os
.path
.join(self
.workdir
, *name
.split("/")))
845 def native_file_name(self
, name
):
846 return self
.__native
_file
_name
(self
.adjust_name(name
))
848 def wait_for_time_change(self
, path
, touch
):
850 Wait for newly assigned file system modification timestamps for the
851 given path to become large enough for the timestamp difference to be
852 correctly recognized by both this Python based testing framework and
853 the Boost Jam executable being tested. May optionally touch the given
854 path to set its modification timestamp to the new value.
857 self
.__wait
_for
_time
_change
(path
, touch
, last_build_time
=False)
859 def __build_timestamp_resolution(self
):
861 Returns the minimum path modification timestamp resolution supported
862 by the used Boost Jam executable.
865 dir = tempfile
.mkdtemp("bjam_version_info")
867 jam_script
= "timestamp_resolution.jam"
868 f
= open(os
.path
.join(dir, jam_script
), "w")
870 f
.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;")
873 p
= subprocess
.Popen([self
.program
[0], "-d0", "-f%s" % jam_script
],
874 stdout
=subprocess
.PIPE
, cwd
=dir, universal_newlines
=True)
875 out
, err
= p
.communicate()
877 shutil
.rmtree(dir, ignore_errors
=False)
879 if p
.returncode
!= 0:
880 raise TestEnvironmentError("Unexpected return code (%s) when "
881 "detecting Boost Jam's minimum supported path modification "
882 "timestamp resolution version information." % p
.returncode
)
884 raise TestEnvironmentError("Unexpected error output (%s) when "
885 "detecting Boost Jam's minimum supported path modification "
886 "timestamp resolution version information." % err
)
888 r
= re
.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out
)
890 # Older Boost Jam versions did not report their minimum supported
891 # path modification timestamp resolution and did not actually
892 # support path modification timestamp resolutions finer than 1
894 # TODO: Phase this support out to avoid such fallback code from
895 # possibly covering up other problems.
897 if r
.group(1) != "00" or r
.group(2) != "00": # hours, minutes
898 raise TestEnvironmentError("Boost Jam with too coarse minimum "
899 "supported path modification timestamp resolution (%s:%s:%s)."
900 % (r
.group(1), r
.group(2), r
.group(3)))
901 return float(r
.group(3)) # seconds.nanoseconds
903 def __ensure_newer_than_last_build(self
, path
):
905 Updates the given path's modification timestamp after waiting for the
906 newly assigned file system modification timestamp to become large
907 enough for the timestamp difference between it and the last build
908 timestamp to be correctly recognized by both this Python based testing
909 framework and the Boost Jam executable being tested. Does nothing if
910 there is no 'last build' information available.
913 if self
.last_build_timestamp
:
914 self
.__wait
_for
_time
_change
(path
, touch
=True, last_build_time
=True)
916 def __expect_lines(self
, data
, lines
, expected
):
918 Checks whether the given data contains the given lines.
920 Data may be specified as a single string containing text lines
921 separated by newline characters.
923 Lines may be specified in any of the following forms:
924 * Single string containing text lines separated by newlines - the
925 given lines are searched for in the given data without any extra
926 data lines between them.
927 * Container of strings containing text lines separated by newlines
928 - the given lines are searched for in the given data with extra
929 data lines allowed between lines belonging to different strings.
930 * Container of strings containing text lines separated by newlines
931 and containers containing strings - the same as above with the
932 internal containers containing strings being interpreted as if
933 all their content was joined together into a single string
934 separated by newlines.
936 A newline at the end of any multi-line lines string is interpreted as
937 an expected extra trailig empty line.
939 # str.splitlines() trims at most one trailing newline while we want the
940 # trailing newline to indicate that there should be an extra empty line
942 splitlines
= lambda x
: (x
+ "\n").splitlines()
946 elif data
.__class
__ is str:
947 data
= splitlines(data
)
949 if lines
.__class
__ is str:
950 lines
= [splitlines(lines
)]
954 if x
.__class
__ is str:
959 if _contains_lines(data
, lines
) != bool(expected
):
962 output
= ["Did not find expected lines:"]
964 output
= ["Found unexpected lines:"]
966 for line_sequence
in lines
:
972 output
.extend(" > " + line
for line
in line_sequence
)
973 output
.append("in output:")
974 output
.extend(" > " + line
for line
in data
)
975 annotation("failure", "\n".join(output
))
978 def __ignore_elements(self
, list, wildcard
):
979 """Removes in-place 'list' elements matching the given 'wildcard'."""
980 list[:] = filter(lambda x
, w
=wildcard
: not fnmatch
.fnmatch(x
, w
), list)
982 def __makedirs(self
, path
, wait
):
984 Creates a folder with the given path, together with any missing
985 parent folders. If WAIT is set, makes sure any newly created folders
986 have modification timestamps newer than the ones left behind by the
993 while path
and path
not in stack
and not os
.path
.isdir(path
):
995 path
= os
.path
.dirname(path
)
999 self
.__ensure
_newer
_than
_last
_build
(path
)
1005 def __python_timestamp_resolution(self
, path
, minimum_resolution
):
1007 Returns the modification timestamp resolution for the given path
1008 supported by the used Python interpreter/OS/filesystem combination.
1009 Will not check for resolutions less than the given minimum value. Will
1010 change the path's modification timestamp in the process.
1013 0 - nanosecond resolution supported
1014 positive decimal - timestamp resolution in seconds
1017 # Note on Python's floating point timestamp support:
1018 # Python interpreter versions prior to Python 2.3 did not support
1019 # floating point timestamps. Versions 2.3 through 3.3 may or may not
1020 # support it depending on the configuration (may be toggled by calling
1021 # os.stat_float_times(True/False) at program startup, disabled by
1022 # default prior to Python 2.5 and enabled by default since). Python 3.3
1023 # deprecated this configuration and 3.4 removed support for it after
1024 # which floating point timestamps are always supported.
1025 ver
= sys
.version_info
[0:2]
1026 python_nanosecond_support
= ver
>= (3, 4) or (ver
>= (2, 3) and
1027 os
.stat_float_times())
1029 # Minimal expected floating point difference used to account for
1030 # possible imprecise floating point number representations. We want
1031 # this number to be small (at least smaller than 0.0001) but still
1032 # large enough that we can be sure that increasing a floating point
1033 # value by 2 * eta guarantees the value read back will be increased by
1037 stats_orig
= os
.stat(path
)
1038 def test_time(diff
):
1039 """Returns whether a timestamp difference is detectable."""
1040 os
.utime(path
, (stats_orig
.st_atime
, stats_orig
.st_mtime
+ diff
))
1041 return os
.stat(path
).st_mtime
> stats_orig
.st_mtime
+ eta
1043 # Test for nanosecond timestamp resolution support.
1044 if not minimum_resolution
and python_nanosecond_support
:
1045 if test_time(2 * eta
):
1048 # Detect the filesystem timestamp resolution. Note that there is no
1049 # need to make this code 'as fast as possible' as, this function gets
1050 # called before having to sleep until the next detectable modification
1051 # timestamp value and that, since we already know nanosecond resolution
1052 # is not supported, will surely take longer than whatever we do here to
1053 # detect this minimal detectable modification timestamp resolution.
1055 if not python_nanosecond_support
:
1056 # If Python does not support nanosecond timestamp resolution we
1057 # know the minimum possible supported timestamp resolution is 1
1059 minimum_resolution
= max(1, minimum_resolution
)
1060 index
= max(1, int(minimum_resolution
/ step
))
1061 while step
* index
< minimum_resolution
:
1062 # Floating point number representation errors may cause our
1063 # initially calculated start index to be too small if calculated
1067 # Do not simply add up the steps to avoid cumulative floating point
1068 # number representation errors.
1071 raise TestEnvironmentError("File systems with too coarse "
1072 "modification timestamp resolutions not supported.")
1077 def __wait_for_time_change(self
, path
, touch
, last_build_time
):
1079 Wait until a newly assigned file system modification timestamp for
1080 the given path is large enough for the timestamp difference between it
1081 and the last build timestamp or the path's original file system
1082 modification timestamp (depending on the last_build_time flag) to be
1083 correctly recognized by both this Python based testing framework and
1084 the Boost Jam executable being tested. May optionally touch the given
1085 path to set its modification timestamp to the new value.
1088 assert self
.last_build_timestamp
or not last_build_time
1089 stats_orig
= os
.stat(path
)
1092 start_time
= self
.last_build_timestamp
1094 start_time
= stats_orig
.st_mtime
1096 build_resolution
= self
.__build
_timestamp
_resolution
()
1097 assert build_resolution
>= 0
1099 # Check whether the current timestamp is already new enough.
1100 if stats_orig
.st_mtime
> start_time
and (not build_resolution
or
1101 stats_orig
.st_mtime
>= start_time
+ build_resolution
):
1104 resolution
= self
.__python
_timestamp
_resolution
(path
, build_resolution
)
1105 assert resolution
>= build_resolution
1107 # Implementation notes:
1108 # * Theoretically time.sleep() API might get interrupted too soon
1109 # (never actually encountered).
1110 # * We encountered cases where we sleep just long enough for the
1111 # filesystem's modifiction timestamp to change to the desired value,
1112 # but after waking up, the read timestamp is still just a tiny bit
1113 # too small (encountered on Windows). This is most likely caused by
1114 # imprecise floating point timestamp & sleep interval representation
1115 # used by Python. Note though that we never encountered a case where
1116 # more than one additional tiny sleep() call was needed to remedy
1118 # * We try to wait long enough for the timestamp to change, but do not
1119 # want to waste processing time by waiting too long. The main
1120 # problem is that when we have a coarse resolution, the actual times
1121 # get rounded and we do not know the exact sleep time needed for the
1122 # difference between two such times to pass. E.g. if we have a 1
1123 # second resolution and the original and the current file timestamps
1124 # are both 10 seconds then it could be that the current time is
1125 # 10.99 seconds and that we can wait for just one hundredth of a
1126 # second for the current file timestamp to reach its next value, and
1127 # using a longer sleep interval than that would just be wasting
1130 os
.utime(path
, None)
1131 c
= os
.stat(path
).st_mtime
1133 if c
> start_time
and (not build_resolution
or c
>= start_time
1134 + build_resolution
):
1136 if c
<= start_time
- resolution
:
1137 # Move close to the desired timestamp in one sleep, but not
1138 # close enough for timestamp rounding to potentially cause
1139 # us to wait too long.
1140 if start_time
- c
> 5:
1142 error_message
= ("Last build time recorded as "
1143 "being a future event, causing a too long "
1144 "wait period. Something must have played "
1145 "around with the system clock.")
1147 error_message
= ("Original path modification "
1148 "timestamp set to far into the future or "
1149 "something must have played around with the "
1150 "system clock, causing a too long wait "
1151 "period.\nPath: '%s'" % path
)
1152 raise TestEnvironmentError(message
)
1153 _sleep(start_time
- c
)
1155 # We are close to the desired timestamp so take baby sleeps
1156 # to avoid sleeping too long.
1157 _sleep(max(0.01, resolution
/ 10))
1161 _sleep(max(0.01, start_time
- c
))
1164 os
.utime(path
, (stats_orig
.st_atime
, stats_orig
.st_mtime
))
1168 def __init__(self
, s
=""):
1170 if s
.__class
__ is str:
1171 # Have to handle escaped spaces correctly.
1172 elements
= s
.replace("\ ", "\001").split()
1175 self
.l
= [e
.replace("\001", " ") for e
in elements
]
1180 def __getitem__(self
, key
):
1183 def __setitem__(self
, key
, value
):
1186 def __delitem__(self
, key
):
1193 return "%s.List(%r)" % (self
.__module
__, " ".join(self
.l
))
1195 def __mul__(self
, other
):
1197 if not isinstance(other
, List
):
1201 result
.l
.append(f
+ s
)
1204 def __rmul__(self
, other
):
1205 if not isinstance(other
, List
):
1207 return List
.__mul
__(other
, self
)
1209 def __add__(self
, other
):
1211 result
.l
= self
.l
[:] + other
.l
[:]
1215 def _contains_lines(data
, lines
):
1216 data_line_count
= len(data
)
1217 expected_line_count
= reduce(lambda x
, y
: x
+ len(y
), lines
, 0)
1219 for expected
in lines
:
1220 if expected_line_count
> data_line_count
- index
:
1222 expected_line_count
-= len(expected
)
1223 index
= _match_line_sequence(data
, index
, data_line_count
-
1224 expected_line_count
, expected
)
1230 def _match_line_sequence(data
, start
, end
, lines
):
1233 for index
in xrange(start
, end
- len(lines
) + 1):
1235 for expected
in lines
:
1236 if not fnmatch
.fnmatch(data
[data_index
], expected
):
1246 raise TestEnvironmentError("Test environment error: sleep period of "
1247 "more than 5 seconds requested. Most likely caused by a file with "
1248 "its modification timestamp set to sometime in the future.")
1252 ###############################################################################
1256 ###############################################################################
1258 # Make os.stat() return file modification times as floats instead of integers
1259 # to get the best possible file timestamp resolution available. The exact
1260 # resolution depends on the underlying file system and the Python os.stat()
1261 # implementation. The better the resolution we achieve, the shorter we need to
1262 # wait for files we create to start getting new timestamps.
1265 # * os.stat_float_times() function first introduced in Python 2.3. and
1266 # suggested for deprecation in Python 3.3.
1267 # * On Python versions 2.5+ we do not need to do this as there os.stat()
1268 # returns floating point file modification times by default.
1269 # * Windows CPython implementations prior to version 2.5 do not support file
1270 # modification timestamp resolutions of less than 1 second no matter whether
1271 # these timestamps are returned as integer or floating point values.
1272 # * Python documentation states that this should be set in a program's
1273 # __main__ module to avoid affecting other libraries that might not be ready
1274 # to support floating point timestamps. Since we use no such external
1275 # libraries, we ignore this warning to make it easier to enable this feature
1276 # in both our single & multiple-test scripts.
1277 if (2, 3) <= sys
.version_info
< (2, 5) and not os
.stat_float_times():
1278 os
.stat_float_times(True)
1281 # Quickie tests. Should use doctest instead.
1282 if __name__
== "__main__":
1283 assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']"
1284 assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')"
1286 assert _contains_lines([], [])
1287 assert _contains_lines([], [[]])
1288 assert _contains_lines([], [[], []])
1289 assert _contains_lines([], [[], [], []])
1290 assert not _contains_lines([], [[""]])
1291 assert not _contains_lines([], [["a"]])
1293 assert _contains_lines([""], [])
1294 assert _contains_lines(["a"], [])
1295 assert _contains_lines(["a", "b"], [])
1296 assert _contains_lines(["a", "b"], [[], [], []])
1298 assert _contains_lines([""], [[""]])
1299 assert not _contains_lines([""], [["a"]])
1300 assert not _contains_lines(["a"], [[""]])
1301 assert _contains_lines(["a", "", "b", ""], [["a"]])
1302 assert _contains_lines(["a", "", "b", ""], [[""]])
1303 assert _contains_lines(["a", "", "b"], [["b"]])
1304 assert not _contains_lines(["a", "b"], [[""]])
1305 assert not _contains_lines(["a", "", "b", ""], [["c"]])
1306 assert _contains_lines(["a", "", "b", "x"], [["x"]])
1308 data
= ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
1309 assert _contains_lines(data
, [["1", "2"]])
1310 assert not _contains_lines(data
, [["2", "1"]])
1311 assert not _contains_lines(data
, [["1", "3"]])
1312 assert not _contains_lines(data
, [["1", "3"]])
1313 assert _contains_lines(data
, [["1"], ["2"]])
1314 assert _contains_lines(data
, [["1"], [], [], [], ["2"]])
1315 assert _contains_lines(data
, [["1"], ["3"]])
1316 assert not _contains_lines(data
, [["3"], ["1"]])
1317 assert _contains_lines(data
, [["3"], ["7"], ["8"]])
1318 assert not _contains_lines(data
, [["1"], ["3", "5"]])
1319 assert not _contains_lines(data
, [["1"], [""], ["5"]])
1320 assert not _contains_lines(data
, [["1"], ["5"], ["3"]])
1321 assert not _contains_lines(data
, [["1"], ["5", "3"]])
1323 assert not _contains_lines(data
, [[" 3"]])
1324 assert not _contains_lines(data
, [["3 "]])
1325 assert not _contains_lines(data
, [["3", ""]])
1326 assert not _contains_lines(data
, [["", "3"]])
1328 print("tests passed")