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