2 TestCommon.py: a testing framework for commands and scripts
3 with commonly useful error handling
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.
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.
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.
24 A TestCommon environment object is created via the usual invocation:
27 test = TestCommon.TestCommon()
29 You can use all of the TestCmd keyword arguments when instantiating a
30 TestCommon object; see the TestCmd documentation for details.
32 Here is an overview of the methods and keyword arguments that are
33 provided by the TestCommon class:
35 test.must_be_writable('file1', ['file2', ...])
37 test.must_contain('file', 'required text\n')
39 test.must_exist('file1', ['file2', ...])
41 test.must_match('file', "expected contents\n")
43 test.must_not_be_writable('file1', ['file2', ...])
45 test.must_not_exist('file1', ['file2', ...])
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)
53 The TestCommon module also provides the following variables
55 TestCommon.python_executable
58 TestCommon.shobj_suffix
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.
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
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.
82 __author__ = "Steven Knight <knight at baldmt dot com>"
83 __revision__ = "TestCommon.py 0.26.D001 2007/08/20 21:58:58 knight"
95 from TestCmd import __all__
97 __all__.extend([ 'TestCommon',
109 # Variables that describe the prefixes and suffixes on this system.
110 if sys.platform == 'win32':
113 shobj_suffix = '.obj'
118 elif sys.platform == 'cygwin':
126 elif string.find(sys.platform, 'irix') != -1:
134 elif string.find(sys.platform, 'darwin') != -1:
141 dll_suffix = '.dylib'
156 def simple_diff(a, b, fromfile='', tofile='',
157 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
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.
163 sm = difflib.SequenceMatcher(None, a, b)
165 return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2)
167 for op, a1, a2, b1, b2 in sm.get_opcodes():
169 result.append("%sd%d" % (comma(a1, a2), b1))
170 result.extend(map(lambda l: '< ' + l, a[a1:a2]))
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]))
178 result.extend(map(lambda l: '> ' + l, b[b1:b2]))
182 return type(e) is types.ListType \
183 or isinstance(e, UserList.UserList)
186 mode = os.stat(f)[stat.ST_MODE]
187 return mode & stat.S_IWUSR
189 def separate_files(flist):
193 if os.path.exists(f):
197 return existing, missing
199 class TestFailed(Exception):
200 def __init__(self, args=None):
203 class TestNoResult(Exception):
204 def __init__(self, args=None):
207 if os.name == 'posix':
208 def _failed(self, status = 0):
209 if self.status is None or status is None:
211 if os.WIFSIGNALED(self.status):
213 return _status(self) != status
215 if os.WIFEXITED(self.status):
216 return os.WEXITSTATUS(self.status)
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
226 class TestCommon(TestCmd):
228 # Additional methods from the Perl Test::Cmd::Common module
229 # that we may wish to add in the future:
231 # $test->subdir('subdir', ...);
233 # $test->copy('src_file', 'dst_file');
235 def __init__(self, **kw):
236 """Initialize a new TestCommon instance. This involves just
237 calling the base class initialization, and then changing directory
240 apply(TestCmd.__init__, [self], kw)
241 os.chdir(self.workdir)
247 self.diff_function = simple_diff
248 #self.diff_function = difflib.context_diff
249 #self.diff_function = difflib.unified_diff
254 def banner(self, s, width=None):
256 width = self.banner_width
257 return s + self.banner_char * (width - len(s))
262 def diff(self, a, b, name, *args, **kw):
263 print self.banner('Expected %s' % name)
265 print self.banner('Actual %s' % name)
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)
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
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)
286 print "Missing files: `%s'" % string.join(missing, "', `")
288 print "Unwritable files: `%s'" % string.join(unwritable, "', `")
289 self.fail_test(missing + unwritable)
291 def must_contain(self, file, required, mode = 'rb'):
292 """Ensures that the specified file contains the required text.
294 file_contents = self.read(file, mode)
295 contains = (string.find(file_contents, required) != -1)
297 print "File `%s' does not contain required string." % file
298 print self.banner('Required string ')
300 print self.banner('%s contents ' % file)
302 self.fail_test(not contains)
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.
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)
313 print "Missing files: `%s'" % string.join(missing, "', `")
314 self.fail_test(missing)
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
322 file_contents = self.read(file, mode)
324 self.fail_test(not self.match(file_contents, expect))
325 except KeyboardInterrupt:
328 print "Unexpected contents of `%s'" % file
329 self.diff(expect, file_contents, 'contents ')
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.
338 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
339 existing = filter(os.path.exists, files)
341 print "Unexpected files exist: `%s'" % string.join(existing, "', `")
342 self.fail_test(existing)
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
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)
356 print "Missing files: `%s'" % string.join(missing, "', `")
358 print "Writable files: `%s'" % string.join(writable, "', `")
359 self.fail_test(missing + writable)
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.
365 The arguments are the same as the base TestCmd.run() method,
366 with the addition of:
368 options Extra options that get appended to the beginning
371 stdout The expected standard output from
372 the command. A value of None means
373 don't test standard output.
375 stderr The expected error output from
376 the command. A value of None means
377 don't test error output.
379 status The expected exit status from the
380 command. A value of None means don't
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 = "").
388 if arguments is None:
391 arguments = options + " " + arguments
392 kw['arguments'] = arguments
399 apply(TestCmd.run, [self], kw)
400 except KeyboardInterrupt:
403 print self.banner('STDOUT ')
405 print self.banner('STDERR ')
408 if _failed(self, status):
411 expect = " (expected %s)" % str(status)
412 print "%s returned %s%s" % (self.program, str(_status(self)), expect)
413 print self.banner('STDOUT ')
415 print self.banner('STDERR ')
418 if not stdout is None and not match(self.stdout(), stdout):
419 self.diff(stdout, self.stdout(), 'STDOUT ')
420 stderr = self.stderr()
422 print self.banner('STDERR ')
425 if not stderr is None and not match(self.stderr(), stderr):
426 print self.banner('STDOUT ')
428 self.diff(stderr, self.stderr(), 'STDERR ')
431 def skip_test(self, message="Skipping test.\n"):
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.
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.)
449 sys.stdout.write(message)
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)
459 # We're under the development directory for this change,
460 # so this is an Aegis invocation; pass the test (exit 0).