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