~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to testsweet.py

  • Committer: Martin Pool
  • Date: 2005-07-29 19:53:21 UTC
  • Revision ID: mbp@sourcefrog.net-20050729195321-6d9eef1a43dd6c81
- notes from discussion on splitting and joining files

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
* utilities to run external commands and check their return code
31
31
  and/or output
32
32
 
33
 
Test cases should normally subclass testsweet.TestCase.  The test runner should
 
33
Test cases should normally subclass TestBase.  The test runner should
34
34
call runsuite().
35
35
 
36
36
This is meant to become independent of bzr, though that's not quite
37
37
true yet.
38
38
"""  
39
39
 
40
 
import unittest
41
 
import sys
 
40
 
 
41
from unittest import TestResult, TestCase
42
42
 
43
43
# XXX: Don't need this anymore now we depend on python2.4
44
44
def _need_subprocess():
50
50
    pass
51
51
 
52
52
 
 
53
 
53
54
class TestSkipped(Exception):
54
55
    """Indicates that a test was intentionally skipped, rather than failing."""
55
56
    # XXX: Not used yet
56
57
 
57
58
 
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.
 
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.
63
64
    """
64
65
    
65
66
    # TODO: Special methods to invoke bzr, so that we can run it
68
69
    OVERRIDE_PYTHON = None # to run with alternative python 'python'
69
70
    BZRPATH = 'bzr'
70
71
 
 
72
    _log_buf = ""
 
73
 
 
74
 
71
75
    def setUp(self):
72
 
        super(TestCase, self).setUp()
73
 
        # setup a temporary log for the test 
74
 
        import time
75
 
        import os
76
 
        import tempfile
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
 
76
        super(TestBase, self).setUp()
82
77
        self.log("%s setup" % self.id())
83
78
 
 
79
 
84
80
    def tearDown(self):
85
 
        sys.stdout = self.real_stdout
86
 
        sys.stderr = self.real_stderr
 
81
        super(TestBase, self).tearDown()
87
82
        self.log("%s teardown" % self.id())
88
83
        self.log('')
89
 
        super(TestCase, self).tearDown()
90
 
 
91
 
    def log(self, msg):
92
 
        """Log a message to a progress file"""
93
 
        print >>self.TEST_LOG, msg
94
 
 
95
 
    def check_inventory_shape(self, inv, shape):
96
 
        """
97
 
        Compare an inventory to a list of expected names.
98
 
 
99
 
        Fail if they are not precisely equal.
100
 
        """
101
 
        extras = []
102
 
        shape = list(shape)             # copy
103
 
        for path, ie in inv.entries():
104
 
            name = path.replace('\\', '/')
105
 
            if ie.kind == 'dir':
106
 
                name = name + '/'
107
 
            if name in shape:
108
 
                shape.remove(name)
109
 
            else:
110
 
                extras.append(name)
111
 
        if shape:
112
 
            self.fail("expected paths not found in inventory: %r" % shape)
113
 
        if extras:
114
 
            self.fail("unexpected paths found in inventory: %r" % extras)
115
 
     
116
 
    def _get_log(self):
117
 
        """Get the log the test case used. This can only be called once,
118
 
        after which an exception will be raised.
119
 
        """
120
 
        self.TEST_LOG.flush()
121
 
        log = open(self.TEST_LOG.name, 'rt').read()
122
 
        self.TEST_LOG.close()
123
 
        return log
124
 
 
125
 
 
126
 
class FunctionalTestCase(TestCase):
127
 
    """Base class for tests that perform function testing - running bzr,
128
 
    using files on disk, and similar activities.
129
 
 
130
 
    InTempDir is an old alias for FunctionalTestCase.
131
 
    """
132
 
 
133
 
    TEST_ROOT = None
134
 
    _TEST_NAME = 'test'
135
 
 
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")
143
 
 
144
 
    def _make_test_root(self):
145
 
        import os
146
 
        import shutil
147
 
        import tempfile
148
 
        
149
 
        if FunctionalTestCase.TEST_ROOT is not None:
150
 
            return
151
 
        FunctionalTestCase.TEST_ROOT = os.path.abspath(
152
 
                                 tempfile.mkdtemp(suffix='.tmp',
153
 
                                                  prefix=self._TEST_NAME + '-',
154
 
                                                  dir=os.curdir))
155
 
    
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'))
159
 
 
160
 
    def setUp(self):
161
 
        super(FunctionalTestCase, self).setUp()
162
 
        import os
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)
168
 
        
169
 
    def tearDown(self):
170
 
        import os
171
 
        os.chdir(self._currentdir)
172
 
        super(FunctionalTestCase, self).tearDown()
 
84
 
173
85
 
174
86
    def formcmd(self, cmd):
175
87
        if isinstance(cmd, basestring):
176
88
            cmd = cmd.split()
 
89
 
177
90
        if cmd[0] == 'bzr':
178
91
            cmd[0] = self.BZRPATH
179
92
            if self.OVERRIDE_PYTHON:
180
93
                cmd.insert(0, self.OVERRIDE_PYTHON)
 
94
 
181
95
        self.log('$ %r' % cmd)
 
96
 
182
97
        return cmd
183
98
 
 
99
 
184
100
    def runcmd(self, cmd, retcode=0):
185
101
        """Run one command and check the return code.
186
102
 
195
111
        except ImportError, e:
196
112
            _need_subprocess()
197
113
            raise
 
114
 
 
115
 
198
116
        cmd = self.formcmd(cmd)
 
117
 
199
118
        self.log('$ ' + ' '.join(cmd))
200
119
        actual_retcode = call(cmd, stdout=self.TEST_LOG, stderr=self.TEST_LOG)
 
120
 
201
121
        if retcode != actual_retcode:
202
122
            raise CommandFailed("test failed: %r returned %d, expected %d"
203
123
                                % (cmd, actual_retcode, retcode))
204
124
 
 
125
 
205
126
    def backtick(self, cmd, retcode=0):
206
127
        """Run a command and return its output"""
207
128
        try:
245
166
                f = file(name, 'wt')
246
167
                print >>f, "contents of", name
247
168
                f.close()
248
 
                
249
 
InTempDir = FunctionalTestCase
250
 
 
251
 
 
252
 
class _MyResult(unittest._TextTestResult):
 
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):
253
232
    """
254
233
    Custom TestResult.
255
234
 
256
235
    No special behaviour for now.
257
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
 
258
243
 
259
244
    def startTest(self, test):
260
 
        unittest.TestResult.startTest(self, test)
261
245
        # TODO: Maybe show test.shortDescription somewhere?
262
246
        what = test.id()
263
247
        # python2.3 has the bad habit of just "runit" for doctests
264
248
        if what == 'runit':
265
249
            what = test.shortDescription()
266
 
        if self.showAll:
267
 
            self.stream.write('%-60.60s' % what)
268
 
        self.stream.flush()
 
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
 
269
264
 
270
265
    def addError(self, test, err):
271
 
        super(_MyResult, self).addError(test, err)
272
 
        self.stream.flush()
 
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)
273
270
 
274
271
    def addFailure(self, test, err):
275
 
        super(_MyResult, self).addFailure(test, err)
276
 
        self.stream.flush()
 
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)
277
276
 
278
277
    def addSuccess(self, test):
279
 
        if self.showAll:
280
 
            self.stream.writeln('OK')
281
 
        elif self.dots:
282
 
            self.stream.write('~')
283
 
        self.stream.flush()
284
 
        unittest.TestResult.addSuccess(self, test)
285
 
 
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()
296
 
 
297
 
 
298
 
class TextTestRunner(unittest.TextTestRunner):
299
 
 
300
 
    def _makeResult(self):
301
 
        return _MyResult(self.stream, self.descriptions, self.verbosity)
 
278
        if self.style == 'verbose':
 
279
            print >>self.out, 'OK'
 
280
        TestResult.addSuccess(self, test)
 
281
 
302
282
 
303
283
 
304
284
def run_suite(suite, name='test', verbose=False):
 
285
    import os
305
286
    import shutil
306
 
    FunctionalTestCase._TEST_NAME = name
307
 
    if verbose:
308
 
        verbosity = 2
309
 
    else:
310
 
        verbosity = 1
311
 
    runner = TextTestRunner(stream=sys.stdout,
312
 
                            descriptions=0,
313
 
                            verbosity=verbosity)
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) 
320
 
    else:
321
 
        print "Failed tests working directories are in '%s'\n" % FunctionalTestCase.TEST_ROOT
 
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
 
322
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