~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Martin Pool
  • Date: 2005-05-25 00:48:22 UTC
  • Revision ID: mbp@sourcefrog.net-20050525004822-7665484961d59734
- Refactor diff code into one that works purely on 
  Tree objects

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""These tests are tests about the source code of bzrlib itself.
18
 
 
19
 
They are useful for testing code quality, checking coverage metric etc.
20
 
"""
21
 
 
22
 
# import system imports here
23
 
import os
24
 
import parser
25
 
import re
26
 
import symbol
27
 
import sys
28
 
import token
29
 
 
30
 
#import bzrlib specific imports here
31
 
from bzrlib import (
32
 
    osutils,
33
 
    )
34
 
import bzrlib.branch
35
 
from bzrlib.tests import (
36
 
    TestCase,
37
 
    TestSkipped,
38
 
    )
39
 
 
40
 
 
41
 
# Files which are listed here will be skipped when testing for Copyright (or
42
 
# GPL) statements.
43
 
COPYRIGHT_EXCEPTIONS = ['bzrlib/lsprof.py', 'bzrlib/_bencode_py.py',
44
 
    'bzrlib/doc_generate/sphinx_conf.py']
45
 
 
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)
50
 
# but for compatibility with previous releases, we don't want to move it.
51
 
#
52
 
# sphinx_conf is semi-autogenerated.
53
 
 
54
 
 
55
 
class TestSourceHelper(TestCase):
56
 
 
57
 
    def source_file_name(self, package):
58
 
        """Return the path of the .py file for package."""
59
 
        if getattr(sys, "frozen", None) is not None:
60
 
            raise TestSkipped("can't test sources in frozen distributions.")
61
 
        path = package.__file__
62
 
        if path[-1] in 'co':
63
 
            return path[:-1]
64
 
        else:
65
 
            return path
66
 
 
67
 
 
68
 
class TestApiUsage(TestSourceHelper):
69
 
 
70
 
    def find_occurences(self, rule, filename):
71
 
        """Find the number of occurences of rule in a file."""
72
 
        occurences = 0
73
 
        source = file(filename, 'r')
74
 
        for line in source:
75
 
            if line.find(rule) > -1:
76
 
                occurences += 1
77
 
        return occurences
78
 
 
79
 
    def test_branch_working_tree(self):
80
 
        """Test that the number of uses of working_tree in branch is stable."""
81
 
        occurences = self.find_occurences('self.working_tree()',
82
 
                                          self.source_file_name(bzrlib.branch))
83
 
        # do not even think of increasing this number. If you think you need to
84
 
        # increase it, then you almost certainly are doing something wrong as
85
 
        # the relationship from working_tree to branch is one way.
86
 
        # Note that this is an exact equality so that when the number drops,
87
 
        #it is not given a buffer but rather has this test updated immediately.
88
 
        self.assertEqual(0, occurences)
89
 
 
90
 
    def test_branch_WorkingTree(self):
91
 
        """Test that the number of uses of working_tree in branch is stable."""
92
 
        occurences = self.find_occurences('WorkingTree',
93
 
                                          self.source_file_name(bzrlib.branch))
94
 
        # Do not even think of increasing this number. If you think you need to
95
 
        # increase it, then you almost certainly are doing something wrong as
96
 
        # the relationship from working_tree to branch is one way.
97
 
        # As of 20070809, there are no longer any mentions at all.
98
 
        self.assertEqual(0, occurences)
99
 
 
100
 
 
101
 
class TestSource(TestSourceHelper):
102
 
 
103
 
    def get_bzrlib_dir(self):
104
 
        """Get the path to the root of bzrlib"""
105
 
        source = self.source_file_name(bzrlib)
106
 
        source_dir = os.path.dirname(source)
107
 
 
108
 
        # Avoid the case when bzrlib is packaged in a zip file
109
 
        if not os.path.isdir(source_dir):
110
 
            raise TestSkipped('Cannot find bzrlib source directory. Expected %s'
111
 
                              % source_dir)
112
 
        return source_dir
113
 
 
114
 
    def get_source_files(self, extensions=None):
115
 
        """Yield all source files for bzr and bzrlib
116
 
 
117
 
        :param our_files_only: If true, exclude files from included libraries
118
 
            or plugins.
119
 
        """
120
 
        bzrlib_dir = self.get_bzrlib_dir()
121
 
        if extensions is None:
122
 
            extensions = ('.py',)
123
 
 
124
 
        # This is the front-end 'bzr' script
125
 
        bzr_path = self.get_bzr_path()
126
 
        yield bzr_path
127
 
 
128
 
        for root, dirs, files in os.walk(bzrlib_dir):
129
 
            for d in dirs:
130
 
                if d.endswith('.tmp'):
131
 
                    dirs.remove(d)
132
 
            for f in files:
133
 
                for extension in extensions:
134
 
                    if f.endswith(extension):
135
 
                        break
136
 
                else:
137
 
                    # Did not match the accepted extensions
138
 
                    continue
139
 
                yield osutils.pathjoin(root, f)
140
 
 
141
 
    def get_source_file_contents(self, extensions=None):
142
 
        for fname in self.get_source_files(extensions=extensions):
143
 
            f = open(fname, 'rb')
144
 
            try:
145
 
                text = f.read()
146
 
            finally:
147
 
                f.close()
148
 
            yield fname, text
149
 
 
150
 
    def is_our_code(self, fname):
151
 
        """Return true if it's a "real" part of bzrlib rather than external code"""
152
 
        if '/util/' in fname or '/plugins/' in fname:
153
 
            return False
154
 
        else:
155
 
            return True
156
 
 
157
 
    def is_copyright_exception(self, fname):
158
 
        """Certain files are allowed to be different"""
159
 
        if not self.is_our_code(fname):
160
 
            # We don't ask that external utilities or plugins be
161
 
            # (C) Canonical Ltd
162
 
            return True
163
 
        for exc in COPYRIGHT_EXCEPTIONS:
164
 
            if fname.endswith(exc):
165
 
                return True
166
 
        return False
167
 
 
168
 
    def is_license_exception(self, fname):
169
 
        """Certain files are allowed to be different"""
170
 
        if not self.is_our_code(fname):
171
 
            return True
172
 
        for exc in LICENSE_EXCEPTIONS:
173
 
            if fname.endswith(exc):
174
 
                return True
175
 
        return False
176
 
 
177
 
    def test_tmpdir_not_in_source_files(self):
178
 
        """When scanning for source files, we don't descend test tempdirs"""
179
 
        for filename in self.get_source_files():
180
 
            if re.search(r'test....\.tmp', filename):
181
 
                self.fail("get_source_file() returned filename %r "
182
 
                          "from within a temporary directory"
183
 
                          % filename)
184
 
 
185
 
    def test_copyright(self):
186
 
        """Test that all .py and .pyx files have a valid copyright statement"""
187
 
        incorrect = []
188
 
 
189
 
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
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
 
            )
195
 
 
196
 
        for fname, text in self.get_source_file_contents(
197
 
                extensions=('.py', '.pyx')):
198
 
            if self.is_copyright_exception(fname):
199
 
                continue
200
 
            match = copyright_canonical_re.search(text)
201
 
            if not match:
202
 
                match = copyright_re.search(text)
203
 
                if match:
204
 
                    incorrect.append((fname, 'found: %s' % (match.group(),)))
205
 
                else:
206
 
                    incorrect.append((fname, 'no copyright line found\n'))
207
 
            else:
208
 
                if 'by Canonical' in match.group():
209
 
                    incorrect.append((fname,
210
 
                        'should not have: "by Canonical": %s'
211
 
                        % (match.group(),)))
212
 
 
213
 
        if incorrect:
214
 
            help_text = ["Some files have missing or incorrect copyright"
215
 
                         " statements.",
216
 
                         "",
217
 
                         "Please either add them to the list of"
218
 
                         " COPYRIGHT_EXCEPTIONS in"
219
 
                         " bzrlib/tests/test_source.py",
220
 
                         # this is broken to prevent a false match
221
 
                         "or add '# Copyright (C)"
222
 
                         " 2007 Canonical Ltd' to these files:",
223
 
                         "",
224
 
                        ]
225
 
            for fname, comment in incorrect:
226
 
                help_text.append(fname)
227
 
                help_text.append((' '*4) + comment)
228
 
 
229
 
            self.fail('\n'.join(help_text))
230
 
 
231
 
    def test_gpl(self):
232
 
        """Test that all .py and .pyx files have a GPL disclaimer."""
233
 
        incorrect = []
234
 
 
235
 
        gpl_txt = """
236
 
# This program is free software; you can redistribute it and/or modify
237
 
# it under the terms of the GNU General Public License as published by
238
 
# the Free Software Foundation; either version 2 of the License, or
239
 
# (at your option) any later version.
240
 
#
241
 
# This program is distributed in the hope that it will be useful,
242
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
243
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
244
 
# GNU General Public License for more details.
245
 
#
246
 
# You should have received a copy of the GNU General Public License
247
 
# along with this program; if not, write to the Free Software
248
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
249
 
"""
250
 
        gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
251
 
 
252
 
        for fname, text in self.get_source_file_contents(
253
 
                extensions=('.py', '.pyx')):
254
 
            if self.is_license_exception(fname):
255
 
                continue
256
 
            if not gpl_re.search(text):
257
 
                incorrect.append(fname)
258
 
 
259
 
        if incorrect:
260
 
            help_text = ['Some files have missing or incomplete GPL statement',
261
 
                         "",
262
 
                         "Please either add them to the list of"
263
 
                         " LICENSE_EXCEPTIONS in"
264
 
                         " bzrlib/tests/test_source.py",
265
 
                         "Or add the following text to the beginning:",
266
 
                         gpl_txt
267
 
                        ]
268
 
            for fname in incorrect:
269
 
                help_text.append((' '*4) + fname)
270
 
 
271
 
            self.fail('\n'.join(help_text))
272
 
 
273
 
    def _push_file(self, dict_, fname, line_no):
274
 
        if fname not in dict_:
275
 
            dict_[fname] = [line_no]
276
 
        else:
277
 
            dict_[fname].append(line_no)
278
 
 
279
 
    def _format_message(self, dict_, message):
280
 
        files = ["%s: %s" % (f, ', '.join([str(i+1) for i in lines]))
281
 
                for f, lines in dict_.items()]
282
 
        files.sort()
283
 
        return message + '\n\n    %s' % ('\n    '.join(files))
284
 
 
285
 
    def test_coding_style(self):
286
 
        """Check if bazaar code conforms to some coding style conventions.
287
 
 
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
292
 
 
293
 
        Print how many files have
294
 
         * trailing white space
295
 
         * lines longer than 79 chars
296
 
        """
297
 
        tabs = {}
298
 
        trailing_ws = {}
299
 
        illegal_newlines = {}
300
 
        long_lines = {}
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 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)
318
 
            if not lines[-1].endswith('\n'):
319
 
                no_newline_at_eof.append(fname)
320
 
        problems = []
321
 
        if tabs:
322
 
            problems.append(self._format_message(tabs,
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)))
329
 
        if illegal_newlines:
330
 
            problems.append(self._format_message(illegal_newlines,
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)))
336
 
        if no_newline_at_eof:
337
 
            no_newline_at_eof.sort()
338
 
            problems.append("The following source files doesn't have a "
339
 
                "newline at the end:"
340
 
               '\n\n    %s'
341
 
               % ('\n    '.join(no_newline_at_eof)))
342
 
        if problems:
343
 
            self.fail('\n\n'.join(problems))
344
 
 
345
 
    def test_no_asserts(self):
346
 
        """bzr shouldn't use the 'assert' statement."""
347
 
        # assert causes too much variation between -O and not, and tends to
348
 
        # give bad errors to the user
349
 
        def search(x):
350
 
            # scan down through x for assert statements, report any problems
351
 
            # this is a bit cheesy; it may get some false positives?
352
 
            if x[0] == symbol.assert_stmt:
353
 
                return True
354
 
            elif x[0] == token.NAME:
355
 
                # can't search further down
356
 
                return False
357
 
            for sub in x[1:]:
358
 
                if sub and search(sub):
359
 
                    return True
360
 
            return False
361
 
        badfiles = []
362
 
        assert_re = re.compile(r'\bassert\b')
363
 
        for fname, text in self.get_source_file_contents():
364
 
            if not self.is_our_code(fname):
365
 
                continue
366
 
            if not assert_re.search(text):
367
 
                continue
368
 
            ast = parser.ast2tuple(parser.suite(text))
369
 
            if search(ast):
370
 
                badfiles.append(fname)
371
 
        if badfiles:
372
 
            self.fail(
373
 
                "these files contain an assert statement and should not:\n%s"
374
 
                % '\n'.join(badfiles))
375
 
 
376
 
    def test_extension_exceptions(self):
377
 
        """Extension functions should propagate exceptions.
378
 
 
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.
382
 
        """
383
 
        both_exc_and_no_exc = []
384
 
        missing_except = []
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
 
                              )
394
 
        for fname, text in self.get_source_file_contents(
395
 
                extensions=('.pyx',)):
396
 
            known_classes = set([m[-1] for m in class_re.findall(text)])
397
 
            cdefs = except_re.findall(text)
398
 
            for sig, func, exc_clause, no_exc_comment in cdefs:
399
 
                if sig.startswith('api '):
400
 
                    sig = sig[4:]
401
 
                if not sig or sig in known_classes:
402
 
                    sig = 'object'
403
 
                if 'nogil' in exc_clause:
404
 
                    exc_clause = exc_clause.replace('nogil', '').strip()
405
 
                if exc_clause and no_exc_comment:
406
 
                    both_exc_and_no_exc.append((fname, func))
407
 
                if sig != 'object' and not (exc_clause or no_exc_comment):
408
 
                    missing_except.append((fname, func))
409
 
        error_msg = []
410
 
        if both_exc_and_no_exc:
411
 
            error_msg.append('The following functions had "cannot raise" comments'
412
 
                             ' but did have an except clause set:')
413
 
            for fname, func in both_exc_and_no_exc:
414
 
                error_msg.append('%s:%s' % (fname, func))
415
 
            error_msg.extend(('', ''))
416
 
        if missing_except:
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".')
420
 
            for fname, func in missing_except:
421
 
                error_msg.append('%s:%s' % (fname, func))
422
 
            error_msg.extend(('', ''))
423
 
        if error_msg:
424
 
            self.fail('\n'.join(error_msg))