841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
1 |
# Copyright (C) 2005 by Canonical Ltd
|
2 |
||
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.
|
|
7 |
||
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.
|
|
12 |
||
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
|
|
16 |
||
17 |
||
18 |
"""Enhanced layer on unittest.
|
|
19 |
||
20 |
This does several things:
|
|
21 |
||
22 |
* nicer reporting as tests run
|
|
23 |
||
24 |
* test code can log messages into a buffer that is recorded to disk
|
|
25 |
and displayed if the test fails
|
|
26 |
||
27 |
* tests can be run in a separate directory, which is useful for code that
|
|
28 |
wants to create files
|
|
29 |
||
30 |
* utilities to run external commands and check their return code
|
|
31 |
and/or output
|
|
32 |
||
33 |
Test cases should normally subclass TestBase. The test runner should
|
|
34 |
call runsuite().
|
|
35 |
||
36 |
This is meant to become independent of bzr, though that's not quite
|
|
37 |
true yet.
|
|
38 |
"""
|
|
39 |
||
40 |
||
41 |
from unittest import TestResult, TestCase |
|
42 |
||
992
by Martin Pool
doc |
43 |
# XXX: Don't need this anymore now we depend on python2.4
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
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") |
|
47 |
||
48 |
||
49 |
class CommandFailed(Exception): |
|
50 |
pass
|
|
51 |
||
52 |
||
53 |
||
54 |
class TestSkipped(Exception): |
|
55 |
"""Indicates that a test was intentionally skipped, rather than failing."""
|
|
56 |
# XXX: Not used yet
|
|
57 |
||
58 |
||
59 |
class TestBase(TestCase): |
|
60 |
"""Base class for bzr test cases.
|
|
61 |
||
62 |
Just defines some useful helper functions; doesn't actually test
|
|
63 |
anything.
|
|
64 |
"""
|
|
65 |
||
66 |
# TODO: Special methods to invoke bzr, so that we can run it
|
|
67 |
# through a specified Python intepreter
|
|
68 |
||
69 |
OVERRIDE_PYTHON = None # to run with alternative python 'python' |
|
70 |
BZRPATH = 'bzr' |
|
71 |
||
72 |
_log_buf = "" |
|
73 |
||
74 |
||
75 |
def setUp(self): |
|
76 |
super(TestBase, self).setUp() |
|
77 |
self.log("%s setup" % self.id()) |
|
78 |
||
79 |
||
80 |
def tearDown(self): |
|
81 |
super(TestBase, self).tearDown() |
|
82 |
self.log("%s teardown" % self.id()) |
|
83 |
self.log('') |
|
842
by Martin Pool
- don't say runit when running tests under python2.3 dammit |
84 |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
85 |
|
86 |
def formcmd(self, cmd): |
|
87 |
if isinstance(cmd, basestring): |
|
88 |
cmd = cmd.split() |
|
89 |
||
90 |
if cmd[0] == 'bzr': |
|
91 |
cmd[0] = self.BZRPATH |
|
92 |
if self.OVERRIDE_PYTHON: |
|
93 |
cmd.insert(0, self.OVERRIDE_PYTHON) |
|
94 |
||
95 |
self.log('$ %r' % cmd) |
|
96 |
||
97 |
return cmd |
|
98 |
||
99 |
||
100 |
def runcmd(self, cmd, retcode=0): |
|
101 |
"""Run one command and check the return code.
|
|
102 |
||
103 |
Returns a tuple of (stdout,stderr) strings.
|
|
104 |
||
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."""
|
|
108 |
try: |
|
109 |
import shutil |
|
110 |
from subprocess import call |
|
111 |
except ImportError, e: |
|
112 |
_need_subprocess() |
|
113 |
raise
|
|
114 |
||
115 |
||
116 |
cmd = self.formcmd(cmd) |
|
117 |
||
118 |
self.log('$ ' + ' '.join(cmd)) |
|
119 |
actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG) |
|
120 |
||
121 |
if retcode != actual_retcode: |
|
122 |
raise CommandFailed("test failed: %r returned %d, expected %d" |
|
123 |
% (cmd, actual_retcode, retcode)) |
|
124 |
||
125 |
||
126 |
def backtick(self, cmd, retcode=0): |
|
127 |
"""Run a command and return its output"""
|
|
128 |
try: |
|
129 |
import shutil |
|
130 |
from subprocess import Popen, PIPE |
|
131 |
except ImportError, e: |
|
132 |
_need_subprocess() |
|
133 |
raise
|
|
134 |
||
135 |
cmd = self.formcmd(cmd) |
|
136 |
child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG) |
|
137 |
outd, errd = child.communicate() |
|
138 |
self.log(outd) |
|
139 |
actual_retcode = child.wait() |
|
140 |
||
141 |
outd = outd.replace('\r', '') |
|
142 |
||
143 |
if retcode != actual_retcode: |
|
144 |
raise CommandFailed("test failed: %r returned %d, expected %d" |
|
145 |
% (cmd, actual_retcode, retcode)) |
|
146 |
||
147 |
return outd |
|
148 |
||
149 |
||
150 |
||
151 |
def build_tree(self, shape): |
|
152 |
"""Build a test tree according to a pattern.
|
|
153 |
||
154 |
shape is a sequence of file specifications. If the final
|
|
155 |
character is '/', a directory is created.
|
|
156 |
||
157 |
This doesn't add anything to a branch.
|
|
158 |
"""
|
|
159 |
# XXX: It's OK to just create them using forward slashes on windows?
|
|
160 |
import os |
|
161 |
for name in shape: |
|
162 |
assert isinstance(name, basestring) |
|
163 |
if name[-1] == '/': |
|
164 |
os.mkdir(name[:-1]) |
|
165 |
else: |
|
166 |
f = file(name, 'wt') |
|
167 |
print >>f, "contents of", name |
|
168 |
f.close() |
|
169 |
||
170 |
||
171 |
def log(self, msg): |
|
172 |
"""Log a message to a progress file"""
|
|
994
by Martin Pool
doc |
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?
|
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
179 |
self._log_buf = self._log_buf + str(msg) + '\n' |
180 |
print >>self.TEST_LOG, msg |
|
181 |
||
182 |
||
183 |
def check_inventory_shape(self, inv, shape): |
|
184 |
"""
|
|
185 |
Compare an inventory to a list of expected names.
|
|
186 |
||
187 |
Fail if they are not precisely equal.
|
|
188 |
"""
|
|
189 |
extras = [] |
|
190 |
shape = list(shape) # copy |
|
191 |
for path, ie in inv.entries(): |
|
192 |
name = path.replace('\\', '/') |
|
193 |
if ie.kind == 'dir': |
|
194 |
name = name + '/' |
|
195 |
if name in shape: |
|
196 |
shape.remove(name) |
|
197 |
else: |
|
198 |
extras.append(name) |
|
199 |
if shape: |
|
200 |
self.fail("expected paths not found in inventory: %r" % shape) |
|
201 |
if extras: |
|
202 |
self.fail("unexpected paths found in inventory: %r" % extras) |
|
203 |
||
204 |
||
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: |
|
916
by Martin Pool
- typo in testsweet |
209 |
self.log("expected: %r" % expect) |
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
210 |
self.log("actually: %r" % contents) |
211 |
self.fail("contents of %s not as expected") |
|
212 |
||
213 |
||
214 |
||
215 |
class InTempDir(TestBase): |
|
216 |
"""Base class for tests run in a temporary branch."""
|
|
217 |
def setUp(self): |
|
218 |
import os |
|
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) |
|
222 |
||
223 |
def tearDown(self): |
|
224 |
import os |
|
225 |
os.chdir(self.TEST_ROOT) |
|
226 |
||
227 |
||
228 |
||
229 |
||
230 |
||
231 |
class _MyResult(TestResult): |
|
232 |
"""
|
|
233 |
Custom TestResult.
|
|
234 |
||
235 |
No special behaviour for now.
|
|
236 |
"""
|
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
237 |
def __init__(self, out, style): |
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
238 |
self.out = out |
239 |
TestResult.__init__(self) |
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
240 |
assert style in ('none', 'progress', 'verbose') |
241 |
self.style = style |
|
242 |
||
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
243 |
|
244 |
def startTest(self, test): |
|
245 |
# TODO: Maybe show test.shortDescription somewhere?
|
|
842
by Martin Pool
- don't say runit when running tests under python2.3 dammit |
246 |
what = test.id() |
247 |
# python2.3 has the bad habit of just "runit" for doctests
|
|
248 |
if what == 'runit': |
|
249 |
what = test.shortDescription() |
|
250 |
||
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
251 |
if self.style == 'verbose': |
252 |
print >>self.out, '%-60.60s' % what, |
|
253 |
self.out.flush() |
|
254 |
elif self.style == 'progress': |
|
255 |
self.out.write('~') |
|
256 |
self.out.flush() |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
257 |
TestResult.startTest(self, test) |
258 |
||
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
259 |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
260 |
def stopTest(self, test): |
261 |
# print
|
|
262 |
TestResult.stopTest(self, test) |
|
263 |
||
264 |
||
265 |
def addError(self, test, err): |
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
266 |
if self.style == 'verbose': |
267 |
print >>self.out, 'ERROR' |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
268 |
TestResult.addError(self, test, err) |
269 |
_show_test_failure('error', test, err, self.out) |
|
270 |
||
271 |
def addFailure(self, test, err): |
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
272 |
if self.style == 'verbose': |
273 |
print >>self.out, 'FAILURE' |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
274 |
TestResult.addFailure(self, test, err) |
275 |
_show_test_failure('failure', test, err, self.out) |
|
276 |
||
277 |
def addSuccess(self, test): |
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
278 |
if self.style == 'verbose': |
279 |
print >>self.out, 'OK' |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
280 |
TestResult.addSuccess(self, test) |
281 |
||
282 |
||
283 |
||
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
284 |
def run_suite(suite, name='test', verbose=False): |
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
285 |
import os |
286 |
import shutil |
|
287 |
import time |
|
288 |
import sys |
|
289 |
||
290 |
_setup_test_log(name) |
|
291 |
_setup_test_dir(name) |
|
292 |
print
|
|
293 |
||
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 |
|
298 |
try: |
|
965
by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it |
299 |
if verbose: |
300 |
style = 'verbose' |
|
301 |
else: |
|
302 |
style = 'progress' |
|
303 |
result = _MyResult(real_stdout, style) |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
304 |
suite.run(result) |
305 |
finally: |
|
306 |
sys.stdout = real_stdout |
|
307 |
sys.stderr = real_stderr |
|
308 |
||
309 |
_show_results(result) |
|
310 |
||
311 |
return result.wasSuccessful() |
|
312 |
||
313 |
||
314 |
||
315 |
def _setup_test_log(name): |
|
316 |
import time |
|
317 |
import os |
|
318 |
||
319 |
log_filename = os.path.abspath(name + '.log') |
|
320 |
TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered |
|
321 |
||
322 |
print >>TestBase.TEST_LOG, "tests run at " + time.ctime() |
|
323 |
print '%-30s %s' % ('test log', log_filename) |
|
324 |
||
325 |
||
326 |
def _setup_test_dir(name): |
|
327 |
import os |
|
328 |
import shutil |
|
329 |
||
330 |
TestBase.ORIG_DIR = os.getcwdu() |
|
331 |
TestBase.TEST_ROOT = os.path.abspath(name + '.tmp') |
|
332 |
||
333 |
print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT) |
|
334 |
||
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) |
|
339 |
||
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')) |
|
343 |
||
344 |
||
345 |
||
346 |
def _show_results(result): |
|
347 |
print
|
|
348 |
print '%4d tests run' % result.testsRun |
|
349 |
print '%4d errors' % len(result.errors) |
|
350 |
print '%4d failures' % len(result.failures) |
|
351 |
||
352 |
||
353 |
||
354 |
def _show_test_failure(kind, case, exc_info, out): |
|
355 |
from traceback import print_exception |
|
993
by Martin Pool
- better display of test failures in non-verbose mode |
356 |
|
357 |
print >>out |
|
841
by Martin Pool
- Start splitting bzr-independent parts of the test framework into |
358 |
print >>out, '-' * 60 |
359 |
print >>out, case |
|
360 |
||
361 |
desc = case.shortDescription() |
|
362 |
if desc: |
|
363 |
print >>out, ' (%s)' % desc |
|
364 |
||
365 |
print_exception(exc_info[0], exc_info[1], exc_info[2], None, out) |
|
366 |
||
367 |
if isinstance(case, TestBase): |
|
368 |
print >>out |
|
369 |
print >>out, 'log from this test:' |
|
370 |
print >>out, case._log_buf |
|
371 |
||
372 |
print >>out, '-' * 60 |
|
373 |
||
374 |