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 8from __future__ import print_function 9 10import TestCmd 11 12import copy 13import fnmatch 14import glob 15import math 16import os 17import os.path 18import re 19import shutil 20try: 21 from StringIO import StringIO 22except: 23 from io import StringIO 24import subprocess 25import sys 26import tempfile 27import time 28import traceback 29import tree 30import types 31 32from xml.sax.saxutils import escape 33 34try: 35 from functools import reduce 36except: 37 pass 38 39 40def isstr(data): 41 return isinstance(data, (type(''), type(u''))) 42 43 44class TestEnvironmentError(Exception): 45 pass 46 47 48annotations = [] 49 50 51def 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 63def flush_annotations(xml=0): 64 global annotations 65 for ann in annotations: 66 print_annotation(ann[0], ann[1], xml) 67 annotations = [] 68 69 70def clear_annotations(): 71 global annotations 72 annotations = [] 73 74 75defer_annotations = 0 76 77def set_defer_annotations(n): 78 global defer_annotations 79 defer_annotations = n 80 81 82def 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 90def 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 97def 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. 106cygwin = hasattr(os, "uname") and os.uname()[0].lower().startswith("cygwin") 107windows = cygwin or os.environ.get("OS", "").lower().startswith("windows") 108 109if cygwin: 110 default_os = "cygwin" 111elif windows: 112 default_os = "windows" 113elif hasattr(os, "uname"): 114 default_os = os.uname()[0].lower() 115 116def prepare_prefixes_and_suffixes(toolset, target_os=default_os): 117 prepare_suffix_map(toolset, target_os) 118 prepare_library_prefix(toolset, target_os) 119 120 121def prepare_suffix_map(toolset, target_os=default_os): 122 """ 123 Set up suffix translation performed by the Boost Build testing framework 124 to accommodate different toolsets generating targets of the same type using 125 different filename extensions (suffixes). 126 127 """ 128 global suffixes 129 suffixes = {} 130 if target_os == "cygwin": 131 suffixes[".lib"] = ".a" 132 suffixes[".obj"] = ".o" 133 suffixes[".implib"] = ".lib.a" 134 elif target_os == "windows": 135 if toolset == "gcc": 136 # MinGW 137 suffixes[".lib"] = ".a" 138 suffixes[".obj"] = ".o" 139 suffixes[".implib"] = ".dll.a" 140 else: 141 # Everything else Windows 142 suffixes[".implib"] = ".lib" 143 else: 144 suffixes[".exe"] = "" 145 suffixes[".dll"] = ".so" 146 suffixes[".lib"] = ".a" 147 suffixes[".obj"] = ".o" 148 suffixes[".implib"] = ".no_implib_files_on_this_platform" 149 150 if target_os == "darwin": 151 suffixes[".dll"] = ".dylib" 152 153 154def prepare_library_prefix(toolset, target_os=default_os): 155 """ 156 Setup whether Boost Build is expected to automatically prepend prefixes 157 to its built library targets. 158 159 """ 160 global lib_prefix 161 lib_prefix = "lib" 162 163 global dll_prefix 164 if target_os == "cygwin": 165 dll_prefix = "cyg" 166 elif target_os == "windows" and toolset != "gcc": 167 dll_prefix = None 168 else: 169 dll_prefix = "lib" 170 171 172def re_remove(sequence, regex): 173 me = re.compile(regex) 174 result = list(filter(lambda x: me.match(x), sequence)) 175 if not result: 176 raise ValueError() 177 for r in result: 178 sequence.remove(r) 179 180 181def glob_remove(sequence, pattern): 182 result = list(fnmatch.filter(sequence, pattern)) 183 if not result: 184 raise ValueError() 185 for r in result: 186 sequence.remove(r) 187 188 189class Tester(TestCmd.TestCmd): 190 """Main tester class for Boost Build. 191 192 Optional arguments: 193 194 `arguments` - Arguments passed to the run executable. 195 `executable` - Name of the executable to invoke. 196 `match` - Function to use for compating actual and 197 expected file contents. 198 `boost_build_path` - Boost build path to be passed to the run 199 executable. 200 `translate_suffixes` - Whether to update suffixes on the the file 201 names passed from the test script so they 202 match those actually created by the current 203 toolset. For example, static library files 204 are specified by using the .lib suffix but 205 when the "gcc" toolset is used it actually 206 creates them using the .a suffix. 207 `pass_toolset` - Whether the test system should pass the 208 specified toolset to the run executable. 209 `use_test_config` - Whether the test system should tell the run 210 executable to read in the test_config.jam 211 configuration file. 212 `ignore_toolset_requirements` - Whether the test system should tell the run 213 executable to ignore toolset requirements. 214 `workdir` - Absolute directory where the test will be 215 run from. 216 `pass_d0` - If set, when tests are not explicitly run 217 in verbose mode, they are run as silent 218 (-d0 & --quiet Boost Jam options). 219 220 Optional arguments inherited from the base class: 221 222 `description` - Test description string displayed in case 223 of a failed test. 224 `subdir` - List of subdirectories to automatically 225 create under the working directory. Each 226 subdirectory needs to be specified 227 separately, parent coming before its child. 228 `verbose` - Flag that may be used to enable more 229 verbose test system output. Note that it 230 does not also enable more verbose build 231 system output like the --verbose command 232 line option does. 233 """ 234 def __init__(self, arguments=None, executable="b2", 235 match=TestCmd.match_exact, boost_build_path=None, 236 translate_suffixes=True, pass_toolset=True, use_test_config=True, 237 ignore_toolset_requirements=False, workdir="", pass_d0=False, 238 **keywords): 239 240 assert arguments.__class__ is not str 241 self.original_workdir = os.path.dirname(__file__) 242 if workdir and not os.path.isabs(workdir): 243 raise ("Parameter workdir <%s> must point to an absolute " 244 "directory: " % workdir) 245 246 self.last_build_timestamp = 0 247 self.translate_suffixes = translate_suffixes 248 self.use_test_config = use_test_config 249 250 self.toolset = get_toolset() 251 self.pass_toolset = pass_toolset 252 self.ignore_toolset_requirements = ignore_toolset_requirements 253 254 prepare_prefixes_and_suffixes(pass_toolset and self.toolset or "gcc") 255 256 use_default_bjam = "--default-bjam" in sys.argv 257 258 if not use_default_bjam: 259 jam_build_dir = "" 260 261 # Find where jam_src is located. Try for the debug version if it is 262 # lying around. 263 srcdir = os.path.join(os.path.dirname(__file__), "..", "src") 264 dirs = [os.path.join(srcdir, "engine", jam_build_dir + ".debug"), 265 os.path.join(srcdir, "engine", jam_build_dir)] 266 for d in dirs: 267 if os.path.exists(d): 268 jam_build_dir = d 269 break 270 else: 271 print("Cannot find built Boost.Jam") 272 sys.exit(1) 273 274 verbosity = ["-d0", "--quiet"] 275 if not pass_d0: 276 verbosity = [] 277 if "--verbose" in sys.argv: 278 keywords["verbose"] = True 279 verbosity = ["-d2"] 280 self.verbosity = verbosity 281 282 if boost_build_path is None: 283 boost_build_path = self.original_workdir + "/.." 284 285 program_list = [] 286 if use_default_bjam: 287 program_list.append(executable) 288 else: 289 program_list.append(os.path.join(jam_build_dir, executable)) 290 program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"') 291 if arguments: 292 program_list += arguments 293 294 TestCmd.TestCmd.__init__(self, program=program_list, match=match, 295 workdir=workdir, inpath=use_default_bjam, **keywords) 296 297 os.chdir(self.workdir) 298 299 def cleanup(self): 300 try: 301 TestCmd.TestCmd.cleanup(self) 302 os.chdir(self.original_workdir) 303 except AttributeError: 304 # When this is called during TestCmd.TestCmd.__del__ we can have 305 # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in 306 # this case. 307 pass 308 309 def set_toolset(self, toolset, target_os=default_os): 310 self.toolset = toolset 311 self.pass_toolset = True 312 prepare_prefixes_and_suffixes(toolset, target_os) 313 314 315 # 316 # Methods that change the working directory's content. 317 # 318 def set_tree(self, tree_location): 319 # It is not possible to remove the current directory. 320 d = os.getcwd() 321 os.chdir(os.path.dirname(self.workdir)) 322 shutil.rmtree(self.workdir, ignore_errors=False) 323 324 if not os.path.isabs(tree_location): 325 tree_location = os.path.join(self.original_workdir, tree_location) 326 shutil.copytree(tree_location, self.workdir) 327 328 os.chdir(d) 329 def make_writable(unused, dir, entries): 330 for e in entries: 331 name = os.path.join(dir, e) 332 os.chmod(name, os.stat(name).st_mode | 0o222) 333 for root, _, files in os.walk("."): 334 make_writable(None, root, files) 335 336 def write(self, file, content, wait=True): 337 nfile = self.native_file_name(file) 338 self.__makedirs(os.path.dirname(nfile), wait) 339 if not type(content) == bytes: 340 content = content.encode() 341 f = open(nfile, "wb") 342 try: 343 f.write(content) 344 finally: 345 f.close() 346 self.__ensure_newer_than_last_build(nfile) 347 348 def copy(self, src, dst): 349 try: 350 self.write(dst, self.read(src, binary=True)) 351 except: 352 self.fail_test(1) 353 354 def copy_preserving_timestamp(self, src, dst): 355 src_name = self.native_file_name(src) 356 dst_name = self.native_file_name(dst) 357 stats = os.stat(src_name) 358 self.write(dst, self.__read(src, binary=True)) 359 os.utime(dst_name, (stats.st_atime, stats.st_mtime)) 360 361 def touch(self, names, wait=True): 362 if isstr(names): 363 names = [names] 364 for name in names: 365 path = self.native_file_name(name) 366 if wait: 367 self.__ensure_newer_than_last_build(path) 368 else: 369 os.utime(path, None) 370 371 def rm(self, names): 372 if not type(names) == list: 373 names = [names] 374 375 if names == ["."]: 376 # If we are deleting the entire workspace, there is no need to wait 377 # for a clock tick. 378 self.last_build_timestamp = 0 379 380 # Avoid attempts to remove the current directory. 381 os.chdir(self.original_workdir) 382 for name in names: 383 n = glob.glob(self.native_file_name(name)) 384 if n: n = n[0] 385 if not n: 386 n = self.glob_file(name.replace("$toolset", self.toolset + "*") 387 ) 388 if n: 389 if os.path.isdir(n): 390 shutil.rmtree(n, ignore_errors=False) 391 else: 392 os.unlink(n) 393 394 # Create working dir root again in case we removed it. 395 if not os.path.exists(self.workdir): 396 os.mkdir(self.workdir) 397 os.chdir(self.workdir) 398 399 def expand_toolset(self, name): 400 """ 401 Expands $toolset placeholder in the given file to the name of the 402 toolset currently being tested. 403 404 """ 405 self.write(name, self.read(name).replace("$toolset", self.toolset)) 406 407 def dump_stdio(self): 408 annotation("STDOUT", self.stdout()) 409 annotation("STDERR", self.stderr()) 410 411 def run_build_system(self, extra_args=None, subdir="", stdout=None, 412 stderr="", status=0, match=None, pass_toolset=None, 413 use_test_config=None, ignore_toolset_requirements=None, 414 expected_duration=None, **kw): 415 416 assert extra_args.__class__ is not str 417 418 if os.path.isabs(subdir): 419 print("You must pass a relative directory to subdir <%s>." % subdir 420 ) 421 return 422 423 self.previous_tree, dummy = tree.build_tree(self.workdir) 424 self.wait_for_time_change_since_last_build() 425 426 if match is None: 427 match = self.match 428 429 if pass_toolset is None: 430 pass_toolset = self.pass_toolset 431 432 if use_test_config is None: 433 use_test_config = self.use_test_config 434 435 if ignore_toolset_requirements is None: 436 ignore_toolset_requirements = self.ignore_toolset_requirements 437 438 try: 439 kw["program"] = [] 440 kw["program"] += self.program 441 if extra_args: 442 kw["program"] += extra_args 443 if not extra_args or not any(a.startswith("-j") for a in extra_args): 444 kw["program"] += ["-j1"] 445 if stdout is None and not any(a.startswith("-d") for a in kw["program"]): 446 kw["program"] += self.verbosity 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 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): 495 stdout_test = match(self.stdout(), stdout) 496 annotation("failure", "Unexpected stdout") 497 annotation("Expected STDOUT", stdout) 498 annotation("Actual STDOUT", self.stdout()) 499 stderr = self.stderr() 500 if stderr: 501 annotation("STDERR", stderr) 502 self.maybe_do_diff(self.stdout(), stdout, stdout_test) 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): 510 stderr_test = match(actual_stderr, stderr) 511 annotation("failure", "Unexpected stderr") 512 annotation("Expected STDERR", stderr) 513 annotation("Actual STDERR", self.stderr()) 514 annotation("STDOUT", self.stdout()) 515 self.maybe_do_diff(actual_stderr, stderr, stderr_test) 516 self.fail_test(1, dump_stdio=False) 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 526 self.__ignore_junk() 527 528 def glob_file(self, name): 529 name = self.adjust_name(name) 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): 536 result = self.__native_file_name(f) 537 break 538 if not result: 539 result = glob.glob(self.__native_file_name(name)) 540 if result: 541 result = result[0] 542 return result 543 544 def __read(self, name, binary=False): 545 try: 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 560 def read(self, name, binary=False): 561 name = self.glob_file(name) 562 return self.__read(name, binary=binary) 563 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.decode().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() 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 701 def __ignore_junk(self): 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 self.ignore("bin/standalone/msvc/*/msvc-setup.bat") 711 712 # Debug builds of bjam built with gcc produce this profiling data. 713 self.ignore("gmon.out") 714 self.ignore("*/gmon.out") 715 716 # Boost Build's 'configure' functionality (unfinished at the time) 717 # produces this file. 718 self.ignore("bin/config.log") 719 self.ignore("bin/project-cache.jam") 720 721 # Compiled Python files created when running Python based Boost Build. 722 self.ignore("*.pyc") 723 724 # OSX/Darwin files and dirs. 725 self.ignore("*.dSYM/*") 726 727 def expect_nothing_more(self): 728 if not self.unexpected_difference.empty(): 729 annotation("failure", "Unexpected changes found") 730 output = StringIO() 731 self.unexpected_difference.pprint(output) 732 annotation("unexpected changes", output.getvalue()) 733 self.fail_test(1) 734 735 def expect_output_lines(self, lines, expected=True): 736 self.__expect_lines(self.stdout(), lines, expected) 737 738 def expect_content_lines(self, filename, line, expected=True): 739 self.__expect_lines(self.read_and_strip(filename), line, expected) 740 741 def expect_content(self, name, content, exact=False): 742 actual = self.read(name) 743 content = content.replace("$toolset", self.toolset + "*") 744 745 matched = False 746 if exact: 747 matched = fnmatch.fnmatch(actual, content) 748 else: 749 def sorted_(z): 750 z.sort(key=lambda x: x.lower().replace("\\", "/")) 751 return z 752 actual_ = list(map(lambda x: sorted_(x.split()), actual.splitlines())) 753 content_ = list(map(lambda x: sorted_(x.split()), content.splitlines())) 754 if len(actual_) == len(content_): 755 matched = map( 756 lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y), 757 actual_, content_) 758 matched = reduce( 759 lambda x, y: x and reduce( 760 lambda a, b: a and b, 761 y, True), 762 matched, True) 763 764 if not matched: 765 print("Expected:\n") 766 print(content) 767 print("Got:\n") 768 print(actual) 769 self.fail_test(1) 770 771 def maybe_do_diff(self, actual, expected, result=None): 772 if os.environ.get("DO_DIFF"): 773 e = tempfile.mktemp("expected") 774 a = tempfile.mktemp("actual") 775 f = open(e, "w") 776 f.write(expected) 777 f.close() 778 f = open(a, "w") 779 f.write(actual) 780 f.close() 781 print("DIFFERENCE") 782 # Current diff should return 1 to indicate 'different input files' 783 # but some older diff versions may return 0 and depending on the 784 # exact Python/OS platform version, os.system() call may gobble up 785 # the external process's return code and return 0 itself. 786 if os.system('diff -u "%s" "%s"' % (e, a)) not in [0, 1]: 787 print('Unable to compute difference: diff -u "%s" "%s"' % (e, a 788 )) 789 os.unlink(e) 790 os.unlink(a) 791 elif type(result) is TestCmd.MatchError: 792 print(result.message) 793 else: 794 print("Set environmental variable 'DO_DIFF' to examine the " 795 "difference.") 796 797 # Internal methods. 798 def adjust_lib_name(self, name): 799 global lib_prefix 800 global dll_prefix 801 result = name 802 803 pos = name.rfind(".") 804 if pos != -1: 805 suffix = name[pos:] 806 if suffix == ".lib": 807 (head, tail) = os.path.split(name) 808 if lib_prefix: 809 tail = lib_prefix + tail 810 result = os.path.join(head, tail) 811 elif suffix == ".dll" or suffix == ".implib": 812 (head, tail) = os.path.split(name) 813 if dll_prefix: 814 tail = dll_prefix + tail 815 result = os.path.join(head, tail) 816 # If we want to use this name in a Jamfile, we better convert \ to /, 817 # as otherwise we would have to quote \. 818 result = result.replace("\\", "/") 819 return result 820 821 def adjust_suffix(self, name): 822 if not self.translate_suffixes: 823 return name 824 pos = name.rfind(".") 825 if pos == -1: 826 return name 827 suffix = name[pos:] 828 return name[:pos] + suffixes.get(suffix, suffix) 829 830 # Acceps either a string or a list of strings and returns a list of 831 # strings. Adjusts suffixes on all names. 832 def adjust_names(self, names): 833 if isstr(names): 834 names = [names] 835 r = map(self.adjust_lib_name, names) 836 r = map(self.adjust_suffix, r) 837 r = map(lambda x, t=self.toolset: x.replace("$toolset", t + "*"), r) 838 return list(r) 839 840 def adjust_name(self, name): 841 return self.adjust_names(name)[0] 842 843 def __native_file_name(self, name): 844 return os.path.normpath(os.path.join(self.workdir, *name.split("/"))) 845 846 def native_file_name(self, name): 847 return self.__native_file_name(self.adjust_name(name)) 848 849 def wait_for_time_change(self, path, touch): 850 """ 851 Wait for newly assigned file system modification timestamps for the 852 given path to become large enough for the timestamp difference to be 853 correctly recognized by both this Python based testing framework and 854 the Boost Jam executable being tested. May optionally touch the given 855 path to set its modification timestamp to the new value. 856 857 """ 858 self.__wait_for_time_change(path, touch, last_build_time=False) 859 860 def wait_for_time_change_since_last_build(self): 861 """ 862 Wait for newly assigned file system modification timestamps to 863 become large enough for the timestamp difference to be 864 correctly recognized by the Python based testing framework. 865 Does not care about Jam's timestamp resolution, since we 866 only need this to detect touched files. 867 """ 868 if self.last_build_timestamp: 869 timestamp_file = "timestamp-3df2f2317e15e4a9" 870 open(timestamp_file, "wb").close() 871 self.__wait_for_time_change_impl(timestamp_file, 872 self.last_build_timestamp, 873 self.__python_timestamp_resolution(timestamp_file, 0), 0) 874 os.unlink(timestamp_file) 875 876 def __build_timestamp_resolution(self): 877 """ 878 Returns the minimum path modification timestamp resolution supported 879 by the used Boost Jam executable. 880 881 """ 882 dir = tempfile.mkdtemp("bjam_version_info") 883 try: 884 jam_script = "timestamp_resolution.jam" 885 f = open(os.path.join(dir, jam_script), "w") 886 try: 887 f.write("EXIT $(JAM_TIMESTAMP_RESOLUTION) : 0 ;") 888 finally: 889 f.close() 890 p = subprocess.Popen([self.program[0], "-d0", "-f%s" % jam_script], 891 stdout=subprocess.PIPE, cwd=dir, universal_newlines=True) 892 out, err = p.communicate() 893 finally: 894 shutil.rmtree(dir, ignore_errors=False) 895 896 if p.returncode != 0: 897 raise TestEnvironmentError("Unexpected return code (%s) when " 898 "detecting Boost Jam's minimum supported path modification " 899 "timestamp resolution version information." % p.returncode) 900 if err: 901 raise TestEnvironmentError("Unexpected error output (%s) when " 902 "detecting Boost Jam's minimum supported path modification " 903 "timestamp resolution version information." % err) 904 905 r = re.match("([0-9]{2}):([0-9]{2}):([0-9]{2}\\.[0-9]{9})$", out) 906 if not r: 907 # Older Boost Jam versions did not report their minimum supported 908 # path modification timestamp resolution and did not actually 909 # support path modification timestamp resolutions finer than 1 910 # second. 911 # TODO: Phase this support out to avoid such fallback code from 912 # possibly covering up other problems. 913 return 1 914 if r.group(1) != "00" or r.group(2) != "00": # hours, minutes 915 raise TestEnvironmentError("Boost Jam with too coarse minimum " 916 "supported path modification timestamp resolution (%s:%s:%s)." 917 % (r.group(1), r.group(2), r.group(3))) 918 return float(r.group(3)) # seconds.nanoseconds 919 920 def __ensure_newer_than_last_build(self, path): 921 """ 922 Updates the given path's modification timestamp after waiting for the 923 newly assigned file system modification timestamp to become large 924 enough for the timestamp difference between it and the last build 925 timestamp to be correctly recognized by both this Python based testing 926 framework and the Boost Jam executable being tested. Does nothing if 927 there is no 'last build' information available. 928 929 """ 930 if self.last_build_timestamp: 931 self.__wait_for_time_change(path, touch=True, last_build_time=True) 932 933 def __expect_lines(self, data, lines, expected): 934 """ 935 Checks whether the given data contains the given lines. 936 937 Data may be specified as a single string containing text lines 938 separated by newline characters. 939 940 Lines may be specified in any of the following forms: 941 * Single string containing text lines separated by newlines - the 942 given lines are searched for in the given data without any extra 943 data lines between them. 944 * Container of strings containing text lines separated by newlines 945 - the given lines are searched for in the given data with extra 946 data lines allowed between lines belonging to different strings. 947 * Container of strings containing text lines separated by newlines 948 and containers containing strings - the same as above with the 949 internal containers containing strings being interpreted as if 950 all their content was joined together into a single string 951 separated by newlines. 952 953 A newline at the end of any multi-line lines string is interpreted as 954 an expected extra trailig empty line. 955 """ 956 # str.splitlines() trims at most one trailing newline while we want the 957 # trailing newline to indicate that there should be an extra empty line 958 # at the end. 959 def splitlines(x): 960 return (x + "\n").splitlines() 961 962 if data is None: 963 data = [] 964 elif isstr(data): 965 data = splitlines(data) 966 967 if isstr(lines): 968 lines = [splitlines(lines)] 969 else: 970 expanded = [] 971 for x in lines: 972 if isstr(x): 973 x = splitlines(x) 974 expanded.append(x) 975 lines = expanded 976 977 if _contains_lines(data, lines) != bool(expected): 978 output = [] 979 if expected: 980 output = ["Did not find expected lines:"] 981 else: 982 output = ["Found unexpected lines:"] 983 first = True 984 for line_sequence in lines: 985 if line_sequence: 986 if first: 987 first = False 988 else: 989 output.append("...") 990 output.extend(" > " + line for line in line_sequence) 991 output.append("in output:") 992 output.extend(" > " + line for line in data) 993 annotation("failure", "\n".join(output)) 994 self.fail_test(1) 995 996 def __ignore_elements(self, things, wildcard): 997 """Removes in-place 'things' elements matching the given 'wildcard'.""" 998 things[:] = list(filter(lambda x: not fnmatch.fnmatch(x, wildcard), things)) 999 1000 def __makedirs(self, path, wait): 1001 """ 1002 Creates a folder with the given path, together with any missing 1003 parent folders. If WAIT is set, makes sure any newly created folders 1004 have modification timestamps newer than the ones left behind by the 1005 last build run. 1006 1007 """ 1008 try: 1009 if wait: 1010 stack = [] 1011 while path and path not in stack and not os.path.isdir(path): 1012 stack.append(path) 1013 path = os.path.dirname(path) 1014 while stack: 1015 path = stack.pop() 1016 os.mkdir(path) 1017 self.__ensure_newer_than_last_build(path) 1018 else: 1019 os.makedirs(path) 1020 except Exception: 1021 pass 1022 1023 def __python_timestamp_resolution(self, path, minimum_resolution): 1024 """ 1025 Returns the modification timestamp resolution for the given path 1026 supported by the used Python interpreter/OS/filesystem combination. 1027 Will not check for resolutions less than the given minimum value. Will 1028 change the path's modification timestamp in the process. 1029 1030 Return values: 1031 0 - nanosecond resolution supported 1032 positive decimal - timestamp resolution in seconds 1033 1034 """ 1035 # Note on Python's floating point timestamp support: 1036 # Python interpreter versions prior to Python 2.3 did not support 1037 # floating point timestamps. Versions 2.3 through 3.3 may or may not 1038 # support it depending on the configuration (may be toggled by calling 1039 # os.stat_float_times(True/False) at program startup, disabled by 1040 # default prior to Python 2.5 and enabled by default since). Python 3.3 1041 # deprecated this configuration and 3.4 removed support for it after 1042 # which floating point timestamps are always supported. 1043 ver = sys.version_info[0:2] 1044 python_nanosecond_support = ver >= (3, 4) or (ver >= (2, 3) and 1045 os.stat_float_times()) 1046 1047 # Minimal expected floating point difference used to account for 1048 # possible imprecise floating point number representations. We want 1049 # this number to be small (at least smaller than 0.0001) but still 1050 # large enough that we can be sure that increasing a floating point 1051 # value by 2 * eta guarantees the value read back will be increased by 1052 # at least eta. 1053 eta = 0.00005 1054 1055 stats_orig = os.stat(path) 1056 def test_time(diff): 1057 """Returns whether a timestamp difference is detectable.""" 1058 os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime + diff)) 1059 return os.stat(path).st_mtime > stats_orig.st_mtime + eta 1060 1061 # Test for nanosecond timestamp resolution support. 1062 if not minimum_resolution and python_nanosecond_support: 1063 if test_time(2 * eta): 1064 return 0 1065 1066 # Detect the filesystem timestamp resolution. Note that there is no 1067 # need to make this code 'as fast as possible' as, this function gets 1068 # called before having to sleep until the next detectable modification 1069 # timestamp value and that, since we already know nanosecond resolution 1070 # is not supported, will surely take longer than whatever we do here to 1071 # detect this minimal detectable modification timestamp resolution. 1072 step = 0.1 1073 if not python_nanosecond_support: 1074 # If Python does not support nanosecond timestamp resolution we 1075 # know the minimum possible supported timestamp resolution is 1 1076 # second. 1077 minimum_resolution = max(1, minimum_resolution) 1078 index = max(1, int(minimum_resolution / step)) 1079 while step * index < minimum_resolution: 1080 # Floating point number representation errors may cause our 1081 # initially calculated start index to be too small if calculated 1082 # directly. 1083 index += 1 1084 while True: 1085 # Do not simply add up the steps to avoid cumulative floating point 1086 # number representation errors. 1087 next = step * index 1088 if next > 10: 1089 raise TestEnvironmentError("File systems with too coarse " 1090 "modification timestamp resolutions not supported.") 1091 if test_time(next): 1092 return next 1093 index += 1 1094 1095 def __wait_for_time_change(self, path, touch, last_build_time): 1096 """ 1097 Wait until a newly assigned file system modification timestamp for 1098 the given path is large enough for the timestamp difference between it 1099 and the last build timestamp or the path's original file system 1100 modification timestamp (depending on the last_build_time flag) to be 1101 correctly recognized by both this Python based testing framework and 1102 the Boost Jam executable being tested. May optionally touch the given 1103 path to set its modification timestamp to the new value. 1104 1105 """ 1106 assert self.last_build_timestamp or not last_build_time 1107 stats_orig = os.stat(path) 1108 1109 if last_build_time: 1110 start_time = self.last_build_timestamp 1111 else: 1112 start_time = stats_orig.st_mtime 1113 1114 build_resolution = self.__build_timestamp_resolution() 1115 assert build_resolution >= 0 1116 1117 # Check whether the current timestamp is already new enough. 1118 if stats_orig.st_mtime > start_time and (not build_resolution or 1119 stats_orig.st_mtime >= start_time + build_resolution): 1120 return 1121 1122 resolution = self.__python_timestamp_resolution(path, build_resolution) 1123 assert resolution >= build_resolution 1124 self.__wait_for_time_change_impl(path, start_time, resolution, build_resolution) 1125 1126 if not touch: 1127 os.utime(path, (stats_orig.st_atime, stats_orig.st_mtime)) 1128 1129 def __wait_for_time_change_impl(self, path, start_time, resolution, build_resolution): 1130 # Implementation notes: 1131 # * Theoretically time.sleep() API might get interrupted too soon 1132 # (never actually encountered). 1133 # * We encountered cases where we sleep just long enough for the 1134 # filesystem's modifiction timestamp to change to the desired value, 1135 # but after waking up, the read timestamp is still just a tiny bit 1136 # too small (encountered on Windows). This is most likely caused by 1137 # imprecise floating point timestamp & sleep interval representation 1138 # used by Python. Note though that we never encountered a case where 1139 # more than one additional tiny sleep() call was needed to remedy 1140 # the situation. 1141 # * We try to wait long enough for the timestamp to change, but do not 1142 # want to waste processing time by waiting too long. The main 1143 # problem is that when we have a coarse resolution, the actual times 1144 # get rounded and we do not know the exact sleep time needed for the 1145 # difference between two such times to pass. E.g. if we have a 1 1146 # second resolution and the original and the current file timestamps 1147 # are both 10 seconds then it could be that the current time is 1148 # 10.99 seconds and that we can wait for just one hundredth of a 1149 # second for the current file timestamp to reach its next value, and 1150 # using a longer sleep interval than that would just be wasting 1151 # time. 1152 while True: 1153 os.utime(path, None) 1154 c = os.stat(path).st_mtime 1155 if resolution: 1156 if c > start_time and (not build_resolution or c >= start_time 1157 + build_resolution): 1158 break 1159 if c <= start_time - resolution: 1160 # Move close to the desired timestamp in one sleep, but not 1161 # close enough for timestamp rounding to potentially cause 1162 # us to wait too long. 1163 if start_time - c > 5: 1164 if last_build_time: 1165 error_message = ("Last build time recorded as " 1166 "being a future event, causing a too long " 1167 "wait period. Something must have played " 1168 "around with the system clock.") 1169 else: 1170 error_message = ("Original path modification " 1171 "timestamp set to far into the future or " 1172 "something must have played around with the " 1173 "system clock, causing a too long wait " 1174 "period.\nPath: '%s'" % path) 1175 raise TestEnvironmentError(message) 1176 _sleep(start_time - c) 1177 else: 1178 # We are close to the desired timestamp so take baby sleeps 1179 # to avoid sleeping too long. 1180 _sleep(max(0.01, resolution / 10)) 1181 else: 1182 if c > start_time: 1183 break 1184 _sleep(max(0.01, start_time - c)) 1185 1186 1187class List: 1188 def __init__(self, s=""): 1189 elements = [] 1190 if isstr(s): 1191 # Have to handle escaped spaces correctly. 1192 elements = s.replace("\ ", "\001").split() 1193 else: 1194 elements = s 1195 self.l = [e.replace("\001", " ") for e in elements] 1196 1197 def __len__(self): 1198 return len(self.l) 1199 1200 def __getitem__(self, key): 1201 return self.l[key] 1202 1203 def __setitem__(self, key, value): 1204 self.l[key] = value 1205 1206 def __delitem__(self, key): 1207 del self.l[key] 1208 1209 def __str__(self): 1210 return str(self.l) 1211 1212 def __repr__(self): 1213 return "%s.List(%r)" % (self.__module__, " ".join(self.l)) 1214 1215 def __mul__(self, other): 1216 result = List() 1217 if not isinstance(other, List): 1218 other = List(other) 1219 for f in self: 1220 for s in other: 1221 result.l.append(f + s) 1222 return result 1223 1224 def __rmul__(self, other): 1225 if not isinstance(other, List): 1226 other = List(other) 1227 return List.__mul__(other, self) 1228 1229 def __add__(self, other): 1230 result = List() 1231 result.l = self.l[:] + other.l[:] 1232 return result 1233 1234 1235def _contains_lines(data, lines): 1236 data_line_count = len(data) 1237 expected_line_count = reduce(lambda x, y: x + len(y), lines, 0) 1238 index = 0 1239 for expected in lines: 1240 if expected_line_count > data_line_count - index: 1241 return False 1242 expected_line_count -= len(expected) 1243 index = _match_line_sequence(data, index, data_line_count - 1244 expected_line_count, expected) 1245 if index < 0: 1246 return False 1247 return True 1248 1249 1250def _match_line_sequence(data, start, end, lines): 1251 if not lines: 1252 return start 1253 for index in range(start, end - len(lines) + 1): 1254 data_index = index 1255 for expected in lines: 1256 if not fnmatch.fnmatch(data[data_index], expected): 1257 break 1258 data_index += 1 1259 else: 1260 return data_index 1261 return -1 1262 1263 1264def _sleep(delay): 1265 if delay > 5: 1266 raise TestEnvironmentError("Test environment error: sleep period of " 1267 "more than 5 seconds requested. Most likely caused by a file with " 1268 "its modification timestamp set to sometime in the future.") 1269 time.sleep(delay) 1270 1271 1272############################################################################### 1273# 1274# Initialization. 1275# 1276############################################################################### 1277 1278# Make os.stat() return file modification times as floats instead of integers 1279# to get the best possible file timestamp resolution available. The exact 1280# resolution depends on the underlying file system and the Python os.stat() 1281# implementation. The better the resolution we achieve, the shorter we need to 1282# wait for files we create to start getting new timestamps. 1283# 1284# Additional notes: 1285# * os.stat_float_times() function first introduced in Python 2.3. and 1286# suggested for deprecation in Python 3.3. 1287# * On Python versions 2.5+ we do not need to do this as there os.stat() 1288# returns floating point file modification times by default. 1289# * Windows CPython implementations prior to version 2.5 do not support file 1290# modification timestamp resolutions of less than 1 second no matter whether 1291# these timestamps are returned as integer or floating point values. 1292# * Python documentation states that this should be set in a program's 1293# __main__ module to avoid affecting other libraries that might not be ready 1294# to support floating point timestamps. Since we use no such external 1295# libraries, we ignore this warning to make it easier to enable this feature 1296# in both our single & multiple-test scripts. 1297if (2, 3) <= sys.version_info < (2, 5) and not os.stat_float_times(): 1298 os.stat_float_times(True) 1299 1300 1301# Quickie tests. Should use doctest instead. 1302if __name__ == "__main__": 1303 assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']" 1304 assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')" 1305 1306 assert _contains_lines([], []) 1307 assert _contains_lines([], [[]]) 1308 assert _contains_lines([], [[], []]) 1309 assert _contains_lines([], [[], [], []]) 1310 assert not _contains_lines([], [[""]]) 1311 assert not _contains_lines([], [["a"]]) 1312 1313 assert _contains_lines([""], []) 1314 assert _contains_lines(["a"], []) 1315 assert _contains_lines(["a", "b"], []) 1316 assert _contains_lines(["a", "b"], [[], [], []]) 1317 1318 assert _contains_lines([""], [[""]]) 1319 assert not _contains_lines([""], [["a"]]) 1320 assert not _contains_lines(["a"], [[""]]) 1321 assert _contains_lines(["a", "", "b", ""], [["a"]]) 1322 assert _contains_lines(["a", "", "b", ""], [[""]]) 1323 assert _contains_lines(["a", "", "b"], [["b"]]) 1324 assert not _contains_lines(["a", "b"], [[""]]) 1325 assert not _contains_lines(["a", "", "b", ""], [["c"]]) 1326 assert _contains_lines(["a", "", "b", "x"], [["x"]]) 1327 1328 data = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] 1329 assert _contains_lines(data, [["1", "2"]]) 1330 assert not _contains_lines(data, [["2", "1"]]) 1331 assert not _contains_lines(data, [["1", "3"]]) 1332 assert not _contains_lines(data, [["1", "3"]]) 1333 assert _contains_lines(data, [["1"], ["2"]]) 1334 assert _contains_lines(data, [["1"], [], [], [], ["2"]]) 1335 assert _contains_lines(data, [["1"], ["3"]]) 1336 assert not _contains_lines(data, [["3"], ["1"]]) 1337 assert _contains_lines(data, [["3"], ["7"], ["8"]]) 1338 assert not _contains_lines(data, [["1"], ["3", "5"]]) 1339 assert not _contains_lines(data, [["1"], [""], ["5"]]) 1340 assert not _contains_lines(data, [["1"], ["5"], ["3"]]) 1341 assert not _contains_lines(data, [["1"], ["5", "3"]]) 1342 1343 assert not _contains_lines(data, [[" 3"]]) 1344 assert not _contains_lines(data, [["3 "]]) 1345 assert not _contains_lines(data, [["3", ""]]) 1346 assert not _contains_lines(data, [["", "3"]]) 1347 1348 print("tests passed") 1349