~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Patch Queue Manager
  • Date: 2011-09-22 14:12:18 UTC
  • mfrom: (6155.3.1 jam)
  • Revision ID: pqm@pqm.ubuntu.com-20110922141218-86s4uu6nqvourw4f
(jameinel) Cleanup comments bzrlib/smart/__init__.py (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#   Authors: Robert Collins <robert.collins@canonical.com>
 
1
# Copyright (C) 2005-2011 Canonical Ltd
3
2
#
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
13
12
#
14
13
# You should have received a copy of the GNU General Public License
15
14
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
16
 
18
17
"""These tests are tests about the source code of bzrlib itself.
19
18
 
20
19
They are useful for testing code quality, checking coverage metric etc.
21
20
"""
22
21
 
23
 
# import system imports here
24
22
import os
 
23
import parser
25
24
import re
 
25
import symbol
26
26
import sys
 
27
import token
27
28
 
28
 
#import bzrlib specific imports here
29
29
from bzrlib import (
30
30
    osutils,
31
31
    )
32
32
import bzrlib.branch
33
 
from bzrlib.tests import TestCase, TestSkipped
 
33
from bzrlib.tests import (
 
34
    TestCase,
 
35
    TestSkipped,
 
36
    )
34
37
 
35
38
 
36
39
# Files which are listed here will be skipped when testing for Copyright (or
37
40
# GPL) statements.
38
 
COPYRIGHT_EXCEPTIONS = ['bzrlib/lsprof.py']
 
41
COPYRIGHT_EXCEPTIONS = [
 
42
    'bzrlib/_bencode_py.py',
 
43
    'bzrlib/doc_generate/conf.py',
 
44
    'bzrlib/lsprof.py',
 
45
    ]
39
46
 
40
 
LICENSE_EXCEPTIONS = ['bzrlib/lsprof.py']
 
47
LICENSE_EXCEPTIONS = [
 
48
    'bzrlib/_bencode_py.py',
 
49
    'bzrlib/doc_generate/conf.py',
 
50
    'bzrlib/lsprof.py',
 
51
    ]
41
52
# Technically, 'bzrlib/lsprof.py' should be 'bzrlib/util/lsprof.py',
42
53
# (we do not check bzrlib/util/, since that is code bundled from elsewhere)
43
54
# but for compatibility with previous releases, we don't want to move it.
 
55
#
 
56
# sphinx_conf is semi-autogenerated.
44
57
 
45
58
 
46
59
class TestSourceHelper(TestCase):
47
60
 
48
61
    def source_file_name(self, package):
49
62
        """Return the path of the .py file for package."""
 
63
        if getattr(sys, "frozen", None) is not None:
 
64
            raise TestSkipped("can't test sources in frozen distributions.")
50
65
        path = package.__file__
51
66
        if path[-1] in 'co':
52
67
            return path[:-1]
72
87
        # do not even think of increasing this number. If you think you need to
73
88
        # increase it, then you almost certainly are doing something wrong as
74
89
        # the relationship from working_tree to branch is one way.
75
 
        # Note that this is an exact equality so that when the number drops, 
 
90
        # Note that this is an exact equality so that when the number drops,
76
91
        #it is not given a buffer but rather has this test updated immediately.
77
92
        self.assertEqual(0, occurences)
78
93
 
80
95
        """Test that the number of uses of working_tree in branch is stable."""
81
96
        occurences = self.find_occurences('WorkingTree',
82
97
                                          self.source_file_name(bzrlib.branch))
83
 
        # do not even think of increasing this number. If you think you need to
 
98
        # Do not even think of increasing this number. If you think you need to
84
99
        # increase it, then you almost certainly are doing something wrong as
85
100
        # the relationship from working_tree to branch is one way.
86
 
        # This number should be 4 (import NoWorkingTree and WorkingTree, 
87
 
        # raise NoWorkingTree from working_tree(), and construct a working tree
88
 
        # there) but a merge that regressed this was done before this test was
89
 
        # written. Note that this is an exact equality so that when the number
90
 
        # drops, it is not given a buffer but rather this test updated
91
 
        # immediately.
92
 
        self.assertEqual(2, occurences)
 
101
        # As of 20070809, there are no longer any mentions at all.
 
102
        self.assertEqual(0, occurences)
93
103
 
94
104
 
95
105
class TestSource(TestSourceHelper):
101
111
 
102
112
        # Avoid the case when bzrlib is packaged in a zip file
103
113
        if not os.path.isdir(source_dir):
104
 
            raise TestSkipped('Cannot find bzrlib source directory. Expected %s'
105
 
                              % source_dir)
 
114
            raise TestSkipped(
 
115
                'Cannot find bzrlib source directory. Expected %s'
 
116
                % source_dir)
106
117
        return source_dir
107
118
 
108
 
    def get_source_files(self):
109
 
        """yield all source files for bzr and bzrlib"""
 
119
    def get_source_files(self, extensions=None):
 
120
        """Yield all source files for bzr and bzrlib
 
121
 
 
122
        :param our_files_only: If true, exclude files from included libraries
 
123
            or plugins.
 
124
        """
110
125
        bzrlib_dir = self.get_bzrlib_dir()
 
126
        if extensions is None:
 
127
            extensions = ('.py',)
111
128
 
112
129
        # This is the front-end 'bzr' script
113
130
        bzr_path = self.get_bzr_path()
118
135
                if d.endswith('.tmp'):
119
136
                    dirs.remove(d)
120
137
            for f in files:
121
 
                if not f.endswith('.py'):
 
138
                for extension in extensions:
 
139
                    if f.endswith(extension):
 
140
                        break
 
141
                else:
 
142
                    # Did not match the accepted extensions
122
143
                    continue
123
144
                yield osutils.pathjoin(root, f)
124
145
 
125
 
    def get_source_file_contents(self):
126
 
        for fname in self.get_source_files():
 
146
    def get_source_file_contents(self, extensions=None):
 
147
        for fname in self.get_source_files(extensions=extensions):
127
148
            f = open(fname, 'rb')
128
149
            try:
129
150
                text = f.read()
131
152
                f.close()
132
153
            yield fname, text
133
154
 
 
155
    def is_our_code(self, fname):
 
156
        """True if it's a "real" part of bzrlib rather than external code"""
 
157
        if '/util/' in fname or '/plugins/' in fname:
 
158
            return False
 
159
        else:
 
160
            return True
 
161
 
134
162
    def is_copyright_exception(self, fname):
135
163
        """Certain files are allowed to be different"""
136
 
        if '/util/' in fname or '/plugins/' in fname:
 
164
        if not self.is_our_code(fname):
137
165
            # We don't ask that external utilities or plugins be
138
166
            # (C) Canonical Ltd
139
167
            return True
140
 
 
141
168
        for exc in COPYRIGHT_EXCEPTIONS:
142
169
            if fname.endswith(exc):
143
170
                return True
144
 
 
145
171
        return False
146
172
 
147
173
    def is_license_exception(self, fname):
148
174
        """Certain files are allowed to be different"""
149
 
        if '/util/' in fname or '/plugins/' in fname:
150
 
            # We don't ask that external utilities or plugins be
151
 
            # (C) Canonical Ltd
 
175
        if not self.is_our_code(fname):
152
176
            return True
153
 
 
154
177
        for exc in LICENSE_EXCEPTIONS:
155
178
            if fname.endswith(exc):
156
179
                return True
157
 
 
158
180
        return False
159
181
 
160
182
    def test_tmpdir_not_in_source_files(self):
166
188
                          % filename)
167
189
 
168
190
    def test_copyright(self):
169
 
        """Test that all .py files have a valid copyright statement"""
170
 
        # These are files which contain a different copyright statement
171
 
        # and that is okay.
 
191
        """Test that all .py and .pyx files have a valid copyright statement"""
172
192
        incorrect = []
173
193
 
174
194
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
175
195
        copyright_canonical_re = re.compile(
176
 
            r'# Copyright \(C\) ' # Opening "# Copyright (C)"
177
 
            r'(\d+)(, \d+)*' # Followed by a series of dates
178
 
            r'.*Canonical Ltd' # And containing 'Canonical Ltd'
179
 
            )
 
196
            r'# Copyright \(C\) '  # Opening "# Copyright (C)"
 
197
            r'(\d+)(, \d+)*'       # followed by a series of dates
 
198
            r'.*Canonical Ltd')    # and containing 'Canonical Ltd'.
180
199
 
181
 
        for fname, text in self.get_source_file_contents():
 
200
        for fname, text in self.get_source_file_contents(
 
201
                extensions=('.py', '.pyx')):
182
202
            if self.is_copyright_exception(fname):
183
203
                continue
184
204
            match = copyright_canonical_re.search(text)
203
223
                         " bzrlib/tests/test_source.py",
204
224
                         # this is broken to prevent a false match
205
225
                         "or add '# Copyright (C)"
206
 
                         " 2006 Canonical Ltd' to these files:",
 
226
                         " 2007 Canonical Ltd' to these files:",
207
227
                         "",
208
228
                        ]
209
229
            for fname, comment in incorrect:
210
230
                help_text.append(fname)
211
 
                help_text.append((' '*4) + comment)
 
231
                help_text.append((' ' * 4) + comment)
212
232
 
213
233
            self.fail('\n'.join(help_text))
214
234
 
215
235
    def test_gpl(self):
216
 
        """Test that all .py files have a GPL disclaimer"""
 
236
        """Test that all .py and .pyx files have a GPL disclaimer."""
217
237
        incorrect = []
218
238
 
219
239
        gpl_txt = """
229
249
#
230
250
# You should have received a copy of the GNU General Public License
231
251
# along with this program; if not, write to the Free Software
232
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
252
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
233
253
"""
234
254
        gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
235
255
 
236
 
        for fname, text in self.get_source_file_contents():
 
256
        for fname, text in self.get_source_file_contents(
 
257
                extensions=('.py', '.pyx')):
237
258
            if self.is_license_exception(fname):
238
259
                continue
239
260
            if not gpl_re.search(text):
246
267
                         " LICENSE_EXCEPTIONS in"
247
268
                         " bzrlib/tests/test_source.py",
248
269
                         "Or add the following text to the beginning:",
249
 
                         gpl_txt
250
 
                        ]
 
270
                         gpl_txt]
251
271
            for fname in incorrect:
252
 
                help_text.append((' '*4) + fname)
 
272
                help_text.append((' ' * 4) + fname)
253
273
 
254
274
            self.fail('\n'.join(help_text))
255
275
 
256
 
    def test_no_tabs(self):
257
 
        """bzrlib source files should not contain any tab characters."""
258
 
        incorrect = []
259
 
 
 
276
    def _push_file(self, dict_, fname, line_no):
 
277
        if fname not in dict_:
 
278
            dict_[fname] = [line_no]
 
279
        else:
 
280
            dict_[fname].append(line_no)
 
281
 
 
282
    def _format_message(self, dict_, message):
 
283
        files = ["%s: %s" % (f, ', '.join([str(i + 1) for i in lines]))
 
284
                for f, lines in dict_.items()]
 
285
        files.sort()
 
286
        return message + '\n\n    %s' % ('\n    '.join(files))
 
287
 
 
288
    def test_coding_style(self):
 
289
        """Check if bazaar code conforms to some coding style conventions.
 
290
 
 
291
        Generally we expect PEP8, but we do not generally strictly enforce
 
292
        this, and there are existing files that do not comply.  The 'pep8'
 
293
        tool, available separately, will check for more cases.
 
294
 
 
295
        This test only enforces conditions that are globally true at the
 
296
        moment, and that should cause a patch to be rejected: spaces rather
 
297
        than tabs, unix newlines, and a newline at the end of the file.
 
298
        """
 
299
        tabs = {}
 
300
        illegal_newlines = {}
 
301
        no_newline_at_eof = []
 
302
        for fname, text in self.get_source_file_contents(
 
303
                extensions=('.py', '.pyx')):
 
304
            if not self.is_our_code(fname):
 
305
                continue
 
306
            lines = text.splitlines(True)
 
307
            last_line_no = len(lines) - 1
 
308
            for line_no, line in enumerate(lines):
 
309
                if '\t' in line:
 
310
                    self._push_file(tabs, fname, line_no)
 
311
                if not line.endswith('\n') or line.endswith('\r\n'):
 
312
                    if line_no != last_line_no:  # not no_newline_at_eof
 
313
                        self._push_file(illegal_newlines, fname, line_no)
 
314
            if not lines[-1].endswith('\n'):
 
315
                no_newline_at_eof.append(fname)
 
316
        problems = []
 
317
        if tabs:
 
318
            problems.append(self._format_message(tabs,
 
319
                'Tab characters were found in the following source files.'
 
320
                '\nThey should either be replaced by "\\t" or by spaces:'))
 
321
        if illegal_newlines:
 
322
            problems.append(self._format_message(illegal_newlines,
 
323
                'Non-unix newlines were found in the following source files:'))
 
324
        if no_newline_at_eof:
 
325
            no_newline_at_eof.sort()
 
326
            problems.append("The following source files doesn't have a "
 
327
                "newline at the end:"
 
328
               '\n\n    %s'
 
329
               % ('\n    '.join(no_newline_at_eof)))
 
330
        if problems:
 
331
            self.fail('\n\n'.join(problems))
 
332
 
 
333
    def test_no_asserts(self):
 
334
        """bzr shouldn't use the 'assert' statement."""
 
335
        # assert causes too much variation between -O and not, and tends to
 
336
        # give bad errors to the user
 
337
        def search(x):
 
338
            # scan down through x for assert statements, report any problems
 
339
            # this is a bit cheesy; it may get some false positives?
 
340
            if x[0] == symbol.assert_stmt:
 
341
                return True
 
342
            elif x[0] == token.NAME:
 
343
                # can't search further down
 
344
                return False
 
345
            for sub in x[1:]:
 
346
                if sub and search(sub):
 
347
                    return True
 
348
            return False
 
349
        badfiles = []
 
350
        assert_re = re.compile(r'\bassert\b')
260
351
        for fname, text in self.get_source_file_contents():
261
 
            if '/util/' in fname or '/plugins/' in fname:
262
 
                continue
263
 
            if '\t' in text:
264
 
                incorrect.append(fname)
265
 
 
266
 
        if incorrect:
267
 
            self.fail('Tab characters were found in the following source files.'
268
 
              '\nThey should either be replaced by "\\t" or by spaces:'
269
 
              '\n\n    %s'
270
 
              % ('\n    '.join(incorrect)))
 
352
            if not self.is_our_code(fname):
 
353
                continue
 
354
            if not assert_re.search(text):
 
355
                continue
 
356
            ast = parser.ast2tuple(parser.suite(text))
 
357
            if search(ast):
 
358
                badfiles.append(fname)
 
359
        if badfiles:
 
360
            self.fail(
 
361
                "these files contain an assert statement and should not:\n%s"
 
362
                % '\n'.join(badfiles))
 
363
 
 
364
    def test_extension_exceptions(self):
 
365
        """Extension functions should propagate exceptions.
 
366
 
 
367
        Either they should return an object, have an 'except' clause, or
 
368
        have a "# cannot_raise" to indicate that we've audited them and
 
369
        defined them as not raising exceptions.
 
370
        """
 
371
        both_exc_and_no_exc = []
 
372
        missing_except = []
 
373
        class_re = re.compile(r'^(cdef\s+)?(public\s+)?'
 
374
                              r'(api\s+)?class (\w+).*:', re.MULTILINE)
 
375
        extern_class_re = re.compile(r'## extern cdef class (\w+)',
 
376
                                     re.MULTILINE)
 
377
        except_re = re.compile(
 
378
            r'cdef\s+'        # start with cdef
 
379
            r'([\w *]*?)\s*'  # this is the return signature
 
380
            r'(\w+)\s*\('     # the function name
 
381
            r'[^)]*\)\s*'     # parameters
 
382
            r'(.*)\s*:'       # the except clause
 
383
            r'\s*(#\s*cannot[- _]raise)?')  # cannot raise comment
 
384
        for fname, text in self.get_source_file_contents(
 
385
                extensions=('.pyx',)):
 
386
            known_classes = set([m[-1] for m in class_re.findall(text)])
 
387
            known_classes.update(extern_class_re.findall(text))
 
388
            cdefs = except_re.findall(text)
 
389
            for sig, func, exc_clause, no_exc_comment in cdefs:
 
390
                if sig.startswith('api '):
 
391
                    sig = sig[4:]
 
392
                if not sig or sig in known_classes:
 
393
                    sig = 'object'
 
394
                if 'nogil' in exc_clause:
 
395
                    exc_clause = exc_clause.replace('nogil', '').strip()
 
396
                if exc_clause and no_exc_comment:
 
397
                    both_exc_and_no_exc.append((fname, func))
 
398
                if sig != 'object' and not (exc_clause or no_exc_comment):
 
399
                    missing_except.append((fname, func))
 
400
        error_msg = []
 
401
        if both_exc_and_no_exc:
 
402
            error_msg.append(
 
403
                'The following functions had "cannot raise" comments'
 
404
                ' but did have an except clause set:')
 
405
            for fname, func in both_exc_and_no_exc:
 
406
                error_msg.append('%s:%s' % (fname, func))
 
407
            error_msg.extend(('', ''))
 
408
        if missing_except:
 
409
            error_msg.append(
 
410
                'The following functions have fixed return types,'
 
411
                ' but no except clause.')
 
412
            error_msg.append(
 
413
                'Either add an except or append "# cannot_raise".')
 
414
            for fname, func in missing_except:
 
415
                error_msg.append('%s:%s' % (fname, func))
 
416
            error_msg.extend(('', ''))
 
417
        if error_msg:
 
418
            self.fail('\n'.join(error_msg))