e12aa4c53a3d5ae08785402b6211c0497ba85d64
[scons.git] / etc / 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
42     test.workdir_set('prefix')
43     test.workdir_set('')
44
45     test.workpath('file')
46     test.workpath('subdir', 'file')
47
48     test.subdir('subdir', ...)
49
50     test.write('file', "contents\n")
51     test.write(['subdir', 'file'], "contents\n")
52
53     test.read('file')
54     test.read(['subdir', 'file'])
55     test.read('file', mode)
56     test.read(['subdir', 'file'], mode)
57
58     test.writable('dir', 1)
59     test.writable('dir', None)
60
61     test.preserve(condition, ...)
62
63     test.cleanup(condition)
64
65     test.run(program = 'program_or_script_to_run',
66              interpreter = 'script_interpreter',
67              arguments = 'arguments to pass to program',
68              chdir = 'directory_to_chdir_to',
69              stdin = 'input to feed to the program\n')
70
71     test.pass_test()
72     test.pass_test(condition)
73     test.pass_test(condition, function)
74
75     test.fail_test()
76     test.fail_test(condition)
77     test.fail_test(condition, function)
78     test.fail_test(condition, function, skip)
79
80     test.no_result()
81     test.no_result(condition)
82     test.no_result(condition, function)
83     test.no_result(condition, function, skip)
84
85     test.stdout()
86     test.stdout(run)
87
88     test.stderr()
89     test.stderr(run)
90
91     test.symlink(target, link)
92
93     test.match(actual, expected)
94
95     test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n")
96     test.match_exact(["actual 1\n", "actual 2\n"],
97                      ["expected 1\n", "expected 2\n"])
98
99     test.match_re("actual 1\nactual 2\n", regex_string)
100     test.match_re(["actual 1\n", "actual 2\n"], list_of_regexes)
101
102     test.match_re_dotall("actual 1\nactual 2\n", regex_string)
103     test.match_re_dotall(["actual 1\n", "actual 2\n"], list_of_regexes)
104
105     test.sleep()
106     test.sleep(seconds)
107
108     test.where_is('foo')
109     test.where_is('foo', 'PATH1:PATH2')
110     test.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
111
112     test.unlink('file')
113     test.unlink('subdir', 'file')
114
115 The TestCmd module provides pass_test(), fail_test(), and no_result()
116 unbound functions that report test results for use with the Aegis change
117 management system.  These methods terminate the test immediately,
118 reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
119 status 0 (success), 1 or 2 respectively.  This allows for a distinction
120 between an actual failed test and a test that could not be properly
121 evaluated because of an external condition (such as a full file system
122 or incorrect permissions).
123
124     import TestCmd
125
126     TestCmd.pass_test()
127     TestCmd.pass_test(condition)
128     TestCmd.pass_test(condition, function)
129
130     TestCmd.fail_test()
131     TestCmd.fail_test(condition)
132     TestCmd.fail_test(condition, function)
133     TestCmd.fail_test(condition, function, skip)
134
135     TestCmd.no_result()
136     TestCmd.no_result(condition)
137     TestCmd.no_result(condition, function)
138     TestCmd.no_result(condition, function, skip)
139
140 The TestCmd module also provides unbound functions that handle matching
141 in the same way as the match_*() methods described above.
142
143     import TestCmd
144
145     test = TestCmd.TestCmd(match = TestCmd.match_exact)
146
147     test = TestCmd.TestCmd(match = TestCmd.match_re)
148
149     test = TestCmd.TestCmd(match = TestCmd.match_re_dotall)
150
151 Lastly, the where_is() method also exists in an unbound function
152 version.
153
154     import TestCmd
155
156     TestCmd.where_is('foo')
157     TestCmd.where_is('foo', 'PATH1:PATH2')
158     TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4')
159 """
160
161 # Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
162 # This module is free software, and you may redistribute it and/or modify
163 # it under the same terms as Python itself, so long as this copyright message
164 # and disclaimer are retained in their original form.
165 #
166 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
167 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
168 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
169 # DAMAGE.
170 #
171 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
172 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
173 # PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
174 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
175 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
176
177 __author__ = "Steven Knight <knight at baldmt dot com>"
178 __revision__ = "TestCmd.py 0.13.D002 2004/11/20 08:34:16 knight"
179 __version__ = "0.13"
180
181 import os
182 import os.path
183 import popen2
184 import re
185 import shutil
186 import stat
187 import string
188 import sys
189 import tempfile
190 import time
191 import traceback
192 import types
193 import UserList
194
195 __all__ = [ 'fail_test', 'no_result', 'pass_test',
196             'match_exact', 'match_re', 'match_re_dotall',
197             'python_executable', 'TestCmd' ]
198
199 def is_List(e):
200     return type(e) is types.ListType \
201         or isinstance(e, UserList.UserList)
202
203 try:
204     from UserString import UserString
205 except ImportError:
206     class UserString:
207         pass
208
209 if hasattr(types, 'UnicodeType'):
210     def is_String(e):
211         return type(e) is types.StringType \
212             or type(e) is types.UnicodeType \
213             or isinstance(e, UserString)
214 else:
215     def is_String(e):
216         return type(e) is types.StringType or isinstance(e, UserString)
217
218 tempfile.template = 'testcmd.'
219
220 if os.name == 'posix':
221
222     def escape_cmd(arg):
223         "escape shell special characters"
224         slash = '\\'
225         special = '"$'
226
227         arg = string.replace(arg, slash, slash+slash)
228         for c in special:
229             arg = string.replace(arg, c, slash+c)
230
231         return '"' + arg + '"'
232
233 else:
234
235     # Windows does not allow special characters in file names
236     # anyway, so no need for an escape function, we will just quote
237     # the arg.
238     escape_cmd = lambda x: '"' + x + '"'
239
240 _Cleanup = []
241
242 def _clean():
243     global _Cleanup
244     list = _Cleanup[:]
245     _Cleanup = []
246     list.reverse()
247     for test in list:
248         test.cleanup()
249
250 sys.exitfunc = _clean
251
252 def _caller(tblist, skip):
253     string = ""
254     arr = []
255     for file, line, name, text in tblist:
256         if file[-10:] == "TestCmd.py":
257                 break
258         arr = [(file, line, name, text)] + arr
259     atfrom = "at"
260     for file, line, name, text in arr[skip:]:
261         if name == "?":
262             name = ""
263         else:
264             name = " (" + name + ")"
265         string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
266         atfrom = "\tfrom"
267     return string
268
269 def fail_test(self = None, condition = 1, function = None, skip = 0):
270     """Cause the test to fail.
271
272     By default, the fail_test() method reports that the test FAILED
273     and exits with a status of 1.  If a condition argument is supplied,
274     the test fails only if the condition is true.
275     """
276     if not condition:
277         return
278     if not function is None:
279         function()
280     of = ""
281     desc = ""
282     sep = " "
283     if not self is None:
284         if self.program:
285             of = " of " + self.program
286             sep = "\n\t"
287         if self.description:
288             desc = " [" + self.description + "]"
289             sep = "\n\t"
290
291     at = _caller(traceback.extract_stack(), skip)
292     sys.stderr.write("FAILED test" + of + desc + sep + at)
293
294     sys.exit(1)
295
296 def no_result(self = None, condition = 1, function = None, skip = 0):
297     """Causes a test to exit with no valid result.
298
299     By default, the no_result() method reports NO RESULT for the test
300     and exits with a status of 2.  If a condition argument is supplied,
301     the test fails only if the condition is true.
302     """
303     if not condition:
304         return
305     if not function is None:
306         function()
307     of = ""
308     desc = ""
309     sep = " "
310     if not self is None:
311         if self.program:
312             of = " of " + self.program
313             sep = "\n\t"
314         if self.description:
315             desc = " [" + self.description + "]"
316             sep = "\n\t"
317
318     at = _caller(traceback.extract_stack(), skip)
319     sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
320
321     sys.exit(2)
322
323 def pass_test(self = None, condition = 1, function = None):
324     """Causes a test to pass.
325
326     By default, the pass_test() method reports PASSED for the test
327     and exits with a status of 0.  If a condition argument is supplied,
328     the test passes only if the condition is true.
329     """
330     if not condition:
331         return
332     if not function is None:
333         function()
334     sys.stderr.write("PASSED\n")
335     sys.exit(0)
336
337 def match_exact(lines = None, matches = None):
338     """
339     """
340     if not is_List(lines):
341         lines = string.split(lines, "\n")
342     if not is_List(matches):
343         matches = string.split(matches, "\n")
344     if len(lines) != len(matches):
345         return
346     for i in range(len(lines)):
347         if lines[i] != matches[i]:
348             return
349     return 1
350
351 def match_re(lines = None, res = None):
352     """
353     """
354     if not is_List(lines):
355         lines = string.split(lines, "\n")
356     if not is_List(res):
357         res = string.split(res, "\n")
358     if len(lines) != len(res):
359         return
360     for i in range(len(lines)):
361         if not re.compile("^" + res[i] + "$").search(lines[i]):
362             return
363     return 1
364
365 def match_re_dotall(lines = None, res = None):
366     """
367     """
368     if not type(lines) is type(""):
369         lines = string.join(lines, "\n")
370     if not type(res) is type(""):
371         res = string.join(res, "\n")
372     if re.compile("^" + res + "$", re.DOTALL).match(lines):
373         return 1
374
375 if os.name == 'java':
376
377     python_executable = os.path.join(sys.prefix, 'jython')
378
379 else:
380
381     python_executable = sys.executable
382
383 if sys.platform == 'win32':
384
385     default_sleep_seconds = 2
386
387     def where_is(file, path=None, pathext=None):
388         if path is None:
389             path = os.environ['PATH']
390         if is_String(path):
391             path = string.split(path, os.pathsep)
392         if pathext is None:
393             pathext = os.environ['PATHEXT']
394         if is_String(pathext):
395             pathext = string.split(pathext, os.pathsep)
396         for ext in pathext:
397             if string.lower(ext) == string.lower(file[-len(ext):]):
398                 pathext = ['']
399                 break
400         for dir in path:
401             f = os.path.join(dir, file)
402             for ext in pathext:
403                 fext = f + ext
404                 if os.path.isfile(fext):
405                     return fext
406         return None
407
408 else:
409
410     def where_is(file, path=None, pathext=None):
411         if path is None:
412             path = os.environ['PATH']
413         if is_String(path):
414             path = string.split(path, os.pathsep)
415         for dir in path:
416             f = os.path.join(dir, file)
417             if os.path.isfile(f):
418                 try:
419                     st = os.stat(f)
420                 except:
421                     continue
422                 if stat.S_IMODE(st[stat.ST_MODE]) & 0111:
423                     return f
424         return None
425
426     default_sleep_seconds = 1
427
428 class TestCmd:
429     """Class TestCmd
430     """
431
432     def __init__(self, description = None,
433                        program = None,
434                        interpreter = None,
435                        workdir = None,
436                        subdir = None,
437                        verbose = 0,
438                        match = None,
439                        combine = 0):
440         self._cwd = os.getcwd()
441         self.description_set(description)
442         self.program_set(program)
443         self.interpreter_set(interpreter)
444         self.verbose_set(verbose)
445         self.combine = combine
446         if not match is None:
447             self.match_func = match
448         else:
449             self.match_func = match_re
450         self._dirlist = []
451         self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
452         if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
453             self._preserve['pass_test'] = os.environ['PRESERVE']
454             self._preserve['fail_test'] = os.environ['PRESERVE']
455             self._preserve['no_result'] = os.environ['PRESERVE']
456         else:
457             try:
458                 self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
459             except KeyError:
460                 pass
461             try:
462                 self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
463             except KeyError:
464                 pass
465             try:
466                 self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
467             except KeyError:
468                 pass
469         self._stdout = []
470         self._stderr = []
471         self.status = None
472         self.condition = 'no_result'
473         self.workdir_set(workdir)
474         self.subdir(subdir)
475
476     def __del__(self):
477         self.cleanup()
478
479     def __repr__(self):
480         return "%x" % id(self)
481
482     def cleanup(self, condition = None):
483         """Removes any temporary working directories for the specified
484         TestCmd environment.  If the environment variable PRESERVE was
485         set when the TestCmd environment was created, temporary working
486         directories are not removed.  If any of the environment variables
487         PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
488         when the TestCmd environment was created, then temporary working
489         directories are not removed if the test passed, failed, or had
490         no result, respectively.  Temporary working directories are also
491         preserved for conditions specified via the preserve method.
492
493         Typically, this method is not called directly, but is used when
494         the script exits to clean up temporary working directories as
495         appropriate for the exit status.
496         """
497         if not self._dirlist:
498             return
499         os.chdir(self._cwd)
500         self.workdir = None
501         if condition is None:
502             condition = self.condition
503         if self._preserve[condition]:
504             for dir in self._dirlist:
505                 print "Preserved directory", dir
506         else:
507             list = self._dirlist[:]
508             list.reverse()
509             for dir in list:
510                 self.writable(dir, 1)
511                 shutil.rmtree(dir, ignore_errors = 1)
512             self._dirlist = []
513
514         try:
515             global _Cleanup
516             _Cleanup.remove(self)
517         except (AttributeError, ValueError):
518             pass
519
520     def description_set(self, description):
521         """Set the description of the functionality being tested.
522         """
523         self.description = description
524
525 #    def diff(self):
526 #        """Diff two arrays.
527 #        """
528
529     def fail_test(self, condition = 1, function = None, skip = 0):
530         """Cause the test to fail.
531         """
532         if not condition:
533             return
534         self.condition = 'fail_test'
535         fail_test(self = self,
536                   condition = condition,
537                   function = function,
538                   skip = skip)
539
540     def interpreter_set(self, interpreter):
541         """Set the program to be used to interpret the program
542         under test as a script.
543         """
544         self.interpreter = interpreter
545
546     def match(self, lines, matches):
547         """Compare actual and expected file contents.
548         """
549         return self.match_func(lines, matches)
550
551     def match_exact(self, lines, matches):
552         """Compare actual and expected file contents.
553         """
554         return match_exact(lines, matches)
555
556     def match_re(self, lines, res):
557         """Compare actual and expected file contents.
558         """
559         return match_re(lines, res)
560
561     def match_re_dotall(self, lines, res):
562         """Compare actual and expected file contents.
563         """
564         return match_re_dotall(lines, res)
565
566     def no_result(self, condition = 1, function = None, skip = 0):
567         """Report that the test could not be run.
568         """
569         if not condition:
570             return
571         self.condition = 'no_result'
572         no_result(self = self,
573                   condition = condition,
574                   function = function,
575                   skip = skip)
576
577     def pass_test(self, condition = 1, function = None):
578         """Cause the test to pass.
579         """
580         if not condition:
581             return
582         self.condition = 'pass_test'
583         pass_test(self = self, condition = condition, function = function)
584
585     def preserve(self, *conditions):
586         """Arrange for the temporary working directories for the
587         specified TestCmd environment to be preserved for one or more
588         conditions.  If no conditions are specified, arranges for
589         the temporary working directories to be preserved for all
590         conditions.
591         """
592         if conditions is ():
593             conditions = ('pass_test', 'fail_test', 'no_result')
594         for cond in conditions:
595             self._preserve[cond] = 1
596
597     def program_set(self, program):
598         """Set the executable program or script to be tested.
599         """
600         if program and not os.path.isabs(program):
601             program = os.path.join(self._cwd, program)
602         self.program = program
603
604     def read(self, file, mode = 'rb'):
605         """Reads and returns the contents of the specified file name.
606         The file name may be a list, in which case the elements are
607         concatenated with the os.path.join() method.  The file is
608         assumed to be under the temporary working directory unless it
609         is an absolute path name.  The I/O mode for the file may
610         be specified; it must begin with an 'r'.  The default is
611         'rb' (binary read).
612         """
613         if is_List(file):
614             file = apply(os.path.join, tuple(file))
615         if not os.path.isabs(file):
616             file = os.path.join(self.workdir, file)
617         if mode[0] != 'r':
618             raise ValueError, "mode must begin with 'r'"
619         return open(file, mode).read()
620
621     def run(self, program = None,
622                   interpreter = None,
623                   arguments = None,
624                   chdir = None,
625                   stdin = None):
626         """Runs a test of the program or script for the test
627         environment.  Standard output and error output are saved for
628         future retrieval via the stdout() and stderr() methods.
629         """
630         if chdir:
631             oldcwd = os.getcwd()
632             if not os.path.isabs(chdir):
633                 chdir = os.path.join(self.workpath(chdir))
634             if self.verbose:
635                 sys.stderr.write("chdir(" + chdir + ")\n")
636             os.chdir(chdir)
637         cmd = None
638         if program:
639             if not os.path.isabs(program):
640                 program = os.path.join(self._cwd, program)
641             cmd = escape_cmd(program)
642             if interpreter:
643                 cmd = interpreter + " " + cmd
644         else:
645             cmd = escape_cmd(self.program)
646             if self.interpreter:
647                 cmd =  self.interpreter + " " + cmd
648         if arguments:
649             cmd = cmd + " " + arguments
650         if self.verbose:
651             sys.stderr.write(cmd + "\n")
652         try:
653             p = popen2.Popen3(cmd, 1)
654         except AttributeError:
655             (tochild, fromchild, childerr) = os.popen3(cmd)
656             if stdin:
657                 if is_List(stdin):
658                     for line in stdin:
659                         tochild.write(line)
660                 else:
661                     tochild.write(stdin)
662             tochild.close()
663             out = fromchild.read()
664             err = childerr.read()
665             if self.combine:
666                 self._stdout.append(out + err)
667             else:
668                 self._stdout.append(out)
669                 self._stderr.append(err)
670             fromchild.close()
671             self.status = childerr.close()
672             if not self.status:
673                 self.status = 0
674         except:
675             raise
676         else:
677             if stdin:
678                 if is_List(stdin):
679                     for line in stdin:
680                         p.tochild.write(line)
681                 else:
682                     p.tochild.write(stdin)
683             p.tochild.close()
684             out = p.fromchild.read()
685             err = p.childerr.read()
686             if self.combine:
687                 self._stdout.append(out + err)
688             else:
689                 self._stdout.append(out)
690                 self._stderr.append(err)
691             self.status = p.wait()
692         if chdir:
693             os.chdir(oldcwd)
694
695     def sleep(self, seconds = default_sleep_seconds):
696         """Sleeps at least the specified number of seconds.  If no
697         number is specified, sleeps at least the minimum number of
698         seconds necessary to advance file time stamps on the current
699         system.  Sleeping more seconds is all right.
700         """
701         time.sleep(seconds)
702
703     def stderr(self, run = None):
704         """Returns the error output from the specified run number.
705         If there is no specified run number, then returns the error
706         output of the last run.  If the run number is less than zero,
707         then returns the error output from that many runs back from the
708         current run.
709         """
710         if not run:
711             run = len(self._stderr)
712         elif run < 0:
713             run = len(self._stderr) + run
714         run = run - 1
715         return self._stderr[run]
716
717     def stdout(self, run = None):
718         """Returns the standard output from the specified run number.
719         If there is no specified run number, then returns the standard
720         output of the last run.  If the run number is less than zero,
721         then returns the standard output from that many runs back from
722         the current run.
723         """
724         if not run:
725             run = len(self._stdout)
726         elif run < 0:
727             run = len(self._stdout) + run
728         run = run - 1
729         return self._stdout[run]
730
731     def subdir(self, *subdirs):
732         """Create new subdirectories under the temporary working
733         directory, one for each argument.  An argument may be a list,
734         in which case the list elements are concatenated using the
735         os.path.join() method.  Subdirectories multiple levels deep
736         must be created using a separate argument for each level:
737
738                 test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
739
740         Returns the number of subdirectories actually created.
741         """
742         count = 0
743         for sub in subdirs:
744             if sub is None:
745                 continue
746             if is_List(sub):
747                 sub = apply(os.path.join, tuple(sub))
748             new = os.path.join(self.workdir, sub)
749             try:
750                 os.mkdir(new)
751             except:
752                 pass
753             else:
754                 count = count + 1
755         return count
756
757     def symlink(self, target, link):
758         """Creates a symlink to the specified target.
759         The link name may be a list, in which case the elements are
760         concatenated with the os.path.join() method.  The link is
761         assumed to be under the temporary working directory unless it
762         is an absolute path name. The target is *not* assumed to be
763         under the temporary working directory.
764         """
765         if is_List(link):
766             link = apply(os.path.join, tuple(link))
767         if not os.path.isabs(link):
768             link = os.path.join(self.workdir, link)
769         os.symlink(target, link)
770
771     def unlink(self, file):
772         """Unlinks the specified file name.
773         The file name may be a list, in which case the elements are
774         concatenated with the os.path.join() method.  The file is
775         assumed to be under the temporary working directory unless it
776         is an absolute path name.
777         """
778         if is_List(file):
779             file = apply(os.path.join, tuple(file))
780         if not os.path.isabs(file):
781             file = os.path.join(self.workdir, file)
782         os.unlink(file)
783
784     def verbose_set(self, verbose):
785         """Set the verbose level.
786         """
787         self.verbose = verbose
788
789     def where_is(self, file, path=None, pathext=None):
790         """Find an executable file.
791         """
792         if is_List(file):
793             file = apply(os.path.join, tuple(file))
794         if not os.path.isabs(file):
795             file = where_is(file, path, pathext)
796         return file
797
798     def workdir_set(self, path):
799         """Creates a temporary working directory with the specified
800         path name.  If the path is a null string (''), a unique
801         directory name is created.
802         """
803         if (path != None):
804             if path == '':
805                 path = tempfile.mktemp()
806             if path != None:
807                 os.mkdir(path)
808             # We'd like to set self.workdir like this:
809             #     self.workdir = path
810             # But symlinks in the path will report things
811             # differently from os.getcwd(), so chdir there
812             # and back to fetch the canonical path.
813             cwd = os.getcwd()
814             os.chdir(path)
815             self.workdir = os.getcwd()
816             os.chdir(cwd)
817             # Uppercase the drive letter since the case of drive
818             # letters is pretty much random on win32:
819             drive,rest = os.path.splitdrive(self.workdir)
820             if drive:
821                 self.workdir = string.upper(drive) + rest
822             #
823             self._dirlist.append(self.workdir)
824             global _Cleanup
825             try:
826                 _Cleanup.index(self)
827             except ValueError:
828                 _Cleanup.append(self)
829         else:
830             self.workdir = None
831
832     def workpath(self, *args):
833         """Returns the absolute path name to a subdirectory or file
834         within the current temporary working directory.  Concatenates
835         the temporary working directory name with the specified
836         arguments using the os.path.join() method.
837         """
838         return apply(os.path.join, (self.workdir,) + tuple(args))
839
840     def writable(self, top, write):
841         """Make the specified directory tree writable (write == 1)
842         or not (write == None).
843         """
844
845         def _walk_chmod(arg, dirname, names):
846             st = os.stat(dirname)
847             os.chmod(dirname, arg(st[stat.ST_MODE]))
848             for name in names:
849                 n = os.path.join(dirname, name)
850                 st = os.stat(n)
851                 os.chmod(n, arg(st[stat.ST_MODE]))
852
853         def _mode_writable(mode):
854             return stat.S_IMODE(mode|0200)
855
856         def _mode_non_writable(mode):
857             return stat.S_IMODE(mode&~0200)
858
859         if write:
860             f = _mode_writable
861         else:
862             f = _mode_non_writable
863         if os.path.isfile(top):
864             st = os.stat(top)
865             os.chmod(top, f(st[stat.ST_MODE]))
866         else:
867             try:
868                 os.path.walk(top, _walk_chmod, f)
869             except:
870                 pass # ignore any problems changing modes
871
872     def write(self, file, content, mode = 'wb'):
873         """Writes the specified content text (second argument) to the
874         specified file name (first argument).  The file name may be
875         a list, in which case the elements are concatenated with the
876         os.path.join() method.  The file is created under the temporary
877         working directory.  Any subdirectories in the path must already
878         exist.  The I/O mode for the file may be specified; it must
879         begin with a 'w'.  The default is 'wb' (binary write).
880         """
881         if is_List(file):
882             file = apply(os.path.join, tuple(file))
883         if not os.path.isabs(file):
884             file = os.path.join(self.workdir, file)
885         if mode[0] != 'w':
886             raise ValueError, "mode must begin with 'w'"
887         open(file, mode).write(content)