f4c7c541dd364e990a05e6f3b08343c78c545d83
[scons.git] / QMTest / TestCommon.py
1 """
2 TestCommon.py:  a testing framework for commands and scripts
3                 with commonly useful error handling
4
5 The TestCommon module provides a simple, high-level interface for writing
6 tests of executable commands and scripts, especially commands and scripts
7 that interact with the file system.  All methods throw exceptions and
8 exit on failure, with useful error messages.  This makes a number of
9 explicit checks unnecessary, making the test scripts themselves simpler
10 to write and easier to read.
11
12 The TestCommon class is a subclass of the TestCmd class.  In essence,
13 TestCommon is a wrapper that handles common TestCmd error conditions in
14 useful ways.  You can use TestCommon directly, or subclass it for your
15 program and add additional (or override) methods to tailor it to your
16 program's specific needs.  Alternatively, the TestCommon class serves
17 as a useful example of how to define your own TestCmd subclass.
18
19 As a subclass of TestCmd, TestCommon provides access to all of the
20 variables and methods from the TestCmd module.  Consequently, you can
21 use any variable or method documented in the TestCmd module without
22 having to explicitly import TestCmd.
23
24 A TestCommon environment object is created via the usual invocation:
25
26     import TestCommon
27     test = TestCommon.TestCommon()
28
29 You can use all of the TestCmd keyword arguments when instantiating a
30 TestCommon object; see the TestCmd documentation for details.
31
32 Here is an overview of the methods and keyword arguments that are
33 provided by the TestCommon class:
34
35     test.must_be_writable('file1', ['file2', ...])
36
37     test.must_contain('file', 'required text\n')
38
39     test.must_exist('file1', ['file2', ...])
40
41     test.must_match('file', "expected contents\n")
42
43     test.must_not_be_writable('file1', ['file2', ...])
44
45     test.must_not_exist('file1', ['file2', ...])
46
47     test.run(options = "options to be prepended to arguments",
48              stdout = "expected standard output from the program",
49              stderr = "expected error output from the program",
50              status = expected_status,
51              match = match_function)
52
53 The TestCommon module also provides the following variables
54
55     TestCommon.python_executable
56     TestCommon.exe_suffix
57     TestCommon.obj_suffix
58     TestCommon.shobj_suffix
59     TestCommon.lib_prefix
60     TestCommon.lib_suffix
61     TestCommon.dll_prefix
62     TestCommon.dll_suffix
63
64 """
65
66 # Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight
67 # This module is free software, and you may redistribute it and/or modify
68 # it under the same terms as Python itself, so long as this copyright message
69 # and disclaimer are retained in their original form.
70 #
71 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
72 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
73 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
74 # DAMAGE.
75 #
76 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
77 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
78 # PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
79 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
80 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
81
82 __author__ = "Steven Knight <knight at baldmt dot com>"
83 __revision__ = "TestCommon.py 0.26.D001 2007/08/20 21:58:58 knight"
84 __version__ = "0.26"
85
86 import os
87 import os.path
88 import stat
89 import string
90 import sys
91 import types
92 import UserList
93
94 from TestCmd import *
95 from TestCmd import __all__
96
97 __all__.extend([ 'TestCommon',
98                  'TestFailed',
99                  'TestNoResult',
100                  'exe_suffix',
101                  'obj_suffix',
102                  'shobj_suffix',
103                  'lib_prefix',
104                  'lib_suffix',
105                  'dll_prefix',
106                  'dll_suffix',
107                ])
108
109 # Variables that describe the prefixes and suffixes on this system.
110 if sys.platform == 'win32':
111     exe_suffix   = '.exe'
112     obj_suffix   = '.obj'
113     shobj_suffix = '.obj'
114     lib_prefix   = ''
115     lib_suffix   = '.lib'
116     dll_prefix   = ''
117     dll_suffix   = '.dll'
118 elif sys.platform == 'cygwin':
119     exe_suffix   = '.exe'
120     obj_suffix   = '.o'
121     shobj_suffix = '.os'
122     lib_prefix   = 'lib'
123     lib_suffix   = '.a'
124     dll_prefix   = ''
125     dll_suffix   = '.dll'
126 elif string.find(sys.platform, 'irix') != -1:
127     exe_suffix   = ''
128     obj_suffix   = '.o'
129     shobj_suffix = '.o'
130     lib_prefix   = 'lib'
131     lib_suffix   = '.a'
132     dll_prefix   = 'lib'
133     dll_suffix   = '.so'
134 elif string.find(sys.platform, 'darwin') != -1:
135     exe_suffix   = ''
136     obj_suffix   = '.o'
137     shobj_suffix = '.os'
138     lib_prefix   = 'lib'
139     lib_suffix   = '.a'
140     dll_prefix   = 'lib'
141     dll_suffix   = '.dylib'
142 else:
143     exe_suffix   = ''
144     obj_suffix   = '.o'
145     shobj_suffix = '.os'
146     lib_prefix   = 'lib'
147     lib_suffix   = '.a'
148     dll_prefix   = 'lib'
149     dll_suffix   = '.so'
150
151 try:
152     import difflib
153 except ImportError:
154     pass
155 else:
156     def simple_diff(a, b, fromfile='', tofile='',
157                     fromfiledate='', tofiledate='', n=3, lineterm='\n'):
158         """
159         A function with the same calling signature as difflib.context_diff
160         (diff -c) and difflib.unified_diff (diff -u) but which prints
161         output like the simple, unadorned 'diff" command.
162         """
163         sm = difflib.SequenceMatcher(None, a, b)
164         def comma(x1, x2):
165             return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2)
166         result = []
167         for op, a1, a2, b1, b2 in sm.get_opcodes():
168             if op == 'delete':
169                 result.append("%sd%d" % (comma(a1, a2), b1))
170                 result.extend(map(lambda l: '< ' + l, a[a1:a2]))
171             elif op == 'insert':
172                 result.append("%da%s" % (a1, comma(b1, b2)))
173                 result.extend(map(lambda l: '> ' + l, b[b1:b2]))
174             elif op == 'replace':
175                 result.append("%sc%s" % (comma(a1, a2), comma(b1, b2)))
176                 result.extend(map(lambda l: '< ' + l, a[a1:a2]))
177                 result.append('---')
178                 result.extend(map(lambda l: '> ' + l, b[b1:b2]))
179         return result
180
181 def is_List(e):
182     return type(e) is types.ListType \
183         or isinstance(e, UserList.UserList)
184
185 def is_writable(f):
186     mode = os.stat(f)[stat.ST_MODE]
187     return mode & stat.S_IWUSR
188
189 def separate_files(flist):
190     existing = []
191     missing = []
192     for f in flist:
193         if os.path.exists(f):
194             existing.append(f)
195         else:
196             missing.append(f)
197     return existing, missing
198
199 class TestFailed(Exception):
200     def __init__(self, args=None):
201         self.args = args
202
203 class TestNoResult(Exception):
204     def __init__(self, args=None):
205         self.args = args
206
207 if os.name == 'posix':
208     def _failed(self, status = 0):
209         if self.status is None or status is None:
210             return None
211         if os.WIFSIGNALED(self.status):
212             return None
213         return _status(self) != status
214     def _status(self):
215         if os.WIFEXITED(self.status):
216             return os.WEXITSTATUS(self.status)
217         else:
218             return None
219 elif os.name == 'nt':
220     def _failed(self, status = 0):
221         return not (self.status is None or status is None) and \
222                self.status != status
223     def _status(self):
224         return self.status
225
226 class TestCommon(TestCmd):
227
228     # Additional methods from the Perl Test::Cmd::Common module
229     # that we may wish to add in the future:
230     #
231     #  $test->subdir('subdir', ...);
232     #
233     #  $test->copy('src_file', 'dst_file');
234
235     def __init__(self, **kw):
236         """Initialize a new TestCommon instance.  This involves just
237         calling the base class initialization, and then changing directory
238         to the workdir.
239         """
240         apply(TestCmd.__init__, [self], kw)
241         os.chdir(self.workdir)
242         try:
243             difflib
244         except NameError:
245             pass
246         else:
247             self.diff_function = simple_diff
248             #self.diff_function = difflib.context_diff
249             #self.diff_function = difflib.unified_diff
250
251     banner_char = '='
252     banner_width = 80
253
254     def banner(self, s, width=None):
255         if width is None:
256             width = self.banner_width
257         return s + self.banner_char * (width - len(s))
258
259     try:
260         difflib
261     except NameError:
262         def diff(self, a, b, name, *args, **kw):
263             print self.banner('Expected %s' % name)
264             print a
265             print self.banner('Actual %s' % name)
266             print b
267     else:
268         def diff(self, a, b, name, *args, **kw):
269             print self.banner(name)
270             args = (a.splitlines(), b.splitlines()) + args
271             lines = apply(self.diff_function, args, kw)
272             for l in lines:
273                 print l
274
275     def must_be_writable(self, *files):
276         """Ensures that the specified file(s) exist and are writable.
277         An individual file can be specified as a list of directory names,
278         in which case the pathname will be constructed by concatenating
279         them.  Exits FAILED if any of the files does not exist or is
280         not writable.
281         """
282         files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
283         existing, missing = separate_files(files)
284         unwritable = filter(lambda x, iw=is_writable: not iw(x), existing)
285         if missing:
286             print "Missing files: `%s'" % string.join(missing, "', `")
287         if unwritable:
288             print "Unwritable files: `%s'" % string.join(unwritable, "', `")
289         self.fail_test(missing + unwritable)
290
291     def must_contain(self, file, required, mode = 'rb'):
292         """Ensures that the specified file contains the required text.
293         """
294         file_contents = self.read(file, mode)
295         contains = (string.find(file_contents, required) != -1)
296         if not contains:
297             print "File `%s' does not contain required string." % file
298             print self.banner('Required string ')
299             print required
300             print self.banner('%s contents ' % file)
301             print file_contents
302             self.fail_test(not contains)
303
304     def must_exist(self, *files):
305         """Ensures that the specified file(s) must exist.  An individual
306         file be specified as a list of directory names, in which case the
307         pathname will be constructed by concatenating them.  Exits FAILED
308         if any of the files does not exist.
309         """
310         files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
311         missing = filter(lambda x: not os.path.exists(x), files)
312         if missing:
313             print "Missing files: `%s'" % string.join(missing, "', `")
314             self.fail_test(missing)
315
316     def must_match(self, file, expect, mode = 'rb'):
317         """Matches the contents of the specified file (first argument)
318         against the expected contents (second argument).  The expected
319         contents are a list of lines or a string which will be split
320         on newlines.
321         """
322         file_contents = self.read(file, mode)
323         try:
324             self.fail_test(not self.match(file_contents, expect))
325         except KeyboardInterrupt:
326             raise
327         except:
328             print "Unexpected contents of `%s'" % file
329             self.diff(expect, file_contents, 'contents ')
330             raise
331
332     def must_not_exist(self, *files):
333         """Ensures that the specified file(s) must not exist.
334         An individual file be specified as a list of directory names, in
335         which case the pathname will be constructed by concatenating them.
336         Exits FAILED if any of the files exists.
337         """
338         files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
339         existing = filter(os.path.exists, files)
340         if existing:
341             print "Unexpected files exist: `%s'" % string.join(existing, "', `")
342             self.fail_test(existing)
343
344
345     def must_not_be_writable(self, *files):
346         """Ensures that the specified file(s) exist and are not writable.
347         An individual file can be specified as a list of directory names,
348         in which case the pathname will be constructed by concatenating
349         them.  Exits FAILED if any of the files does not exist or is
350         writable.
351         """
352         files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
353         existing, missing = separate_files(files)
354         writable = filter(is_writable, existing)
355         if missing:
356             print "Missing files: `%s'" % string.join(missing, "', `")
357         if writable:
358             print "Writable files: `%s'" % string.join(writable, "', `")
359         self.fail_test(missing + writable)
360
361     def run(self, options = None, arguments = None,
362                   stdout = None, stderr = '', status = 0, **kw):
363         """Runs the program under test, checking that the test succeeded.
364
365         The arguments are the same as the base TestCmd.run() method,
366         with the addition of:
367
368                 options Extra options that get appended to the beginning
369                         of the arguments.
370
371                 stdout  The expected standard output from
372                         the command.  A value of None means
373                         don't test standard output.
374
375                 stderr  The expected error output from
376                         the command.  A value of None means
377                         don't test error output.
378
379                 status  The expected exit status from the
380                         command.  A value of None means don't
381                         test exit status.
382
383         By default, this expects a successful exit (status = 0), does
384         not test standard output (stdout = None), and expects that error
385         output is empty (stderr = "").
386         """
387         if options:
388             if arguments is None:
389                 arguments = options
390             else:
391                 arguments = options + " " + arguments
392         kw['arguments'] = arguments
393         try:
394             match = kw['match']
395             del kw['match']
396         except KeyError:
397             match = self.match
398         try:
399             apply(TestCmd.run, [self], kw)
400         except KeyboardInterrupt:
401             raise
402         except:
403             print self.banner('STDOUT ')
404             print self.stdout()
405             print self.banner('STDERR ')
406             print self.stderr()
407             raise
408         if _failed(self, status):
409             expect = ''
410             if status != 0:
411                 expect = " (expected %s)" % str(status)
412             print "%s returned %s%s" % (self.program, str(_status(self)), expect)
413             print self.banner('STDOUT ')
414             print self.stdout()
415             print self.banner('STDERR ')
416             print self.stderr()
417             raise TestFailed
418         if not stdout is None and not match(self.stdout(), stdout):
419             self.diff(stdout, self.stdout(), 'STDOUT ')
420             stderr = self.stderr()
421             if stderr:
422                 print self.banner('STDERR ')
423                 print stderr
424             raise TestFailed
425         if not stderr is None and not match(self.stderr(), stderr):
426             print self.banner('STDOUT ')
427             print self.stdout()
428             self.diff(stderr, self.stderr(), 'STDERR ')
429             raise TestFailed
430
431     def skip_test(self, message="Skipping test.\n"):
432         """Skips a test.
433
434         Proper test-skipping behavior is dependent on the external
435         TESTCOMMON_PASS_SKIPS environment variable.  If set, we treat
436         the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
437         In either case, we print the specified message as an indication
438         that the substance of the test was skipped.
439
440         (This was originally added to support development under Aegis.
441         Technically, skipping a test is a NO RESULT, but Aegis would
442         treat that as a test failure and prevent the change from going to
443         the next step.  Since we ddn't want to force anyone using Aegis
444         to have to install absolutely every tool used by the tests, we
445         would actually report to Aegis that a skipped test has PASSED
446         so that the workflow isn't held up.)
447         """
448         if message:
449             sys.stdout.write(message)
450             sys.stdout.flush()
451         pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
452         if pass_skips in [None, 0, '0']:
453             # skip=1 means skip this function when showing where this
454             # result came from.  They only care about the line where the
455             # script called test.skip_test(), not the line number where
456             # we call test.no_result().
457             self.no_result(skip=1)
458         else:
459             # We're under the development directory for this change,
460             # so this is an Aegis invocation; pass the test (exit 0).
461             self.pass_test()