~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-07-22 22:37:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050722223753-7dced4e32d3ce21d
- add the start of a test for inventory file-id matching

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