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.exactly_contain_all_lines(output, lines, ['title', find])
45
test.must_exist('file1', ['file2', ...])
47
test.must_match('file', "expected contents\n")
49
test.must_not_be_writable('file1', ['file2', ...])
51
test.must_not_contain('file', 'banned text\n')
53
test.must_not_contain_any_line(output, lines, ['title', find])
55
test.must_not_exist('file1', ['file2', ...])
57
test.run(options = "options to be prepended to arguments",
58
stdout = "expected standard output from the program",
59
stderr = "expected error output from the program",
60
status = expected_status,
61
match = match_function)
63
The TestCommon module also provides the following variables
69
TestCommon.shobj_prefix
70
TestCommon.shobj_suffix
78
# Copyright 2000-2010 Steven Knight
79
# This module is free software, and you may redistribute it and/or modify
80
# it under the same terms as Python itself, so long as this copyright message
81
# and disclaimer are retained in their original form.
83
# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
84
# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
85
# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
88
# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
89
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
90
# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
91
# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
92
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
94
__author__ = "Steven Knight <knight at baldmt dot com>"
95
__revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight"
103
from collections import UserList
105
# no 'collections' module or no UserList in collections
106
exec('from UserList import UserList')
108
from TestCmd import *
109
from TestCmd import __all__
111
__all__.extend([ 'TestCommon',
122
# Variables that describe the prefixes and suffixes on this system.
123
if sys.platform == 'win32':
126
shobj_suffix = '.obj'
132
elif sys.platform == 'cygwin':
141
elif sys.platform.find('irix') != -1:
150
elif sys.platform.find('darwin') != -1:
158
dll_suffix = '.dylib'
159
elif sys.platform.find('sunos') != -1:
167
dll_suffix = '.dylib'
179
return isinstance(e, (list,UserList))
182
mode = os.stat(f)[stat.ST_MODE]
183
return mode & stat.S_IWUSR
185
def separate_files(flist):
189
if os.path.exists(f):
193
return existing, missing
195
if os.name == 'posix':
196
def _failed(self, status = 0):
197
if self.status is None or status is None:
199
return _status(self) != status
202
elif os.name == 'nt':
203
def _failed(self, status = 0):
204
return not (self.status is None or status is None) and \
205
self.status != status
209
class TestCommon(TestCmd):
211
# Additional methods from the Perl Test::Cmd::Common module
212
# that we may wish to add in the future:
214
# $test->subdir('subdir', ...);
216
# $test->copy('src_file', 'dst_file');
218
def __init__(self, **kw):
219
"""Initialize a new TestCommon instance. This involves just
220
calling the base class initialization, and then changing directory
223
TestCmd.__init__(self, **kw)
224
os.chdir(self.workdir)
226
def must_be_writable(self, *files):
227
"""Ensures that the specified file(s) exist and are writable.
228
An individual file can be specified as a list of directory names,
229
in which case the pathname will be constructed by concatenating
230
them. Exits FAILED if any of the files does not exist or is
233
files = [is_List(x) and os.path.join(*x) or x for x in files]
234
existing, missing = separate_files(files)
235
unwritable = [x for x in existing if not is_writable(x)]
237
print "Missing files: `%s'" % "', `".join(missing)
239
print "Unwritable files: `%s'" % "', `".join(unwritable)
240
self.fail_test(missing + unwritable)
242
def must_contain(self, file, required, mode = 'rb'):
243
"""Ensures that the specified file contains the required text.
245
file_contents = self.read(file, mode)
246
contains = (file_contents.find(required) != -1)
248
print "File `%s' does not contain required string." % file
249
print self.banner('Required string ')
251
print self.banner('%s contents ' % file)
253
self.fail_test(not contains)
255
def must_contain_all_lines(self, output, lines, title=None, find=None):
256
"""Ensures that the specified output string (first argument)
257
contains all of the specified lines (second argument).
259
An optional third argument can be used to describe the type
260
of output being searched, and only shows up in failure output.
262
An optional fourth argument can be used to supply a different
263
function, of the form "find(line, output), to use when searching
264
for lines in the output.
267
find = lambda o, l: o.find(l) != -1
270
if not find(output, line):
276
sys.stdout.write("Missing expected lines from %s:\n" % title)
278
sys.stdout.write(' ' + repr(line) + '\n')
279
sys.stdout.write(self.banner(title + ' '))
280
sys.stdout.write(output)
283
def must_contain_any_line(self, output, lines, title=None, find=None):
284
"""Ensures that the specified output string (first argument)
285
contains at least one of the specified lines (second argument).
287
An optional third argument can be used to describe the type
288
of output being searched, and only shows up in failure output.
290
An optional fourth argument can be used to supply a different
291
function, of the form "find(line, output), to use when searching
292
for lines in the output.
295
find = lambda o, l: o.find(l) != -1
297
if find(output, line):
302
sys.stdout.write("Missing any expected line from %s:\n" % title)
304
sys.stdout.write(' ' + repr(line) + '\n')
305
sys.stdout.write(self.banner(title + ' '))
306
sys.stdout.write(output)
309
def exactly_contain_all_lines(self, output, expect, title=None, find=None):
310
"""Ensures that the specified output string (first argument)
311
contains all of the lines in the expected string (second argument)
314
An optional third argument can be used to describe the type
315
of output being searched, and only shows up in failure output.
317
An optional fourth argument can be used to supply a different
318
function, of the form "find(line, output), to use when searching
319
for lines in the output.
321
out = output.splitlines()
322
exp = expect.splitlines()
323
if sorted(out) == sorted(exp):
324
# early out for exact match
334
found = find(out, line)
340
if not missing and not out:
341
# all lines were matched
347
sys.stdout.write("Missing expected lines from %s:\n" % title)
349
sys.stdout.write(' ' + repr(line) + '\n')
350
sys.stdout.write(self.banner('missing %s ' % title))
352
sys.stdout.write("Extra unexpected lines from %s:\n" % title)
354
sys.stdout.write(' ' + repr(line) + '\n')
355
sys.stdout.write(self.banner('extra %s ' % title))
359
def must_contain_lines(self, lines, output, title=None):
360
# Deprecated; retain for backwards compatibility.
361
return self.must_contain_all_lines(output, lines, title)
363
def must_exist(self, *files):
364
"""Ensures that the specified file(s) must exist. An individual
365
file be specified as a list of directory names, in which case the
366
pathname will be constructed by concatenating them. Exits FAILED
367
if any of the files does not exist.
369
files = [is_List(x) and os.path.join(*x) or x for x in files]
370
missing = [x for x in files if not os.path.exists(x)]
372
print "Missing files: `%s'" % "', `".join(missing)
373
self.fail_test(missing)
375
def must_match(self, file, expect, mode = 'rb'):
376
"""Matches the contents of the specified file (first argument)
377
against the expected contents (second argument). The expected
378
contents are a list of lines or a string which will be split
381
file_contents = self.read(file, mode)
383
self.fail_test(not self.match(file_contents, expect))
384
except KeyboardInterrupt:
387
print "Unexpected contents of `%s'" % file
388
self.diff(expect, file_contents, 'contents ')
391
def must_not_contain(self, file, banned, mode = 'rb'):
392
"""Ensures that the specified file doesn't contain the banned text.
394
file_contents = self.read(file, mode)
395
contains = (file_contents.find(banned) != -1)
397
print "File `%s' contains banned string." % file
398
print self.banner('Banned string ')
400
print self.banner('%s contents ' % file)
402
self.fail_test(contains)
404
def must_not_contain_any_line(self, output, lines, title=None, find=None):
405
"""Ensures that the specified output string (first argument)
406
does not contain any of the specified lines (second argument).
408
An optional third argument can be used to describe the type
409
of output being searched, and only shows up in failure output.
411
An optional fourth argument can be used to supply a different
412
function, of the form "find(line, output), to use when searching
413
for lines in the output.
416
find = lambda o, l: o.find(l) != -1
419
if find(output, line):
420
unexpected.append(line)
425
sys.stdout.write("Unexpected lines in %s:\n" % title)
426
for line in unexpected:
427
sys.stdout.write(' ' + repr(line) + '\n')
428
sys.stdout.write(self.banner(title + ' '))
429
sys.stdout.write(output)
432
def must_not_contain_lines(self, lines, output, title=None):
433
return self.must_not_contain_any_line(output, lines, title)
435
def must_not_exist(self, *files):
436
"""Ensures that the specified file(s) must not exist.
437
An individual file be specified as a list of directory names, in
438
which case the pathname will be constructed by concatenating them.
439
Exits FAILED if any of the files exists.
441
files = [is_List(x) and os.path.join(*x) or x for x in files]
442
existing = list(filter(os.path.exists, files))
444
print "Unexpected files exist: `%s'" % "', `".join(existing)
445
self.fail_test(existing)
448
def must_not_be_writable(self, *files):
449
"""Ensures that the specified file(s) exist and are not writable.
450
An individual file can be specified as a list of directory names,
451
in which case the pathname will be constructed by concatenating
452
them. Exits FAILED if any of the files does not exist or is
455
files = [is_List(x) and os.path.join(*x) or x for x in files]
456
existing, missing = separate_files(files)
457
writable = list(filter(is_writable, existing))
459
print "Missing files: `%s'" % "', `".join(missing)
461
print "Writable files: `%s'" % "', `".join(writable)
462
self.fail_test(missing + writable)
464
def _complete(self, actual_stdout, expected_stdout,
465
actual_stderr, expected_stderr, status, match):
467
Post-processes running a subcommand, checking for failure
468
status and displaying output appropriately.
470
if _failed(self, status):
473
expect = " (expected %s)" % str(status)
474
print "%s returned %s%s" % (self.program, str(_status(self)), expect)
475
print self.banner('STDOUT ')
477
print self.banner('STDERR ')
480
if not expected_stdout is None and not match(actual_stdout, expected_stdout):
481
self.diff(expected_stdout, actual_stdout, 'STDOUT ')
483
print self.banner('STDERR ')
486
if not expected_stderr is None and not match(actual_stderr, expected_stderr):
487
print self.banner('STDOUT ')
489
self.diff(expected_stderr, actual_stderr, 'STDERR ')
492
def start(self, program = None,
495
universal_newlines = None,
498
Starts a program or script for the test environment.
500
This handles the "options" keyword argument and exceptions.
503
options = kw['options']
509
if arguments is None:
512
arguments = options + " " + arguments
514
return TestCmd.start(self, program, interpreter, arguments, universal_newlines,
516
except KeyboardInterrupt:
519
print self.banner('STDOUT ')
524
print self.banner('STDERR ')
529
cmd_args = self.command_args(program, interpreter, arguments)
530
sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
533
def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
535
Finishes and waits for the process being run under control of
536
the specified popen argument. Additional arguments are similar
537
to those of the run() method:
539
stdout The expected standard output from
540
the command. A value of None means
541
don't test standard output.
543
stderr The expected error output from
544
the command. A value of None means
545
don't test error output.
547
status The expected exit status from the
548
command. A value of None means don't
551
TestCmd.finish(self, popen, **kw)
552
match = kw.get('match', self.match)
553
self._complete(self.stdout(), stdout,
554
self.stderr(), stderr, status, match)
556
def run(self, options = None, arguments = None,
557
stdout = None, stderr = '', status = 0, **kw):
558
"""Runs the program under test, checking that the test succeeded.
560
The arguments are the same as the base TestCmd.run() method,
561
with the addition of:
563
options Extra options that get appended to the beginning
566
stdout The expected standard output from
567
the command. A value of None means
568
don't test standard output.
570
stderr The expected error output from
571
the command. A value of None means
572
don't test error output.
574
status The expected exit status from the
575
command. A value of None means don't
578
By default, this expects a successful exit (status = 0), does
579
not test standard output (stdout = None), and expects that error
580
output is empty (stderr = "").
583
if arguments is None:
586
arguments = options + " " + arguments
587
kw['arguments'] = arguments
593
TestCmd.run(self, **kw)
594
self._complete(self.stdout(), stdout,
595
self.stderr(), stderr, status, match)
597
def skip_test(self, message="Skipping test.\n"):
600
Proper test-skipping behavior is dependent on the external
601
TESTCOMMON_PASS_SKIPS environment variable. If set, we treat
602
the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
603
In either case, we print the specified message as an indication
604
that the substance of the test was skipped.
606
(This was originally added to support development under Aegis.
607
Technically, skipping a test is a NO RESULT, but Aegis would
608
treat that as a test failure and prevent the change from going to
609
the next step. Since we ddn't want to force anyone using Aegis
610
to have to install absolutely every tool used by the tests, we
611
would actually report to Aegis that a skipped test has PASSED
612
so that the workflow isn't held up.)
615
sys.stdout.write(message)
617
pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
618
if pass_skips in [None, 0, '0']:
619
# skip=1 means skip this function when showing where this
620
# result came from. They only care about the line where the
621
# script called test.skip_test(), not the line number where
622
# we call test.no_result().
623
self.no_result(skip=1)
625
# We're under the development directory for this change,
626
# so this is an Aegis invocation; pass the test (exit 0).
631
# indent-tabs-mode:nil
633
# vim: set expandtab tabstop=4 shiftwidth=4: