latest from svn
[boost:build.git] / v2 / test / BoostBuild.py
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
8 import TestCmd
9 import copy
10 import fnmatch
11 import glob
12 import math
13 import os
14 import re
15 import shutil
16 import string
17 import StringIO
18 import sys
19 import tempfile
20 import time
21 import traceback
22 import tree
23 import types
24
25 from xml.sax.saxutils import escape
26
27
28 annotations = []
29
30
31 def print_annotation(name, value, xml):
32     """Writes some named bits of information about test run.
33     """
34     if xml:      
35         print escape(name) + " {{{"
36         print escape(value)
37         print "}}}"
38     else:
39         print name + " {{{"
40         print value
41         print "}}}"
42         
43 def flush_annotations(xml=0):
44     global annotations
45     for ann in annotations:
46         print_annotation(ann[0], ann[1], xml)
47     annotations = []
48
49
50 defer_annotations = 0
51
52
53 def set_defer_annotations(n):
54     global defer_annotations
55     defer_annotations = n
56
57
58 def annotation(name, value):
59     """Records an annotation about the test run.
60     """
61     annotations.append((name, value))
62     if not defer_annotations:
63         flush_annotations()
64
65
66 def get_toolset():
67     toolset = None;
68     for arg in sys.argv[1:]:
69         if not arg.startswith('-'):
70             toolset = arg
71     return toolset or 'gcc'
72
73
74 # Detect the host OS.
75 windows = False
76 if os.environ.get('OS', '').lower().startswith('windows') or \
77        os.__dict__.has_key('uname') and \
78        os.uname()[0].lower().startswith('cygwin'):
79     windows = True
80
81
82 suffixes = {}
83
84
85 # Prepare the map of suffixes
86 def prepare_suffix_map(toolset):
87     global windows
88     global suffixes
89     suffixes = {'.exe': '', '.dll': '.so', '.lib': '.a', '.obj': '.o'}
90     suffixes['.implib'] = '.no_implib_files_on_this_platform'
91     if windows:
92         suffixes = {}
93         if toolset in ["gcc"]:
94             suffixes['.lib'] = '.a' # static libs have '.a' suffix with mingw...
95             suffixes['.obj'] = '.o'
96         suffixes['.implib'] = '.lib'
97     if os.__dict__.has_key('uname') and (os.uname()[0] == 'Darwin'):
98         suffixes['.dll'] = '.dylib'
99
100
101 def re_remove(sequence, regex):
102     me = re.compile(regex)
103     result = filter(lambda x: me.match(x), sequence)
104     if 0 == len(result):
105         raise ValueError()
106     for r in result:
107         sequence.remove(r)
108
109
110 def glob_remove(sequence, pattern):
111     result = fnmatch.filter(sequence, pattern)
112     if 0 == len(result):
113         raise ValueError()
114     for r in result:
115         sequence.remove(r)
116
117
118 # Configuration stating whether Boost Build is expected to automatically prepend
119 # prefixes to built library targets.
120 lib_prefix = True
121 dll_prefix = True
122 if windows:
123     dll_prefix = False
124
125
126 #
127 # FIXME: this is copy-pasted from TestSCons.py
128 # Should be moved to TestCmd.py?
129 #
130 if os.name == 'posix':
131     def _failed(self, status=0):
132         if self.status is None:
133             return None
134         return _status(self) != status
135     def _status(self):
136         if os.WIFEXITED(self.status):
137             return os.WEXITSTATUS(self.status)
138         else:
139             return -1
140 elif os.name == 'nt':
141     def _failed(self, status=0):
142         return not self.status is None and self.status != status
143     def _status(self):
144         return self.status
145
146
147 class Tester(TestCmd.TestCmd):
148     """Main tester class for Boost Build.
149
150     Optional arguments:
151
152     `arguments`                   - Arguments passed to the run executable.
153     `executable`                  - Name of the executable to invoke.
154     `match`                       - Function to use for compating actual and
155                                     expected file contents.
156     `boost_build_path`            - Boost build path to be passed to the run
157                                     executable.
158     `translate_suffixes`          - Whether to update suffixes on the the file
159                                     names passed from the test script so they
160                                     match those actually created by the current
161                                     toolset. For example, static library files
162                                     are specified by using the .lib suffix but
163                                     when the 'gcc' toolset is used it actually
164                                     creates them using the .a suffix.
165     `pass_toolset`                - Whether the test system should pass the
166                                     specified toolset to the run executable.
167     `use_test_config`             - Whether the test system should tell the run
168                                     executable to read in the test_config.jam
169                                     configuration file.
170     `ignore_toolset_requirements` - Whether the test system should tell the run
171                                     executable to ignore toolset requirements.
172     `workdir`                     - indicates an absolute directory where the
173                                     test will be run from.
174
175     Optional arguments inherited from the base class:
176
177     `description`                 - Test description string displayed in case of
178                                     a failed test.
179     `subdir'                      - List of subdirectories to automatically
180                                     create under the working directory. Each
181                                     subdirectory needs to be specified
182                                     separately parent coming before its child.
183     `verbose`                     - Flag that may be used to enable more verbose
184                                     test system output. Note that it does not
185                                     also enable more verbose build system
186                                     output like the --verbose command line
187                                     option does.
188     """
189     def __init__(self, arguments="", executable="bjam",
190         match=TestCmd.match_exact, boost_build_path=None,
191         translate_suffixes=True, pass_toolset=True, use_test_config=True,
192         ignore_toolset_requirements=True, workdir="", **keywords):
193
194         self.original_workdir = os.getcwd()
195         if workdir != '' and not os.path.isabs(workdir):
196             raise "Parameter workdir <"+workdir+"> must point to an absolute directory: "
197
198         self.last_build_time_start = 0
199         self.last_build_time_finish = 0
200         self.translate_suffixes = translate_suffixes
201         self.use_test_config = use_test_config
202
203         self.toolset = get_toolset()
204         self.pass_toolset = pass_toolset
205         self.ignore_toolset_requirements = ignore_toolset_requirements
206
207         prepare_suffix_map(pass_toolset and self.toolset or 'gcc')
208
209         if not '--default-bjam' in sys.argv:
210             jam_build_dir = ""
211             if os.name == 'nt':
212                 jam_build_dir = "bin.ntx86"
213             elif (os.name == 'posix') and os.__dict__.has_key('uname'):
214                 if os.uname()[0].lower().startswith('cygwin'):
215                     jam_build_dir = "bin.cygwinx86"
216                     if 'TMP' in os.environ and os.environ['TMP'].find('~') != -1:
217                         print 'Setting $TMP to /tmp to get around problem with short path names'
218                         os.environ['TMP'] = '/tmp'
219                 elif os.uname()[0] == 'Linux':
220                     cpu = os.uname()[4]
221                     if re.match("i.86", cpu):
222                         jam_build_dir = "bin.linuxx86";
223                     else:
224                         jam_build_dir = "bin.linux" + os.uname()[4]
225                 elif os.uname()[0] == 'SunOS':
226                     jam_build_dir = "bin.solaris"
227                 elif os.uname()[0] == 'Darwin':
228                     if os.uname()[4] == 'i386':
229                         jam_build_dir = "bin.macosxx86"
230                     else:
231                         jam_build_dir = "bin.macosxppc"
232                 elif os.uname()[0] == "AIX":
233                     jam_build_dir = "bin.aix"
234                 elif os.uname()[0] == "IRIX64":
235                     jam_build_dir = "bin.irix"
236                 elif os.uname()[0] == "FreeBSD":
237                     jam_build_dir = "bin.freebsd"
238                 elif os.uname()[0] == "OSF1":
239                     jam_build_dir = "bin.osf"
240                 else:
241                     raise "Don't know directory where Jam is built for this system: " + os.name + "/" + os.uname()[0]
242             else:
243                 raise "Don't know directory where Jam is built for this system: " + os.name
244
245             # Find where jam_src is located. Try for the debug version if it is
246             # lying around.
247             dirs = [os.path.join('../engine/src', jam_build_dir + '.debug'),
248                     os.path.join('../engine/src', jam_build_dir),
249                     ]
250             for d in dirs:
251                 if os.path.exists(d):
252                     jam_build_dir = d
253                     break
254             else:
255                 print "Cannot find built Boost.Jam"
256                 sys.exit(1)
257
258         verbosity = ['-d0', '--quiet']
259         if '--verbose' in sys.argv:
260             keywords['verbose'] = True
261             verbosity = ['-d+2']
262
263         if boost_build_path is None:
264             boost_build_path = self.original_workdir + "/.."
265
266         program_list = []
267
268         if '--default-bjam' in sys.argv:
269             program_list.append(executable)
270             inpath_bjam = True
271         else:
272             program_list.append(os.path.join(jam_build_dir, executable))
273             inpath_bjam = None
274         program_list.append('-sBOOST_BUILD_PATH="' + boost_build_path + '"')
275         if verbosity:
276             program_list += verbosity
277         if arguments:
278             program_list += arguments.split(" ")
279
280         TestCmd.TestCmd.__init__(
281             self
282             , program=program_list
283             , match=match
284             , workdir=workdir
285             , inpath=inpath_bjam
286             , **keywords)
287
288         os.chdir(self.workdir)
289
290     def cleanup(self):
291         try:
292             TestCmd.TestCmd.cleanup(self)
293             os.chdir(self.original_workdir)
294         except AttributeError:
295             # When this is called during TestCmd.TestCmd.__del__ we can have
296             # both 'TestCmd' and 'os' unavailable in our scope. Do nothing in
297             # this case.
298             pass
299
300     #
301     # Methods that change the working directory's content.
302     #
303     def set_tree(self, tree_location):
304         # It is not possible to remove the current directory.
305         d = os.getcwd()
306         os.chdir(os.path.dirname(self.workdir))
307         shutil.rmtree(self.workdir, ignore_errors=False)
308
309         if not os.path.isabs(tree_location):
310             tree_location = os.path.join(self.original_workdir, tree_location)
311         shutil.copytree(tree_location, self.workdir)
312
313         os.chdir(d)
314
315         def make_writable(unused, dir, entries):
316             for e in entries:
317                 name = os.path.join(dir, e)
318                 os.chmod(name, os.stat(name)[0] | 0222)
319
320         os.path.walk(".", make_writable, None)
321
322     def write(self, file, content):
323         self.wait_for_time_change_since_last_build()
324         nfile = self.native_file_name(file)
325         try:
326             os.makedirs(os.path.dirname(nfile))
327         except Exception, e:
328             pass
329         open(nfile, "wb").write(content)
330
331     def rename(self, old, new):
332         try:
333             os.makedirs(os.path.dirname(new))
334         except:
335             pass
336
337         try:
338             os.remove(new)
339         except:
340             pass
341
342         os.rename(old, new)
343         self.touch(new);
344
345     def copy(self, src, dst):
346         self.wait_for_time_change_since_last_build()
347         try:
348             self.write(dst, self.read(src, 1))
349         except:
350             self.fail_test(1)
351
352     def copy_preserving_timestamp(self, src, dst):
353         src_name = self.native_file_name(src)
354         dst_name = self.native_file_name(dst)
355         stats = os.stat(src_name)
356         self.write(dst, self.read(src, 1))
357         os.utime(dst_name, (stats.st_atime, stats.st_mtime))
358
359     def touch(self, names):
360         self.wait_for_time_change_since_last_build()
361         for name in self.adjust_names(names):
362             os.utime(self.native_file_name(name), None)
363
364     def rm(self, names):
365         self.wait_for_time_change_since_last_build()
366         if not type(names) == types.ListType:
367             names = [names]
368
369         # Avoid attempts to remove the current directory.
370         os.chdir(self.original_workdir)
371         for name in names:
372             n = self.native_file_name(name)
373             n = glob.glob(n)
374             if n: n = n[0]
375             if not n:
376                 n = self.glob_file(string.replace(name, "$toolset", self.toolset+"*"))
377             if n:
378                 if os.path.isdir(n):
379                     shutil.rmtree(n, ignore_errors=False)
380                 else:
381                     os.unlink(n)
382
383         # Create working dir root again in case we removed it.
384         if not os.path.exists(self.workdir):
385             os.mkdir(self.workdir)
386         os.chdir(self.workdir)
387
388     def expand_toolset(self, name):
389         """Expands $toolset in the given file to tested toolset.
390         """
391         content = self.read(name)
392         content = string.replace(content, "$toolset", self.toolset)
393         self.write(name, content)
394
395     def dump_stdio(self):
396         annotation("STDOUT", self.stdout())
397         annotation("STDERR", self.stderr())
398
399     #
400     #   FIXME: Large portion copied from TestSCons.py, should be moved?
401     #
402     def run_build_system(self, extra_args="", subdir="", stdout=None, stderr="",
403         status=0, match=None, pass_toolset=None, use_test_config=None,
404         ignore_toolset_requirements=None, expected_duration=None, **kw):
405
406         self.last_build_time_start = time.time()
407
408         try:
409             if os.path.isabs(subdir):
410                 if stderr:
411                     print "You must pass a relative directory to subdir <"+subdir+">."
412                 status = 1
413                 return
414
415             self.previous_tree = tree.build_tree(self.workdir)
416
417             if match is None:
418                 match = self.match
419
420             if pass_toolset is None:
421                 pass_toolset = self.pass_toolset
422
423             if use_test_config is None:
424                 use_test_config = self.use_test_config
425
426             if ignore_toolset_requirements is None:
427                 ignore_toolset_requirements = self.ignore_toolset_requirements
428
429             try:
430                 kw['program'] = []
431                 kw['program'] += self.program
432                 if extra_args:
433                     kw['program'] += extra_args.split(" ")
434                 if pass_toolset:
435                     kw['program'].append("toolset=" + self.toolset)
436                 if use_test_config:
437                     kw['program'].append('--test-config="%s"'
438                         % os.path.join(self.original_workdir, "test-config.jam"))
439                 if ignore_toolset_requirements:
440                     kw['program'].append("--ignore-toolset-requirements")
441                 if "--python" in sys.argv:
442                     kw['program'].append("--python")
443                 kw['chdir'] = subdir
444                 self.last_program_invocation = kw['program']
445                 apply(TestCmd.TestCmd.run, [self], kw)
446             except:
447                 self.dump_stdio()
448                 raise
449         finally:
450             self.last_build_time_finish = time.time()
451
452         if (status != None) and _failed(self, status):
453             expect = ''
454             if status != 0:
455                 expect = " (expected %d)" % status
456
457             annotation("failure", '"%s" returned %d%s'
458                 % (kw['program'], _status(self), expect))
459             
460             annotation("reason", "unexpected status returned by bjam")
461             self.fail_test(1)
462
463         if not (stdout is None) and not match(self.stdout(), stdout):
464             annotation("failure", "Unexpected stdout")
465             annotation("Expected STDOUT", stdout)
466             annotation("Actual STDOUT", self.stdout())
467             stderr = self.stderr()
468             if stderr:
469                 annotation("STDERR", stderr)
470             self.maybe_do_diff(self.stdout(), stdout)
471             self.fail_test(1, dump_stdio=False)
472
473         # Intel tends to produce some messages to stderr which make tests fail.
474         intel_workaround = re.compile("^xi(link|lib): executing.*\n", re.M)
475         actual_stderr = re.sub(intel_workaround, "", self.stderr())
476
477         if not (stderr is None) and not match(actual_stderr, stderr):
478             annotation("failure", "Unexpected stderr")
479             annotation("Expected STDERR", stderr)
480             annotation("Actual STDERR", self.stderr())
481             annotation("STDOUT", self.stdout())
482             self.maybe_do_diff(actual_stderr, stderr)
483             self.fail_test(1, dump_stdio=False)
484
485         if not expected_duration is None:
486             actual_duration = self.last_build_time_finish - self.last_build_time_start 
487             if (actual_duration > expected_duration):
488                 print "Test run lasted %f seconds while it was expected to " \
489                     "finish in under %f seconds." % (actual_duration,
490                     expected_duration)
491                 self.fail_test(1, dump_stdio=False)
492
493         self.tree = tree.build_tree(self.workdir)
494         self.difference = tree.trees_difference(self.previous_tree, self.tree)
495         self.difference.ignore_directories()
496         self.unexpected_difference = copy.deepcopy(self.difference)
497
498     def glob_file(self, name):
499         result = None
500         if hasattr(self, 'difference'):
501             for f in self.difference.added_files+self.difference.modified_files+self.difference.touched_files:
502                 if fnmatch.fnmatch(f, name):
503                     result = self.native_file_name(f)
504                     break
505         if not result:
506             result = glob.glob(self.native_file_name(name))
507             if result:
508                 result = result[0]
509         return result
510
511     def read(self, name, binary=False):
512         try:
513             if self.toolset:
514                 name = string.replace(name, "$toolset", self.toolset+"*")
515             name = self.glob_file(name)
516             openMode = "r"
517             if binary:
518                 openMode += "b"
519             else:
520                 openMode += "U"
521             return open(name, openMode).read()
522         except:
523             annotation("failure", "Could not open '%s'" % name)
524             self.fail_test(1)
525             return ''
526
527     def read_and_strip(self, name):
528         if not self.glob_file(name):
529             return ''
530         f = open(self.glob_file(name), "rb")
531         lines = f.readlines()
532         result = string.join(map(string.rstrip, lines), "\n")
533         if lines and lines[-1][-1] == '\n':
534             return result + '\n'
535         else:
536             return result
537
538     def fail_test(self, condition, dump_stdio=True, *args):
539         if not condition:
540             return
541
542         if hasattr(self, 'difference'):
543             f = StringIO.StringIO()
544             self.difference.pprint(f)
545             annotation("changes caused by the last build command", f.getvalue())
546
547         if dump_stdio:
548             self.dump_stdio()
549
550         if '--preserve' in sys.argv:
551             print
552             print "*** Copying the state of working dir into 'failed_test' ***"
553             print
554             path = os.path.join(self.original_workdir, "failed_test")
555             if os.path.isdir(path):
556                 shutil.rmtree(path, ignore_errors=False)
557             elif os.path.exists(path):
558                 raise "Path " + path + " already exists and is not a directory";
559             shutil.copytree(self.workdir, path)
560             print "The failed command was:"
561             print ' '.join(self.last_program_invocation)
562
563         at = TestCmd.caller(traceback.extract_stack(), 0)
564         annotation("stacktrace", at)
565         sys.exit(1)
566
567     # A number of methods below check expectations with actual difference
568     # between directory trees before and after a build. All the 'expect*'
569     # methods require exact names to be passed. All the 'ignore*' methods allow
570     # wildcards.
571
572     # All names can be lists, which are taken to be directory components.
573     def expect_addition(self, names):
574         for name in self.adjust_names(names):
575             try:
576                 glob_remove(self.unexpected_difference.added_files, name)
577             except:
578                 annotation("failure", "File %s not added as expected" % name)
579                 self.fail_test(1)
580
581     def ignore_addition(self, wildcard):
582         self.ignore_elements(self.unexpected_difference.added_files, wildcard)
583
584     def expect_removal(self, names):
585         for name in self.adjust_names(names):
586             try:
587                 glob_remove(self.unexpected_difference.removed_files, name)
588             except:
589                 annotation("failure", "File %s not removed as expected" % name)
590                 self.fail_test(1)
591
592     def ignore_removal(self, wildcard):
593         self.ignore_elements(self.unexpected_difference.removed_files, wildcard)
594
595     def expect_modification(self, names):
596         for name in self.adjust_names(names):
597             try:
598                 glob_remove(self.unexpected_difference.modified_files, name)
599             except:
600                 annotation("failure", "File %s not modified as expected" % name)
601                 self.fail_test(1)
602
603     def ignore_modification(self, wildcard):
604         self.ignore_elements(self.unexpected_difference.modified_files, \
605             wildcard)
606
607     def expect_touch(self, names):
608         d = self.unexpected_difference
609         for name in self.adjust_names(names):
610             # We need to check both touched and modified files. The reason is
611             # that:
612             #   (1) Windows binaries such as obj, exe or dll files have slight
613             #       differences even with identical inputs due to Windows PE
614             #       format headers containing an internal timestamp.
615             #   (2) Intel's compiler for Linux has the same behaviour.
616             filesets = [d.modified_files, d.touched_files]
617
618             while filesets:
619                 try:
620                     glob_remove(filesets[-1], name)
621                     break
622                 except ValueError:
623                     filesets.pop()
624
625             if not filesets:
626                 annotation("failure", "File %s not touched as expected" % name)
627                 self.fail_test(1)
628
629     def ignore_touch(self, wildcard):
630         self.ignore_elements(self.unexpected_difference.touched_files, wildcard)
631
632     def ignore(self, wildcard):
633         self.ignore_elements(self.unexpected_difference.added_files, wildcard)
634         self.ignore_elements(self.unexpected_difference.removed_files, wildcard)
635         self.ignore_elements(self.unexpected_difference.modified_files, wildcard)
636         self.ignore_elements(self.unexpected_difference.touched_files, wildcard)
637
638     def expect_nothing(self, names):
639         for name in self.adjust_names(names):
640             if name in self.difference.added_files:
641                 annotation("failure",
642                     "File %s added, but no action was expected" % name)
643                 self.fail_test(1)
644             if name in self.difference.removed_files:
645                 annotation("failure",
646                     "File %s removed, but no action was expected" % name)
647                 self.fail_test(1)
648                 pass
649             if name in self.difference.modified_files:
650                 annotation("failure",
651                     "File %s modified, but no action was expected" % name)
652                 self.fail_test(1)
653             if name in self.difference.touched_files:
654                 annotation("failure",
655                     "File %s touched, but no action was expected" % name)
656                 self.fail_test(1)
657
658     def expect_nothing_more(self):
659         # Not totally sure about this change, but I do not see a good
660         # alternative.
661         if windows:
662             self.ignore('*.ilk')       # MSVC incremental linking files.
663             self.ignore('*.pdb')       # MSVC program database files.
664             self.ignore('*.rsp')       # Response files.
665             self.ignore('*.tds')       # Borland debug symbols.
666             self.ignore('*.manifest')  # MSVC DLL manifests.            
667
668         # Debug builds of bjam built with gcc produce this profiling data.
669         self.ignore('gmon.out')
670         self.ignore('*/gmon.out')
671
672         self.ignore("bin/config.log")
673
674         self.ignore("*.pyc")
675
676         if not self.unexpected_difference.empty():
677             annotation('failure', 'Unexpected changes found')
678             output = StringIO.StringIO()
679             self.unexpected_difference.pprint(output)
680             annotation("unexpected changes", output.getvalue())
681             self.fail_test(1)
682
683     def __expect_line(self, content, expected, expected_to_exist):
684         expected = expected.strip()
685         lines = content.splitlines()
686         found = False
687         for line in lines:
688             line = line.strip()
689             if fnmatch.fnmatch(line, expected):
690                 found = True
691                 break
692
693         if expected_to_exist and not found:
694             annotation("failure",
695                 "Did not find expected line:\n%s\nin output:\n%s" %
696                 (expected, content))
697             self.fail_test(1)
698         if not expected_to_exist and found:
699             annotation("failure",
700                 "Found an unexpected line:\n%s\nin output:\n%s" %
701                 (expected, content))
702             self.fail_test(1)
703
704     def expect_output_line(self, line, expected_to_exist=True):
705         self.__expect_line(self.stdout(), line, expected_to_exist)
706
707     def expect_content_line(self, name, line, expected_to_exist=True):
708         content = self.__read_file(name)
709         self.__expect_line(content, line, expected_to_exist)
710
711     def __read_file(self, name, exact=False):
712         name = self.adjust_names(name)[0]
713         result = ""
714         try:
715             if exact:
716                 result = self.read(name)
717             else:
718                 result = string.replace(self.read_and_strip(name), "\\", "/")
719         except (IOError, IndexError):
720             print "Note: could not open file", name
721             self.fail_test(1)
722         return result
723
724     def expect_content(self, name, content, exact=False):
725         actual = self.__read_file(name, exact)
726         content = string.replace(content, "$toolset", self.toolset+"*")
727
728         matched = False
729         if exact:
730             matched = fnmatch.fnmatch(actual, content)
731         else:
732             def sorted_(x):
733                 x.sort()
734                 return x
735             actual_ = map(lambda x: sorted_(x.split()), actual.splitlines())
736             content_ = map(lambda x: sorted_(x.split()), content.splitlines())
737             if len(actual_) == len(content_):
738                 matched = map(
739                     lambda x, y: map(lambda n, p: fnmatch.fnmatch(n, p), x, y),
740                     actual_, content_)
741                 matched = reduce(
742                     lambda x, y: x and reduce(
743                         lambda a, b: a and b,
744                     y),
745                     matched)
746
747         if not matched:
748             print "Expected:\n"
749             print content
750             print "Got:\n"
751             print actual
752             self.fail_test(1)
753
754     def maybe_do_diff(self, actual, expected):
755         if os.environ.has_key("DO_DIFF") and os.environ["DO_DIFF"] != '':
756             e = tempfile.mktemp("expected")
757             a = tempfile.mktemp("actual")
758             open(e, "w").write(expected)
759             open(a, "w").write(actual)
760             print "DIFFERENCE"
761             if os.system("diff -u " + e + " " + a):
762                 print "Unable to compute difference: diff -u %s %s" % (e, a)
763             os.unlink(e)
764             os.unlink(a)
765         else:
766             print "Set environmental variable 'DO_DIFF' to examine difference."
767
768     # Helpers.
769     def mul(self, *arguments):
770         if len(arguments) == 0:
771             return None
772
773         here = arguments[0]
774         if type(here) == type(''):
775             here = [here]
776
777         if len(arguments) > 1:
778             there = apply(self.mul, arguments[1:])
779             result = []
780             for i in here:
781                 for j in there:
782                     result.append(i + j)
783             return result
784
785         return here
786
787     # Internal methods.
788     def ignore_elements(self, list, wildcard):
789         """Removes in-place, element of 'list' that match the given wildcard.
790         """
791         list[:] = filter(lambda x, w=wildcard: not fnmatch.fnmatch(x, w), list)
792
793     def adjust_lib_name(self, name):
794         global lib_prefix
795         result = name
796
797         pos = string.rfind(name, ".")
798         if pos != -1:
799             suffix = name[pos:]
800             if suffix == ".lib":
801                 (head, tail) = os.path.split(name)
802                 if lib_prefix:
803                     tail = "lib" + tail
804                     result = os.path.join(head, tail)
805             elif suffix == ".dll":
806                 (head, tail) = os.path.split(name)
807                 if dll_prefix:
808                     tail = "lib" + tail
809                     result = os.path.join(head, tail)
810         # If we want to use this name in a Jamfile, we better convert \ to /, as
811         # otherwise we would have to quote \.
812         result = string.replace(result, "\\", "/")
813         return result
814
815     def adjust_suffix(self, name):
816         if not self.translate_suffixes:
817             return name
818
819         pos = string.rfind(name, ".")
820         if pos != -1:
821             suffix = name[pos:]
822             name = name[:pos]
823
824             if suffixes.has_key(suffix):
825                 suffix = suffixes[suffix]
826         else:
827             suffix = ''
828
829         return name + suffix
830
831     # Acceps either a string or a list of strings and returns a list of strings.
832     # Adjusts suffixes on all names.
833     def adjust_names(self, names):
834         if type(names) == types.StringType:
835             names = [names]
836         r = map(self.adjust_lib_name, names)
837         r = map(self.adjust_suffix, r)
838         r = map(lambda x, t=self.toolset: string.replace(x, "$toolset", t+"*"), r)
839         return r
840
841     def native_file_name(self, name):
842         name = self.adjust_names(name)[0]
843         elements = string.split(name, "/")
844         return os.path.normpath(apply(os.path.join, [self.workdir]+elements))
845
846     # Wait while time is no longer equal to the time last "run_build_system"
847     # call finished. Used to avoid subsequent builds treating existing files as
848     # 'current'.
849     def wait_for_time_change_since_last_build(self):
850         while 1:
851             # In fact, I'm not sure why "+ 2" as opposed to "+ 1" is needed but
852             # empirically, "+ 1" sometimes causes 'touch' and other functions
853             # not to bump the file time enough for a rebuild to happen.
854             if math.floor(time.time()) < math.floor(self.last_build_time_finish) + 2:
855                 time.sleep(0.1)
856             else:
857                 break
858
859
860 class List:
861
862     def __init__(self, s=""):
863         elements = []
864         if isinstance(s, type("")):
865             # Have to handle espaced spaces correctly.
866             s = string.replace(s, "\ ", '\001')
867             elements = string.split(s)
868         else:
869             elements = s;
870
871         self.l = []
872         for e in elements:
873             self.l.append(string.replace(e, '\001', ' '))
874
875     def __len__(self):
876         return len(self.l)
877
878     def __getitem__(self, key):
879         return self.l[key]
880
881     def __setitem__(self, key, value):
882         self.l[key] = value
883
884     def __delitem__(self, key):
885         del self.l[key]
886
887     def __str__(self):
888         return str(self.l)
889
890     def __repr__(self):
891         return (self.__module__ + '.List('
892                  + repr(string.join(self.l, ' '))
893                  + ')')
894
895     def __mul__(self, other):
896         result = List()
897         if not isinstance(other, List):
898             other = List(other)
899         for f in self:
900             for s in other:
901                 result.l.append(f + s)
902         return result
903
904     def __rmul__(self, other):
905         if not isinstance(other, List):
906             other = List(other)
907         return List.__mul__(other, self)
908
909     def __add__(self, other):
910         result = List()
911         result.l = self.l[:] + other.l[:]
912         return result
913
914 # Quickie tests. Should use doctest instead.
915 if __name__ == '__main__':
916     assert str(List("foo bar") * "/baz") == "['foo/baz', 'bar/baz']"
917     assert repr("foo/" * List("bar baz")) == "__main__.List('foo/bar foo/baz')"
918     print 'tests passed'