]> git.proxmox.com Git - ceph.git/blame - ceph/src/boost/tools/build/test/BoostBuild.py
update sources to v12.2.3
[ceph.git] / ceph / src / boost / tools / build / test / BoostBuild.py
CommitLineData
7c673cae
FG
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)
7
8import TestCmd
9
10import copy
11import fnmatch
12import glob
13import math
14import os
15import os.path
16import re
17import shutil
18import StringIO
19import subprocess
20import sys
21import tempfile
22import time
23import traceback
24import tree
25import types
26
27from xml.sax.saxutils import escape
28
29
30class TestEnvironmentError(Exception):
31 pass
32
33
34annotations = []
35
36
37def print_annotation(name, value, xml):
38 """Writes some named bits of information about the current test run."""
39 if xml:
40 print escape(name) + " {{{"
41 print escape(value)
42 print "}}}"
43 else:
44 print name + " {{{"
45 print value
46 print "}}}"
47
48
49def flush_annotations(xml=0):
50 global annotations
51 for ann in annotations:
52 print_annotation(ann[0], ann[1], xml)
53 annotations = []
54
55
56def clear_annotations():
57 global annotations
58 annotations = []
59
60
61defer_annotations = 0
62
63def set_defer_annotations(n):
64 global defer_annotations
65 defer_annotations = n
66
67
68def annotate_stack_trace(tb=None):
69 if tb:
70 trace = TestCmd.caller(traceback.extract_tb(tb), 0)
71 else:
72 trace = TestCmd.caller(traceback.extract_stack(), 1)
73 annotation("stacktrace", trace)
74
75
76def annotation(name, value):
77 """Records an annotation about the test run."""
78 annotations.append((name, value))
79 if not defer_annotations:
80 flush_annotations()
81
82
83def get_toolset():
84 toolset = None
85 for arg in sys.argv[1:]:
86 if not arg.startswith("-"):
87 toolset = arg
88 return toolset or "gcc"
89
90
91# Detect the host OS.
92cygwin = hasattr(os, "uname") and os.uname()[0].lower().startswith("cygwin")
93windows = cygwin or os.environ.get("OS", "").lower().startswith("windows")
94
95
96def prepare_prefixes_and_suffixes(toolset):
97 prepare_suffix_map(toolset)
98 prepare_library_prefix(toolset)
99
100
101def prepare_suffix_map(toolset):
102 """
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).
106
107 """
108 global suffixes
109 suffixes = {}
110 if windows:
111 if toolset == "gcc":
112 suffixes[".lib"] = ".a" # mingw static libs use suffix ".a".
113 suffixes[".obj"] = ".o"
114 if cygwin:
115 suffixes[".implib"] = ".lib.a"
116 else:
117 suffixes[".implib"] = ".lib"
118 else:
119 suffixes[".exe"] = ""
120 suffixes[".dll"] = ".so"
121 suffixes[".lib"] = ".a"
122 suffixes[".obj"] = ".o"
123 suffixes[".implib"] = ".no_implib_files_on_this_platform"
124
125 if hasattr(os, "uname") and os.uname()[0] == "Darwin":
126 suffixes[".dll"] = ".dylib"
127
128
129def prepare_library_prefix(toolset):
130 """
131 Setup whether Boost Build is expected to automatically prepend prefixes
132 to its built library targets.
133
134 """
135 global lib_prefix
136 lib_prefix = "lib"
137
138 global dll_prefix
139 if cygwin:
140 dll_prefix = "cyg"
141 elif windows and toolset != "gcc":
142 dll_prefix = None
143 else:
144 dll_prefix = "lib"
145
146
147def re_remove(sequence, regex):
148 me = re.compile(regex)
149 result = filter(lambda x: me.match(x), sequence)
150 if not result:
151 raise ValueError()
152 for r in result:
153 sequence.remove(r)
154
155
156def glob_remove(sequence, pattern):
157 result = fnmatch.filter(sequence, pattern)
158 if not result:
159 raise ValueError()
160 for r in result:
161 sequence.remove(r)
162
163
164class Tester(TestCmd.TestCmd):
165 """Main tester class for Boost Build.
166
167 Optional arguments:
168
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
174 executable.
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
186 configuration file.
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
190 run from.
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).
194
195 Optional arguments inherited from the base class:
196
197 `description` - Test description string displayed in case
198 of a failed test.
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
207 line option does.
208 """
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,
b32b8144 212 ignore_toolset_requirements=False, workdir="", pass_d0=True,
7c673cae
FG
213 **keywords):
214
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)
220
221 self.last_build_timestamp = 0
222 self.translate_suffixes = translate_suffixes
223 self.use_test_config = use_test_config
224
225 self.toolset = get_toolset()
226 self.pass_toolset = pass_toolset
227 self.ignore_toolset_requirements = ignore_toolset_requirements
228
229 prepare_prefixes_and_suffixes(pass_toolset and self.toolset or "gcc")
230
231 use_default_bjam = "--default-bjam" in sys.argv
232
233 if not use_default_bjam:
234 jam_build_dir = ""
235 if os.name == "nt":
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":
246 cpu = os.uname()[4]
247 if re.match("i.86", cpu):
248 jam_build_dir = "bin.linuxx86"
249 else:
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"
258 else:
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"
268 else:
269 raise ("Do not know directory where Jam is built for this "
270 "system: %s/%s" % (os.name, os.uname()[0]))
271 else:
272 raise ("Do not know directory where Jam is built for this "
273 "system: %s" % os.name)
274
275 # Find where jam_src is located. Try for the debug version if it is
276 # lying around.
277 dirs = [os.path.join("..", "src", "engine", jam_build_dir + ".debug"),
278 os.path.join("..", "src", "engine", jam_build_dir)]
279 for d in dirs:
280 if os.path.exists(d):
281 jam_build_dir = d
282 break
283 else:
284 print("Cannot find built Boost.Jam")
285 sys.exit(1)
286
287 verbosity = ["-d0", "--quiet"]
288 if not pass_d0:
289 verbosity = []
290 if "--verbose" in sys.argv:
291 keywords["verbose"] = True
292 verbosity = ["-d+2"]
293
294 if boost_build_path is None:
295 boost_build_path = self.original_workdir + "/.."
296
297 program_list = []
298 if use_default_bjam:
299 program_list.append(executable)
300 else:
301 program_list.append(os.path.join(jam_build_dir, executable))
302 program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"')
303 if verbosity:
304 program_list += verbosity
305 if arguments:
306 program_list += arguments
307
308 TestCmd.TestCmd.__init__(self, program=program_list, match=match,
309 workdir=workdir, inpath=use_default_bjam, **keywords)
310
311 os.chdir(self.workdir)
312
313 def cleanup(self):
314 try:
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
320 # this case.
321 pass
322
323 #
324 # Methods that change the working directory's content.
325 #
326 def set_tree(self, tree_location):
327 # It is not possible to remove the current directory.
328 d = os.getcwd()
329 os.chdir(os.path.dirname(self.workdir))
330 shutil.rmtree(self.workdir, ignore_errors=False)
331
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)
335
336 os.chdir(d)
337 def make_writable(unused, dir, entries):
338 for e in 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)
342
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")
347 try:
348 f.write(content)
349 finally:
350 f.close()
351 self.__ensure_newer_than_last_build(nfile)
352
353 def copy(self, src, dst):
354 try:
b32b8144 355 self.write(dst, self.read(src, binary=True))
7c673cae
FG
356 except:
357 self.fail_test(1)
358
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)
b32b8144 363 self.write(dst, self.__read(src, binary=True))
7c673cae
FG
364 os.utime(dst_name, (stats.st_atime, stats.st_mtime))
365
366 def touch(self, names, wait=True):
367 if names.__class__ is str:
368 names = [names]
369 for name in names:
370 path = self.native_file_name(name)
371 if wait:
372 self.__ensure_newer_than_last_build(path)
373 else:
374 os.utime(path, None)
375
376 def rm(self, names):
377 if not type(names) == types.ListType:
378 names = [names]
379
380 if names == ["."]:
381 # If we are deleting the entire workspace, there is no need to wait
382 # for a clock tick.
383 self.last_build_timestamp = 0
384
385 # Avoid attempts to remove the current directory.
386 os.chdir(self.original_workdir)
387 for name in names:
388 n = glob.glob(self.native_file_name(name))
389 if n: n = n[0]
390 if not n:
391 n = self.glob_file(name.replace("$toolset", self.toolset + "*")
392 )
393 if n:
394 if os.path.isdir(n):
395 shutil.rmtree(n, ignore_errors=False)
396 else:
397 os.unlink(n)
398
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)
403
404 def expand_toolset(self, name):
405 """
406 Expands $toolset placeholder in the given file to the name of the
407 toolset currently being tested.
408
409 """
410 self.write(name, self.read(name).replace("$toolset", self.toolset))
411
412 def dump_stdio(self):
413 annotation("STDOUT", self.stdout())
414 annotation("STDERR", self.stderr())
415
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):
420
421 assert extra_args.__class__ is not str
422
423 if os.path.isabs(subdir):
424 print("You must pass a relative directory to subdir <%s>." % subdir
425 )
426 return
427
428 self.previous_tree, dummy = tree.build_tree(self.workdir)
429
430 if match is None:
431 match = self.match
432
433 if pass_toolset is None:
434 pass_toolset = self.pass_toolset
435
436 if use_test_config is None:
437 use_test_config = self.use_test_config
438
439 if ignore_toolset_requirements is None:
440 ignore_toolset_requirements = self.ignore_toolset_requirements
441
442 try:
443 kw["program"] = []
444 kw["program"] += self.program
445 if extra_args:
446 kw["program"] += extra_args
447 if pass_toolset:
448 kw["program"].append("toolset=" + self.toolset)
449 if use_test_config:
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")
461 kw["chdir"] = subdir
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()
466 except:
467 self.dump_stdio()
468 raise
469
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
479
480 self.difference.ignore_directories()
481 self.unexpected_difference = copy.deepcopy(self.difference)
482
483 if (status and self.status) is not None and self.status != status:
484 expect = ""
485 if status != 0:
486 expect = " (expected %d)" % status
487
488 annotation("failure", '"%s" returned %d%s' % (kw["program"],
489 self.status, expect))
490
491 annotation("reason", "unexpected status returned by bjam")
492 self.fail_test(1)
493
494 if stdout is not None and not match(self.stdout(), stdout):
b32b8144 495 stdout_test = match(self.stdout(), stdout)
7c673cae
FG
496 annotation("failure", "Unexpected stdout")
497 annotation("Expected STDOUT", stdout)
498 annotation("Actual STDOUT", self.stdout())
499 stderr = self.stderr()
500 if stderr:
501 annotation("STDERR", stderr)
b32b8144 502 self.maybe_do_diff(self.stdout(), stdout, stdout_test)
7c673cae
FG
503 self.fail_test(1, dump_stdio=False)
504
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())
508
509 if stderr is not None and not match(actual_stderr, stderr):
b32b8144 510 stderr_test = match(actual_stderr, stderr)
7c673cae
FG
511 annotation("failure", "Unexpected stderr")
512 annotation("Expected STDERR", stderr)
513 annotation("Actual STDERR", self.stderr())
514 annotation("STDOUT", self.stdout())
b32b8144 515 self.maybe_do_diff(actual_stderr, stderr, stderr_test)
7c673cae
FG
516 self.fail_test(1, dump_stdio=False)
517
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,
523 expected_duration))
524 self.fail_test(1, dump_stdio=False)
525
b32b8144
FG
526 self.__ignore_junk()
527
7c673cae 528 def glob_file(self, name):
b32b8144 529 name = self.adjust_name(name)
7c673cae
FG
530 result = None
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):
b32b8144 536 result = self.__native_file_name(f)
7c673cae
FG
537 break
538 if not result:
b32b8144 539 result = glob.glob(self.__native_file_name(name))
7c673cae
FG
540 if result:
541 result = result[0]
542 return result
543
b32b8144 544 def __read(self, name, binary=False):
7c673cae 545 try:
7c673cae
FG
546 openMode = "r"
547 if binary:
548 openMode += "b"
549 else:
550 openMode += "U"
551 f = open(name, openMode)
552 result = f.read()
553 f.close()
554 return result
555 except:
556 annotation("failure", "Could not open '%s'" % name)
557 self.fail_test(1)
558 return ""
559
b32b8144
FG
560 def read(self, name, binary=False):
561 name = self.glob_file(name)
562 return self.__read(name, binary=binary)
563
7c673cae
FG
564 def read_and_strip(self, name):
565 if not self.glob_file(name):
566 return ""
567 f = open(self.glob_file(name), "rb")
568 lines = f.readlines()
569 f.close()
570 result = "\n".join(x.rstrip() for x in lines)
571 if lines and lines[-1][-1] != "\n":
572 return result + "\n"
573 return result
574
575 def fail_test(self, condition, dump_difference=True, dump_stdio=True,
576 dump_stack=True):
577 if not condition:
578 return
579
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",
584 f.getvalue())
585
586 if dump_stdio:
587 self.dump_stdio()
588
589 if "--preserve" in sys.argv:
590 print
591 print "*** Copying the state of working dir into 'failed_test' ***"
592 print
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)
601
602 if dump_stack:
603 annotate_stack_trace()
604 sys.exit(1)
605
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
609 # wildcards.
610
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):
614 try:
615 glob_remove(self.unexpected_difference.added_files, name)
616 except:
617 annotation("failure", "File %s not added as expected" % name)
618 self.fail_test(1)
619
620 def ignore_addition(self, wildcard):
621 self.__ignore_elements(self.unexpected_difference.added_files,
622 wildcard)
623
624 def expect_removal(self, names):
625 for name in self.adjust_names(names):
626 try:
627 glob_remove(self.unexpected_difference.removed_files, name)
628 except:
629 annotation("failure", "File %s not removed as expected" % name)
630 self.fail_test(1)
631
632 def ignore_removal(self, wildcard):
633 self.__ignore_elements(self.unexpected_difference.removed_files,
634 wildcard)
635
636 def expect_modification(self, names):
637 for name in self.adjust_names(names):
638 try:
639 glob_remove(self.unexpected_difference.modified_files, name)
640 except:
641 annotation("failure", "File %s not modified as expected" %
642 name)
643 self.fail_test(1)
644
645 def ignore_modification(self, wildcard):
646 self.__ignore_elements(self.unexpected_difference.modified_files,
647 wildcard)
648
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
653 # that:
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]
659
660 while filesets:
661 try:
662 glob_remove(filesets[-1], name)
663 break
664 except ValueError:
665 filesets.pop()
666
667 if not filesets:
668 annotation("failure", "File %s not touched as expected" % name)
669 self.fail_test(1)
670
671 def ignore_touch(self, wildcard):
672 self.__ignore_elements(self.unexpected_difference.touched_files,
673 wildcard)
674
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)
680
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)
686 self.fail_test(1)
687 if name in self.difference.removed_files:
688 annotation("failure",
689 "File %s removed, but no action was expected" % name)
690 self.fail_test(1)
691 pass
692 if name in self.difference.modified_files:
693 annotation("failure",
694 "File %s modified, but no action was expected" % name)
695 self.fail_test(1)
696 if name in self.difference.touched_files:
697 annotation("failure",
698 "File %s touched, but no action was expected" % name)
699 self.fail_test(1)
700
b32b8144 701 def __ignore_junk(self):
7c673cae
FG
702 # Not totally sure about this change, but I do not see a good
703 # alternative.
704 if windows:
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.
710
711 # Debug builds of bjam built with gcc produce this profiling data.
712 self.ignore("gmon.out")
713 self.ignore("*/gmon.out")
714
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")
719
720 # Compiled Python files created when running Python based Boost Build.
721 self.ignore("*.pyc")
722
b32b8144
FG
723 # OSX/Darwin files and dirs.
724 self.ignore("*.dSYM/*")
725
726 def expect_nothing_more(self):
7c673cae
FG
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())
732 self.fail_test(1)
733
734 def expect_output_lines(self, lines, expected=True):
735 self.__expect_lines(self.stdout(), lines, expected)
736
737 def expect_content_lines(self, filename, line, expected=True):
b32b8144 738 self.__expect_lines(self.read_and_strip(filename), line, expected)
7c673cae
FG
739
740 def expect_content(self, name, content, exact=False):
b32b8144 741 actual = self.read(name)
7c673cae
FG
742 content = content.replace("$toolset", self.toolset + "*")
743
744 matched = False
745 if exact:
746 matched = fnmatch.fnmatch(actual, content)
747 else:
748 def sorted_(x):
b32b8144 749 x.sort(lambda x, y: cmp(x.lower().replace("\\","/"), y.lower().replace("\\","/")))
7c673cae
FG
750 return x
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_):
754 matched = map(
755 lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y),
756 actual_, content_)
757 matched = reduce(
758 lambda x, y: x and reduce(
759 lambda a, b: a and b,
760 y),
761 matched)
762
763 if not matched:
764 print "Expected:\n"
765 print content
766 print "Got:\n"
767 print actual
768 self.fail_test(1)
769
b32b8144 770 def maybe_do_diff(self, actual, expected, result=None):
7c673cae
FG
771 if os.environ.get("DO_DIFF"):
772 e = tempfile.mktemp("expected")
773 a = tempfile.mktemp("actual")
774 f = open(e, "w")
775 f.write(expected)
776 f.close()
777 f = open(a, "w")
778 f.write(actual)
779 f.close()
780 print("DIFFERENCE")
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
787 ))
788 os.unlink(e)
789 os.unlink(a)
b32b8144
FG
790 elif type(result) is TestCmd.MatchError:
791 print(result.message)
7c673cae
FG
792 else:
793 print("Set environmental variable 'DO_DIFF' to examine the "
794 "difference.")
795
796 # Internal methods.
797 def adjust_lib_name(self, name):
798 global lib_prefix
799 global dll_prefix
800 result = name
801
802 pos = name.rfind(".")
803 if pos != -1:
804 suffix = name[pos:]
805 if suffix == ".lib":
806 (head, tail) = os.path.split(name)
807 if lib_prefix:
808 tail = lib_prefix + tail
809 result = os.path.join(head, tail)
810 elif suffix == ".dll":
811 (head, tail) = os.path.split(name)
812 if dll_prefix:
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("\\", "/")
818 return result
819
820 def adjust_suffix(self, name):
821 if not self.translate_suffixes:
822 return name
823 pos = name.rfind(".")
824 if pos == -1:
825 return name
826 suffix = name[pos:]
827 return name[:pos] + suffixes.get(suffix, suffix)
828
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:
833 names = [names]
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)
837 return r
b32b8144
FG
838
839 def adjust_name(self, name):
840 return self.adjust_names(name)[0]
7c673cae 841
b32b8144 842 def __native_file_name(self, name):
7c673cae
FG
843 return os.path.normpath(os.path.join(self.workdir, *name.split("/")))
844
b32b8144
FG
845 def native_file_name(self, name):
846 return self.__native_file_name(self.adjust_name(name))
847
7c673cae
FG
848 def wait_for_time_change(self, path, touch):
849 """
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.
855
856 """
857 self.__wait_for_time_change(path, touch, last_build_time=False)
858
859 def __build_timestamp_resolution(self):
860 """
861 Returns the minimum path modification timestamp resolution supported
862 by the used Boost Jam executable.
863
864 """
865 dir = tempfile.mkdtemp("bjam_version_info")
866 try:
867 jam_script = "timestamp_resolution.jam"
868 f = open(os.path.join(dir, jam_script), "w")
869 try:
870 f.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;")
871 finally:
872 f.close()
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()
876 finally:
877 shutil.rmtree(dir, ignore_errors=False)
878
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)
883 if err:
884 raise TestEnvironmentError("Unexpected error output (%s) when "
885 "detecting Boost Jam's minimum supported path modification "
886 "timestamp resolution version information." % err)
887
888 r = re.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out)
889 if not r:
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
893 # second.
894 # TODO: Phase this support out to avoid such fallback code from
895 # possibly covering up other problems.
896 return 1
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
902
903 def __ensure_newer_than_last_build(self, path):
904 """
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.
911
912 """
913 if self.last_build_timestamp:
914 self.__wait_for_time_change(path, touch=True, last_build_time=True)
915
916 def __expect_lines(self, data, lines, expected):
917 """
918 Checks whether the given data contains the given lines.
919
920 Data may be specified as a single string containing text lines
921 separated by newline characters.
922
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.
935
936 A newline at the end of any multi-line lines string is interpreted as
937 an expected extra trailig empty line.
938 """
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
941 # at the end.
942 splitlines = lambda x : (x + "\n").splitlines()
943
944 if data is None:
945 data = []
946 elif data.__class__ is str:
947 data = splitlines(data)
948
949 if lines.__class__ is str:
950 lines = [splitlines(lines)]
951 else:
952 expanded = []
953 for x in lines:
954 if x.__class__ is str:
955 x = splitlines(x)
956 expanded.append(x)
957 lines = expanded
958
959 if _contains_lines(data, lines) != bool(expected):
960 output = []
961 if expected:
962 output = ["Did not find expected lines:"]
963 else:
964 output = ["Found unexpected lines:"]
965 first = True
966 for line_sequence in lines:
967 if line_sequence:
968 if first:
969 first = False
970 else:
971 output.append("...")
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))
976 self.fail_test(1)
977
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)
981
982 def __makedirs(self, path, wait):
983 """
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
987 last build run.
988
989 """
990 try:
991 if wait:
992 stack = []
993 while path and path not in stack and not os.path.isdir(path):
994 stack.append(path)
995 path = os.path.dirname(path)
996 while stack:
997 path = stack.pop()
998 os.mkdir(path)
999 self.__ensure_newer_than_last_build(path)
1000 else:
1001 os.makedirs(path)
1002 except Exception:
1003 pass
1004
1005 def __python_timestamp_resolution(self, path, minimum_resolution):
1006 """
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.
1011
1012 Return values:
1013 0 - nanosecond resolution supported
1014 positive decimal - timestamp resolution in seconds
1015
1016 """
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())
1028
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
1034 # at least eta.
1035 eta = 0.00005
1036
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
1042
1043 # Test for nanosecond timestamp resolution support.
1044 if not minimum_resolution and python_nanosecond_support:
1045 if test_time(2 * eta):
1046 return 0
1047
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.
1054 step = 0.1
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
1058 # second.
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
1064 # directly.
1065 index += 1
1066 while True:
1067 # Do not simply add up the steps to avoid cumulative floating point
1068 # number representation errors.
1069 next = step * index
1070 if next > 10:
1071 raise TestEnvironmentError("File systems with too coarse "
1072 "modification timestamp resolutions not supported.")
1073 if test_time(next):
1074 return next
1075 index += 1
1076
7c673cae
FG
1077 def __wait_for_time_change(self, path, touch, last_build_time):
1078 """
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.
1086
1087 """
1088 assert self.last_build_timestamp or not last_build_time
1089 stats_orig = os.stat(path)
1090
1091 if last_build_time:
1092 start_time = self.last_build_timestamp
1093 else:
1094 start_time = stats_orig.st_mtime
1095
1096 build_resolution = self.__build_timestamp_resolution()
1097 assert build_resolution >= 0
1098
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):
1102 return
1103
1104 resolution = self.__python_timestamp_resolution(path, build_resolution)
1105 assert resolution >= build_resolution
1106
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
1117 # the situation.
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
1128 # time.
1129 while True:
1130 os.utime(path, None)
1131 c = os.stat(path).st_mtime
1132 if resolution:
1133 if c > start_time and (not build_resolution or c >= start_time
1134 + build_resolution):
1135 break
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:
1141 if last_build_time:
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.")
1146 else:
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)
1154 else:
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))
1158 else:
1159 if c > start_time:
1160 break
1161 _sleep(max(0.01, start_time - c))
1162
1163 if not touch:
1164 os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime))
1165
1166
1167class List:
1168 def __init__(self, s=""):
1169 elements = []
1170 if s.__class__ is str:
1171 # Have to handle escaped spaces correctly.
1172 elements = s.replace("\ ", "\001").split()
1173 else:
1174 elements = s
1175 self.l = [e.replace("\001", " ") for e in elements]
1176
1177 def __len__(self):
1178 return len(self.l)
1179
1180 def __getitem__(self, key):
1181 return self.l[key]
1182
1183 def __setitem__(self, key, value):
1184 self.l[key] = value
1185
1186 def __delitem__(self, key):
1187 del self.l[key]
1188
1189 def __str__(self):
1190 return str(self.l)
1191
1192 def __repr__(self):
1193 return "%s.List(%r)" % (self.__module__, " ".join(self.l))
1194
1195 def __mul__(self, other):
1196 result = List()
1197 if not isinstance(other, List):
1198 other = List(other)
1199 for f in self:
1200 for s in other:
1201 result.l.append(f + s)
1202 return result
1203
1204 def __rmul__(self, other):
1205 if not isinstance(other, List):
1206 other = List(other)
1207 return List.__mul__(other, self)
1208
1209 def __add__(self, other):
1210 result = List()
1211 result.l = self.l[:] + other.l[:]
1212 return result
1213
1214
1215def _contains_lines(data, lines):
1216 data_line_count = len(data)
1217 expected_line_count = reduce(lambda x, y: x + len(y), lines, 0)
1218 index = 0
1219 for expected in lines:
1220 if expected_line_count > data_line_count - index:
1221 return False
1222 expected_line_count -= len(expected)
1223 index = _match_line_sequence(data, index, data_line_count -
1224 expected_line_count, expected)
1225 if index < 0:
1226 return False
1227 return True
1228
1229
1230def _match_line_sequence(data, start, end, lines):
1231 if not lines:
1232 return start
1233 for index in xrange(start, end - len(lines) + 1):
1234 data_index = index
1235 for expected in lines:
1236 if not fnmatch.fnmatch(data[data_index], expected):
1237 break;
1238 data_index += 1
1239 else:
1240 return data_index
1241 return -1
1242
1243
1244def _sleep(delay):
1245 if delay > 5:
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.")
1249 time.sleep(delay)
1250
1251
1252###############################################################################
1253#
1254# Initialization.
1255#
1256###############################################################################
1257
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.
1263#
1264# Additional notes:
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.
1277if (2, 3) <= sys.version_info < (2, 5) and not os.stat_float_times():
1278 os.stat_float_times(True)
1279
1280
1281# Quickie tests. Should use doctest instead.
1282if __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')"
1285
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"]])
1292
1293 assert _contains_lines([""], [])
1294 assert _contains_lines(["a"], [])
1295 assert _contains_lines(["a", "b"], [])
1296 assert _contains_lines(["a", "b"], [[], [], []])
1297
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"]])
1307
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"]])
1322
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"]])
1327
1328 print("tests passed")