1
# Copyright (C) 2005 by Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Enhanced layer on unittest.
20
This does several things:
22
* nicer reporting as tests run
24
* test code can log messages into a buffer that is recorded to disk
25
and displayed if the test fails
27
* tests can be run in a separate directory, which is useful for code that
30
* utilities to run external commands and check their return code
33
Test cases should normally subclass testsweet.TestCase. The test runner should
36
This is meant to become independent of bzr, though that's not quite
43
# XXX: Don't need this anymore now we depend on python2.4
44
def _need_subprocess():
45
sys.stderr.write("sorry, this test suite requires the subprocess module\n"
46
"this is shipped with python2.4 and available separately for 2.3\n")
49
class CommandFailed(Exception):
53
class TestSkipped(Exception):
54
"""Indicates that a test was intentionally skipped, rather than failing."""
58
class TestCase(unittest.TestCase):
59
"""Base class for bzr unit tests.
61
Tests that need access to disk resources should subclass
62
FunctionalTestCase not TestCase.
65
# TODO: Special methods to invoke bzr, so that we can run it
66
# through a specified Python intepreter
68
OVERRIDE_PYTHON = None # to run with alternative python 'python'
71
def apply_redirected(self, stdin=None, stdout=None, stderr=None,
72
a_callable=None, *args, **kwargs):
73
"""Call callable with redirected std io pipes.
75
Returns the return code."""
76
from StringIO import StringIO
77
if not callable(a_callable):
78
raise ValueError("a_callable must be callable.")
82
stdout = self.TEST_LOG
84
stderr = self.TEST_LOG
85
real_stdin = sys.stdin
86
real_stdout = sys.stdout
87
real_stderr = sys.stderr
93
result = a_callable(*args, **kwargs)
95
sys.stdout = real_stdout
96
sys.stderr = real_stderr
97
sys.stdin = real_stdin
101
super(TestCase, self).setUp()
102
# setup a temporary log for the test
104
self.TEST_LOG = tempfile.NamedTemporaryFile(mode='wt', bufsize=0)
105
self.log("%s setup" % self.id())
108
self.log("%s teardown" % self.id())
110
super(TestCase, self).tearDown()
113
"""Log a message to a progress file"""
114
print >>self.TEST_LOG, msg
116
def check_inventory_shape(self, inv, shape):
118
Compare an inventory to a list of expected names.
120
Fail if they are not precisely equal.
123
shape = list(shape) # copy
124
for path, ie in inv.entries():
125
name = path.replace('\\', '/')
133
self.fail("expected paths not found in inventory: %r" % shape)
135
self.fail("unexpected paths found in inventory: %r" % extras)
138
"""Get the log the test case used. This can only be called once,
139
after which an exception will be raised.
141
self.TEST_LOG.flush()
142
log = open(self.TEST_LOG.name, 'rt').read()
143
self.TEST_LOG.close()
147
class FunctionalTestCase(TestCase):
148
"""Base class for tests that perform function testing - running bzr,
149
using files on disk, and similar activities.
151
InTempDir is an old alias for FunctionalTestCase.
157
def check_file_contents(self, filename, expect):
158
self.log("check contents of file %s" % filename)
159
contents = file(filename, 'r').read()
160
if contents != expect:
161
self.log("expected: %r" % expect)
162
self.log("actually: %r" % contents)
163
self.fail("contents of %s not as expected")
165
def _make_test_root(self):
170
if FunctionalTestCase.TEST_ROOT is not None:
172
FunctionalTestCase.TEST_ROOT = os.path.abspath(
173
tempfile.mkdtemp(suffix='.tmp',
174
prefix=self._TEST_NAME + '-',
177
# make a fake bzr directory there to prevent any tests propagating
178
# up onto the source directory's real branch
179
os.mkdir(os.path.join(FunctionalTestCase.TEST_ROOT, '.bzr'))
182
super(FunctionalTestCase, self).setUp()
184
self._make_test_root()
185
self._currentdir = os.getcwdu()
186
self.test_dir = os.path.join(self.TEST_ROOT, self.id())
187
os.mkdir(self.test_dir)
188
os.chdir(self.test_dir)
192
os.chdir(self._currentdir)
193
super(FunctionalTestCase, self).tearDown()
195
def formcmd(self, cmd):
196
if isinstance(cmd, basestring):
199
cmd[0] = self.BZRPATH
200
if self.OVERRIDE_PYTHON:
201
cmd.insert(0, self.OVERRIDE_PYTHON)
202
self.log('$ %r' % cmd)
205
def runcmd(self, cmd, retcode=0):
206
"""Run one command and check the return code.
208
Returns a tuple of (stdout,stderr) strings.
210
If a single string is based, it is split into words.
211
For commands that are not simple space-separated words, please
212
pass a list instead."""
215
from subprocess import call
216
except ImportError, e:
219
cmd = self.formcmd(cmd)
220
self.log('$ ' + ' '.join(cmd))
221
actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
222
if retcode != actual_retcode:
223
raise CommandFailed("test failed: %r returned %d, expected %d"
224
% (cmd, actual_retcode, retcode))
226
def backtick(self, cmd, retcode=0):
227
"""Run a command and return its output"""
230
from subprocess import Popen, PIPE
231
except ImportError, e:
234
cmd = self.formcmd(cmd)
235
child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
236
outd, errd = child.communicate()
238
actual_retcode = child.wait()
239
outd = outd.replace('\r', '')
240
if retcode != actual_retcode:
241
raise CommandFailed("test failed: %r returned %d, expected %d"
242
% (cmd, actual_retcode, retcode))
245
def build_tree(self, shape):
246
"""Build a test tree according to a pattern.
248
shape is a sequence of file specifications. If the final
249
character is '/', a directory is created.
251
This doesn't add anything to a branch.
253
# XXX: It's OK to just create them using forward slashes on windows?
256
assert isinstance(name, basestring)
261
print >>f, "contents of", name
264
InTempDir = FunctionalTestCase
267
class _MyResult(unittest._TextTestResult):
271
No special behaviour for now.
274
def startTest(self, test):
275
unittest.TestResult.startTest(self, test)
276
# TODO: Maybe show test.shortDescription somewhere?
278
# python2.3 has the bad habit of just "runit" for doctests
280
what = test.shortDescription()
282
self.stream.write('%-60.60s' % what)
285
def addError(self, test, err):
286
super(_MyResult, self).addError(test, err)
289
def addFailure(self, test, err):
290
super(_MyResult, self).addFailure(test, err)
293
def addSuccess(self, test):
295
self.stream.writeln('OK')
297
self.stream.write('~')
299
unittest.TestResult.addSuccess(self, test)
301
def printErrorList(self, flavour, errors):
302
for test, err in errors:
303
self.stream.writeln(self.separator1)
304
self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
305
self.stream.writeln(self.separator2)
306
self.stream.writeln("%s" % err)
307
if isinstance(test, TestCase):
308
self.stream.writeln()
309
self.stream.writeln('log from this test:')
310
print >>self.stream, test._get_log()
313
class TextTestRunner(unittest.TextTestRunner):
315
def _makeResult(self):
316
return _MyResult(self.stream, self.descriptions, self.verbosity)
319
def run_suite(suite, name='test', verbose=False):
321
FunctionalTestCase._TEST_NAME = name
326
runner = TextTestRunner(stream=sys.stdout,
329
result = runner.run(suite)
330
# This is still a little bogus,
331
# but only a little. Folk not using our testrunner will
332
# have to delete their temp directories themselves.
333
if result.wasSuccessful():
334
shutil.rmtree(FunctionalTestCase.TEST_ROOT)
336
print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
337
return result.wasSuccessful()