~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Martin Pool
  • Date: 2009-06-19 10:00:56 UTC
  • mto: This revision was merged to the branch mainline in revision 4464.
  • Revision ID: mbp@sourcefrog.net-20090619100056-fco5ooae2ybl88ne
Fix copyrights and remove assert statement from doc_generate

Show diffs side-by-side

added added

removed removed

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