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_contain_all_lines(output, lines, ['title', find])
41 test.must_contain_any_line(output, lines, ['title', find])
43 test.must_exist('file1', ['file2', ...])
45 test.must_match('file', "expected contents\n")
47 test.must_not_be_writable('file1', ['file2', ...])
49 test.must_not_contain('file', 'banned text\n')
51 test.must_not_contain_any_line(output, lines, ['title', find])
53 test.must_not_exist('file1', ['file2', ...])
55 test.run(options = "options to be prepended to arguments",
56 stdout = "expected standard output from the program",
57 stderr = "expected error output from the program",
58 status = expected_status,
59 match = match_function)
61 The TestCommon module also provides the following variables
63 TestCommon.python_executable
66 TestCommon.shobj_prefix
67 TestCommon.shobj_suffix
75 # Copyright 2000-2010 Steven Knight
76 # This module is free software, and you may redistribute it and/or modify
77 # it under the same terms as Python itself, so long as this copyright message
78 # and disclaimer are retained in their original form.
80 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
81 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
82 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
85 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
86 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
87 # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
88 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
89 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
90 from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS
92 __author__ = "Steven Knight <knight at baldmt dot com>"
93 __revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight"
103 from TestCmd import *
104 from TestCmd import __all__
106 __all__.extend([ 'TestCommon',
117 # Variables that describe the prefixes and suffixes on this system.
118 if sys.platform == 'win32':
121 shobj_suffix = '.obj'
127 elif sys.platform == 'cygwin':
136 elif sys.platform.find('irix') != -1:
145 elif sys.platform.find('darwin') != -1:
153 dll_suffix = '.dylib'
154 elif sys.platform.find('sunos') != -1:
162 dll_suffix = '.dylib'
174 return isinstance(e, list) \
175 or isinstance(e, UserList.UserList)
178 mode = os.stat(f)[stat.ST_MODE]
179 return mode & stat.S_IWUSR
181 def separate_files(flist):
185 if os.path.exists(f):
189 return existing, missing
191 if os.name == 'posix':
192 def _failed(self, status = 0):
193 if self.status is None or status is None:
195 return _status(self) != status
198 elif os.name == 'nt':
199 def _failed(self, status = 0):
200 return not (self.status is None or status is None) and \
201 self.status != status
205 class TestCommon(TestCmd):
207 # Additional methods from the Perl Test::Cmd::Common module
208 # that we may wish to add in the future:
210 # $test->subdir('subdir', ...);
212 # $test->copy('src_file', 'dst_file');
214 def __init__(self, **kw):
215 """Initialize a new TestCommon instance. This involves just
216 calling the base class initialization, and then changing directory
219 TestCmd.__init__(self, **kw)
220 os.chdir(self.workdir)
222 def must_be_writable(self, *files):
223 """Ensures that the specified file(s) exist and are writable.
224 An individual file can be specified as a list of directory names,
225 in which case the pathname will be constructed by concatenating
226 them. Exits FAILED if any of the files does not exist or is
229 files = [is_List(x) and os.path.join(*x) or x for x in files]
230 existing, missing = separate_files(files)
231 unwritable = [x for x in existing if not is_writable(x)]
233 print "Missing files: `%s'" % "', `".join(missing)
235 print "Unwritable files: `%s'" % "', `".join(unwritable)
236 self.fail_test(missing + unwritable)
238 def must_contain(self, file, required, mode = 'rb'):
239 """Ensures that the specified file contains the required text.
241 file_contents = self.read(file, mode)
242 contains = (file_contents.find(required) != -1)
244 print "File `%s' does not contain required string." % file
245 print self.banner('Required string ')
247 print self.banner('%s contents ' % file)
249 self.fail_test(not contains)
251 def must_contain_all_lines(self, output, lines, title=None, find=None):
252 """Ensures that the specified output string (first argument)
253 contains all of the specified lines (second argument).
255 An optional third argument can be used to describe the type
256 of output being searched, and only shows up in failure output.
258 An optional fourth argument can be used to supply a different
259 function, of the form "find(line, output), to use when searching
260 for lines in the output.
263 find = lambda o, l: o.find(l) != -1
266 if not find(output, line):
272 sys.stdout.write("Missing expected lines from %s:\n" % title)
274 sys.stdout.write(' ' + repr(line) + '\n')
275 sys.stdout.write(self.banner(title + ' '))
276 sys.stdout.write(output)
279 def must_contain_any_line(self, output, lines, title=None, find=None):
280 """Ensures that the specified output string (first argument)
281 contains at least one of the specified lines (second argument).
283 An optional third argument can be used to describe the type
284 of output being searched, and only shows up in failure output.
286 An optional fourth argument can be used to supply a different
287 function, of the form "find(line, output), to use when searching
288 for lines in the output.
291 find = lambda o, l: o.find(l) != -1
293 if find(output, line):
298 sys.stdout.write("Missing any expected line from %s:\n" % title)
300 sys.stdout.write(' ' + repr(line) + '\n')
301 sys.stdout.write(self.banner(title + ' '))
302 sys.stdout.write(output)
305 def must_contain_lines(self, lines, output, title=None):
306 # Deprecated; retain for backwards compatibility.
307 return self.must_contain_all_lines(output, lines, title)
309 def must_exist(self, *files):
310 """Ensures that the specified file(s) must exist. An individual
311 file be specified as a list of directory names, in which case the
312 pathname will be constructed by concatenating them. Exits FAILED
313 if any of the files does not exist.
315 files = [is_List(x) and os.path.join(*x) or x for x in files]
316 missing = [x for x in files if not os.path.exists(x)]
318 print "Missing files: `%s'" % "', `".join(missing)
319 self.fail_test(missing)
321 def must_match(self, file, expect, mode = 'rb'):
322 """Matches the contents of the specified file (first argument)
323 against the expected contents (second argument). The expected
324 contents are a list of lines or a string which will be split
327 file_contents = self.read(file, mode)
329 self.fail_test(not self.match(file_contents, expect))
330 except KeyboardInterrupt:
333 print "Unexpected contents of `%s'" % file
334 self.diff(expect, file_contents, 'contents ')
337 def must_not_contain(self, file, banned, mode = 'rb'):
338 """Ensures that the specified file doesn't contain the banned text.
340 file_contents = self.read(file, mode)
341 contains = (file_contents.find(banned) != -1)
343 print "File `%s' contains banned string." % file
344 print self.banner('Banned string ')
346 print self.banner('%s contents ' % file)
348 self.fail_test(contains)
350 def must_not_contain_any_line(self, output, lines, title=None, find=None):
351 """Ensures that the specified output string (first argument)
352 does not contain any of the specified lines (second argument).
354 An optional third argument can be used to describe the type
355 of output being searched, and only shows up in failure output.
357 An optional fourth argument can be used to supply a different
358 function, of the form "find(line, output), to use when searching
359 for lines in the output.
362 find = lambda o, l: o.find(l) != -1
365 if find(output, line):
366 unexpected.append(line)
371 sys.stdout.write("Unexpected lines in %s:\n" % title)
372 for line in unexpected:
373 sys.stdout.write(' ' + repr(line) + '\n')
374 sys.stdout.write(self.banner(title + ' '))
375 sys.stdout.write(output)
378 def must_not_contain_lines(self, lines, output, title=None):
379 return self.must_not_contain_any_line(output, lines, title)
381 def must_not_exist(self, *files):
382 """Ensures that the specified file(s) must not exist.
383 An individual file be specified as a list of directory names, in
384 which case the pathname will be constructed by concatenating them.
385 Exits FAILED if any of the files exists.
387 files = [is_List(x) and os.path.join(*x) or x for x in files]
388 existing = list(filter(os.path.exists, files))
390 print "Unexpected files exist: `%s'" % "', `".join(existing)
391 self.fail_test(existing)
394 def must_not_be_writable(self, *files):
395 """Ensures that the specified file(s) exist and are not writable.
396 An individual file can be specified as a list of directory names,
397 in which case the pathname will be constructed by concatenating
398 them. Exits FAILED if any of the files does not exist or is
401 files = [is_List(x) and os.path.join(*x) or x for x in files]
402 existing, missing = separate_files(files)
403 writable = list(filter(is_writable, existing))
405 print "Missing files: `%s'" % "', `".join(missing)
407 print "Writable files: `%s'" % "', `".join(writable)
408 self.fail_test(missing + writable)
410 def _complete(self, actual_stdout, expected_stdout,
411 actual_stderr, expected_stderr, status, match):
413 Post-processes running a subcommand, checking for failure
414 status and displaying output appropriately.
416 if _failed(self, status):
419 expect = " (expected %s)" % str(status)
420 print "%s returned %s%s" % (self.program, str(_status(self)), expect)
421 print self.banner('STDOUT ')
423 print self.banner('STDERR ')
426 if not expected_stdout is None and not match(actual_stdout, expected_stdout):
427 self.diff(expected_stdout, actual_stdout, 'STDOUT ')
429 print self.banner('STDERR ')
432 if not expected_stderr is None and not match(actual_stderr, expected_stderr):
433 print self.banner('STDOUT ')
435 self.diff(expected_stderr, actual_stderr, 'STDERR ')
438 def start(self, program = None,
441 universal_newlines = None,
444 Starts a program or script for the test environment.
446 This handles the "options" keyword argument and exceptions.
449 options = kw['options']
455 if arguments is None:
458 arguments = options + " " + arguments
460 return TestCmd.start(self, program, interpreter, arguments, universal_newlines,
462 except KeyboardInterrupt:
465 print self.banner('STDOUT ')
470 print self.banner('STDERR ')
475 cmd_args = self.command_args(program, interpreter, arguments)
476 sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
479 def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
481 Finishes and waits for the process being run under control of
482 the specified popen argument. Additional arguments are similar
483 to those of the run() method:
485 stdout The expected standard output from
486 the command. A value of None means
487 don't test standard output.
489 stderr The expected error output from
490 the command. A value of None means
491 don't test error output.
493 status The expected exit status from the
494 command. A value of None means don't
497 TestCmd.finish(self, popen, **kw)
498 match = kw.get('match', self.match)
499 self._complete(self.stdout(), stdout,
500 self.stderr(), stderr, status, match)
502 def run(self, options = None, arguments = None,
503 stdout = None, stderr = '', status = 0, **kw):
504 """Runs the program under test, checking that the test succeeded.
506 The arguments are the same as the base TestCmd.run() method,
507 with the addition of:
509 options Extra options that get appended to the beginning
512 stdout The expected standard output from
513 the command. A value of None means
514 don't test standard output.
516 stderr The expected error output from
517 the command. A value of None means
518 don't test error output.
520 status The expected exit status from the
521 command. A value of None means don't
524 By default, this expects a successful exit (status = 0), does
525 not test standard output (stdout = None), and expects that error
526 output is empty (stderr = "").
529 if arguments is None:
532 arguments = options + " " + arguments
533 kw['arguments'] = arguments
539 TestCmd.run(self, **kw)
540 self._complete(self.stdout(), stdout,
541 self.stderr(), stderr, status, match)
543 def skip_test(self, message="Skipping test.\n"):
546 Proper test-skipping behavior is dependent on the external
547 TESTCOMMON_PASS_SKIPS environment variable. If set, we treat
548 the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
549 In either case, we print the specified message as an indication
550 that the substance of the test was skipped.
552 (This was originally added to support development under Aegis.
553 Technically, skipping a test is a NO RESULT, but Aegis would
554 treat that as a test failure and prevent the change from going to
555 the next step. Since we ddn't want to force anyone using Aegis
556 to have to install absolutely every tool used by the tests, we
557 would actually report to Aegis that a skipped test has PASSED
558 so that the workflow isn't held up.)
561 sys.stdout.write(message)
563 pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
564 if pass_skips in [None, 0, '0']:
565 # skip=1 means skip this function when showing where this
566 # result came from. They only care about the line where the
567 # script called test.skip_test(), not the line number where
568 # we call test.no_result().
569 self.no_result(skip=1)
571 # We're under the development directory for this change,
572 # so this is an Aegis invocation; pass the test (exit 0).
577 # indent-tabs-mode:nil
579 # vim: set expandtab tabstop=4 shiftwidth=4: