08642cb848166382d0031649222740b91be84def
[scons.git] / QMTest / TestCmd.py
1 """
2 TestCmd.py:  a testing framework for commands and scripts.
3
4 The TestCmd module provides a framework for portable automated testing
5 of executable commands and scripts (in any language, not just Python),
6 especially commands and scripts that require file system interaction.
7
8 In addition to running tests and evaluating conditions, the TestCmd
9 module manages and cleans up one or more temporary workspace
10 directories, and provides methods for creating files and directories in
11 those workspace directories from in-line data, here-documents), allowing
12 tests to be completely self-contained.
13
14 A TestCmd environment object is created via the usual invocation:
15
16     import TestCmd
17     test = TestCmd.TestCmd()
18
19 There are a bunch of keyword arguments that you can use at instantiation
20 time:
21
22     test = TestCmd.TestCmd(description = 'string',
23                            program = 'program_or_script_to_test',
24                            interpreter = 'script_interpreter',
25                            workdir = 'prefix',
26                            subdir = 'subdir',
27                            verbose = Boolean,
28                            match = default_match_function,
29                            combine = Boolean)
30
31 There are a bunch of methods that let you do a bunch of different
32 things.  Here is an overview of them:
33
34     test.verbose_set(1)
35
36     test.description_set('string')
37
38     test.program_set('program_or_script_to_test')
39
40     test.interpreter_set('script_interpreter')
41     test.interpreter_set(['script_interpreter', 'arg'])
42
43     test.workdir_set('prefix')
44     test.workdir_set('')
45
46     test.workpath('file')
47     test.workpath('subdir', 'file')
48
49     test.subdir('subdir', ...)
50
51     test.write('file', "contents\n")
52     test.write(['subdir', 'file'], "contents\n")
53
54     test.read('file')
55     test.read(['subdir', 'file'])
56     test.read('file', mode)
57     test.read(['subdir', 'file'], mode)
58
59     test.writable('dir', 1)
60     test.writable('dir', None)
61
62     test.preserve(condition, ...)
63
64     test.cleanup(condition)
65
66     test.run(program = 'program_or_script_to_run',
67              interpreter = 'script_interpreter',
68              arguments = 'arguments to pass to program',
69              chdir = 'directory_to_chdir_to',
70              stdin = 'input to feed to the program\n')
71
72     test.pass_test()
73     test.pass_test(condition)
74     test.pass_test(condition, function)
75
76     test.fail_test()
77     test.fail_test(condition)
78     test.fail_test(condition, function)
79     test.fail_test(condition, function, skip)
80
81     test.no_result()
82     test.no_result(condition)
83     test.no_result(condition, function)
84     test.no_result(condition, function, skip)
85
86     test.stdout()
87     test.stdout(run)
88
89     test.stderr()
90     test.stderr(run)
91
92     test.symlink(target, link)
93
94     test.match(actual, expected)
95
96     test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n")
97     test.match_exact(["actual 1\n", "actual 2\n"],
98                      ["expected 1\n", "expected 2\n"])
99
100     test.match_re("actual 1\nactual 2\n", regex_string)
101     test.match_re(["actual 1\n", "actual 2\n"], list_of_regexes)
102
103     test.match_re_dotall("actual 1\nactual 2\n", regex_string)
104     test.match_re_dotall(["actual 1\n", "actual 2\n"], list_of_regexes)
105
106     test.tempdir()
107     test.tempdir('temporary-directory')
108
109     test.sleep()
110     test.sleep(seconds)
111
112     test.where_is('foo')
113     test.where_is('foo', 'PATH1:PATH2')
114     test.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
115
116     test.unlink('file')
117     test.unlink('subdir', 'file')
118
119 The TestCmd module provides pass_test(), fail_test(), and no_result()
120 unbound functions that report test results for use with the Aegis change
121 management system.  These methods terminate the test immediately,
122 reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
123 status 0 (success), 1 or 2 respectively.  This allows for a distinction
124 between an actual failed test and a test that could not be properly
125 evaluated because of an external condition (such as a full file system
126 or incorrect permissions).
127
128     import TestCmd
129
130     TestCmd.pass_test()
131     TestCmd.pass_test(condition)
132     TestCmd.pass_test(condition, function)
133
134     TestCmd.fail_test()
135     TestCmd.fail_test(condition)
136     TestCmd.fail_test(condition, function)
137     TestCmd.fail_test(condition, function, skip)
138
139     TestCmd.no_result()
140     TestCmd.no_result(condition)
141     TestCmd.no_result(condition, function)
142     TestCmd.no_result(condition, function, skip)
143
144 The TestCmd module also provides unbound functions that handle matching
145 in the same way as the match_*() methods described above.
146
147     import TestCmd
148
149     test = TestCmd.TestCmd(match = TestCmd.match_exact)
150
151     test = TestCmd.TestCmd(match = TestCmd.match_re)
152
153     test = TestCmd.TestCmd(match = TestCmd.match_re_dotall)
154
155 Lastly, the where_is() method also exists in an unbound function
156 version.
157
158     import TestCmd
159
160     TestCmd.where_is('foo')
161     TestCmd.where_is('foo', 'PATH1:PATH2')
162     TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
163 """
164
165 # Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
166 # This module is free software, and you may redistribute it and/or modify
167 # it under the same terms as Python itself, so long as this copyright message
168 # and disclaimer are retained in their original form.
169 #
170 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
171 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
172 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
173 # DAMAGE.
174 #
175 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
176 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
177 # PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
178 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
179 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
180
181 __author__ = "Steven Knight <knight at baldmt dot com>"
182 __revision__ = "TestCmd.py 0.26.D001 2007/08/20 21:58:58 knight"
183 __version__ = "0.26"
184
185 import os
186 import os.path
187 import popen2
188 import re
189 import shutil
190 import stat
191 import string
192 import sys
193 import tempfile
194 import time
195 import traceback
196 import types
197 import UserList
198
199 __all__ = [
200     'diff_re',
201     'fail_test',
202     'no_result',
203     'pass_test',
204     'match_exact',
205     'match_re',
206     'match_re_dotall',
207     'python_executable',
208     'TestCmd'
209 ]
210
211 def is_List(e):
212     return type(e) is types.ListType \
213         or isinstance(e, UserList.UserList)
214
215 try:
216     from UserString import UserString
217 except ImportError:
218     class UserString:
219         pass
220
221 if hasattr(types, 'UnicodeType'):
222     def is_String(e):
223         return type(e) is types.StringType \
224             or type(e) is types.UnicodeType \
225             or isinstance(e, UserString)
226 else:
227     def is_String(e):
228         return type(e) is types.StringType or isinstance(e, UserString)
229
230 tempfile.template = 'testcmd.'
231 if os.name in ('posix', 'nt'):
232     tempfile.template = 'testcmd.' + str(os.getpid()) + '.'
233 else:
234     tempfile.template = 'testcmd.'
235
236 re_space = re.compile('\s')
237
238 _Cleanup = []
239
240 def _clean():
241     global _Cleanup
242     cleanlist = filter(None, _Cleanup)
243     del _Cleanup[:]
244     cleanlist.reverse()
245     for test in cleanlist:
246         test.cleanup()
247
248 sys.exitfunc = _clean
249
250 class Collector:
251     def __init__(self, top):
252         self.entries = [top]
253     def __call__(self, arg, dirname, names):
254         pathjoin = lambda n, d=dirname: os.path.join(d, n)
255         self.entries.extend(map(pathjoin, names))
256
257 def _caller(tblist, skip):
258     string = ""
259     arr = []
260     for file, line, name, text in tblist:
261         if file[-10:] == "TestCmd.py":
262                 break
263         arr = [(file, line, name, text)] + arr
264     atfrom = "at"
265     for file, line, name, text in arr[skip:]:
266         if name == "?":
267             name = ""
268         else:
269             name = " (" + name + ")"
270         string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
271         atfrom = "\tfrom"
272     return string
273
274 def fail_test(self = None, condition = 1, function = None, skip = 0):
275     """Cause the test to fail.
276
277     By default, the fail_test() method reports that the test FAILED
278     and exits with a status of 1.  If a condition argument is supplied,
279     the test fails only if the condition is true.
280     """
281     if not condition:
282         return
283     if not function is None:
284         function()
285     of = ""
286     desc = ""
287     sep = " "
288     if not self is None:
289         if self.program:
290             of = " of " + self.program
291             sep = "\n\t"
292         if self.description:
293             desc = " [" + self.description + "]"
294             sep = "\n\t"
295
296     at = _caller(traceback.extract_stack(), skip)
297     sys.stderr.write("FAILED test" + of + desc + sep + at)
298
299     sys.exit(1)
300
301 def no_result(self = None, condition = 1, function = None, skip = 0):
302     """Causes a test to exit with no valid result.
303
304     By default, the no_result() method reports NO RESULT for the test
305     and exits with a status of 2.  If a condition argument is supplied,
306     the test fails only if the condition is true.
307     """
308     if not condition:
309         return
310     if not function is None:
311         function()
312     of = ""
313     desc = ""
314     sep = " "
315     if not self is None:
316         if self.program:
317             of = " of " + self.program
318             sep = "\n\t"
319         if self.description:
320             desc = " [" + self.description + "]"
321             sep = "\n\t"
322
323     at = _caller(traceback.extract_stack(), skip)
324     sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
325
326     sys.exit(2)
327
328 def pass_test(self = None, condition = 1, function = None):
329     """Causes a test to pass.
330
331     By default, the pass_test() method reports PASSED for the test
332     and exits with a status of 0.  If a condition argument is supplied,
333     the test passes only if the condition is true.
334     """
335     if not condition:
336         return
337     if not function is None:
338         function()
339     sys.stderr.write("PASSED\n")
340     sys.exit(0)
341
342 def match_exact(lines = None, matches = None):
343     """
344     """
345     if not is_List(lines):
346         lines = string.split(lines, "\n")
347     if not is_List(matches):
348         matches = string.split(matches, "\n")
349     if len(lines) != len(matches):
350         return
351     for i in range(len(lines)):
352         if lines[i] != matches[i]:
353             return
354     return 1
355
356 def match_re(lines = None, res = None):
357     """
358     """
359     if not is_List(lines):
360         lines = string.split(lines, "\n")
361     if not is_List(res):
362         res = string.split(res, "\n")
363     if len(lines) != len(res):
364         return
365     for i in range(len(lines)):
366         if not re.compile("^" + res[i] + "$").search(lines[i]):
367             return
368     return 1
369
370 def match_re_dotall(lines = None, res = None):
371     """
372     """
373     if not type(lines) is type(""):
374         lines = string.join(lines, "\n")
375     if not type(res) is type(""):
376         res = string.join(res, "\n")
377     if re.compile("^" + res + "$", re.DOTALL).match(lines):
378         return 1
379
380 def diff_re(a, b, fromfile='', tofile='',
381                 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
382     """
383     A simple "diff" of two sets of lines when the expected lines
384     are regular expressions.  This is a really dumb thing that
385     just compares each line in turn, so it doesn't look for
386     chunks of matching lines and the like--but at least it lets
387     you know exactly which line first didn't compare correctl...
388     """
389     result = []
390     diff = len(a) - len(b)
391     if diff < 0:
392         a = a + ['']*(-diff)
393     elif diff > 0:
394         b = b + ['']*diff
395     i = 0
396     for aline, bline in zip(a, b):
397         if not re.compile("^" + aline + "$").search(bline):
398             result.append("%sc%s" % (i+1, i+1))
399             result.append('< ' + repr(a[i]))
400             result.append('---')
401             result.append('> ' + repr(b[i]))
402         i = i+1
403     return result
404
405 if os.name == 'java':
406
407     python_executable = os.path.join(sys.prefix, 'jython')
408
409 else:
410
411     python_executable = sys.executable
412
413 if sys.platform == 'win32':
414
415     default_sleep_seconds = 2
416
417     def where_is(file, path=None, pathext=None):
418         if path is None:
419             path = os.environ['PATH']
420         if is_String(path):
421             path = string.split(path, os.pathsep)
422         if pathext is None:
423             pathext = os.environ['PATHEXT']
424         if is_String(pathext):
425             pathext = string.split(pathext, os.pathsep)
426         for ext in pathext:
427             if string.lower(ext) == string.lower(file[-len(ext):]):
428                 pathext = ['']
429                 break
430         for dir in path:
431             f = os.path.join(dir, file)
432             for ext in pathext:
433                 fext = f + ext
434                 if os.path.isfile(fext):
435                     return fext
436         return None
437
438 else:
439
440     def where_is(file, path=None, pathext=None):
441         if path is None:
442             path = os.environ['PATH']
443         if is_String(path):
444             path = string.split(path, os.pathsep)
445         for dir in path:
446             f = os.path.join(dir, file)
447             if os.path.isfile(f):
448                 try:
449                     st = os.stat(f)
450                 except OSError:
451                     continue
452                 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
453                     return f
454         return None
455
456     default_sleep_seconds = 1
457
458 class TestCmd:
459     """Class TestCmd
460     """
461
462     def __init__(self, description = None,
463                        program = None,
464                        interpreter = None,
465                        workdir = None,
466                        subdir = None,
467                        verbose = None,
468                        match = None,
469                        combine = 0,
470                        universal_newlines = 1):
471         self._cwd = os.getcwd()
472         self.description_set(description)
473         self.program_set(program)
474         self.interpreter_set(interpreter)
475         if verbose is None:
476             try:
477                 verbose = max( 0, int(os.environ.get('TESTCMD_VERBOSE', 0)) )
478             except ValueError:
479                 verbose = 0
480         self.verbose_set(verbose)
481         self.combine = combine
482         self.universal_newlines = universal_newlines
483         if not match is None:
484             self.match_func = match
485         else:
486             self.match_func = match_re
487         self._dirlist = []
488         self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
489         if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
490             self._preserve['pass_test'] = os.environ['PRESERVE']
491             self._preserve['fail_test'] = os.environ['PRESERVE']
492             self._preserve['no_result'] = os.environ['PRESERVE']
493         else:
494             try:
495                 self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
496             except KeyError:
497                 pass
498             try:
499                 self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
500             except KeyError:
501                 pass
502             try:
503                 self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
504             except KeyError:
505                 pass
506         self._stdout = []
507         self._stderr = []
508         self.status = None
509         self.condition = 'no_result'
510         self.workdir_set(workdir)
511         self.subdir(subdir)
512
513     def __del__(self):
514         self.cleanup()
515
516     def __repr__(self):
517         return "%x" % id(self)
518
519     if os.name == 'posix':
520
521         def escape(self, arg):
522             "escape shell special characters"
523             slash = '\\'
524             special = '"$'
525
526             arg = string.replace(arg, slash, slash+slash)
527             for c in special:
528                 arg = string.replace(arg, c, slash+c)
529
530             if re_space.search(arg):
531                 arg = '"' + arg + '"'
532             return arg
533
534     else:
535
536         # Windows does not allow special characters in file names
537         # anyway, so no need for an escape function, we will just quote
538         # the arg.
539         def escape(self, arg):
540             if re_space.search(arg):
541                 arg = '"' + arg + '"'
542             return arg
543
544     def canonicalize(self, path):
545         if is_List(path):
546             path = apply(os.path.join, tuple(path))
547         if not os.path.isabs(path):
548             path = os.path.join(self.workdir, path)
549         return path
550
551     def cleanup(self, condition = None):
552         """Removes any temporary working directories for the specified
553         TestCmd environment.  If the environment variable PRESERVE was
554         set when the TestCmd environment was created, temporary working
555         directories are not removed.  If any of the environment variables
556         PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
557         when the TestCmd environment was created, then temporary working
558         directories are not removed if the test passed, failed, or had
559         no result, respectively.  Temporary working directories are also
560         preserved for conditions specified via the preserve method.
561
562         Typically, this method is not called directly, but is used when
563         the script exits to clean up temporary working directories as
564         appropriate for the exit status.
565         """
566         if not self._dirlist:
567             return
568         os.chdir(self._cwd)
569         self.workdir = None
570         if condition is None:
571             condition = self.condition
572         if self._preserve[condition]:
573             for dir in self._dirlist:
574                 print "Preserved directory", dir
575         else:
576             list = self._dirlist[:]
577             list.reverse()
578             for dir in list:
579                 self.writable(dir, 1)
580                 shutil.rmtree(dir, ignore_errors = 1)
581             self._dirlist = []
582
583         try:
584             global _Cleanup
585             _Cleanup.remove(self)
586         except (AttributeError, ValueError):
587             pass
588
589     def chmod(self, path, mode):
590         """Changes permissions on the specified file or directory
591         path name."""
592         path = self.canonicalize(path)
593         os.chmod(path, mode)
594
595     def description_set(self, description):
596         """Set the description of the functionality being tested.
597         """
598         self.description = description
599
600 #    def diff(self):
601 #        """Diff two arrays.
602 #        """
603
604     def fail_test(self, condition = 1, function = None, skip = 0):
605         """Cause the test to fail.
606         """
607         if not condition:
608             return
609         self.condition = 'fail_test'
610         fail_test(self = self,
611                   condition = condition,
612                   function = function,
613                   skip = skip)
614
615     def interpreter_set(self, interpreter):
616         """Set the program to be used to interpret the program
617         under test as a script.
618         """
619         self.interpreter = interpreter
620
621     def match(self, lines, matches):
622         """Compare actual and expected file contents.
623         """
624         return self.match_func(lines, matches)
625
626     def match_exact(self, lines, matches):
627         """Compare actual and expected file contents.
628         """
629         return match_exact(lines, matches)
630
631     def match_re(self, lines, res):
632         """Compare actual and expected file contents.
633         """
634         return match_re(lines, res)
635
636     def match_re_dotall(self, lines, res):
637         """Compare actual and expected file contents.
638         """
639         return match_re_dotall(lines, res)
640
641     def no_result(self, condition = 1, function = None, skip = 0):
642         """Report that the test could not be run.
643         """
644         if not condition:
645             return
646         self.condition = 'no_result'
647         no_result(self = self,
648                   condition = condition,
649                   function = function,
650                   skip = skip)
651
652     def pass_test(self, condition = 1, function = None):
653         """Cause the test to pass.
654         """
655         if not condition:
656             return
657         self.condition = 'pass_test'
658         pass_test(self = self, condition = condition, function = function)
659
660     def preserve(self, *conditions):
661         """Arrange for the temporary working directories for the
662         specified TestCmd environment to be preserved for one or more
663         conditions.  If no conditions are specified, arranges for
664         the temporary working directories to be preserved for all
665         conditions.
666         """
667         if conditions is ():
668             conditions = ('pass_test', 'fail_test', 'no_result')
669         for cond in conditions:
670             self._preserve[cond] = 1
671
672     def program_set(self, program):
673         """Set the executable program or script to be tested.
674         """
675         if program and not os.path.isabs(program):
676             program = os.path.join(self._cwd, program)
677         self.program = program
678
679     def read(self, file, mode = 'rb'):
680         """Reads and returns the contents of the specified file name.
681         The file name may be a list, in which case the elements are
682         concatenated with the os.path.join() method.  The file is
683         assumed to be under the temporary working directory unless it
684         is an absolute path name.  The I/O mode for the file may
685         be specified; it must begin with an 'r'.  The default is
686         'rb' (binary read).
687         """
688         file = self.canonicalize(file)
689         if mode[0] != 'r':
690             raise ValueError, "mode must begin with 'r'"
691         return open(file, mode).read()
692
693     def run(self, program = None,
694                   interpreter = None,
695                   arguments = None,
696                   chdir = None,
697                   stdin = None,
698                   universal_newlines = None):
699         """Runs a test of the program or script for the test
700         environment.  Standard output and error output are saved for
701         future retrieval via the stdout() and stderr() methods.
702
703         The specified program will have the original directory
704         prepending unless it is enclosed in a [list].
705         """
706         if chdir:
707             oldcwd = os.getcwd()
708             if not os.path.isabs(chdir):
709                 chdir = os.path.join(self.workpath(chdir))
710             if self.verbose:
711                 sys.stderr.write("chdir(" + chdir + ")\n")
712             os.chdir(chdir)
713         if program:
714             if type(program) == type('') and not os.path.isabs(program):
715                 program = os.path.join(self._cwd, program)
716         else:
717             program = self.program
718             if not interpreter:
719                 interpreter = self.interpreter
720         if not type(program) in [type([]), type(())]:
721             program = [program]
722         cmd = list(program)
723         if interpreter:
724             if not type(interpreter) in [type([]), type(())]:
725                 interpreter = [interpreter]
726             cmd = list(interpreter) + cmd
727         if arguments:
728             if type(arguments) == type(''):
729                 arguments = string.split(arguments)
730             cmd.extend(arguments)
731         cmd_string = string.join(map(self.escape, cmd), ' ')
732         if self.verbose:
733             sys.stderr.write(cmd_string + "\n")
734         if universal_newlines is None:
735             universal_newlines = self.universal_newlines
736
737         try:
738             import subprocess
739         except ImportError:
740             try:
741                 Popen3 = popen2.Popen3
742             except AttributeError:
743                 class Popen3:
744                     def __init__(self, command):
745                         (stdin, stdout, stderr) = os.popen3(' ' + command)
746                         self.stdin = stdin
747                         self.stdout = stdout
748                         self.stderr = stderr
749                     def close_output(self):
750                         self.stdout.close()
751                         self.resultcode = self.stderr.close()
752                     def wait(self):
753                         return self.resultcode
754                 if sys.platform == 'win32' and cmd_string[0] == '"':
755                     cmd_string = '"' + cmd_string + '"'
756                 p = Popen3(cmd_string)
757             else:
758                 p = Popen3(cmd, 1)
759                 p.stdin = p.tochild
760                 p.stdout = p.fromchild
761                 p.stderr = p.childerr
762         else:
763             p = subprocess.Popen(cmd,
764                                  stdin=subprocess.PIPE,
765                                  stdout=subprocess.PIPE,
766                                  stderr=subprocess.PIPE,
767                                  universal_newlines=universal_newlines)
768
769         if stdin:
770             if is_List(stdin):
771                 for line in stdin:
772                     p.stdin.write(line)
773             else:
774                 p.stdin.write(stdin)
775         p.stdin.close()
776
777         out = p.stdout.read()
778         err = p.stderr.read()
779         try:
780             p.close_output()
781         except AttributeError:
782             p.stdout.close()
783             p.stderr.close()
784
785         self.status = p.wait()
786         if not self.status:
787             self.status = 0
788
789         if self.combine:
790             self._stdout.append(out + err)
791         else:
792             self._stdout.append(out)
793             self._stderr.append(err)
794
795         if chdir:
796             os.chdir(oldcwd)
797         if self.verbose >= 2:
798             write = sys.stdout.write
799             write('============ STATUS: %d\n' % self.status)
800             out = self.stdout()
801             if out or self.verbose >= 3:
802                 write('============ BEGIN STDOUT (len=%d):\n' % len(out))
803                 write(out)
804                 write('============ END STDOUT\n')
805             err = self.stderr()
806             if err or self.verbose >= 3:
807                 write('============ BEGIN STDERR (len=%d)\n' % len(err))
808                 write(err)
809                 write('============ END STDERR\n')
810
811     def sleep(self, seconds = default_sleep_seconds):
812         """Sleeps at least the specified number of seconds.  If no
813         number is specified, sleeps at least the minimum number of
814         seconds necessary to advance file time stamps on the current
815         system.  Sleeping more seconds is all right.
816         """
817         time.sleep(seconds)
818
819     def stderr(self, run = None):
820         """Returns the error output from the specified run number.
821         If there is no specified run number, then returns the error
822         output of the last run.  If the run number is less than zero,
823         then returns the error output from that many runs back from the
824         current run.
825         """
826         if not run:
827             run = len(self._stderr)
828         elif run < 0:
829             run = len(self._stderr) + run
830         run = run - 1
831         return self._stderr[run]
832
833     def stdout(self, run = None):
834         """Returns the standard output from the specified run number.
835         If there is no specified run number, then returns the standard
836         output of the last run.  If the run number is less than zero,
837         then returns the standard output from that many runs back from
838         the current run.
839         """
840         if not run:
841             run = len(self._stdout)
842         elif run < 0:
843             run = len(self._stdout) + run
844         run = run - 1
845         return self._stdout[run]
846
847     def subdir(self, *subdirs):
848         """Create new subdirectories under the temporary working
849         directory, one for each argument.  An argument may be a list,
850         in which case the list elements are concatenated using the
851         os.path.join() method.  Subdirectories multiple levels deep
852         must be created using a separate argument for each level:
853
854                 test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
855
856         Returns the number of subdirectories actually created.
857         """
858         count = 0
859         for sub in subdirs:
860             if sub is None:
861                 continue
862             if is_List(sub):
863                 sub = apply(os.path.join, tuple(sub))
864             new = os.path.join(self.workdir, sub)
865             try:
866                 os.mkdir(new)
867             except OSError:
868                 pass
869             else:
870                 count = count + 1
871         return count
872
873     def symlink(self, target, link):
874         """Creates a symlink to the specified target.
875         The link name may be a list, in which case the elements are
876         concatenated with the os.path.join() method.  The link is
877         assumed to be under the temporary working directory unless it
878         is an absolute path name. The target is *not* assumed to be
879         under the temporary working directory.
880         """
881         link = self.canonicalize(link)
882         os.symlink(target, link)
883
884     def tempdir(self, path=None):
885         """Creates a temporary directory.
886         A unique directory name is generated if no path name is specified.
887         The directory is created, and will be removed when the TestCmd
888         object is destroyed.
889         """
890         if path is None:
891             try:
892                 path = tempfile.mktemp(prefix=tempfile.template)
893             except TypeError:
894                 path = tempfile.mktemp()
895         os.mkdir(path)
896
897         # Symlinks in the path will report things
898         # differently from os.getcwd(), so chdir there
899         # and back to fetch the canonical path.
900         cwd = os.getcwd()
901         try:
902             os.chdir(path)
903             path = os.getcwd()
904         finally:
905             os.chdir(cwd)
906
907         # Uppercase the drive letter since the case of drive
908         # letters is pretty much random on win32:
909         drive,rest = os.path.splitdrive(path)
910         if drive:
911             path = string.upper(drive) + rest
912
913         #
914         self._dirlist.append(path)
915         global _Cleanup
916         try:
917             _Cleanup.index(self)
918         except ValueError:
919             _Cleanup.append(self)
920
921         return path
922
923     def touch(self, path, mtime=None):
924         """Updates the modification time on the specified file or
925         directory path name.  The default is to update to the
926         current time if no explicit modification time is specified.
927         """
928         path = self.canonicalize(path)
929         atime = os.path.getatime(path)
930         if mtime is None:
931             mtime = time.time()
932         os.utime(path, (atime, mtime))
933
934     def unlink(self, file):
935         """Unlinks the specified file name.
936         The file name may be a list, in which case the elements are
937         concatenated with the os.path.join() method.  The file is
938         assumed to be under the temporary working directory unless it
939         is an absolute path name.
940         """
941         file = self.canonicalize(file)
942         os.unlink(file)
943
944     def verbose_set(self, verbose):
945         """Set the verbose level.
946         """
947         self.verbose = verbose
948
949     def where_is(self, file, path=None, pathext=None):
950         """Find an executable file.
951         """
952         if is_List(file):
953             file = apply(os.path.join, tuple(file))
954         if not os.path.isabs(file):
955             file = where_is(file, path, pathext)
956         return file
957
958     def workdir_set(self, path):
959         """Creates a temporary working directory with the specified
960         path name.  If the path is a null string (''), a unique
961         directory name is created.
962         """
963         if (path != None):
964             if path == '':
965                 path = None
966             path = self.tempdir(path)
967         self.workdir = path
968
969     def workpath(self, *args):
970         """Returns the absolute path name to a subdirectory or file
971         within the current temporary working directory.  Concatenates
972         the temporary working directory name with the specified
973         arguments using the os.path.join() method.
974         """
975         return apply(os.path.join, (self.workdir,) + tuple(args))
976
977     def readable(self, top, read=1):
978         """Make the specified directory tree readable (read == 1)
979         or not (read == None).
980         """
981
982         if read:
983             def do_chmod(fname):
984                 try: st = os.stat(fname)
985                 except OSError: pass
986                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0400))
987         else:
988             def do_chmod(fname):
989                 try: st = os.stat(fname)
990                 except OSError: pass
991                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0400))
992
993         if os.path.isfile(top):
994             # If it's a file, that's easy, just chmod it.
995             do_chmod(top)
996         elif read:
997             # It's a directory and we're trying to turn on read
998             # permission, so it's also pretty easy, just chmod the
999             # directory and then chmod every entry on our walk down the
1000             # tree.  Because os.path.walk() is top-down, we'll enable
1001             # read permission on any directories that have it disabled
1002             # before os.path.walk() tries to list their contents.
1003             do_chmod(top)
1004
1005             def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
1006                 pathnames = map(lambda n, d=dirname: os.path.join(d, n),
1007                                 names)
1008                 map(lambda p, do=do_chmod: do(p), pathnames)
1009
1010             os.path.walk(top, chmod_entries, None)
1011         else:
1012             # It's a directory and we're trying to turn off read
1013             # permission, which means we have to chmod the directoreis
1014             # in the tree bottom-up, lest disabling read permission from
1015             # the top down get in the way of being able to get at lower
1016             # parts of the tree.  But os.path.walk() visits things top
1017             # down, so we just use an object to collect a list of all
1018             # of the entries in the tree, reverse the list, and then
1019             # chmod the reversed (bottom-up) list.
1020             col = Collector(top)
1021             os.path.walk(top, col, None)
1022             col.entries.reverse()
1023             map(lambda d, do=do_chmod: do(d), col.entries)
1024
1025     def writable(self, top, write=1):
1026         """Make the specified directory tree writable (write == 1)
1027         or not (write == None).
1028         """
1029
1030         if write:
1031             def do_chmod(fname):
1032                 try: st = os.stat(fname)
1033                 except OSError: pass
1034                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0200))
1035         else:
1036             def do_chmod(fname):
1037                 try: st = os.stat(fname)
1038                 except OSError: pass
1039                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0200))
1040
1041         if os.path.isfile(top):
1042             do_chmod(top)
1043         else:
1044             col = Collector(top)
1045             os.path.walk(top, col, None)
1046             map(lambda d, do=do_chmod: do(d), col.entries)
1047
1048     def executable(self, top, execute=1):
1049         """Make the specified directory tree executable (execute == 1)
1050         or not (execute == None).
1051         """
1052
1053         if execute:
1054             def do_chmod(fname):
1055                 try: st = os.stat(fname)
1056                 except OSError: pass
1057                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0100))
1058         else:
1059             def do_chmod(fname):
1060                 try: st = os.stat(fname)
1061                 except OSError: pass
1062                 else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0100))
1063
1064         if os.path.isfile(top):
1065             # If it's a file, that's easy, just chmod it.
1066             do_chmod(top)
1067         elif execute:
1068             # It's a directory and we're trying to turn on execute
1069             # permission, so it's also pretty easy, just chmod the
1070             # directory and then chmod every entry on our walk down the
1071             # tree.  Because os.path.walk() is top-down, we'll enable
1072             # execute permission on any directories that have it disabled
1073             # before os.path.walk() tries to list their contents.
1074             do_chmod(top)
1075
1076             def chmod_entries(arg, dirname, names, do_chmod=do_chmod):
1077                 pathnames = map(lambda n, d=dirname: os.path.join(d, n),
1078                                 names)
1079                 map(lambda p, do=do_chmod: do(p), pathnames)
1080
1081             os.path.walk(top, chmod_entries, None)
1082         else:
1083             # It's a directory and we're trying to turn off execute
1084             # permission, which means we have to chmod the directories
1085             # in the tree bottom-up, lest disabling execute permission from
1086             # the top down get in the way of being able to get at lower
1087             # parts of the tree.  But os.path.walk() visits things top
1088             # down, so we just use an object to collect a list of all
1089             # of the entries in the tree, reverse the list, and then
1090             # chmod the reversed (bottom-up) list.
1091             col = Collector(top)
1092             os.path.walk(top, col, None)
1093             col.entries.reverse()
1094             map(lambda d, do=do_chmod: do(d), col.entries)
1095
1096     def write(self, file, content, mode = 'wb'):
1097         """Writes the specified content text (second argument) to the
1098         specified file name (first argument).  The file name may be
1099         a list, in which case the elements are concatenated with the
1100         os.path.join() method.  The file is created under the temporary
1101         working directory.  Any subdirectories in the path must already
1102         exist.  The I/O mode for the file may be specified; it must
1103         begin with a 'w'.  The default is 'wb' (binary write).
1104         """
1105         file = self.canonicalize(file)
1106         if mode[0] != 'w':
1107             raise ValueError, "mode must begin with 'w'"
1108         open(file, mode).write(content)