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 unit tests.
 
 
61
    Tests that need access to disk resources should subclass 
 
 
62
    FunctionalTestCase not TestCase.
 
 
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'
 
 
72
        super(TestCase, self).setUp()
 
 
73
        # setup a temporary log for the test 
 
 
77
        self.TEST_LOG = tempfile.NamedTemporaryFile(mode='wt', bufsize=0)
 
 
78
        # save stdout & stderr so there's no leakage from code-under-test
 
 
79
        self.real_stdout = sys.stdout
 
 
80
        self.real_stderr = sys.stderr
 
 
81
        sys.stdout = sys.stderr = self.TEST_LOG
 
 
82
        self.log("%s setup" % self.id())
 
 
85
        sys.stdout = self.real_stdout
 
 
86
        sys.stderr = self.real_stderr
 
 
87
        self.log("%s teardown" % self.id())
 
 
89
        super(TestCase, self).tearDown()
 
 
92
        """Log a message to a progress file"""
 
 
93
        print >>self.TEST_LOG, msg
 
 
95
    def check_inventory_shape(self, inv, shape):
 
 
97
        Compare an inventory to a list of expected names.
 
 
99
        Fail if they are not precisely equal.
 
 
102
        shape = list(shape)             # copy
 
 
103
        for path, ie in inv.entries():
 
 
104
            name = path.replace('\\', '/')
 
 
112
            self.fail("expected paths not found in inventory: %r" % shape)
 
 
114
            self.fail("unexpected paths found in inventory: %r" % extras)
 
 
117
        """Get the log the test case used. This can only be called once,
 
 
118
        after which an exception will be raised.
 
 
120
        self.TEST_LOG.flush()
 
 
121
        log = open(self.TEST_LOG.name, 'rt').read()
 
 
122
        self.TEST_LOG.close()
 
 
126
class FunctionalTestCase(TestCase):
 
 
127
    """Base class for tests that perform function testing - running bzr,
 
 
128
    using files on disk, and similar activities.
 
 
130
    InTempDir is an old alias for FunctionalTestCase.
 
 
136
    def check_file_contents(self, filename, expect):
 
 
137
        self.log("check contents of file %s" % filename)
 
 
138
        contents = file(filename, 'r').read()
 
 
139
        if contents != expect:
 
 
140
            self.log("expected: %r" % expect)
 
 
141
            self.log("actually: %r" % contents)
 
 
142
            self.fail("contents of %s not as expected")
 
 
144
    def _make_test_root(self):
 
 
149
        if FunctionalTestCase.TEST_ROOT is not None:
 
 
151
        FunctionalTestCase.TEST_ROOT = os.path.abspath(
 
 
152
                                 tempfile.mkdtemp(suffix='.tmp',
 
 
153
                                                  prefix=self._TEST_NAME + '-',
 
 
156
        # make a fake bzr directory there to prevent any tests propagating
 
 
157
        # up onto the source directory's real branch
 
 
158
        os.mkdir(os.path.join(FunctionalTestCase.TEST_ROOT, '.bzr'))
 
 
161
        super(FunctionalTestCase, self).setUp()
 
 
163
        self._make_test_root()
 
 
164
        self._currentdir = os.getcwdu()
 
 
165
        self.test_dir = os.path.join(self.TEST_ROOT, self.id())
 
 
166
        os.mkdir(self.test_dir)
 
 
167
        os.chdir(self.test_dir)
 
 
171
        os.chdir(self._currentdir)
 
 
172
        super(FunctionalTestCase, self).tearDown()
 
 
174
    def formcmd(self, cmd):
 
 
175
        if isinstance(cmd, basestring):
 
 
178
            cmd[0] = self.BZRPATH
 
 
179
            if self.OVERRIDE_PYTHON:
 
 
180
                cmd.insert(0, self.OVERRIDE_PYTHON)
 
 
181
        self.log('$ %r' % cmd)
 
 
184
    def runcmd(self, cmd, retcode=0):
 
 
185
        """Run one command and check the return code.
 
 
187
        Returns a tuple of (stdout,stderr) strings.
 
 
189
        If a single string is based, it is split into words.
 
 
190
        For commands that are not simple space-separated words, please
 
 
191
        pass a list instead."""
 
 
194
            from subprocess import call
 
 
195
        except ImportError, e:
 
 
198
        cmd = self.formcmd(cmd)
 
 
199
        self.log('$ ' + ' '.join(cmd))
 
 
200
        actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
 
 
201
        if retcode != actual_retcode:
 
 
202
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
 
203
                                % (cmd, actual_retcode, retcode))
 
 
205
    def backtick(self, cmd, retcode=0):
 
 
206
        """Run a command and return its output"""
 
 
209
            from subprocess import Popen, PIPE
 
 
210
        except ImportError, e:
 
 
214
        cmd = self.formcmd(cmd)
 
 
215
        child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
 
 
216
        outd, errd = child.communicate()
 
 
218
        actual_retcode = child.wait()
 
 
220
        outd = outd.replace('\r', '')
 
 
222
        if retcode != actual_retcode:
 
 
223
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
 
224
                                % (cmd, actual_retcode, retcode))
 
 
230
    def build_tree(self, shape):
 
 
231
        """Build a test tree according to a pattern.
 
 
233
        shape is a sequence of file specifications.  If the final
 
 
234
        character is '/', a directory is created.
 
 
236
        This doesn't add anything to a branch.
 
 
238
        # XXX: It's OK to just create them using forward slashes on windows?
 
 
241
            assert isinstance(name, basestring)
 
 
246
                print >>f, "contents of", name
 
 
249
InTempDir = FunctionalTestCase
 
 
252
class _MyResult(unittest._TextTestResult):
 
 
256
    No special behaviour for now.
 
 
259
    def startTest(self, test):
 
 
260
        unittest.TestResult.startTest(self, test)
 
 
261
        # TODO: Maybe show test.shortDescription somewhere?
 
 
263
        # python2.3 has the bad habit of just "runit" for doctests
 
 
265
            what = test.shortDescription()
 
 
267
            self.stream.write('%-60.60s' % what)
 
 
270
    def addError(self, test, err):
 
 
271
        super(_MyResult, self).addError(test, err)
 
 
274
    def addFailure(self, test, err):
 
 
275
        super(_MyResult, self).addFailure(test, err)
 
 
278
    def addSuccess(self, test):
 
 
280
            self.stream.writeln('OK')
 
 
282
            self.stream.write('~')
 
 
284
        unittest.TestResult.addSuccess(self, test)
 
 
286
    def printErrorList(self, flavour, errors):
 
 
287
        for test, err in errors:
 
 
288
            self.stream.writeln(self.separator1)
 
 
289
            self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
 
 
290
            self.stream.writeln(self.separator2)
 
 
291
            self.stream.writeln("%s" % err)
 
 
292
            if isinstance(test, TestCase):
 
 
293
                self.stream.writeln()
 
 
294
                self.stream.writeln('log from this test:')
 
 
295
                print >>self.stream, test._get_log()
 
 
298
class TextTestRunner(unittest.TextTestRunner):
 
 
300
    def _makeResult(self):
 
 
301
        return _MyResult(self.stream, self.descriptions, self.verbosity)
 
 
304
def run_suite(suite, name='test', verbose=False):
 
 
306
    FunctionalTestCase._TEST_NAME = name
 
 
311
    runner = TextTestRunner(stream=sys.stdout,
 
 
314
    result = runner.run(suite)
 
 
315
    # This is still a little bogus, 
 
 
316
    # but only a little. Folk not using our testrunner will
 
 
317
    # have to delete their temp directories themselves.
 
 
318
    if result.wasSuccessful():
 
 
319
        shutil.rmtree(FunctionalTestCase.TEST_ROOT) 
 
 
321
        print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
 
 
322
    return result.wasSuccessful()