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"
104 from TestCmd import *
105 from TestCmd import __all__
107 __all__.extend([ 'TestCommon',
118 # Variables that describe the prefixes and suffixes on this system.
119 if sys.platform == 'win32':
122 shobj_suffix = '.obj'
128 elif sys.platform == 'cygwin':
137 elif sys.platform.find('irix') != -1:
146 elif sys.platform.find('darwin') != -1:
154 dll_suffix = '.dylib'
155 elif sys.platform.find('sunos') != -1:
163 dll_suffix = '.dylib'
175 return type(e) is types.ListType \
176 or isinstance(e, UserList.UserList)
179 mode = os.stat(f)[stat.ST_MODE]
180 return mode & stat.S_IWUSR
182 def separate_files(flist):
186 if os.path.exists(f):
190 return existing, missing
192 if os.name == 'posix':
193 def _failed(self, status = 0):
194 if self.status is None or status is None:
196 return _status(self) != status
199 elif os.name == 'nt':
200 def _failed(self, status = 0):
201 return not (self.status is None or status is None) and \
202 self.status != status
206 class TestCommon(TestCmd):
208 # Additional methods from the Perl Test::Cmd::Common module
209 # that we may wish to add in the future:
211 # $test->subdir('subdir', ...);
213 # $test->copy('src_file', 'dst_file');
215 def __init__(self, **kw):
216 """Initialize a new TestCommon instance. This involves just
217 calling the base class initialization, and then changing directory
220 TestCmd.__init__(self, **kw)
221 os.chdir(self.workdir)
223 def must_be_writable(self, *files):
224 """Ensures that the specified file(s) exist and are writable.
225 An individual file can be specified as a list of directory names,
226 in which case the pathname will be constructed by concatenating
227 them. Exits FAILED if any of the files does not exist or is
230 files = [is_List(x) and os.path.join(*x) or x for x in files]
231 existing, missing = separate_files(files)
232 unwritable = [x for x in existing if not is_writable(x)]
234 print "Missing files: `%s'" % "', `".join(missing)
236 print "Unwritable files: `%s'" % "', `".join(unwritable)
237 self.fail_test(missing + unwritable)
239 def must_contain(self, file, required, mode = 'rb'):
240 """Ensures that the specified file contains the required text.
242 file_contents = self.read(file, mode)
243 contains = (file_contents.find(required) != -1)
245 print "File `%s' does not contain required string." % file
246 print self.banner('Required string ')
248 print self.banner('%s contents ' % file)
250 self.fail_test(not contains)
252 def must_contain_all_lines(self, output, lines, title=None, find=None):
253 """Ensures that the specified output string (first argument)
254 contains all of the specified lines (second argument).
256 An optional third argument can be used to describe the type
257 of output being searched, and only shows up in failure output.
259 An optional fourth argument can be used to supply a different
260 function, of the form "find(line, output), to use when searching
261 for lines in the output.
264 find = lambda o, l: o.find(l) != -1
267 if not find(output, line):
273 sys.stdout.write("Missing expected lines from %s:\n" % title)
275 sys.stdout.write(' ' + repr(line) + '\n')
276 sys.stdout.write(self.banner(title + ' '))
277 sys.stdout.write(output)
280 def must_contain_any_line(self, output, lines, title=None, find=None):
281 """Ensures that the specified output string (first argument)
282 contains at least one of the specified lines (second argument).
284 An optional third argument can be used to describe the type
285 of output being searched, and only shows up in failure output.
287 An optional fourth argument can be used to supply a different
288 function, of the form "find(line, output), to use when searching
289 for lines in the output.
292 find = lambda o, l: o.find(l) != -1
294 if find(output, line):
299 sys.stdout.write("Missing any expected line from %s:\n" % title)
301 sys.stdout.write(' ' + repr(line) + '\n')
302 sys.stdout.write(self.banner(title + ' '))
303 sys.stdout.write(output)
306 def must_contain_lines(self, lines, output, title=None):
307 # Deprecated; retain for backwards compatibility.
308 return self.must_contain_all_lines(output, lines, title)
310 def must_exist(self, *files):
311 """Ensures that the specified file(s) must exist. An individual
312 file be specified as a list of directory names, in which case the
313 pathname will be constructed by concatenating them. Exits FAILED
314 if any of the files does not exist.
316 files = [is_List(x) and os.path.join(*x) or x for x in files]
317 missing = [x for x in files if not os.path.exists(x)]
319 print "Missing files: `%s'" % "', `".join(missing)
320 self.fail_test(missing)
322 def must_match(self, file, expect, mode = 'rb'):
323 """Matches the contents of the specified file (first argument)
324 against the expected contents (second argument). The expected
325 contents are a list of lines or a string which will be split
328 file_contents = self.read(file, mode)
330 self.fail_test(not self.match(file_contents, expect))
331 except KeyboardInterrupt:
334 print "Unexpected contents of `%s'" % file
335 self.diff(expect, file_contents, 'contents ')
338 def must_not_contain(self, file, banned, mode = 'rb'):
339 """Ensures that the specified file doesn't contain the banned text.
341 file_contents = self.read(file, mode)
342 contains = (file_contents.find(banned) != -1)
344 print "File `%s' contains banned string." % file
345 print self.banner('Banned string ')
347 print self.banner('%s contents ' % file)
349 self.fail_test(contains)
351 def must_not_contain_any_line(self, output, lines, title=None, find=None):
352 """Ensures that the specified output string (first argument)
353 does not contain any of the specified lines (second argument).
355 An optional third argument can be used to describe the type
356 of output being searched, and only shows up in failure output.
358 An optional fourth argument can be used to supply a different
359 function, of the form "find(line, output), to use when searching
360 for lines in the output.
363 find = lambda o, l: o.find(l) != -1
366 if find(output, line):
367 unexpected.append(line)
372 sys.stdout.write("Unexpected lines in %s:\n" % title)
373 for line in unexpected:
374 sys.stdout.write(' ' + repr(line) + '\n')
375 sys.stdout.write(self.banner(title + ' '))
376 sys.stdout.write(output)
379 def must_not_contain_lines(self, lines, output, title=None):
380 return self.must_not_contain_any_line(output, lines, title)
382 def must_not_exist(self, *files):
383 """Ensures that the specified file(s) must not exist.
384 An individual file be specified as a list of directory names, in
385 which case the pathname will be constructed by concatenating them.
386 Exits FAILED if any of the files exists.
388 files = [is_List(x) and os.path.join(*x) or x for x in files]
389 existing = list(filter(os.path.exists, files))
391 print "Unexpected files exist: `%s'" % "', `".join(existing)
392 self.fail_test(existing)
395 def must_not_be_writable(self, *files):
396 """Ensures that the specified file(s) exist and are not writable.
397 An individual file can be specified as a list of directory names,
398 in which case the pathname will be constructed by concatenating
399 them. Exits FAILED if any of the files does not exist or is
402 files = [is_List(x) and os.path.join(*x) or x for x in files]
403 existing, missing = separate_files(files)
404 writable = list(filter(is_writable, existing))
406 print "Missing files: `%s'" % "', `".join(missing)
408 print "Writable files: `%s'" % "', `".join(writable)
409 self.fail_test(missing + writable)
411 def _complete(self, actual_stdout, expected_stdout,
412 actual_stderr, expected_stderr, status, match):
414 Post-processes running a subcommand, checking for failure
415 status and displaying output appropriately.
417 if _failed(self, status):
420 expect = " (expected %s)" % str(status)
421 print "%s returned %s%s" % (self.program, str(_status(self)), expect)
422 print self.banner('STDOUT ')
424 print self.banner('STDERR ')
427 if not expected_stdout is None and not match(actual_stdout, expected_stdout):
428 self.diff(expected_stdout, actual_stdout, 'STDOUT ')
430 print self.banner('STDERR ')
433 if not expected_stderr is None and not match(actual_stderr, expected_stderr):
434 print self.banner('STDOUT ')
436 self.diff(expected_stderr, actual_stderr, 'STDERR ')
439 def start(self, program = None,
442 universal_newlines = None,
445 Starts a program or script for the test environment.
447 This handles the "options" keyword argument and exceptions.
450 options = kw['options']
456 if arguments is None:
459 arguments = options + " " + arguments
461 return TestCmd.start(self, program, interpreter, arguments, universal_newlines,
463 except KeyboardInterrupt:
466 print self.banner('STDOUT ')
471 print self.banner('STDERR ')
476 cmd_args = self.command_args(program, interpreter, arguments)
477 sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
480 def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
482 Finishes and waits for the process being run under control of
483 the specified popen argument. Additional arguments are similar
484 to those of the run() method:
486 stdout The expected standard output from
487 the command. A value of None means
488 don't test standard output.
490 stderr The expected error output from
491 the command. A value of None means
492 don't test error output.
494 status The expected exit status from the
495 command. A value of None means don't
498 TestCmd.finish(self, popen, **kw)
499 match = kw.get('match', self.match)
500 self._complete(self.stdout(), stdout,
501 self.stderr(), stderr, status, match)
503 def run(self, options = None, arguments = None,
504 stdout = None, stderr = '', status = 0, **kw):
505 """Runs the program under test, checking that the test succeeded.
507 The arguments are the same as the base TestCmd.run() method,
508 with the addition of:
510 options Extra options that get appended to the beginning
513 stdout The expected standard output from
514 the command. A value of None means
515 don't test standard output.
517 stderr The expected error output from
518 the command. A value of None means
519 don't test error output.
521 status The expected exit status from the
522 command. A value of None means don't
525 By default, this expects a successful exit (status = 0), does
526 not test standard output (stdout = None), and expects that error
527 output is empty (stderr = "").
530 if arguments is None:
533 arguments = options + " " + arguments
534 kw['arguments'] = arguments
540 TestCmd.run(self, **kw)
541 self._complete(self.stdout(), stdout,
542 self.stderr(), stderr, status, match)
544 def skip_test(self, message="Skipping test.\n"):
547 Proper test-skipping behavior is dependent on the external
548 TESTCOMMON_PASS_SKIPS environment variable. If set, we treat
549 the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
550 In either case, we print the specified message as an indication
551 that the substance of the test was skipped.
553 (This was originally added to support development under Aegis.
554 Technically, skipping a test is a NO RESULT, but Aegis would
555 treat that as a test failure and prevent the change from going to
556 the next step. Since we ddn't want to force anyone using Aegis
557 to have to install absolutely every tool used by the tests, we
558 would actually report to Aegis that a skipped test has PASSED
559 so that the workflow isn't held up.)
562 sys.stdout.write(message)
564 pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
565 if pass_skips in [None, 0, '0']:
566 # skip=1 means skip this function when showing where this
567 # result came from. They only care about the line where the
568 # script called test.skip_test(), not the line number where
569 # we call test.no_result().
570 self.no_result(skip=1)
572 # We're under the development directory for this change,
573 # so this is an Aegis invocation; pass the test (exit 0).
578 # indent-tabs-mode:nil
580 # vim: set expandtab tabstop=4 shiftwidth=4: