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 test cases.
61
Just defines some useful helper functions; doesn't actually test
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'
75
super(TestCase, self).setUp()
76
# save stdout & stderr so there's no leakage from code-under-test
77
self.real_stdout = sys.stdout
78
self.real_stderr = sys.stderr
79
sys.stdout = sys.stderr = TestCase.TEST_LOG
80
self.log("%s setup" % self.id())
83
sys.stdout = self.real_stdout
84
sys.stderr = self.real_stderr
85
self.log("%s teardown" % self.id())
87
super(TestCase, self).tearDown()
89
def formcmd(self, cmd):
90
if isinstance(cmd, basestring):
95
if self.OVERRIDE_PYTHON:
96
cmd.insert(0, self.OVERRIDE_PYTHON)
98
self.log('$ %r' % cmd)
103
def runcmd(self, cmd, retcode=0):
104
"""Run one command and check the return code.
106
Returns a tuple of (stdout,stderr) strings.
108
If a single string is based, it is split into words.
109
For commands that are not simple space-separated words, please
110
pass a list instead."""
113
from subprocess import call
114
except ImportError, e:
119
cmd = self.formcmd(cmd)
121
self.log('$ ' + ' '.join(cmd))
122
actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
124
if retcode != actual_retcode:
125
raise CommandFailed("test failed: %r returned %d, expected %d"
126
% (cmd, actual_retcode, retcode))
129
def backtick(self, cmd, retcode=0):
130
"""Run a command and return its output"""
133
from subprocess import Popen, PIPE
134
except ImportError, e:
138
cmd = self.formcmd(cmd)
139
child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
140
outd, errd = child.communicate()
142
actual_retcode = child.wait()
144
outd = outd.replace('\r', '')
146
if retcode != actual_retcode:
147
raise CommandFailed("test failed: %r returned %d, expected %d"
148
% (cmd, actual_retcode, retcode))
154
def build_tree(self, shape):
155
"""Build a test tree according to a pattern.
157
shape is a sequence of file specifications. If the final
158
character is '/', a directory is created.
160
This doesn't add anything to a branch.
162
# XXX: It's OK to just create them using forward slashes on windows?
165
assert isinstance(name, basestring)
170
print >>f, "contents of", name
175
"""Log a message to a progress file"""
176
# XXX: The problem with this is that code that writes straight
177
# to the log file won't be shown when we display the log
178
# buffer; would be better to not have the in-memory buffer and
179
# instead just a log file per test, which is read in and
180
# displayed if the test fails. That seems to imply one log
181
# per test case, not globally. OK?
182
self._log_buf = self._log_buf + str(msg) + '\n'
183
print >>self.TEST_LOG, msg
186
def check_inventory_shape(self, inv, shape):
188
Compare an inventory to a list of expected names.
190
Fail if they are not precisely equal.
193
shape = list(shape) # copy
194
for path, ie in inv.entries():
195
name = path.replace('\\', '/')
203
self.fail("expected paths not found in inventory: %r" % shape)
205
self.fail("unexpected paths found in inventory: %r" % extras)
208
def check_file_contents(self, filename, expect):
209
self.log("check contents of file %s" % filename)
210
contents = file(filename, 'r').read()
211
if contents != expect:
212
self.log("expected: %r" % expect)
213
self.log("actually: %r" % contents)
214
self.fail("contents of %s not as expected")
218
class InTempDir(TestCase):
219
"""Base class for tests run in a temporary branch."""
221
super(InTempDir, self).setUp()
223
self.test_dir = os.path.join(self.TEST_ROOT, self.__class__.__name__)
224
os.mkdir(self.test_dir)
225
os.chdir(self.test_dir)
229
os.chdir(self.TEST_ROOT)
230
super(InTempDir, self).tearDown()
233
class _MyResult(unittest._TextTestResult):
237
No special behaviour for now.
239
def __init__(self, out, style):
240
super(_MyResult, self).__init__(out, False, 0)
242
assert style in ('none', 'progress', 'verbose')
245
def startTest(self, test):
246
super(_MyResult, self).startTest(test)
247
# TODO: Maybe show test.shortDescription somewhere?
249
# python2.3 has the bad habit of just "runit" for doctests
251
what = test.shortDescription()
252
if self.style == 'verbose':
253
print >>self.out, '%-60.60s' % what,
256
def addError(self, test, err):
257
if self.style == 'verbose':
258
print >>self.out, 'ERROR'
259
elif self.style == 'progress':
260
self.stream.write('E')
262
super(_MyResult, self).addError(test, err)
264
def addFailure(self, test, err):
265
if self.style == 'verbose':
266
print >>self.out, 'FAILURE'
267
elif self.style == 'progress':
268
self.stream.write('F')
270
super(_MyResult, self).addFailure(test, err)
272
def addSuccess(self, test):
273
if self.style == 'verbose':
274
print >>self.out, 'OK'
275
elif self.style == 'progress':
276
self.stream.write('~')
278
super(_MyResult, self).addSuccess(test)
280
def printErrors(self):
281
if self.style == 'progress':
282
self.stream.writeln()
283
super(_MyResult, self).printErrors()
285
def printErrorList(self, flavour, errors):
286
for test, err in errors:
287
self.stream.writeln(self.separator1)
288
self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
289
self.stream.writeln(self.separator2)
290
self.stream.writeln("%s" % err)
291
if isinstance(test, TestCase):
292
self.stream.writeln()
293
self.stream.writeln('log from this test:')
294
print >>self.stream, test._log_buf
297
class TestSuite(unittest.TestSuite):
299
def __init__(self, tests=(), name='test'):
300
super(TestSuite, self).__init__(tests)
303
def run(self, result):
308
self._setup_test_log()
309
self._setup_test_dir()
312
return super(TestSuite,self).run(result)
314
def _setup_test_log(self):
318
log_filename = os.path.abspath(self._name + '.log')
320
TestCase.TEST_LOG = open(log_filename, 'wt', buffering=1)
322
print >>TestCase.TEST_LOG, "tests run at " + time.ctime()
323
print '%-30s %s' % ('test log', log_filename)
325
def _setup_test_dir(self):
329
TestCase.ORIG_DIR = os.getcwdu()
330
TestCase.TEST_ROOT = os.path.abspath(self._name + '.tmp')
332
print '%-30s %s' % ('running tests in', TestCase.TEST_ROOT)
334
if os.path.exists(TestCase.TEST_ROOT):
335
shutil.rmtree(TestCase.TEST_ROOT)
336
os.mkdir(TestCase.TEST_ROOT)
337
os.chdir(TestCase.TEST_ROOT)
339
# make a fake bzr directory there to prevent any tests propagating
340
# up onto the source directory's real branch
341
os.mkdir(os.path.join(TestCase.TEST_ROOT, '.bzr'))
344
class TextTestRunner(unittest.TextTestRunner):
346
def __init__(self, stream=sys.stderr, descriptions=1, verbosity=0, style='progress'):
347
super(TextTestRunner, self).__init__(stream, descriptions, verbosity)
350
def _makeResult(self):
351
return _MyResult(self.stream, self.style)
353
# If we want the old 4 line summary output (count, 0 failures, 0 errors)
354
# we can override run() too.
357
def run_suite(a_suite, name='test', verbose=False):
358
suite = TestSuite((a_suite,),name)
363
runner = TextTestRunner(stream=sys.stdout, style=style)
364
result = runner.run(suite)
365
return result.wasSuccessful()