~bzr-pqm/bzr/bzr.dev

841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
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 TestBase.  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
41
from unittest import TestResult, TestCase
42
992 by Martin Pool
doc
43
# XXX: Don't need this anymore now we depend on python2.4
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
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
54
class TestSkipped(Exception):
55
    """Indicates that a test was intentionally skipped, rather than failing."""
56
    # XXX: Not used yet
57
58
59
class TestBase(TestCase):
60
    """Base class for bzr test cases.
61
62
    Just defines some useful helper functions; doesn't actually test
63
    anything.
64
    """
65
    
66
    # TODO: Special methods to invoke bzr, so that we can run it
67
    # through a specified Python intepreter
68
69
    OVERRIDE_PYTHON = None # to run with alternative python 'python'
70
    BZRPATH = 'bzr'
71
72
    _log_buf = ""
73
74
75
    def setUp(self):
76
        super(TestBase, self).setUp()
77
        self.log("%s setup" % self.id())
78
79
80
    def tearDown(self):
81
        super(TestBase, self).tearDown()
82
        self.log("%s teardown" % self.id())
83
        self.log('')
842 by Martin Pool
- don't say runit when running tests under python2.3 dammit
84
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
85
86
    def formcmd(self, cmd):
87
        if isinstance(cmd, basestring):
88
            cmd = cmd.split()
89
90
        if cmd[0] == 'bzr':
91
            cmd[0] = self.BZRPATH
92
            if self.OVERRIDE_PYTHON:
93
                cmd.insert(0, self.OVERRIDE_PYTHON)
94
95
        self.log('$ %r' % cmd)
96
97
        return cmd
98
99
100
    def runcmd(self, cmd, retcode=0):
101
        """Run one command and check the return code.
102
103
        Returns a tuple of (stdout,stderr) strings.
104
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."""
108
        try:
109
            import shutil
110
            from subprocess import call
111
        except ImportError, e:
112
            _need_subprocess()
113
            raise
114
115
116
        cmd = self.formcmd(cmd)
117
118
        self.log('$ ' + ' '.join(cmd))
119
        actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
120
121
        if retcode != actual_retcode:
122
            raise CommandFailed("test failed: %r returned %d, expected %d"
123
                                % (cmd, actual_retcode, retcode))
124
125
126
    def backtick(self, cmd, retcode=0):
127
        """Run a command and return its output"""
128
        try:
129
            import shutil
130
            from subprocess import Popen, PIPE
131
        except ImportError, e:
132
            _need_subprocess()
133
            raise
134
135
        cmd = self.formcmd(cmd)
136
        child = Popen(cmd, stdout=PIPE, stderr=self.TEST_LOG)
137
        outd, errd = child.communicate()
138
        self.log(outd)
139
        actual_retcode = child.wait()
140
141
        outd = outd.replace('\r', '')
142
143
        if retcode != actual_retcode:
144
            raise CommandFailed("test failed: %r returned %d, expected %d"
145
                                % (cmd, actual_retcode, retcode))
146
147
        return outd
148
149
150
151
    def build_tree(self, shape):
152
        """Build a test tree according to a pattern.
153
154
        shape is a sequence of file specifications.  If the final
155
        character is '/', a directory is created.
156
157
        This doesn't add anything to a branch.
158
        """
159
        # XXX: It's OK to just create them using forward slashes on windows?
160
        import os
161
        for name in shape:
162
            assert isinstance(name, basestring)
163
            if name[-1] == '/':
164
                os.mkdir(name[:-1])
165
            else:
166
                f = file(name, 'wt')
167
                print >>f, "contents of", name
168
                f.close()
169
170
171
    def log(self, msg):
172
        """Log a message to a progress file"""
994 by Martin Pool
doc
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?
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
179
        self._log_buf = self._log_buf + str(msg) + '\n'
180
        print >>self.TEST_LOG, msg
181
182
183
    def check_inventory_shape(self, inv, shape):
184
        """
185
        Compare an inventory to a list of expected names.
186
187
        Fail if they are not precisely equal.
188
        """
189
        extras = []
190
        shape = list(shape)             # copy
191
        for path, ie in inv.entries():
192
            name = path.replace('\\', '/')
193
            if ie.kind == 'dir':
194
                name = name + '/'
195
            if name in shape:
196
                shape.remove(name)
197
            else:
198
                extras.append(name)
199
        if shape:
200
            self.fail("expected paths not found in inventory: %r" % shape)
201
        if extras:
202
            self.fail("unexpected paths found in inventory: %r" % extras)
203
204
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:
916 by Martin Pool
- typo in testsweet
209
            self.log("expected: %r" % expect)
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
210
            self.log("actually: %r" % contents)
211
            self.fail("contents of %s not as expected")
212
            
213
214
215
class InTempDir(TestBase):
216
    """Base class for tests run in a temporary branch."""
217
    def setUp(self):
218
        import os
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)
222
        
223
    def tearDown(self):
224
        import os
225
        os.chdir(self.TEST_ROOT)
226
227
228
229
230
231
class _MyResult(TestResult):
232
    """
233
    Custom TestResult.
234
235
    No special behaviour for now.
236
    """
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
237
    def __init__(self, out, style):
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
238
        self.out = out
239
        TestResult.__init__(self)
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
240
        assert style in ('none', 'progress', 'verbose')
241
        self.style = style
242
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
243
244
    def startTest(self, test):
245
        # TODO: Maybe show test.shortDescription somewhere?
842 by Martin Pool
- don't say runit when running tests under python2.3 dammit
246
        what = test.id()
247
        # python2.3 has the bad habit of just "runit" for doctests
248
        if what == 'runit':
249
            what = test.shortDescription()
250
        
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
251
        if self.style == 'verbose':
252
            print >>self.out, '%-60.60s' % what,
253
            self.out.flush()
254
        elif self.style == 'progress':
255
            self.out.write('~')
256
            self.out.flush()
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
257
        TestResult.startTest(self, test)
258
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
259
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
260
    def stopTest(self, test):
261
        # print
262
        TestResult.stopTest(self, test)
263
264
265
    def addError(self, test, err):
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
266
        if self.style == 'verbose':
267
            print >>self.out, 'ERROR'
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
268
        TestResult.addError(self, test, err)
269
        _show_test_failure('error', test, err, self.out)
270
271
    def addFailure(self, test, err):
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
272
        if self.style == 'verbose':
273
            print >>self.out, 'FAILURE'
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
274
        TestResult.addFailure(self, test, err)
275
        _show_test_failure('failure', test, err, self.out)
276
277
    def addSuccess(self, test):
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
278
        if self.style == 'verbose':
279
            print >>self.out, 'OK'
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
280
        TestResult.addSuccess(self, test)
281
282
283
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
284
def run_suite(suite, name='test', verbose=False):
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
285
    import os
286
    import shutil
287
    import time
288
    import sys
289
    
290
    _setup_test_log(name)
291
    _setup_test_dir(name)
292
    print
293
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
298
    try:
965 by Martin Pool
- selftest is less verbose by default, and takes a -v option if you want it
299
        if verbose:
300
            style = 'verbose'
301
        else:
302
            style = 'progress'
303
        result = _MyResult(real_stdout, style)
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
304
        suite.run(result)
305
    finally:
306
        sys.stdout = real_stdout
307
        sys.stderr = real_stderr
308
309
    _show_results(result)
310
311
    return result.wasSuccessful()
312
313
314
315
def _setup_test_log(name):
316
    import time
317
    import os
318
    
319
    log_filename = os.path.abspath(name + '.log')
320
    TestBase.TEST_LOG = open(log_filename, 'wt', buffering=1) # line buffered
321
322
    print >>TestBase.TEST_LOG, "tests run at " + time.ctime()
323
    print '%-30s %s' % ('test log', log_filename)
324
325
326
def _setup_test_dir(name):
327
    import os
328
    import shutil
329
    
330
    TestBase.ORIG_DIR = os.getcwdu()
331
    TestBase.TEST_ROOT = os.path.abspath(name + '.tmp')
332
333
    print '%-30s %s' % ('running tests in', TestBase.TEST_ROOT)
334
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)
339
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'))
343
344
    
345
346
def _show_results(result):
347
     print
348
     print '%4d tests run' % result.testsRun
349
     print '%4d errors' % len(result.errors)
350
     print '%4d failures' % len(result.failures)
351
352
353
354
def _show_test_failure(kind, case, exc_info, out):
355
    from traceback import print_exception
993 by Martin Pool
- better display of test failures in non-verbose mode
356
357
    print >>out
841 by Martin Pool
- Start splitting bzr-independent parts of the test framework into
358
    print >>out, '-' * 60
359
    print >>out, case
360
    
361
    desc = case.shortDescription()
362
    if desc:
363
        print >>out, '   (%s)' % desc
364
         
365
    print_exception(exc_info[0], exc_info[1], exc_info[2], None, out)
366
        
367
    if isinstance(case, TestBase):
368
        print >>out
369
        print >>out, 'log from this test:'
370
        print >>out, case._log_buf
371
         
372
    print >>out, '-' * 60
373
    
374