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