]> git.proxmox.com Git - ceph.git/blob - ceph/src/boost/tools/build/test/BoostBuild.py
e7dee30631959a3605c45fb20b69e8cf4c6e50f8
[ceph.git] / ceph / src / boost / tools / build / test / BoostBuild.py
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)
7
8 from __future__ import print_function
9
10 import TestCmd
11
12 import copy
13 import fnmatch
14 import glob
15 import math
16 import os
17 import os.path
18 import re
19 import shutil
20 try:
21 from StringIO import StringIO
22 except:
23 from io import StringIO
24 import subprocess
25 import sys
26 import tempfile
27 import time
28 import traceback
29 import tree
30 import types
31
32 from xml.sax.saxutils import escape
33
34 try:
35 from functools import reduce
36 except:
37 pass
38
39
40 def isstr(data):
41 return isinstance(data, (type(''), type(u'')))
42
43
44 class TestEnvironmentError(Exception):
45 pass
46
47
48 annotations = []
49
50
51 def print_annotation(name, value, xml):
52 """Writes some named bits of information about the current test run."""
53 if xml:
54 print(escape(name) + " {{{")
55 print(escape(value))
56 print("}}}")
57 else:
58 print(name + " {{{")
59 print(value)
60 print("}}}")
61
62
63 def flush_annotations(xml=0):
64 global annotations
65 for ann in annotations:
66 print_annotation(ann[0], ann[1], xml)
67 annotations = []
68
69
70 def clear_annotations():
71 global annotations
72 annotations = []
73
74
75 defer_annotations = 0
76
77 def set_defer_annotations(n):
78 global defer_annotations
79 defer_annotations = n
80
81
82 def annotate_stack_trace(tb=None):
83 if tb:
84 trace = TestCmd.caller(traceback.extract_tb(tb), 0)
85 else:
86 trace = TestCmd.caller(traceback.extract_stack(), 1)
87 annotation("stacktrace", trace)
88
89
90 def annotation(name, value):
91 """Records an annotation about the test run."""
92 annotations.append((name, value))
93 if not defer_annotations:
94 flush_annotations()
95
96
97 def get_toolset():
98 toolset = None
99 for arg in sys.argv[1:]:
100 if not arg.startswith("-"):
101 toolset = arg
102 return toolset or "gcc"
103
104
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")
108
109 if cygwin:
110 default_os = "cygwin"
111 elif windows:
112 default_os = "windows"
113 elif hasattr(os, "uname"):
114 default_os = os.uname()[0].lower()
115
116
117 def expand_toolset(toolset, target_os=default_os):
118 match = re.match(r'^(clang|intel)(-[\d\.]+|)$', toolset)
119 if match:
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')
124 else:
125 return match.expand(r'\1-linux\2')
126
127 return toolset
128
129
130 def prepare_prefixes_and_suffixes(toolset, target_os=default_os):
131 ind = toolset.find('-')
132 if ind == -1:
133 rtoolset = toolset
134 else:
135 rtoolset = toolset[:ind]
136 prepare_suffix_map(rtoolset, target_os)
137 prepare_library_prefix(rtoolset, target_os)
138
139
140 def prepare_suffix_map(toolset, target_os=default_os):
141 """
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).
145
146 """
147 global suffixes
148 suffixes = {}
149 if target_os == "cygwin":
150 suffixes[".lib"] = ".a"
151 suffixes[".obj"] = ".o"
152 suffixes[".implib"] = ".lib.a"
153 elif target_os == "windows":
154 if toolset == "gcc":
155 # MinGW
156 suffixes[".lib"] = ".a"
157 suffixes[".obj"] = ".o"
158 suffixes[".implib"] = ".dll.a"
159 else:
160 # Everything else Windows
161 suffixes[".implib"] = ".lib"
162 else:
163 suffixes[".exe"] = ""
164 suffixes[".dll"] = ".so"
165 suffixes[".lib"] = ".a"
166 suffixes[".obj"] = ".o"
167 suffixes[".implib"] = ".no_implib_files_on_this_platform"
168
169 if target_os == "darwin":
170 suffixes[".dll"] = ".dylib"
171
172
173 def prepare_library_prefix(toolset, target_os=default_os):
174 """
175 Setup whether Boost Build is expected to automatically prepend prefixes
176 to its built library targets.
177
178 """
179 global lib_prefix
180 lib_prefix = "lib"
181
182 global dll_prefix
183 if target_os == "cygwin":
184 dll_prefix = "cyg"
185 elif target_os == "windows" and toolset != "gcc":
186 dll_prefix = None
187 else:
188 dll_prefix = "lib"
189
190
191 def re_remove(sequence, regex):
192 me = re.compile(regex)
193 result = list(filter(lambda x: me.match(x), sequence))
194 if not result:
195 raise ValueError()
196 for r in result:
197 sequence.remove(r)
198
199
200 def glob_remove(sequence, pattern):
201 result = list(fnmatch.filter(sequence, pattern))
202 if not result:
203 raise ValueError()
204 for r in result:
205 sequence.remove(r)
206
207
208 class Tester(TestCmd.TestCmd):
209 """Main tester class for Boost Build.
210
211 Optional arguments:
212
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
218 executable.
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
230 configuration file.
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
234 run from.
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).
238
239 Optional arguments inherited from the base class:
240
241 `description` - Test description string displayed in case
242 of a failed test.
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
251 line option does.
252 """
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,
257 **keywords):
258
259 if not executable:
260 executable = os.getenv('B2')
261 if not executable:
262 executable = 'b2'
263
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)
269
270 self.last_build_timestamp = 0
271 self.translate_suffixes = translate_suffixes
272 self.use_test_config = use_test_config
273
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
278
279 prepare_prefixes_and_suffixes(pass_toolset and self.toolset or "gcc")
280
281 use_default_bjam = "--default-bjam" in sys.argv
282
283 if not use_default_bjam:
284 jam_build_dir = ""
285
286 # Find where jam_src is located. Try for the debug version if it is
287 # lying around.
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)]
291 for d in dirs:
292 if os.path.exists(d):
293 jam_build_dir = d
294 break
295 else:
296 print("Cannot find built Boost.Jam")
297 sys.exit(1)
298
299 verbosity = ["-d0", "--quiet"]
300 if not pass_d0:
301 verbosity = []
302 if "--verbose" in sys.argv:
303 keywords["verbose"] = True
304 verbosity = ["-d2"]
305 self.verbosity = verbosity
306
307 if boost_build_path is None:
308 boost_build_path = self.original_workdir + "/.."
309
310 program_list = []
311 if use_default_bjam:
312 program_list.append(executable)
313 else:
314 program_list.append(os.path.join(jam_build_dir, executable))
315 program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"')
316 if arguments:
317 program_list += arguments
318
319 TestCmd.TestCmd.__init__(self, program=program_list, match=match,
320 workdir=workdir, inpath=use_default_bjam, **keywords)
321
322 os.chdir(self.workdir)
323
324 def cleanup(self):
325 try:
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
331 # this case.
332 pass
333
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)
339
340
341 #
342 # Methods that change the working directory's content.
343 #
344 def set_tree(self, tree_location):
345 # It is not possible to remove the current directory.
346 d = os.getcwd()
347 os.chdir(os.path.dirname(self.workdir))
348 shutil.rmtree(self.workdir, ignore_errors=False)
349
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)
353
354 os.chdir(d)
355 def make_writable(unused, dir, entries):
356 for e in 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)
361
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")
368 try:
369 f.write(content)
370 finally:
371 f.close()
372 self.__ensure_newer_than_last_build(nfile)
373
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)
378
379 def copy(self, src, dst):
380 try:
381 self.write(dst, self.read(src, binary=True))
382 except:
383 self.fail_test(1)
384
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)
389
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)
394
395 def touch(self, names, wait=True):
396 if isstr(names):
397 names = [names]
398 for name in names:
399 path = self.native_file_name(name)
400 if wait:
401 self.__ensure_newer_than_last_build(path)
402 else:
403 os.utime(path, None)
404
405 def rm(self, names):
406 if not type(names) == list:
407 names = [names]
408
409 if names == ["."]:
410 # If we are deleting the entire workspace, there is no need to wait
411 # for a clock tick.
412 self.last_build_timestamp = 0
413
414 # Avoid attempts to remove the current directory.
415 os.chdir(self.original_workdir)
416 for name in names:
417 n = glob.glob(self.native_file_name(name))
418 if n: n = n[0]
419 if not n:
420 n = self.glob_file(name.replace("$toolset", self.expanded_toolset + "*")
421 )
422 if n:
423 if os.path.isdir(n):
424 shutil.rmtree(n, ignore_errors=False)
425 else:
426 os.unlink(n)
427
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)
432
433 def expand_toolset(self, name):
434 """
435 Expands $toolset placeholder in the given file to the name of the
436 toolset currently being tested.
437
438 """
439 self.write(name, self.read(name).replace("$toolset", self.expanded_toolset))
440
441 def dump_stdio(self):
442 annotation("STDOUT", self.stdout())
443 annotation("STDERR", self.stderr())
444
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):
449
450 assert extra_args.__class__ is not str
451
452 if os.path.isabs(subdir):
453 raise ValueError(
454 "You must pass a relative directory to subdir <%s>." % subdir)
455
456 self.previous_tree, dummy = tree.build_tree(self.workdir)
457 self.wait_for_time_change_since_last_build()
458
459 if match is None:
460 match = self.match
461
462 if pass_toolset is None:
463 pass_toolset = self.pass_toolset
464
465 if use_test_config is None:
466 use_test_config = self.use_test_config
467
468 if ignore_toolset_requirements is None:
469 ignore_toolset_requirements = self.ignore_toolset_requirements
470
471 try:
472 kw["program"] = []
473 kw["program"] += self.program
474 if extra_args:
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
480 if pass_toolset:
481 kw["program"].append("toolset=" + self.toolset)
482 if use_test_config:
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")
494 kw["chdir"] = subdir
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()
499 except:
500 self.dump_stdio()
501 raise
502
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
512
513 self.difference.ignore_directories()
514 self.unexpected_difference = copy.deepcopy(self.difference)
515
516 if (status and self.status) is not None and self.status != status:
517 expect = ""
518 if status != 0:
519 expect = " (expected %d)" % status
520
521 annotation("failure", '"%s" returned %d%s' % (kw["program"],
522 self.status, expect))
523
524 annotation("reason", "unexpected status returned by bjam")
525 self.fail_test(1)
526
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()
533 if stderr:
534 annotation("STDERR", stderr)
535 self.maybe_do_diff(self.stdout(), stdout, stdout_test)
536 self.fail_test(1, dump_stdio=False)
537
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())
541
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)
550
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,
556 expected_duration))
557 self.fail_test(1, dump_stdio=False)
558
559 self.__ignore_junk()
560
561 def glob_file(self, name):
562 name = self.adjust_name(name)
563 result = None
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)
570 break
571 if not result:
572 result = glob.glob(self.__native_file_name(name))
573 if result:
574 result = result[0]
575 return result
576
577 def __read(self, name, binary=False):
578 try:
579 openMode = "r"
580 if binary:
581 openMode += "b"
582 else:
583 openMode += "U"
584 f = open(name, openMode)
585 result = f.read()
586 f.close()
587 return result
588 except:
589 annotation("failure", "Could not open '%s'" % name)
590 self.fail_test(1)
591 return ""
592
593 def read(self, name, binary=False):
594 name = self.glob_file(name)
595 return self.__read(name, binary=binary)
596
597 def read_and_strip(self, name):
598 if not self.glob_file(name):
599 return ""
600 f = open(self.glob_file(name), "rb")
601 lines = f.readlines()
602 f.close()
603 result = "\n".join(x.decode().rstrip() for x in lines)
604 if lines and lines[-1][-1] != "\n":
605 return result + "\n"
606 return result
607
608 def fail_test(self, condition, dump_difference=True, dump_stdio=True,
609 dump_stack=True):
610 if not condition:
611 return
612
613 if dump_difference and hasattr(self, "difference"):
614 f = StringIO()
615 self.difference.pprint(f)
616 annotation("changes caused by the last build command",
617 f.getvalue())
618
619 if dump_stdio:
620 self.dump_stdio()
621
622 if "--preserve" in sys.argv:
623 print()
624 print("*** Copying the state of working dir into 'failed_test' ***")
625 print()
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))
634
635 if dump_stack:
636 annotate_stack_trace()
637 sys.exit(1)
638
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
642 # wildcards.
643
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):
647 try:
648 glob_remove(self.unexpected_difference.added_files, name)
649 except:
650 annotation("failure", "File %s not added as expected" % name)
651 self.fail_test(1)
652
653 def ignore_addition(self, wildcard):
654 self.__ignore_elements(self.unexpected_difference.added_files,
655 wildcard)
656
657 def expect_removal(self, names):
658 for name in self.adjust_names(names):
659 try:
660 glob_remove(self.unexpected_difference.removed_files, name)
661 except:
662 annotation("failure", "File %s not removed as expected" % name)
663 self.fail_test(1)
664
665 def ignore_removal(self, wildcard):
666 self.__ignore_elements(self.unexpected_difference.removed_files,
667 wildcard)
668
669 def expect_modification(self, names):
670 for name in self.adjust_names(names):
671 try:
672 glob_remove(self.unexpected_difference.modified_files, name)
673 except:
674 annotation("failure", "File %s not modified as expected" %
675 name)
676 self.fail_test(1)
677
678 def ignore_modification(self, wildcard):
679 self.__ignore_elements(self.unexpected_difference.modified_files,
680 wildcard)
681
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
686 # that:
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]
692
693 while filesets:
694 try:
695 glob_remove(filesets[-1], name)
696 break
697 except ValueError:
698 filesets.pop()
699
700 if not filesets:
701 annotation("failure", "File %s not touched as expected" % name)
702 self.fail_test(1)
703
704 def ignore_touch(self, wildcard):
705 self.__ignore_elements(self.unexpected_difference.touched_files,
706 wildcard)
707
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)
713
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)
719 self.fail_test(1)
720 if name in self.difference.removed_files:
721 annotation("failure",
722 "File %s removed, but no action was expected" % name)
723 self.fail_test(1)
724 pass
725 if name in self.difference.modified_files:
726 annotation("failure",
727 "File %s modified, but no action was expected" % name)
728 self.fail_test(1)
729 if name in self.difference.touched_files:
730 annotation("failure",
731 "File %s touched, but no action was expected" % name)
732 self.fail_test(1)
733
734 def __ignore_junk(self):
735 # Not totally sure about this change, but I do not see a good
736 # alternative.
737 if windows:
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")
744
745 # Debug builds of bjam built with gcc produce this profiling data.
746 self.ignore("gmon.out")
747 self.ignore("*/gmon.out")
748
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")
753
754 # Compiled Python files created when running Python based Boost Build.
755 self.ignore("*.pyc")
756
757 # OSX/Darwin files and dirs.
758 self.ignore("*.dSYM/*")
759
760 def expect_nothing_more(self):
761 if not self.unexpected_difference.empty():
762 annotation("failure", "Unexpected changes found")
763 output = StringIO()
764 self.unexpected_difference.pprint(output)
765 annotation("unexpected changes", output.getvalue())
766 self.fail_test(1)
767
768 def expect_output_lines(self, lines, expected=True):
769 self.__expect_lines(self.stdout(), lines, expected)
770
771 def expect_content_lines(self, filename, line, expected=True):
772 self.__expect_lines(self.read_and_strip(filename), line, expected)
773
774 def expect_content(self, name, content, exact=False):
775 actual = self.read(name)
776 content = content.replace("$toolset", self.expanded_toolset + "*")
777
778 matched = False
779 if exact:
780 matched = fnmatch.fnmatch(actual, content)
781 else:
782 def sorted_(z):
783 z.sort(key=lambda x: x.lower().replace("\\", "/"))
784 return z
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_):
788 matched = map(
789 lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y),
790 actual_, content_)
791 matched = reduce(
792 lambda x, y: x and reduce(
793 lambda a, b: a and b,
794 y, True),
795 matched, True)
796
797 if not matched:
798 print("Expected:\n")
799 print(content)
800 print("Got:\n")
801 print(actual)
802 self.fail_test(1)
803
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")
808 f = open(e, "w")
809 f.write(expected)
810 f.close()
811 f = open(a, "w")
812 f.write(actual)
813 f.close()
814 print("DIFFERENCE")
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
821 ))
822 os.unlink(e)
823 os.unlink(a)
824 elif type(result) is TestCmd.MatchError:
825 print(result.message)
826 else:
827 print("Set environmental variable 'DO_DIFF' to examine the "
828 "difference.")
829
830 # Internal methods.
831 def adjust_lib_name(self, name):
832 global lib_prefix
833 global dll_prefix
834 result = name
835
836 pos = name.rfind(".")
837 if pos != -1:
838 suffix = name[pos:]
839 if suffix == ".lib":
840 (head, tail) = os.path.split(name)
841 if lib_prefix:
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)
846 if dll_prefix:
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("\\", "/")
852 return result
853
854 def adjust_suffix(self, name):
855 if not self.translate_suffixes:
856 return name
857 pos = name.rfind(".")
858 if pos == -1:
859 return name
860 suffix = name[pos:]
861 return name[:pos] + suffixes.get(suffix, suffix)
862
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):
866 if isstr(names):
867 names = [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)
871 return list(r)
872
873 def adjust_name(self, name):
874 return self.adjust_names(name)[0]
875
876 def __native_file_name(self, name):
877 return os.path.normpath(os.path.join(self.workdir, *name.split("/")))
878
879 def native_file_name(self, name):
880 return self.__native_file_name(self.adjust_name(name))
881
882 def wait_for_time_change(self, path, touch):
883 """
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.
889
890 """
891 self.__wait_for_time_change(path, touch, last_build_time=False)
892
893 def wait_for_time_change_since_last_build(self):
894 """
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.
900 """
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)
908
909 def __build_timestamp_resolution(self):
910 """
911 Returns the minimum path modification timestamp resolution supported
912 by the used Boost Jam executable.
913
914 """
915 dir = tempfile.mkdtemp("bjam_version_info")
916 try:
917 jam_script = "timestamp_resolution.jam"
918 f = open(os.path.join(dir, jam_script), "w")
919 try:
920 f.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;")
921 finally:
922 f.close()
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()
926 finally:
927 shutil.rmtree(dir, ignore_errors=False)
928
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)
933 if err:
934 raise TestEnvironmentError("Unexpected error output (%s) when "
935 "detecting Boost Jam's minimum supported path modification "
936 "timestamp resolution version information." % err)
937
938 r = re.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out)
939 if not r:
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
943 # second.
944 # TODO: Phase this support out to avoid such fallback code from
945 # possibly covering up other problems.
946 return 1
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
952
953 def __ensure_newer_than_last_build(self, path):
954 """
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.
961
962 """
963 if self.last_build_timestamp:
964 self.__wait_for_time_change(path, touch=True, last_build_time=True)
965
966 def __expect_lines(self, data, lines, expected):
967 """
968 Checks whether the given data contains the given lines.
969
970 Data may be specified as a single string containing text lines
971 separated by newline characters.
972
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.
985
986 A newline at the end of any multi-line lines string is interpreted as
987 an expected extra trailig empty line.
988 """
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
991 # at the end.
992 def splitlines(x):
993 return (x + "\n").splitlines()
994
995 if data is None:
996 data = []
997 elif isstr(data):
998 data = splitlines(data)
999
1000 if isstr(lines):
1001 lines = [splitlines(lines)]
1002 else:
1003 expanded = []
1004 for x in lines:
1005 if isstr(x):
1006 x = splitlines(x)
1007 expanded.append(x)
1008 lines = expanded
1009
1010 if _contains_lines(data, lines) != bool(expected):
1011 output = []
1012 if expected:
1013 output = ["Did not find expected lines:"]
1014 else:
1015 output = ["Found unexpected lines:"]
1016 first = True
1017 for line_sequence in lines:
1018 if line_sequence:
1019 if first:
1020 first = False
1021 else:
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))
1027 self.fail_test(1)
1028
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))
1032
1033 def __makedirs(self, path, wait):
1034 """
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
1038 last build run.
1039
1040 """
1041 try:
1042 if wait:
1043 stack = []
1044 while path and path not in stack and not os.path.isdir(path):
1045 stack.append(path)
1046 path = os.path.dirname(path)
1047 while stack:
1048 path = stack.pop()
1049 os.mkdir(path)
1050 self.__ensure_newer_than_last_build(path)
1051 else:
1052 os.makedirs(path)
1053 except Exception:
1054 pass
1055
1056 def __python_timestamp_resolution(self, path, minimum_resolution):
1057 """
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.
1062
1063 Return values:
1064 0 - nanosecond resolution supported
1065 positive decimal - timestamp resolution in seconds
1066
1067 """
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())
1079
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
1085 # at least eta.
1086 eta = 0.00005
1087
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
1093
1094 # Test for nanosecond timestamp resolution support.
1095 if not minimum_resolution and python_nanosecond_support:
1096 if test_time(2 * eta):
1097 return 0
1098
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.
1105 step = 0.1
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
1109 # second.
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
1115 # directly.
1116 index += 1
1117 while True:
1118 # Do not simply add up the steps to avoid cumulative floating point
1119 # number representation errors.
1120 next = step * index
1121 if next > 10:
1122 raise TestEnvironmentError("File systems with too coarse "
1123 "modification timestamp resolutions not supported.")
1124 if test_time(next):
1125 return next
1126 index += 1
1127
1128 def __wait_for_time_change(self, path, touch, last_build_time):
1129 """
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.
1137
1138 """
1139 assert self.last_build_timestamp or not last_build_time
1140 stats_orig = os.stat(path)
1141
1142 if last_build_time:
1143 start_time = self.last_build_timestamp
1144 else:
1145 start_time = stats_orig.st_mtime
1146
1147 build_resolution = self.__build_timestamp_resolution()
1148 assert build_resolution >= 0
1149
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):
1153 return
1154
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)
1158
1159 if not touch:
1160 os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime))
1161
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
1173 # the situation.
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
1184 # time.
1185 while True:
1186 os.utime(path, None)
1187 c = os.stat(path).st_mtime
1188 if resolution:
1189 if c > start_time and (not build_resolution or c >= start_time
1190 + build_resolution):
1191 break
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:
1197 if last_build_time:
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.")
1202 else:
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)
1210 else:
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))
1214 else:
1215 if c > start_time:
1216 break
1217 _sleep(max(0.01, start_time - c))
1218
1219
1220 class List:
1221 def __init__(self, s=""):
1222 elements = []
1223 if isstr(s):
1224 # Have to handle escaped spaces correctly.
1225 elements = s.replace("\ ", "\001").split()
1226 else:
1227 elements = s
1228 self.l = [e.replace("\001", " ") for e in elements]
1229
1230 def __len__(self):
1231 return len(self.l)
1232
1233 def __getitem__(self, key):
1234 return self.l[key]
1235
1236 def __setitem__(self, key, value):
1237 self.l[key] = value
1238
1239 def __delitem__(self, key):
1240 del self.l[key]
1241
1242 def __str__(self):
1243 return str(self.l)
1244
1245 def __repr__(self):
1246 return "%s.List(%r)" % (self.__module__, " ".join(self.l))
1247
1248 def __mul__(self, other):
1249 result = List()
1250 if not isinstance(other, List):
1251 other = List(other)
1252 for f in self:
1253 for s in other:
1254 result.l.append(f + s)
1255 return result
1256
1257 def __rmul__(self, other):
1258 if not isinstance(other, List):
1259 other = List(other)
1260 return List.__mul__(other, self)
1261
1262 def __add__(self, other):
1263 result = List()
1264 result.l = self.l[:] + other.l[:]
1265 return result
1266
1267
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)
1271 index = 0
1272 for expected in lines:
1273 if expected_line_count > data_line_count - index:
1274 return False
1275 expected_line_count -= len(expected)
1276 index = _match_line_sequence(data, index, data_line_count -
1277 expected_line_count, expected)
1278 if index < 0:
1279 return False
1280 return True
1281
1282
1283 def _match_line_sequence(data, start, end, lines):
1284 if not lines:
1285 return start
1286 for index in range(start, end - len(lines) + 1):
1287 data_index = index
1288 for expected in lines:
1289 if not fnmatch.fnmatch(data[data_index], expected):
1290 break
1291 data_index += 1
1292 else:
1293 return data_index
1294 return -1
1295
1296
1297 def _sleep(delay):
1298 if delay > 5:
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.")
1302 time.sleep(delay)
1303
1304
1305 ###############################################################################
1306 #
1307 # Initialization.
1308 #
1309 ###############################################################################
1310
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.
1316 #
1317 # Additional notes:
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)
1332
1333
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')"
1338
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"]])
1345
1346 assert _contains_lines([""], [])
1347 assert _contains_lines(["a"], [])
1348 assert _contains_lines(["a", "b"], [])
1349 assert _contains_lines(["a", "b"], [[], [], []])
1350
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"]])
1360
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"]])
1375
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"]])
1380
1381 print("tests passed")