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
 
 
42
from bzrlib.selftest import TestUtil
 
 
44
# XXX: Don't need this anymore now we depend on python2.4
 
 
45
def _need_subprocess():
 
 
46
    sys.stderr.write("sorry, this test suite requires the subprocess module\n"
 
 
47
                     "this is shipped with python2.4 and available separately for 2.3\n")
 
 
50
class CommandFailed(Exception):
 
 
54
class TestSkipped(Exception):
 
 
55
    """Indicates that a test was intentionally skipped, rather than failing."""
 
 
59
class TestCase(unittest.TestCase):
 
 
60
    """Base class for bzr unit tests.
 
 
62
    Tests that need access to disk resources should subclass 
 
 
63
    FunctionalTestCase not TestCase.
 
 
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'
 
 
72
    def apply_redirected(self, stdin=None, stdout=None, stderr=None,
 
 
73
                         a_callable=None, *args, **kwargs):
 
 
74
        """Call callable with redirected std io pipes.
 
 
76
        Returns the return code."""
 
 
77
        from StringIO import StringIO
 
 
78
        if not callable(a_callable):
 
 
79
            raise ValueError("a_callable must be callable.")
 
 
83
            stdout = self.TEST_LOG
 
 
85
            stderr = self.TEST_LOG
 
 
86
        real_stdin = sys.stdin
 
 
87
        real_stdout = sys.stdout
 
 
88
        real_stderr = sys.stderr
 
 
94
            result = a_callable(*args, **kwargs)
 
 
96
            sys.stdout = real_stdout
 
 
97
            sys.stderr = real_stderr
 
 
98
            sys.stdin = real_stdin
 
 
102
        super(TestCase, self).setUp()
 
 
103
        # setup a temporary log for the test 
 
 
105
        self.TEST_LOG = tempfile.NamedTemporaryFile(mode='wt', bufsize=0)
 
 
106
        self.log("%s setup" % self.id())
 
 
109
        self.log("%s teardown" % self.id())
 
 
111
        super(TestCase, self).tearDown()
 
 
114
        """Log a message to a progress file"""
 
 
115
        print >>self.TEST_LOG, msg
 
 
117
    def check_inventory_shape(self, inv, shape):
 
 
119
        Compare an inventory to a list of expected names.
 
 
121
        Fail if they are not precisely equal.
 
 
124
        shape = list(shape)             # copy
 
 
125
        for path, ie in inv.entries():
 
 
126
            name = path.replace('\\', '/')
 
 
134
            self.fail("expected paths not found in inventory: %r" % shape)
 
 
136
            self.fail("unexpected paths found in inventory: %r" % extras)
 
 
139
        """Get the log the test case used. This can only be called once,
 
 
140
        after which an exception will be raised.
 
 
142
        self.TEST_LOG.flush()
 
 
143
        log = open(self.TEST_LOG.name, 'rt').read()
 
 
144
        self.TEST_LOG.close()
 
 
148
class FunctionalTestCase(TestCase):
 
 
149
    """Base class for tests that perform function testing - running bzr,
 
 
150
    using files on disk, and similar activities.
 
 
152
    InTempDir is an old alias for FunctionalTestCase.
 
 
158
    def check_file_contents(self, filename, expect):
 
 
159
        self.log("check contents of file %s" % filename)
 
 
160
        contents = file(filename, 'r').read()
 
 
161
        if contents != expect:
 
 
162
            self.log("expected: %r" % expect)
 
 
163
            self.log("actually: %r" % contents)
 
 
164
            self.fail("contents of %s not as expected")
 
 
166
    def _make_test_root(self):
 
 
171
        if FunctionalTestCase.TEST_ROOT is not None:
 
 
173
        FunctionalTestCase.TEST_ROOT = os.path.abspath(
 
 
174
                                 tempfile.mkdtemp(suffix='.tmp',
 
 
175
                                                  prefix=self._TEST_NAME + '-',
 
 
178
        # make a fake bzr directory there to prevent any tests propagating
 
 
179
        # up onto the source directory's real branch
 
 
180
        os.mkdir(os.path.join(FunctionalTestCase.TEST_ROOT, '.bzr'))
 
 
183
        super(FunctionalTestCase, self).setUp()
 
 
185
        self._make_test_root()
 
 
186
        self._currentdir = os.getcwdu()
 
 
187
        self.test_dir = os.path.join(self.TEST_ROOT, self.id())
 
 
188
        os.mkdir(self.test_dir)
 
 
189
        os.chdir(self.test_dir)
 
 
193
        os.chdir(self._currentdir)
 
 
194
        super(FunctionalTestCase, self).tearDown()
 
 
196
    def formcmd(self, cmd):
 
 
197
        if isinstance(cmd, basestring):
 
 
200
            cmd[0] = self.BZRPATH
 
 
201
            if self.OVERRIDE_PYTHON:
 
 
202
                cmd.insert(0, self.OVERRIDE_PYTHON)
 
 
203
        self.log('$ %r' % cmd)
 
 
206
    def runcmd(self, cmd, retcode=0):
 
 
207
        """Run one command and check the return code.
 
 
209
        Returns a tuple of (stdout,stderr) strings.
 
 
211
        If a single string is based, it is split into words.
 
 
212
        For commands that are not simple space-separated words, please
 
 
213
        pass a list instead."""
 
 
216
            from subprocess import call
 
 
217
        except ImportError, e:
 
 
220
        cmd = self.formcmd(cmd)
 
 
221
        self.log('$ ' + ' '.join(cmd))
 
 
222
        actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
 
 
223
        if retcode != actual_retcode:
 
 
224
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
 
225
                                % (cmd, actual_retcode, retcode))
 
 
227
    def backtick(self, cmd, retcode=0):
 
 
228
        """Run a command and return its output"""
 
 
231
            from subprocess import Popen, PIPE
 
 
232
        except ImportError, e:
 
 
235
        cmd = self.formcmd(cmd)
 
 
236
        child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
 
 
237
        outd, errd = child.communicate()
 
 
239
        actual_retcode = child.wait()
 
 
240
        outd = outd.replace('\r', '')
 
 
241
        if retcode != actual_retcode:
 
 
242
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
 
243
                                % (cmd, actual_retcode, retcode))
 
 
246
    def build_tree(self, shape):
 
 
247
        """Build a test tree according to a pattern.
 
 
249
        shape is a sequence of file specifications.  If the final
 
 
250
        character is '/', a directory is created.
 
 
252
        This doesn't add anything to a branch.
 
 
254
        # XXX: It's OK to just create them using forward slashes on windows?
 
 
257
            assert isinstance(name, basestring)
 
 
262
                print >>f, "contents of", name
 
 
265
InTempDir = FunctionalTestCase
 
 
268
class EarlyStoppingTestResultAdapter(object):
 
 
269
    """An adapter for TestResult to stop at the first first failure or error"""
 
 
271
    def __init__(self, result):
 
 
272
        self._result = result
 
 
274
    def addError(self, test, err):
 
 
275
        self._result.addError(test, err)
 
 
278
    def addFailure(self, test, err):
 
 
279
        self._result.addFailure(test, err)
 
 
282
    def __getattr__(self, name):
 
 
283
        return getattr(self._result, name)
 
 
285
    def __setattr__(self, name, value):
 
 
286
        if name == '_result':
 
 
287
            object.__setattr__(self, name, value)
 
 
288
        return setattr(self._result, name, value)
 
 
291
class _MyResult(unittest._TextTestResult):
 
 
295
    No special behaviour for now.
 
 
298
    def startTest(self, test):
 
 
299
        unittest.TestResult.startTest(self, test)
 
 
300
        # TODO: Maybe show test.shortDescription somewhere?
 
 
302
        # python2.3 has the bad habit of just "runit" for doctests
 
 
304
            what = test.shortDescription()
 
 
306
            self.stream.write('%-60.60s' % what)
 
 
309
    def addError(self, test, err):
 
 
310
        super(_MyResult, self).addError(test, err)
 
 
313
    def addFailure(self, test, err):
 
 
314
        super(_MyResult, self).addFailure(test, err)
 
 
317
    def addSuccess(self, test):
 
 
319
            self.stream.writeln('OK')
 
 
321
            self.stream.write('~')
 
 
323
        unittest.TestResult.addSuccess(self, test)
 
 
325
    def printErrorList(self, flavour, errors):
 
 
326
        for test, err in errors:
 
 
327
            self.stream.writeln(self.separator1)
 
 
328
            self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
 
 
329
            self.stream.writeln(self.separator2)
 
 
330
            self.stream.writeln("%s" % err)
 
 
331
            if isinstance(test, TestCase):
 
 
332
                self.stream.writeln()
 
 
333
                self.stream.writeln('log from this test:')
 
 
334
                print >>self.stream, test._get_log()
 
 
337
class TextTestRunner(unittest.TextTestRunner):
 
 
339
    def _makeResult(self):
 
 
340
        result = _MyResult(self.stream, self.descriptions, self.verbosity)
 
 
341
        return EarlyStoppingTestResultAdapter(result)
 
 
344
class filteringVisitor(TestUtil.TestVisitor):
 
 
345
    """I accruse all the testCases I visit that pass a regexp filter on id
 
 
349
    def __init__(self, filter):
 
 
351
        TestUtil.TestVisitor.__init__(self)
 
 
353
        self.filter=re.compile(filter)
 
 
356
        """answer the suite we are building"""
 
 
357
        if self._suite is None:
 
 
358
            self._suite=TestUtil.TestSuite()
 
 
361
    def visitCase(self, aCase):
 
 
362
        if self.filter.match(aCase.id()):
 
 
363
            self.suite().addTest(aCase)
 
 
366
def run_suite(suite, name='test', verbose=False, pattern=".*"):
 
 
368
    FunctionalTestCase._TEST_NAME = name
 
 
373
    runner = TextTestRunner(stream=sys.stdout,
 
 
376
    visitor = filteringVisitor(pattern)
 
 
378
    result = runner.run(visitor.suite())
 
 
379
    # This is still a little bogus, 
 
 
380
    # but only a little. Folk not using our testrunner will
 
 
381
    # have to delete their temp directories themselves.
 
 
382
    if result.wasSuccessful():
 
 
383
        if FunctionalTestCase.TEST_ROOT is not None:
 
 
384
            shutil.rmtree(FunctionalTestCase.TEST_ROOT) 
 
 
386
        print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
 
 
387
    return result.wasSuccessful()