/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Robert Collins
  • Date: 2010-05-06 23:41:35 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506234135-yivbzczw1sejxnxc
Lock methods on ``Tree``, ``Branch`` and ``Repository`` are now
expected to return an object which can be used to unlock them. This reduces
duplicate code when using cleanups. The previous 'tokens's returned by
``Branch.lock_write`` and ``Repository.lock_write`` are now attributes
on the result of the lock_write. ``repository.RepositoryWriteLockResult``
and ``branch.BranchWriteLockResult`` document this. (Robert Collins)

``log._get_info_for_log_files`` now takes an add_cleanup callable.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
"""These tests are tests about the source code of breezy itself.
 
17
"""These tests are tests about the source code of bzrlib itself.
18
18
 
19
19
They are useful for testing code quality, checking coverage metric etc.
20
20
"""
21
21
 
 
22
# import system imports here
22
23
import os
23
24
import parser
24
25
import re
26
27
import sys
27
28
import token
28
29
 
29
 
from breezy import (
 
30
#import bzrlib specific imports here
 
31
from bzrlib import (
30
32
    osutils,
31
33
    )
32
 
import breezy.branch
33
 
from breezy.tests import (
34
 
    features,
 
34
import bzrlib.branch
 
35
from bzrlib.tests import (
35
36
    TestCase,
36
37
    TestSkipped,
37
38
    )
39
40
 
40
41
# Files which are listed here will be skipped when testing for Copyright (or
41
42
# GPL) statements.
42
 
COPYRIGHT_EXCEPTIONS = [
43
 
    'breezy/_bencode_py.py',
44
 
    'breezy/doc_generate/conf.py',
45
 
    'breezy/lsprof.py',
46
 
    ]
 
43
COPYRIGHT_EXCEPTIONS = ['bzrlib/lsprof.py', 'bzrlib/_bencode_py.py',
 
44
    'bzrlib/doc_generate/sphinx_conf.py']
47
45
 
48
 
LICENSE_EXCEPTIONS = [
49
 
    'breezy/_bencode_py.py',
50
 
    'breezy/doc_generate/conf.py',
51
 
    'breezy/lsprof.py',
52
 
    ]
53
 
# Technically, 'breezy/lsprof.py' should be 'breezy/util/lsprof.py',
54
 
# (we do not check breezy/util/, since that is code bundled from elsewhere)
 
46
LICENSE_EXCEPTIONS = ['bzrlib/lsprof.py', 'bzrlib/_bencode_py.py',
 
47
    'bzrlib/doc_generate/sphinx_conf.py']
 
48
# Technically, 'bzrlib/lsprof.py' should be 'bzrlib/util/lsprof.py',
 
49
# (we do not check bzrlib/util/, since that is code bundled from elsewhere)
55
50
# but for compatibility with previous releases, we don't want to move it.
56
51
#
57
52
# sphinx_conf is semi-autogenerated.
75
70
    def find_occurences(self, rule, filename):
76
71
        """Find the number of occurences of rule in a file."""
77
72
        occurences = 0
78
 
        source = open(filename, 'r')
 
73
        source = file(filename, 'r')
79
74
        for line in source:
80
75
            if line.find(rule) > -1:
81
76
                occurences += 1
84
79
    def test_branch_working_tree(self):
85
80
        """Test that the number of uses of working_tree in branch is stable."""
86
81
        occurences = self.find_occurences('self.working_tree()',
87
 
                                          self.source_file_name(breezy.branch))
 
82
                                          self.source_file_name(bzrlib.branch))
88
83
        # do not even think of increasing this number. If you think you need to
89
84
        # increase it, then you almost certainly are doing something wrong as
90
85
        # the relationship from working_tree to branch is one way.
91
86
        # Note that this is an exact equality so that when the number drops,
92
 
        # it is not given a buffer but rather has this test updated immediately.
 
87
        #it is not given a buffer but rather has this test updated immediately.
93
88
        self.assertEqual(0, occurences)
94
89
 
95
90
    def test_branch_WorkingTree(self):
96
91
        """Test that the number of uses of working_tree in branch is stable."""
97
92
        occurences = self.find_occurences('WorkingTree',
98
 
                                          self.source_file_name(breezy.branch))
 
93
                                          self.source_file_name(bzrlib.branch))
99
94
        # Do not even think of increasing this number. If you think you need to
100
95
        # increase it, then you almost certainly are doing something wrong as
101
96
        # the relationship from working_tree to branch is one way.
105
100
 
106
101
class TestSource(TestSourceHelper):
107
102
 
108
 
    def get_breezy_dir(self):
109
 
        """Get the path to the root of breezy"""
110
 
        source = self.source_file_name(breezy)
 
103
    def get_bzrlib_dir(self):
 
104
        """Get the path to the root of bzrlib"""
 
105
        source = self.source_file_name(bzrlib)
111
106
        source_dir = os.path.dirname(source)
112
107
 
113
 
        # Avoid the case when breezy is packaged in a zip file
 
108
        # Avoid the case when bzrlib is packaged in a zip file
114
109
        if not os.path.isdir(source_dir):
115
 
            raise TestSkipped(
116
 
                'Cannot find breezy source directory. Expected %s'
117
 
                % source_dir)
 
110
            raise TestSkipped('Cannot find bzrlib source directory. Expected %s'
 
111
                              % source_dir)
118
112
        return source_dir
119
113
 
120
114
    def get_source_files(self, extensions=None):
121
 
        """Yield all source files for bzr and breezy
 
115
        """Yield all source files for bzr and bzrlib
122
116
 
123
117
        :param our_files_only: If true, exclude files from included libraries
124
118
            or plugins.
125
119
        """
126
 
        breezy_dir = self.get_breezy_dir()
 
120
        bzrlib_dir = self.get_bzrlib_dir()
127
121
        if extensions is None:
128
122
            extensions = ('.py',)
129
123
 
130
124
        # This is the front-end 'bzr' script
131
 
        bzr_path = self.get_brz_path()
 
125
        bzr_path = self.get_bzr_path()
132
126
        yield bzr_path
133
127
 
134
 
        for root, dirs, files in os.walk(breezy_dir):
 
128
        for root, dirs, files in os.walk(bzrlib_dir):
135
129
            for d in dirs:
136
130
                if d.endswith('.tmp'):
137
131
                    dirs.remove(d)
146
140
 
147
141
    def get_source_file_contents(self, extensions=None):
148
142
        for fname in self.get_source_files(extensions=extensions):
149
 
            with open(fname, 'r') as f:
150
 
                yield fname, f.read()
 
143
            f = open(fname, 'rb')
 
144
            try:
 
145
                text = f.read()
 
146
            finally:
 
147
                f.close()
 
148
            yield fname, text
151
149
 
152
150
    def is_our_code(self, fname):
153
 
        """True if it's a "real" part of breezy rather than external code"""
 
151
        """Return true if it's a "real" part of bzrlib rather than external code"""
154
152
        if '/util/' in fname or '/plugins/' in fname:
155
153
            return False
156
154
        else:
159
157
    def is_copyright_exception(self, fname):
160
158
        """Certain files are allowed to be different"""
161
159
        if not self.is_our_code(fname):
 
160
            # We don't ask that external utilities or plugins be
 
161
            # (C) Canonical Ltd
162
162
            return True
163
163
        for exc in COPYRIGHT_EXCEPTIONS:
164
164
            if fname.endswith(exc):
187
187
        incorrect = []
188
188
 
189
189
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
190
 
        copyright_statement_re = re.compile(
191
 
            r'# Copyright \(C\) '  # Opening "# Copyright (C)"
192
 
            r'(\d+?)((, |-)\d+)*'  # followed by a series of dates
193
 
            r' [^ ]*')             # and then whoever.
 
190
        copyright_canonical_re = re.compile(
 
191
            r'# Copyright \(C\) ' # Opening "# Copyright (C)"
 
192
            r'(\d+)(, \d+)*' # Followed by a series of dates
 
193
            r'.*Canonical Ltd' # And containing 'Canonical Ltd'
 
194
            )
194
195
 
195
196
        for fname, text in self.get_source_file_contents(
196
197
                extensions=('.py', '.pyx')):
197
198
            if self.is_copyright_exception(fname):
198
199
                continue
199
 
            match = copyright_statement_re.search(text)
 
200
            match = copyright_canonical_re.search(text)
200
201
            if not match:
201
202
                match = copyright_re.search(text)
202
203
                if match:
206
207
            else:
207
208
                if 'by Canonical' in match.group():
208
209
                    incorrect.append((fname,
209
 
                                      'should not have: "by Canonical": %s'
210
 
                                      % (match.group(),)))
 
210
                        'should not have: "by Canonical": %s'
 
211
                        % (match.group(),)))
211
212
 
212
213
        if incorrect:
213
214
            help_text = ["Some files have missing or incorrect copyright"
215
216
                         "",
216
217
                         "Please either add them to the list of"
217
218
                         " COPYRIGHT_EXCEPTIONS in"
218
 
                         " breezy/tests/test_source.py",
 
219
                         " bzrlib/tests/test_source.py",
219
220
                         # this is broken to prevent a false match
220
221
                         "or add '# Copyright (C)"
221
 
                         " 2007 Bazaar hackers' to these files:",
 
222
                         " 2007 Canonical Ltd' to these files:",
222
223
                         "",
223
 
                         ]
 
224
                        ]
224
225
            for fname, comment in incorrect:
225
226
                help_text.append(fname)
226
 
                help_text.append((' ' * 4) + comment)
 
227
                help_text.append((' '*4) + comment)
227
228
 
228
229
            self.fail('\n'.join(help_text))
229
230
 
260
261
                         "",
261
262
                         "Please either add them to the list of"
262
263
                         " LICENSE_EXCEPTIONS in"
263
 
                         " breezy/tests/test_source.py",
 
264
                         " bzrlib/tests/test_source.py",
264
265
                         "Or add the following text to the beginning:",
265
 
                         gpl_txt]
 
266
                         gpl_txt
 
267
                        ]
266
268
            for fname in incorrect:
267
 
                help_text.append((' ' * 4) + fname)
 
269
                help_text.append((' '*4) + fname)
268
270
 
269
271
            self.fail('\n'.join(help_text))
270
272
 
275
277
            dict_[fname].append(line_no)
276
278
 
277
279
    def _format_message(self, dict_, message):
278
 
        files = sorted(["%s: %s" % (f, ', '.join([str(i + 1) for i in lines]))
279
 
                        for f, lines in dict_.items()])
 
280
        files = ["%s: %s" % (f, ', '.join([str(i+1) for i in lines]))
 
281
                for f, lines in dict_.items()]
 
282
        files.sort()
280
283
        return message + '\n\n    %s' % ('\n    '.join(files))
281
284
 
282
285
    def test_coding_style(self):
283
286
        """Check if bazaar code conforms to some coding style conventions.
284
287
 
285
 
        Generally we expect PEP8, but we do not generally strictly enforce
286
 
        this, and there are existing files that do not comply.  The 'pep8'
287
 
        tool, available separately, will check for more cases.
 
288
        Currently we assert that the following is not present:
 
289
         * any tab characters
 
290
         * non-unix newlines
 
291
         * no newline at end of files
288
292
 
289
 
        This test only enforces conditions that are globally true at the
290
 
        moment, and that should cause a patch to be rejected: spaces rather
291
 
        than tabs, unix newlines, and a newline at the end of the file.
 
293
        Print how many files have
 
294
         * trailing white space
 
295
         * lines longer than 79 chars
292
296
        """
293
297
        tabs = {}
 
298
        trailing_ws = {}
294
299
        illegal_newlines = {}
 
300
        long_lines = {}
295
301
        no_newline_at_eof = []
296
302
        for fname, text in self.get_source_file_contents(
297
303
                extensions=('.py', '.pyx')):
303
309
                if '\t' in line:
304
310
                    self._push_file(tabs, fname, line_no)
305
311
                if not line.endswith('\n') or line.endswith('\r\n'):
306
 
                    if line_no != last_line_no:  # not no_newline_at_eof
 
312
                    if line_no != last_line_no: # not no_newline_at_eof
307
313
                        self._push_file(illegal_newlines, fname, line_no)
 
314
                if line.endswith(' \n'):
 
315
                    self._push_file(trailing_ws, fname, line_no)
 
316
                if len(line) > 80:
 
317
                    self._push_file(long_lines, fname, line_no)
308
318
            if not lines[-1].endswith('\n'):
309
319
                no_newline_at_eof.append(fname)
310
320
        problems = []
311
321
        if tabs:
312
322
            problems.append(self._format_message(tabs,
313
 
                                                 'Tab characters were found in the following source files.'
314
 
                                                 '\nThey should either be replaced by "\\t" or by spaces:'))
 
323
                'Tab characters were found in the following source files.'
 
324
                '\nThey should either be replaced by "\\t" or by spaces:'))
 
325
        if trailing_ws:
 
326
            print ("There are %i lines with trailing white space in %i files."
 
327
                % (sum([len(lines) for f, lines in trailing_ws.items()]),
 
328
                    len(trailing_ws)))
315
329
        if illegal_newlines:
316
330
            problems.append(self._format_message(illegal_newlines,
317
 
                                                 'Non-unix newlines were found in the following source files:'))
 
331
                'Non-unix newlines were found in the following source files:'))
 
332
        if long_lines:
 
333
            print ("There are %i lines longer than 79 characters in %i files."
 
334
                % (sum([len(lines) for f, lines in long_lines.items()]),
 
335
                    len(long_lines)))
318
336
        if no_newline_at_eof:
319
337
            no_newline_at_eof.sort()
320
338
            problems.append("The following source files doesn't have a "
321
 
                            "newline at the end:"
322
 
                            '\n\n    %s'
323
 
                            % ('\n    '.join(no_newline_at_eof)))
 
339
                "newline at the end:"
 
340
               '\n\n    %s'
 
341
               % ('\n    '.join(no_newline_at_eof)))
324
342
        if problems:
325
343
            self.fail('\n\n'.join(problems))
326
344
 
327
 
    def test_flake8(self):
328
 
        self.requireFeature(features.flake8)
329
 
        # Older versions of flake8 don't support the 'paths'
330
 
        # variable
331
 
        new_path = list(sys.path)
332
 
        new_path.insert(
333
 
            0, os.path.join(os.path.dirname(__file__), '..', '..', 'tools'))
334
 
        self.overrideAttr(sys, 'path', new_path)
335
 
        from flake8.main.application import Application
336
 
        from flake8.formatting.base import BaseFormatter
337
 
        app = Application()
338
 
        app.config = u'setup.cfg'
339
 
        app.jobs = 1
340
 
 
341
 
        class Formatter(BaseFormatter):
342
 
 
343
 
            def __init__(self):
344
 
                self.errors = []
345
 
 
346
 
            def start(self):
347
 
                pass
348
 
 
349
 
            def stop(self):
350
 
                app.file_checker_manager.report()
351
 
 
352
 
            def handle(self, error):
353
 
                self.errors.append(error)
354
 
 
355
 
        app.formatter = Formatter()
356
 
        app.initialize([])
357
 
        app.run_checks()
358
 
        app.report()
359
 
        self.assertEqual(app.formatter.errors, [])
360
 
 
361
345
    def test_no_asserts(self):
362
346
        """bzr shouldn't use the 'assert' statement."""
363
347
        # assert causes too much variation between -O and not, and tends to
381
365
                continue
382
366
            if not assert_re.search(text):
383
367
                continue
384
 
            st = parser.suite(text)
385
 
            code = parser.st2tuple(st)
386
 
            if search(code):
 
368
            ast = parser.ast2tuple(parser.suite(text))
 
369
            if search(ast):
387
370
                badfiles.append(fname)
388
371
        if badfiles:
389
372
            self.fail(
393
376
    def test_extension_exceptions(self):
394
377
        """Extension functions should propagate exceptions.
395
378
 
396
 
        Either they should return an object, have an 'except' clause, or
397
 
        have a "# cannot_raise" to indicate that we've audited them and
398
 
        defined them as not raising exceptions.
 
379
        Either they should return an object, have an 'except' clause, or have a
 
380
        "# cannot_raise" to indicate that we've audited them and defined them as not
 
381
        raising exceptions.
399
382
        """
400
383
        both_exc_and_no_exc = []
401
384
        missing_except = []
402
 
        common_classes = ('StaticTuple',)
403
 
        class_re = re.compile(r'^(cdef\s+)?(public\s+)?'
404
 
                              r'(api\s+)?class (\w+).*:', re.MULTILINE)
405
 
        except_re = re.compile(
406
 
            r'cdef\s+'        # start with cdef
407
 
            r'([\w *]*?)\s*'  # this is the return signature
408
 
            r'(\w+)\s*\('     # the function name
409
 
            r'[^)]*\)\s*'     # parameters
410
 
            r'(.*)\s*:'       # the except clause
411
 
            r'\s*(#\s*cannot[- _]raise)?')  # cannot raise comment
 
385
        class_re = re.compile(r'^(cdef\s+)?(public\s+)?(api\s+)?class (\w+).*:',
 
386
                              re.MULTILINE)
 
387
        except_re = re.compile(r'cdef\s+' # start with cdef
 
388
                               r'([\w *]*?)\s*' # this is the return signature
 
389
                               r'(\w+)\s*\(' # the function name
 
390
                               r'[^)]*\)\s*' # parameters
 
391
                               r'(.*)\s*:' # the except clause
 
392
                               r'\s*(#\s*cannot[- _]raise)?' # cannot raise comment
 
393
                              )
412
394
        for fname, text in self.get_source_file_contents(
413
395
                extensions=('.pyx',)):
414
 
            known_classes = {m[-1] for m in class_re.findall(text)}
415
 
            known_classes.update(common_classes)
 
396
            known_classes = set([m[-1] for m in class_re.findall(text)])
416
397
            cdefs = except_re.findall(text)
417
398
            for sig, func, exc_clause, no_exc_comment in cdefs:
418
399
                if sig.startswith('api '):
427
408
                    missing_except.append((fname, func))
428
409
        error_msg = []
429
410
        if both_exc_and_no_exc:
430
 
            error_msg.append(
431
 
                'The following functions had "cannot raise" comments'
432
 
                ' but did have an except clause set:')
 
411
            error_msg.append('The following functions had "cannot raise" comments'
 
412
                             ' but did have an except clause set:')
433
413
            for fname, func in both_exc_and_no_exc:
434
414
                error_msg.append('%s:%s' % (fname, func))
435
415
            error_msg.extend(('', ''))
436
416
        if missing_except:
437
 
            error_msg.append(
438
 
                'The following functions have fixed return types,'
439
 
                ' but no except clause.')
440
 
            error_msg.append(
441
 
                'Either add an except or append "# cannot_raise".')
 
417
            error_msg.append('The following functions have fixed return types,'
 
418
                             ' but no except clause.')
 
419
            error_msg.append('Either add an except or append "# cannot_raise".')
442
420
            for fname, func in missing_except:
443
421
                error_msg.append('%s:%s' % (fname, func))
444
422
            error_msg.extend(('', ''))
445
423
        if error_msg:
446
424
            self.fail('\n'.join(error_msg))
447
 
 
448
 
    def test_feature_absolute_import(self):
449
 
        """Using absolute imports means avoiding unnecesary stat and
450
 
        open calls.
451
 
 
452
 
        Make sure that all non-test files have absolute imports enabled.
453
 
        """
454
 
        missing_absolute_import = []
455
 
        for fname, text in self.get_source_file_contents(
456
 
                extensions=('.py', '.pyx')):
457
 
            if "/tests/" in fname or "test_" in fname:
458
 
                # We don't really care about tests
459
 
                continue
460
 
            if "from __future__ import absolute_import" not in text:
461
 
                missing_absolute_import.append(fname)
462
 
 
463
 
        if missing_absolute_import:
464
 
            self.fail(
465
 
                'The following files do not have absolute_import enabled:\n'
466
 
                '\n' + '\n'.join(missing_absolute_import))