Imported Upstream version 1.51.0
[platform/upstream/boost.git] / tools / build / v2 / test / 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 of
5 executable commands and scripts (in any language, not just Python), especially
6 commands and scripts that require file system interaction.
7
8 In addition to running tests and evaluating conditions, the TestCmd module
9 manages and cleans up one or more temporary workspace directories, and provides
10 methods for creating files and directories in those workspace directories from
11 in-line data, here-documents), allowing tests to be completely self-contained.
12
13 A TestCmd environment object is created via the usual invocation:
14
15     test = TestCmd()
16
17 The TestCmd module provides pass_test(), fail_test(), and no_result() unbound
18 methods that report test results for use with the Aegis change management
19 system. These methods terminate the test immediately, reporting PASSED, FAILED
20 or NO RESULT respectively and exiting with status 0 (success), 1 or 2
21 respectively. This allows for a distinction between an actual failed test and a
22 test that could not be properly evaluated because of an external condition (such
23 as a full file system or incorrect permissions).
24 """
25
26 # Copyright 2000 Steven Knight
27 # This module is free software, and you may redistribute it and/or modify
28 # it under the same terms as Python itself, so long as this copyright message
29 # and disclaimer are retained in their original form.
30 #
31 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
32 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
33 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
34 # DAMAGE.
35 #
36 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
37 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
38 # PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
39 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
40 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
41
42 # Copyright 2002-2003 Vladimir Prus.
43 # Copyright 2002-2003 Dave Abrahams.
44 # Copyright 2006 Rene Rivera.
45 # Distributed under the Boost Software License, Version 1.0.
46 #    (See accompanying file LICENSE_1_0.txt or copy at
47 #         http://www.boost.org/LICENSE_1_0.txt)
48
49
50 from string import join, split
51
52 __author__ = "Steven Knight <knight@baldmt.com>"
53 __revision__ = "TestCmd.py 0.D002 2001/08/31 14:56:12 software"
54 __version__ = "0.02"
55
56 from types import *
57
58 import os
59 import os.path
60 import popen2
61 import re
62 import shutil
63 import stat
64 import sys
65 import tempfile
66 import traceback
67
68
69 tempfile.template = 'testcmd.'
70
71 _Cleanup = []
72
73 def _clean():
74     global _Cleanup
75     list = _Cleanup[:]
76     _Cleanup = []
77     list.reverse()
78     for test in list:
79         test.cleanup()
80
81 sys.exitfunc = _clean
82
83
84 def caller(tblist, skip):
85     string = ""
86     arr = []
87     for file, line, name, text in tblist:
88         if file[-10:] == "TestCmd.py":
89                 break
90         arr = [(file, line, name, text)] + arr
91     atfrom = "at"
92     for file, line, name, text in arr[skip:]:
93         if name == "?":
94             name = ""
95         else:
96             name = " (" + name + ")"
97         string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
98         atfrom = "\tfrom"
99     return string
100
101
102 def fail_test(self=None, condition=True, function=None, skip=0):
103     """Cause the test to fail.
104
105     By default, the fail_test() method reports that the test FAILED and exits
106     with a status of 1. If a condition argument is supplied, the test fails only
107     if the condition is true.
108     """
109     if not condition:
110         return
111     if not function is None:
112         function()
113     of = ""
114     desc = ""
115     sep = " "
116     if not self is None:
117         if self.program:
118             of = " of " + join(self.program, " ")
119             sep = "\n\t"
120         if self.description:
121             desc = " [" + self.description + "]"
122             sep = "\n\t"
123
124     at = caller(traceback.extract_stack(), skip)
125
126     sys.stderr.write("FAILED test" + of + desc + sep + at + """
127 in directory: """ + os.getcwd() )
128     sys.exit(1)
129
130
131 def no_result(self=None, condition=True, function=None, skip=0):
132     """Causes a test to exit with no valid result.
133
134     By default, the no_result() method reports NO RESULT for the test and exits
135     with a status of 2. If a condition argument is supplied, the test fails only
136     if the condition is true.
137     """
138     if not condition:
139         return
140     if not function is None:
141         function()
142     of = ""
143     desc = ""
144     sep = " "
145     if not self is None:
146         if self.program:
147             of = " of " + self.program
148             sep = "\n\t"
149         if self.description:
150             desc = " [" + self.description + "]"
151             sep = "\n\t"
152
153     at = caller(traceback.extract_stack(), skip)
154     sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
155     sys.exit(2)
156
157
158 def pass_test(self=None, condition=True, function=None):
159     """Causes a test to pass.
160
161     By default, the pass_test() method reports PASSED for the test and exits
162     with a status of 0. If a condition argument is supplied, the test passes
163     only if the condition is true.
164     """
165     if not condition:
166         return
167     if not function is None:
168         function()
169     sys.stderr.write("PASSED\n")
170     sys.exit(0)
171
172
173 def match_exact(lines=None, matches=None):
174     """Returns whether the given lists or strings containing lines separated
175     using newline characters contain exactly the same data.
176     """
177     if not type(lines) is ListType:
178         lines = split(lines, "\n")
179     if not type(matches) is ListType:
180         matches = split(matches, "\n")
181     if len(lines) != len(matches):
182         return
183     for i in range(len(lines)):
184         if lines[i] != matches[i]:
185             return
186     return 1
187
188
189 def match_re(lines=None, res=None):
190     """Given lists or strings contain lines separated using newline characters.
191     This function matches those lines one by one, interpreting the lines in the
192     res parameter as regular expressions.
193     """
194     if not type(lines) is ListType:
195         lines = split(lines, "\n")
196     if not type(res) is ListType:
197         res = split(res, "\n")
198     if len(lines) != len(res):
199         return
200     for i in range(len(lines)):
201         if not re.compile("^" + res[i] + "$").search(lines[i]):
202             return
203     return 1
204
205
206 class TestCmd:
207     """Class TestCmd.
208     """
209
210     def __init__(self, description=None, program=None, workdir=None,
211         subdir=None, verbose=False, match=None, inpath=None):
212
213         self._cwd = os.getcwd()
214         self.description_set(description)
215         if inpath:
216             self.program = program
217         else:
218             self.program_set(program)
219         self.verbose_set(verbose)
220         if not match is None:
221             self.match_func = match
222         else:
223             self.match_func = match_re
224         self._dirlist = []
225         self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
226         if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
227             self._preserve['pass_test'] = os.environ['PRESERVE']
228             self._preserve['fail_test'] = os.environ['PRESERVE']
229             self._preserve['no_result'] = os.environ['PRESERVE']
230         else:
231             try:
232                 self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
233             except KeyError:
234                 pass
235             try:
236                 self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
237             except KeyError:
238                 pass
239             try:
240                 self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
241             except KeyError:
242                 pass
243         self._stdout = []
244         self._stderr = []
245         self.status = None
246         self.condition = 'no_result'
247         self.workdir_set(workdir)
248         self.subdir(subdir)
249
250     def __del__(self):
251         self.cleanup()
252
253     def __repr__(self):
254         return "%x" % id(self)
255
256     def cleanup(self, condition=None):
257         """Removes any temporary working directories for the specified TestCmd
258         environment. If the environment variable PRESERVE was set when the
259         TestCmd environment was created, temporary working directories are not
260         removed. If any of the environment variables PRESERVE_PASS,
261         PRESERVE_FAIL or PRESERVE_NO_RESULT were set when the TestCmd
262         environment was created, then temporary working directories are not
263         removed if the test passed, failed or had no result, respectively.
264         Temporary working directories are also preserved for conditions
265         specified via the preserve method.
266
267         Typically, this method is not called directly, but is used when the
268         script exits to clean up temporary working directories as appropriate
269         for the exit status.
270         """
271         if not self._dirlist:
272             return
273         if condition is None:
274             condition = self.condition
275         if self._preserve[condition]:
276             for dir in self._dirlist:
277                 print "Preserved directory", dir
278         else:
279             list = self._dirlist[:]
280             list.reverse()
281             for dir in list:
282                 self.writable(dir, 1)
283                 shutil.rmtree(dir, ignore_errors = 1)
284
285         self._dirlist = []
286         self.workdir = None
287         os.chdir(self._cwd)
288         try:
289             global _Cleanup
290             _Cleanup.remove(self)
291         except (AttributeError, ValueError):
292             pass
293
294     def description_set(self, description):
295         """Set the description of the functionality being tested.
296         """
297         self.description = description
298
299     def fail_test(self, condition=True, function=None, skip=0):
300         """Cause the test to fail.
301         """
302         if not condition:
303             return
304         self.condition = 'fail_test'
305         fail_test(self = self,
306                   condition = condition,
307                   function = function,
308                   skip = skip)
309
310     def match(self, lines, matches):
311         """Compare actual and expected file contents.
312         """
313         return self.match_func(lines, matches)
314
315     def match_exact(self, lines, matches):
316         """Compare actual and expected file contents.
317         """
318         return match_exact(lines, matches)
319
320     def match_re(self, lines, res):
321         """Compare actual and expected file contents.
322         """
323         return match_re(lines, res)
324
325     def no_result(self, condition=True, function=None, skip=0):
326         """Report that the test could not be run.
327         """
328         if not condition:
329             return
330         self.condition = 'no_result'
331         no_result(self = self,
332                   condition = condition,
333                   function = function,
334                   skip = skip)
335
336     def pass_test(self, condition=True, function=None):
337         """Cause the test to pass.
338         """
339         if not condition:
340             return
341         self.condition = 'pass_test'
342         pass_test(self = self, condition = condition, function = function)
343
344     def preserve(self, *conditions):
345         """Arrange for the temporary working directories for the specified
346         TestCmd environment to be preserved for one or more conditions. If no
347         conditions are specified, arranges for the temporary working directories
348         to be preserved for all conditions.
349         """
350         if conditions is ():
351             conditions = ('pass_test', 'fail_test', 'no_result')
352         for cond in conditions:
353             self._preserve[cond] = 1
354
355     def program_set(self, program):
356         """Set the executable program or script to be tested.
357         """
358         if program and program[0] and not os.path.isabs(program[0]):
359             program[0] = os.path.join(self._cwd, program[0])
360         self.program = program
361
362     def read(self, file, mode='rb'):
363         """Reads and returns the contents of the specified file name. The file
364         name may be a list, in which case the elements are concatenated with the
365         os.path.join() method. The file is assumed to be under the temporary
366         working directory unless it is an absolute path name. The I/O mode for
367         the file may be specified; it must begin with an 'r'. The default is
368         'rb' (binary read).
369         """
370         if type(file) is ListType:
371             file = apply(os.path.join, tuple(file))
372         if not os.path.isabs(file):
373             file = os.path.join(self.workdir, file)
374         if mode[0] != 'r':
375             raise ValueError, "mode must begin with 'r'"
376         return open(file, mode).read()
377
378     def run(self, program=None, arguments=None, chdir=None, stdin=None):
379         """Runs a test of the program or script for the test environment.
380         Standard output and error output are saved for future retrieval via the
381         stdout() and stderr() methods.
382         """
383         if chdir:
384             oldcwd = os.getcwd()
385             if not os.path.isabs(chdir):
386                 chdir = os.path.join(self.workpath(chdir))
387             if self.verbose:
388                 sys.stderr.write("chdir(" + chdir + ")\n")
389             os.chdir(chdir)
390         cmd = []
391         if program and program[0]:
392             if program[0] != self.program[0] and not os.path.isabs(program[0]):
393                 program[0] = os.path.join(self._cwd, program[0])
394             cmd += program
395         else:
396             cmd += self.program
397         if arguments:
398             cmd += arguments.split(" ")
399         if self.verbose:
400             sys.stderr.write(join(cmd, " ") + "\n")
401         try:
402             p = popen2.Popen3(cmd, 1)
403         except AttributeError:
404             # We end up here in case the popen2.Popen3 class is not available
405             # (e.g. on Windows). We will be using the os.popen3() Python API
406             # which takes a string parameter and so needs its executable quoted
407             # in case its name contains spaces.
408             cmd[0] = '"' + cmd[0] + '"'
409             command_string = join(cmd, " ")
410             if ( os.name == 'nt' ):
411                 # This is a workaround for a longstanding Python bug on Windows
412                 # when using os.popen(), os.system() and similar functions to
413                 # execute a command containing quote characters. The bug seems
414                 # to be related to the quote stripping functionality used by the
415                 # Windows cmd.exe interpreter when its /S is not specified.
416                 #
417                 # Cleaned up quote from the cmd.exe help screen as displayed on
418                 # Windows XP SP2:
419                 #
420                 #   1. If all of the following conditions are met, then quote
421                 #      characters on the command line are preserved:
422                 #
423                 #       - no /S switch
424                 #       - exactly two quote characters
425                 #       - no special characters between the two quote
426                 #         characters, where special is one of: &<>()@^|
427                 #       - there are one or more whitespace characters between
428                 #         the two quote characters
429                 #       - the string between the two quote characters is the
430                 #         name of an executable file.
431                 #
432                 #   2. Otherwise, old behavior is to see if the first character
433                 #      is a quote character and if so, strip the leading
434                 #      character and remove the last quote character on the
435                 #      command line, preserving any text after the last quote
436                 #      character.
437                 #
438                 # This causes some commands containing quotes not to be executed
439                 # correctly. For example:
440                 #
441                 #   "\Long folder name\aaa.exe" --name="Jurko" --no-surname
442                 #
443                 # would get its outermost quotes stripped and would be executed
444                 # as:
445                 #
446                 #   \Long folder name\aaa.exe" --name="Jurko --no-surname
447                 #
448                 # which would report an error about '\Long' not being a valid
449                 # command.
450                 #
451                 # cmd.exe help seems to indicate it would be enough to add an
452                 # extra space character in front of the command to avoid this
453                 # but this does not work, most likely due to the shell first
454                 # stripping all leading whitespace characters from the command.
455                 #
456                 # Solution implemented here is to quote the whole command in
457                 # case it contains any quote characters. Note thought this will
458                 # not work correctly should Python ever fix this bug.
459                 #                                    (01.05.2008.) (Jurko)
460                 if command_string.find('"') != -1:
461                     command_string = '"' + command_string + '"'
462             (tochild, fromchild, childerr) = os.popen3(command_string)
463             if stdin:
464                 if type(stdin) is ListType:
465                     for line in stdin:
466                         tochild.write(line)
467                 else:
468                     tochild.write(stdin)
469             tochild.close()
470             self._stdout.append(fromchild.read())
471             self._stderr.append(childerr.read())
472             fromchild.close()
473             self.status = childerr.close()
474             if not self.status:
475                 self.status = 0
476         except:
477             raise
478         else:
479             if stdin:
480                 if type(stdin) is ListType:
481                     for line in stdin:
482                         p.tochild.write(line)
483                 else:
484                     p.tochild.write(stdin)
485             p.tochild.close()
486             self._stdout.append(p.fromchild.read())
487             self._stderr.append(p.childerr.read())
488             self.status = p.wait()
489
490         if self.verbose:
491             sys.stdout.write(self._stdout[-1])
492             sys.stderr.write(self._stderr[-1])
493
494         if chdir:
495             os.chdir(oldcwd)
496
497     def stderr(self, run=None):
498         """Returns the error output from the specified run number. If there is
499         no specified run number, then returns the error output of the last run.
500         If the run number is less than zero, then returns the error output from
501         that many runs back from the current run.
502         """
503         if not run:
504             run = len(self._stderr)
505         elif run < 0:
506             run = len(self._stderr) + run
507         run = run - 1
508         if (run < 0):
509             return ''
510         return self._stderr[run]
511
512     def stdout(self, run=None):
513         """Returns the standard output from the specified run number. If there
514         is no specified run number, then returns the standard output of the last
515         run. If the run number is less than zero, then returns the standard
516         output from that many runs back from the current run.
517         """
518         if not run:
519             run = len(self._stdout)
520         elif run < 0:
521             run = len(self._stdout) + run
522         run = run - 1
523         if (run < 0):
524             return ''
525         return self._stdout[run]
526
527     def subdir(self, *subdirs):
528         """Create new subdirectories under the temporary working directory, one
529         for each argument. An argument may be a list, in which case the list
530         elements are concatenated using the os.path.join() method.
531         Subdirectories multiple levels deep must be created using a separate
532         argument for each level:
533
534                 test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
535
536         Returns the number of subdirectories actually created.
537         """
538         count = 0
539         for sub in subdirs:
540             if sub is None:
541                 continue
542             if type(sub) is ListType:
543                 sub = apply(os.path.join, tuple(sub))
544             new = os.path.join(self.workdir, sub)
545             try:
546                 os.mkdir(new)
547             except:
548                 pass
549             else:
550                 count = count + 1
551         return count
552
553     def unlink (self, file):
554         """Unlinks the specified file name. The file name may be a list, in
555         which case the elements are concatenated using the os.path.join()
556         method. The file is assumed to be under the temporary working directory
557         unless it is an absolute path name.
558         """
559         if type(file) is ListType:
560             file = apply(os.path.join, tuple(file))
561         if not os.path.isabs(file):
562             file = os.path.join(self.workdir, file)
563         os.unlink(file)
564
565     def verbose_set(self, verbose):
566         """Set the verbose level.
567         """
568         self.verbose = verbose
569
570     def workdir_set(self, path):
571         """Creates a temporary working directory with the specified path name.
572         If the path is a null string (''), a unique directory name is created.
573         """
574
575         if os.path.isabs(path):
576             self.workdir = path
577         else:
578             if (path != None):
579                 if path == '':
580                     path = tempfile.mktemp()
581                 if path != None:
582                     os.mkdir(path)
583                 self._dirlist.append(path)
584                 global _Cleanup
585                 try:
586                     _Cleanup.index(self)
587                 except ValueError:
588                     _Cleanup.append(self)
589                 # We'd like to set self.workdir like this:
590                 #        self.workdir = path
591                 # But symlinks in the path will report things differently from
592                 # os.getcwd(), so chdir there and back to fetch the canonical
593                 # path.
594                 cwd = os.getcwd()
595                 os.chdir(path)
596                 self.workdir = os.getcwd()
597                 os.chdir(cwd)
598             else:
599                 self.workdir = None
600
601     def workpath(self, *args):
602         """Returns the absolute path name to a subdirectory or file within the
603         current temporary working directory. Concatenates the temporary working
604         directory name with the specified arguments using the os.path.join()
605         method.
606         """
607         return apply(os.path.join, (self.workdir,) + tuple(args))
608
609     def writable(self, top, write):
610         """Make the specified directory tree writable (write == 1) or not
611         (write == None).
612         """
613
614         def _walk_chmod(arg, dirname, names):
615             st = os.stat(dirname)
616             os.chmod(dirname, arg(st[stat.ST_MODE]))
617             for name in names:
618                 n = os.path.join(dirname, name)
619                 st = os.stat(n)
620                 os.chmod(n, arg(st[stat.ST_MODE]))
621
622         def _mode_writable(mode):
623             return stat.S_IMODE(mode|0200)
624
625         def _mode_non_writable(mode):
626             return stat.S_IMODE(mode&~0200)
627
628         if write:
629             f = _mode_writable
630         else:
631             f = _mode_non_writable
632         try:
633             os.path.walk(top, _walk_chmod, f)
634         except:
635             pass  # Ignore any problems changing modes.
636
637     def write(self, file, content, mode='wb'):
638         """Writes the specified content text (second argument) to the specified
639         file name (first argument). The file name may be a list, in which case
640         the elements are concatenated using the os.path.join() method. The file
641         is created under the temporary working directory. Any subdirectories in
642         the path must already exist. The I/O mode for the file may be specified;
643         it must begin with a 'w'. The default is 'wb' (binary write).
644         """
645         if type(file) is ListType:
646             file = apply(os.path.join, tuple(file))
647         if not os.path.isabs(file):
648             file = os.path.join(self.workdir, file)
649         if mode[0] != 'w':
650             raise ValueError, "mode must begin with 'w'"
651         open(file, mode).write(content)