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
42
from bzrlib.selftest import TestUtil
44
# XXX: Don't need this anymore now we depend on python2.4
45
def _need_subprocess():
46
sys.stderr.write("sorry, this test suite requires the subprocess module\n"
47
"this is shipped with python2.4 and available separately for 2.3\n")
50
class CommandFailed(Exception):
54
class TestSkipped(Exception):
55
"""Indicates that a test was intentionally skipped, rather than failing."""
59
class TestCase(unittest.TestCase):
60
"""Base class for bzr unit tests.
62
Tests that need access to disk resources should subclass
63
FunctionalTestCase not TestCase.
66
# TODO: Special methods to invoke bzr, so that we can run it
67
# through a specified Python intepreter
69
OVERRIDE_PYTHON = None # to run with alternative python 'python'
72
def apply_redirected(self, stdin=None, stdout=None, stderr=None,
73
a_callable=None, *args, **kwargs):
74
"""Call callable with redirected std io pipes.
76
Returns the return code."""
77
from StringIO import StringIO
78
if not callable(a_callable):
79
raise ValueError("a_callable must be callable.")
83
stdout = self.TEST_LOG
85
stderr = self.TEST_LOG
86
real_stdin = sys.stdin
87
real_stdout = sys.stdout
88
real_stderr = sys.stderr
94
result = a_callable(*args, **kwargs)
96
sys.stdout = real_stdout
97
sys.stderr = real_stderr
98
sys.stdin = real_stdin
102
super(TestCase, self).setUp()
103
# setup a temporary log for the test
105
self.TEST_LOG = tempfile.NamedTemporaryFile(mode='wt', bufsize=0)
106
self.log("%s setup" % self.id())
109
self.log("%s teardown" % self.id())
111
super(TestCase, self).tearDown()
114
"""Log a message to a progress file"""
115
print >>self.TEST_LOG, msg
117
def check_inventory_shape(self, inv, shape):
119
Compare an inventory to a list of expected names.
121
Fail if they are not precisely equal.
124
shape = list(shape) # copy
125
for path, ie in inv.entries():
126
name = path.replace('\\', '/')
134
self.fail("expected paths not found in inventory: %r" % shape)
136
self.fail("unexpected paths found in inventory: %r" % extras)
139
"""Get the log the test case used. This can only be called once,
140
after which an exception will be raised.
142
self.TEST_LOG.flush()
143
log = open(self.TEST_LOG.name, 'rt').read()
144
self.TEST_LOG.close()
148
class FunctionalTestCase(TestCase):
149
"""Base class for tests that perform function testing - running bzr,
150
using files on disk, and similar activities.
152
InTempDir is an old alias for FunctionalTestCase.
158
def check_file_contents(self, filename, expect):
159
self.log("check contents of file %s" % filename)
160
contents = file(filename, 'r').read()
161
if contents != expect:
162
self.log("expected: %r" % expect)
163
self.log("actually: %r" % contents)
164
self.fail("contents of %s not as expected")
166
def _make_test_root(self):
171
if FunctionalTestCase.TEST_ROOT is not None:
173
FunctionalTestCase.TEST_ROOT = os.path.abspath(
174
tempfile.mkdtemp(suffix='.tmp',
175
prefix=self._TEST_NAME + '-',
178
# make a fake bzr directory there to prevent any tests propagating
179
# up onto the source directory's real branch
180
os.mkdir(os.path.join(FunctionalTestCase.TEST_ROOT, '.bzr'))
183
super(FunctionalTestCase, self).setUp()
185
self._make_test_root()
186
self._currentdir = os.getcwdu()
187
self.test_dir = os.path.join(self.TEST_ROOT, self.id())
188
os.mkdir(self.test_dir)
189
os.chdir(self.test_dir)
193
os.chdir(self._currentdir)
194
super(FunctionalTestCase, self).tearDown()
196
def formcmd(self, cmd):
197
if isinstance(cmd, basestring):
200
cmd[0] = self.BZRPATH
201
if self.OVERRIDE_PYTHON:
202
cmd.insert(0, self.OVERRIDE_PYTHON)
203
self.log('$ %r' % cmd)
206
def runcmd(self, cmd, retcode=0):
207
"""Run one command and check the return code.
209
Returns a tuple of (stdout,stderr) strings.
211
If a single string is based, it is split into words.
212
For commands that are not simple space-separated words, please
213
pass a list instead."""
216
from subprocess import call
217
except ImportError, e:
220
cmd = self.formcmd(cmd)
221
self.log('$ ' + ' '.join(cmd))
222
actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
223
if retcode != actual_retcode:
224
raise CommandFailed("test failed: %r returned %d, expected %d"
225
% (cmd, actual_retcode, retcode))
227
def backtick(self, cmd, retcode=0):
228
"""Run a command and return its output"""
231
from subprocess import Popen, PIPE
232
except ImportError, e:
235
cmd = self.formcmd(cmd)
236
child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
237
outd, errd = child.communicate()
239
actual_retcode = child.wait()
240
outd = outd.replace('\r', '')
241
if retcode != actual_retcode:
242
raise CommandFailed("test failed: %r returned %d, expected %d"
243
% (cmd, actual_retcode, retcode))
246
def build_tree(self, shape):
247
"""Build a test tree according to a pattern.
249
shape is a sequence of file specifications. If the final
250
character is '/', a directory is created.
252
This doesn't add anything to a branch.
254
# XXX: It's OK to just create them using forward slashes on windows?
257
assert isinstance(name, basestring)
262
print >>f, "contents of", name
265
InTempDir = FunctionalTestCase
268
class EarlyStoppingTestResultAdapter(object):
269
"""An adapter for TestResult to stop at the first first failure or error"""
271
def __init__(self, result):
272
self._result = result
274
def addError(self, test, err):
275
self._result.addError(test, err)
278
def addFailure(self, test, err):
279
self._result.addFailure(test, err)
282
def __getattr__(self, name):
283
return getattr(self._result, name)
285
def __setattr__(self, name, value):
286
if name == '_result':
287
object.__setattr__(self, name, value)
288
return setattr(self._result, name, value)
291
class _MyResult(unittest._TextTestResult):
295
No special behaviour for now.
298
def startTest(self, test):
299
unittest.TestResult.startTest(self, test)
300
# TODO: Maybe show test.shortDescription somewhere?
302
# python2.3 has the bad habit of just "runit" for doctests
304
what = test.shortDescription()
306
self.stream.write('%-60.60s' % what)
309
def addError(self, test, err):
310
super(_MyResult, self).addError(test, err)
313
def addFailure(self, test, err):
314
super(_MyResult, self).addFailure(test, err)
317
def addSuccess(self, test):
319
self.stream.writeln('OK')
321
self.stream.write('~')
323
unittest.TestResult.addSuccess(self, test)
325
def printErrorList(self, flavour, errors):
326
for test, err in errors:
327
self.stream.writeln(self.separator1)
328
self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
329
self.stream.writeln(self.separator2)
330
self.stream.writeln("%s" % err)
331
if isinstance(test, TestCase):
332
self.stream.writeln()
333
self.stream.writeln('log from this test:')
334
print >>self.stream, test._get_log()
337
class TextTestRunner(unittest.TextTestRunner):
339
def _makeResult(self):
340
result = _MyResult(self.stream, self.descriptions, self.verbosity)
341
return EarlyStoppingTestResultAdapter(result)
344
class filteringVisitor(TestUtil.TestVisitor):
345
"""I accruse all the testCases I visit that pass a regexp filter on id
349
def __init__(self, filter):
351
TestUtil.TestVisitor.__init__(self)
353
self.filter=re.compile(filter)
356
"""answer the suite we are building"""
357
if self._suite is None:
358
self._suite=TestUtil.TestSuite()
361
def visitCase(self, aCase):
362
if self.filter.match(aCase.id()):
363
self.suite().addTest(aCase)
366
def run_suite(suite, name='test', verbose=False, pattern=".*"):
368
FunctionalTestCase._TEST_NAME = name
373
runner = TextTestRunner(stream=sys.stdout,
376
visitor = filteringVisitor(pattern)
378
result = runner.run(visitor.suite())
379
# This is still a little bogus,
380
# but only a little. Folk not using our testrunner will
381
# have to delete their temp directories themselves.
382
if result.wasSuccessful():
383
if FunctionalTestCase.TEST_ROOT is not None:
384
shutil.rmtree(FunctionalTestCase.TEST_ROOT)
386
print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
387
return result.wasSuccessful()