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