~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to testsweet.py

  • Committer: Robert Collins
  • Date: 2005-08-24 05:51:34 UTC
  • mto: (974.1.50) (1185.1.10) (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1139.
  • Revision ID: robertc@robertcollins.net-20050824055134-708a7ee78516ecbc
remove TEST_CLASSES dead code and provide a bzrlib.test_suite() convenience method

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 testsweet.TestCase.  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
import unittest
 
41
import sys
 
42
 
 
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")
 
47
    
 
48
 
 
49
class CommandFailed(Exception):
 
50
    pass
 
51
 
 
52
 
 
53
class TestSkipped(Exception):
 
54
    """Indicates that a test was intentionally skipped, rather than failing."""
 
55
    # XXX: Not used yet
 
56
 
 
57
 
 
58
class TestCase(unittest.TestCase):
 
59
    """Base class for bzr unit tests.
 
60
    
 
61
    Tests that need access to disk resources should subclass 
 
62
    FunctionalTestCase not TestCase.
 
63
    """
 
64
    
 
65
    # TODO: Special methods to invoke bzr, so that we can run it
 
66
    # through a specified Python intepreter
 
67
 
 
68
    OVERRIDE_PYTHON = None # to run with alternative python 'python'
 
69
    BZRPATH = 'bzr'
 
70
 
 
71
    def apply_redirected(self, stdin=None, stdout=None, stderr=None,
 
72
                         a_callable=None, *args, **kwargs):
 
73
        """Call callable with redirected std io pipes.
 
74
 
 
75
        Returns the return code."""
 
76
        from StringIO import StringIO
 
77
        if not callable(a_callable):
 
78
            raise ValueError("a_callable must be callable.")
 
79
        if stdin is None:
 
80
            stdin = StringIO("")
 
81
        if stdout is None:
 
82
            stdout = self.TEST_LOG
 
83
        if stderr is None:
 
84
            stderr = self.TEST_LOG
 
85
        real_stdin = sys.stdin
 
86
        real_stdout = sys.stdout
 
87
        real_stderr = sys.stderr
 
88
        result = None
 
89
        try:
 
90
            sys.stdout = stdout
 
91
            sys.stderr = stderr
 
92
            sys.stdin = stdin
 
93
            result = a_callable(*args, **kwargs)
 
94
        finally:
 
95
            sys.stdout = real_stdout
 
96
            sys.stderr = real_stderr
 
97
            sys.stdin = real_stdin
 
98
        return result
 
99
 
 
100
    def setUp(self):
 
101
        super(TestCase, self).setUp()
 
102
        # setup a temporary log for the test 
 
103
        import tempfile
 
104
        self.TEST_LOG = tempfile.NamedTemporaryFile(mode='wt', bufsize=0)
 
105
        self.log("%s setup" % self.id())
 
106
 
 
107
    def tearDown(self):
 
108
        self.log("%s teardown" % self.id())
 
109
        self.log('')
 
110
        super(TestCase, self).tearDown()
 
111
 
 
112
    def log(self, msg):
 
113
        """Log a message to a progress file"""
 
114
        print >>self.TEST_LOG, msg
 
115
 
 
116
    def check_inventory_shape(self, inv, shape):
 
117
        """
 
118
        Compare an inventory to a list of expected names.
 
119
 
 
120
        Fail if they are not precisely equal.
 
121
        """
 
122
        extras = []
 
123
        shape = list(shape)             # copy
 
124
        for path, ie in inv.entries():
 
125
            name = path.replace('\\', '/')
 
126
            if ie.kind == 'dir':
 
127
                name = name + '/'
 
128
            if name in shape:
 
129
                shape.remove(name)
 
130
            else:
 
131
                extras.append(name)
 
132
        if shape:
 
133
            self.fail("expected paths not found in inventory: %r" % shape)
 
134
        if extras:
 
135
            self.fail("unexpected paths found in inventory: %r" % extras)
 
136
     
 
137
    def _get_log(self):
 
138
        """Get the log the test case used. This can only be called once,
 
139
        after which an exception will be raised.
 
140
        """
 
141
        self.TEST_LOG.flush()
 
142
        log = open(self.TEST_LOG.name, 'rt').read()
 
143
        self.TEST_LOG.close()
 
144
        return log
 
145
 
 
146
 
 
147
class FunctionalTestCase(TestCase):
 
148
    """Base class for tests that perform function testing - running bzr,
 
149
    using files on disk, and similar activities.
 
150
 
 
151
    InTempDir is an old alias for FunctionalTestCase.
 
152
    """
 
153
 
 
154
    TEST_ROOT = None
 
155
    _TEST_NAME = 'test'
 
156
 
 
157
    def check_file_contents(self, filename, expect):
 
158
        self.log("check contents of file %s" % filename)
 
159
        contents = file(filename, 'r').read()
 
160
        if contents != expect:
 
161
            self.log("expected: %r" % expect)
 
162
            self.log("actually: %r" % contents)
 
163
            self.fail("contents of %s not as expected")
 
164
 
 
165
    def _make_test_root(self):
 
166
        import os
 
167
        import shutil
 
168
        import tempfile
 
169
        
 
170
        if FunctionalTestCase.TEST_ROOT is not None:
 
171
            return
 
172
        FunctionalTestCase.TEST_ROOT = os.path.abspath(
 
173
                                 tempfile.mkdtemp(suffix='.tmp',
 
174
                                                  prefix=self._TEST_NAME + '-',
 
175
                                                  dir=os.curdir))
 
176
    
 
177
        # make a fake bzr directory there to prevent any tests propagating
 
178
        # up onto the source directory's real branch
 
179
        os.mkdir(os.path.join(FunctionalTestCase.TEST_ROOT, '.bzr'))
 
180
 
 
181
    def setUp(self):
 
182
        super(FunctionalTestCase, self).setUp()
 
183
        import os
 
184
        self._make_test_root()
 
185
        self._currentdir = os.getcwdu()
 
186
        self.test_dir = os.path.join(self.TEST_ROOT, self.id())
 
187
        os.mkdir(self.test_dir)
 
188
        os.chdir(self.test_dir)
 
189
        
 
190
    def tearDown(self):
 
191
        import os
 
192
        os.chdir(self._currentdir)
 
193
        super(FunctionalTestCase, self).tearDown()
 
194
 
 
195
    def formcmd(self, cmd):
 
196
        if isinstance(cmd, basestring):
 
197
            cmd = cmd.split()
 
198
        if cmd[0] == 'bzr':
 
199
            cmd[0] = self.BZRPATH
 
200
            if self.OVERRIDE_PYTHON:
 
201
                cmd.insert(0, self.OVERRIDE_PYTHON)
 
202
        self.log('$ %r' % cmd)
 
203
        return cmd
 
204
 
 
205
    def runcmd(self, cmd, retcode=0):
 
206
        """Run one command and check the return code.
 
207
 
 
208
        Returns a tuple of (stdout,stderr) strings.
 
209
 
 
210
        If a single string is based, it is split into words.
 
211
        For commands that are not simple space-separated words, please
 
212
        pass a list instead."""
 
213
        try:
 
214
            import shutil
 
215
            from subprocess import call
 
216
        except ImportError, e:
 
217
            _need_subprocess()
 
218
            raise
 
219
        cmd = self.formcmd(cmd)
 
220
        self.log('$ ' + ' '.join(cmd))
 
221
        actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
 
222
        if retcode != actual_retcode:
 
223
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
224
                                % (cmd, actual_retcode, retcode))
 
225
 
 
226
    def backtick(self, cmd, retcode=0):
 
227
        """Run a command and return its output"""
 
228
        try:
 
229
            import shutil
 
230
            from subprocess import Popen, PIPE
 
231
        except ImportError, e:
 
232
            _need_subprocess()
 
233
            raise
 
234
        cmd = self.formcmd(cmd)
 
235
        child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
 
236
        outd, errd = child.communicate()
 
237
        self.log(outd)
 
238
        actual_retcode = child.wait()
 
239
        outd = outd.replace('\r', '')
 
240
        if retcode != actual_retcode:
 
241
            raise CommandFailed("test failed: %r returned %d, expected %d"
 
242
                                % (cmd, actual_retcode, retcode))
 
243
        return outd
 
244
 
 
245
    def build_tree(self, shape):
 
246
        """Build a test tree according to a pattern.
 
247
 
 
248
        shape is a sequence of file specifications.  If the final
 
249
        character is '/', a directory is created.
 
250
 
 
251
        This doesn't add anything to a branch.
 
252
        """
 
253
        # XXX: It's OK to just create them using forward slashes on windows?
 
254
        import os
 
255
        for name in shape:
 
256
            assert isinstance(name, basestring)
 
257
            if name[-1] == '/':
 
258
                os.mkdir(name[:-1])
 
259
            else:
 
260
                f = file(name, 'wt')
 
261
                print >>f, "contents of", name
 
262
                f.close()
 
263
                
 
264
InTempDir = FunctionalTestCase
 
265
 
 
266
 
 
267
class _MyResult(unittest._TextTestResult):
 
268
    """
 
269
    Custom TestResult.
 
270
 
 
271
    No special behaviour for now.
 
272
    """
 
273
 
 
274
    def startTest(self, test):
 
275
        unittest.TestResult.startTest(self, test)
 
276
        # TODO: Maybe show test.shortDescription somewhere?
 
277
        what = test.id()
 
278
        # python2.3 has the bad habit of just "runit" for doctests
 
279
        if what == 'runit':
 
280
            what = test.shortDescription()
 
281
        if self.showAll:
 
282
            self.stream.write('%-60.60s' % what)
 
283
        self.stream.flush()
 
284
 
 
285
    def addError(self, test, err):
 
286
        super(_MyResult, self).addError(test, err)
 
287
        self.stream.flush()
 
288
 
 
289
    def addFailure(self, test, err):
 
290
        super(_MyResult, self).addFailure(test, err)
 
291
        self.stream.flush()
 
292
 
 
293
    def addSuccess(self, test):
 
294
        if self.showAll:
 
295
            self.stream.writeln('OK')
 
296
        elif self.dots:
 
297
            self.stream.write('~')
 
298
        self.stream.flush()
 
299
        unittest.TestResult.addSuccess(self, test)
 
300
 
 
301
    def printErrorList(self, flavour, errors):
 
302
        for test, err in errors:
 
303
            self.stream.writeln(self.separator1)
 
304
            self.stream.writeln("%s: %s" % (flavour,self.getDescription(test)))
 
305
            self.stream.writeln(self.separator2)
 
306
            self.stream.writeln("%s" % err)
 
307
            if isinstance(test, TestCase):
 
308
                self.stream.writeln()
 
309
                self.stream.writeln('log from this test:')
 
310
                print >>self.stream, test._get_log()
 
311
 
 
312
 
 
313
class TextTestRunner(unittest.TextTestRunner):
 
314
 
 
315
    def _makeResult(self):
 
316
        return _MyResult(self.stream, self.descriptions, self.verbosity)
 
317
 
 
318
 
 
319
def run_suite(suite, name='test', verbose=False):
 
320
    import shutil
 
321
    FunctionalTestCase._TEST_NAME = name
 
322
    if verbose:
 
323
        verbosity = 2
 
324
    else:
 
325
        verbosity = 1
 
326
    runner = TextTestRunner(stream=sys.stdout,
 
327
                            descriptions=0,
 
328
                            verbosity=verbosity)
 
329
    result = runner.run(suite)
 
330
    # This is still a little bogus, 
 
331
    # but only a little. Folk not using our testrunner will
 
332
    # have to delete their temp directories themselves.
 
333
    if result.wasSuccessful():
 
334
        shutil.rmtree(FunctionalTestCase.TEST_ROOT) 
 
335
    else:
 
336
        print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
 
337
    return result.wasSuccessful()