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