~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to testsweet.py

  • Committer: Martin Pool
  • Date: 2005-08-02 21:06:41 UTC
  • Revision ID: mbp@sourcefrog.net-20050802210641-9915225ddbc353e6
todo

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 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
 
 
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
 
 
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('')
 
84
 
 
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"""
 
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
 
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:
 
209
            self.log("expected: %r" % expect)
 
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
    """
 
237
    def __init__(self, out, style):
 
238
        self.out = out
 
239
        TestResult.__init__(self)
 
240
        assert style in ('none', 'progress', 'verbose')
 
241
        self.style = style
 
242
 
 
243
 
 
244
    def startTest(self, test):
 
245
        # TODO: Maybe show test.shortDescription somewhere?
 
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
        
 
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()
 
257
        TestResult.startTest(self, test)
 
258
 
 
259
 
 
260
    def stopTest(self, test):
 
261
        # print
 
262
        TestResult.stopTest(self, test)
 
263
 
 
264
 
 
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)
 
270
 
 
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)
 
276
 
 
277
    def addSuccess(self, test):
 
278
        if self.style == 'verbose':
 
279
            print >>self.out, 'OK'
 
280
        TestResult.addSuccess(self, test)
 
281
 
 
282
 
 
283
 
 
284
def run_suite(suite, name='test', verbose=False):
 
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:
 
299
        if verbose:
 
300
            style = 'verbose'
 
301
        else:
 
302
            style = 'progress'
 
303
        result = _MyResult(real_stdout, style)
 
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
 
356
 
 
357
    print >>out
 
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