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 TestBase. The test runner should
36
This is meant to become independent of bzr, though that's not quite
41
from unittest import TestResult, TestCase
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):
54
class TestSkipped(Exception):
55
"""Indicates that a test was intentionally skipped, rather than failing."""
59
class TestBase(TestCase):
60
"""Base class for bzr test cases.
62
Just defines some useful helper functions; doesn't actually test
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'
76
super(TestBase, self).setUp()
77
self.log("%s setup" % self.id())
81
super(TestBase, self).tearDown()
82
self.log("%s teardown" % self.id())
86
def formcmd(self, cmd):
87
if isinstance(cmd, basestring):
92
if self.OVERRIDE_PYTHON:
93
cmd.insert(0, self.OVERRIDE_PYTHON)
95
self.log('$ %r' % cmd)
100
def runcmd(self, cmd, retcode=0):
101
"""Run one command and check the return code.
103
Returns a tuple of (stdout,stderr) strings.
105
If a single string is based, it is split into words.
106
For commands that are not simple space-separated words, please
107
pass a list instead."""
110
from subprocess import call
111
except ImportError, e:
116
cmd = self.formcmd(cmd)
118
self.log('$ ' + ' '.join(cmd))
119
actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
121
if retcode != actual_retcode:
122
raise CommandFailed("test failed: %r returned %d, expected %d"
123
% (cmd, actual_retcode, retcode))
126
def backtick(self, cmd, retcode=0):
127
"""Run a command and return its output"""
130
from subprocess import Popen, PIPE
131
except ImportError, e:
135
cmd = self.formcmd(cmd)
136
child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
137
outd, errd = child.communicate()
139
actual_retcode = child.wait()
141
outd = outd.replace('\r', '')
143
if retcode != actual_retcode:
144
raise CommandFailed("test failed: %r returned %d, expected %d"
145
% (cmd, actual_retcode, retcode))
151
def build_tree(self, shape):
152
"""Build a test tree according to a pattern.
154
shape is a sequence of file specifications. If the final
155
character is '/', a directory is created.
157
This doesn't add anything to a branch.
159
# XXX: It's OK to just create them using forward slashes on windows?
162
assert isinstance(name, basestring)
167
print >>f, "contents of", name
172
"""Log a message to a progress file"""
173
# XXX: The problem with this is that code that writes straight
174
# to the log file won't be shown when we display the log
175
# buffer; would be better to not have the in-memory buffer and
176
# instead just a log file per test, which is read in and
177
# displayed if the test fails. That seems to imply one log
178
# per test case, not globally. OK?
179
self._log_buf = self._log_buf + str(msg) + '\n'
180
print >>self.TEST_LOG, msg
183
def check_inventory_shape(self, inv, shape):
185
Compare an inventory to a list of expected names.
187
Fail if they are not precisely equal.
190
shape = list(shape) # copy
191
for path, ie in inv.entries():
192
name = path.replace('\\', '/')
200
self.fail("expected paths not found in inventory: %r" % shape)
202
self.fail("unexpected paths found in inventory: %r" % extras)
205
def check_file_contents(self, filename, expect):
206
self.log("check contents of file %s" % filename)
207
contents = file(filename, 'r').read()
208
if contents != expect:
209
self.log("expected: %r" % expect)
210
self.log("actually: %r" % contents)
211
self.fail("contents of %s not as expected")
215
class InTempDir(TestBase):
216
"""Base class for tests run in a temporary branch."""
219
self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__)
220
os.mkdir(self.test_dir)
221
os.chdir(self.test_dir)
225
os.chdir(self.TEST_ROOT)
231
class _MyResult(TestResult):
235
No special behaviour for now.
237
def __init__(self, out, style):
239
TestResult.__init__(self)
240
assert style in ('none', 'progress', 'verbose')
244
def startTest(self, test):
245
# TODO: Maybe show test.shortDescription somewhere?
247
# python2.3 has the bad habit of just "runit" for doctests
249
what = test.shortDescription()
251
if self.style == 'verbose':
252
print >>self.out, '%-60.60s' % what,
254
elif self.style == 'progress':
257
TestResult.startTest(self, test)
260
def stopTest(self, test):
262
TestResult.stopTest(self, test)
265
def addError(self, test, err):
266
if self.style == 'verbose':
267
print >>self.out, 'ERROR'
268
TestResult.addError(self, test, err)
269
_show_test_failure('error', test, err, self.out)
271
def addFailure(self, test, err):
272
if self.style == 'verbose':
273
print >>self.out, 'FAILURE'
274
TestResult.addFailure(self, test, err)
275
_show_test_failure('failure', test, err, self.out)
277
def addSuccess(self, test):
278
if self.style == 'verbose':
279
print >>self.out, 'OK'
280
TestResult.addSuccess(self, test)
284
def run_suite(suite, name='test', verbose=False):
290
_setup_test_log(name)
291
_setup_test_dir(name)
294
# save stdout & stderr so there's no leakage from code-under-test
295
real_stdout = sys.stdout
296
real_stderr = sys.stderr
297
sys.stdout = sys.stderr = TestBase.TEST_LOG
303
result = _MyResult(real_stdout, style)
306
sys.stdout = real_stdout
307
sys.stderr = real_stderr
309
_show_results(result)
311
return result.wasSuccessful()
315
def _setup_test_log(name):
319
log_filename = os.path.abspath(name + '.log')
320
TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered
322
print >>TestBase.TEST_LOG, "tests run at " + time.ctime()
323
print '%-30s %s' % ('test log', log_filename)
326
def _setup_test_dir(name):
330
TestBase.ORIG_DIR = os.getcwdu()
331
TestBase.TEST_ROOT = os.path.abspath(name + '.tmp')
333
print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT)
335
if os.path.exists(TestBase.TEST_ROOT):
336
shutil.rmtree(TestBase.TEST_ROOT)
337
os.mkdir(TestBase.TEST_ROOT)
338
os.chdir(TestBase.TEST_ROOT)
340
# make a fake bzr directory there to prevent any tests propagating
341
# up onto the source directory's real branch
342
os.mkdir(os.path.join(TestBase.TEST_ROOT, '.bzr'))
346
def _show_results(result):
348
print '%4d tests run' % result.testsRun
349
print '%4d errors' % len(result.errors)
350
print '%4d failures' % len(result.failures)
354
def _show_test_failure(kind, case, exc_info, out):
355
from traceback import print_exception
358
print >>out, '-' * 60
361
desc = case.shortDescription()
363
print >>out, ' (%s)' % desc
365
print_exception(exc_info[0], exc_info[1], exc_info[2], None, out)
367
if isinstance(case, TestBase):
369
print >>out, 'log from this test:'
370
print >>out, case._log_buf
372
print >>out, '-' * 60