~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Andrew Bennetts
  • Date: 2009-03-10 02:44:15 UTC
  • mto: This revision was merged to the branch mainline in revision 4103.
  • Revision ID: andrew.bennetts@canonical.com-20090310024415-3fl3ie61atq39c81
Fix 'trailing' whitespace (actually just a blank line in an indented docstring).

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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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']
 
46
 
 
47
LICENSE_EXCEPTIONS = ['bzrlib/lsprof.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):
 
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
 
 
120
        # This is the front-end 'bzr' script
 
121
        bzr_path = self.get_bzr_path()
 
122
        yield bzr_path
 
123
 
 
124
        for root, dirs, files in os.walk(bzrlib_dir):
 
125
            for d in dirs:
 
126
                if d.endswith('.tmp'):
 
127
                    dirs.remove(d)
 
128
            for f in files:
 
129
                if not f.endswith('.py'):
 
130
                    continue
 
131
                yield osutils.pathjoin(root, f)
 
132
 
 
133
    def get_source_file_contents(self):
 
134
        for fname in self.get_source_files():
 
135
            f = open(fname, 'rb')
 
136
            try:
 
137
                text = f.read()
 
138
            finally:
 
139
                f.close()
 
140
            yield fname, text
 
141
 
 
142
    def is_our_code(self, fname):
 
143
        """Return true if it's a "real" part of bzrlib rather than external code"""
 
144
        if '/util/' in fname or '/plugins/' in fname:
 
145
            return False
 
146
        else:
 
147
            return True
 
148
 
 
149
    def is_copyright_exception(self, fname):
 
150
        """Certain files are allowed to be different"""
 
151
        if not self.is_our_code(fname):
 
152
            # We don't ask that external utilities or plugins be
 
153
            # (C) Canonical Ltd
 
154
            return True
 
155
        for exc in COPYRIGHT_EXCEPTIONS:
 
156
            if fname.endswith(exc):
 
157
                return True
 
158
        return False
 
159
 
 
160
    def is_license_exception(self, fname):
 
161
        """Certain files are allowed to be different"""
 
162
        if not self.is_our_code(fname):
 
163
            return True
 
164
        for exc in LICENSE_EXCEPTIONS:
 
165
            if fname.endswith(exc):
 
166
                return True
 
167
        return False
 
168
 
 
169
    def test_tmpdir_not_in_source_files(self):
 
170
        """When scanning for source files, we don't descend test tempdirs"""
 
171
        for filename in self.get_source_files():
 
172
            if re.search(r'test....\.tmp', filename):
 
173
                self.fail("get_source_file() returned filename %r "
 
174
                          "from within a temporary directory"
 
175
                          % filename)
 
176
 
 
177
    def test_copyright(self):
 
178
        """Test that all .py files have a valid copyright statement"""
 
179
        # These are files which contain a different copyright statement
 
180
        # and that is okay.
 
181
        incorrect = []
 
182
 
 
183
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
 
184
        copyright_canonical_re = re.compile(
 
185
            r'# Copyright \(C\) ' # Opening "# Copyright (C)"
 
186
            r'(\d+)(, \d+)*' # Followed by a series of dates
 
187
            r'.*Canonical Ltd' # And containing 'Canonical Ltd'
 
188
            )
 
189
 
 
190
        for fname, text in self.get_source_file_contents():
 
191
            if self.is_copyright_exception(fname):
 
192
                continue
 
193
            match = copyright_canonical_re.search(text)
 
194
            if not match:
 
195
                match = copyright_re.search(text)
 
196
                if match:
 
197
                    incorrect.append((fname, 'found: %s' % (match.group(),)))
 
198
                else:
 
199
                    incorrect.append((fname, 'no copyright line found\n'))
 
200
            else:
 
201
                if 'by Canonical' in match.group():
 
202
                    incorrect.append((fname,
 
203
                        'should not have: "by Canonical": %s'
 
204
                        % (match.group(),)))
 
205
 
 
206
        if incorrect:
 
207
            help_text = ["Some files have missing or incorrect copyright"
 
208
                         " statements.",
 
209
                         "",
 
210
                         "Please either add them to the list of"
 
211
                         " COPYRIGHT_EXCEPTIONS in"
 
212
                         " bzrlib/tests/test_source.py",
 
213
                         # this is broken to prevent a false match
 
214
                         "or add '# Copyright (C)"
 
215
                         " 2007 Canonical Ltd' to these files:",
 
216
                         "",
 
217
                        ]
 
218
            for fname, comment in incorrect:
 
219
                help_text.append(fname)
 
220
                help_text.append((' '*4) + comment)
 
221
 
 
222
            self.fail('\n'.join(help_text))
 
223
 
 
224
    def test_gpl(self):
 
225
        """Test that all .py files have a GPL disclaimer"""
 
226
        incorrect = []
 
227
 
 
228
        gpl_txt = """
 
229
# This program is free software; you can redistribute it and/or modify
 
230
# it under the terms of the GNU General Public License as published by
 
231
# the Free Software Foundation; either version 2 of the License, or
 
232
# (at your option) any later version.
 
233
#
 
234
# This program is distributed in the hope that it will be useful,
 
235
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
236
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
237
# GNU General Public License for more details.
 
238
#
 
239
# You should have received a copy of the GNU General Public License
 
240
# along with this program; if not, write to the Free Software
 
241
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
242
"""
 
243
        gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
 
244
 
 
245
        for fname, text in self.get_source_file_contents():
 
246
            if self.is_license_exception(fname):
 
247
                continue
 
248
            if not gpl_re.search(text):
 
249
                incorrect.append(fname)
 
250
 
 
251
        if incorrect:
 
252
            help_text = ['Some files have missing or incomplete GPL statement',
 
253
                         "",
 
254
                         "Please either add them to the list of"
 
255
                         " LICENSE_EXCEPTIONS in"
 
256
                         " bzrlib/tests/test_source.py",
 
257
                         "Or add the following text to the beginning:",
 
258
                         gpl_txt
 
259
                        ]
 
260
            for fname in incorrect:
 
261
                help_text.append((' '*4) + fname)
 
262
 
 
263
            self.fail('\n'.join(help_text))
 
264
 
 
265
    def _push_file(self, dict_, fname, line_no):
 
266
        if fname not in dict_:
 
267
            dict_[fname] = [line_no]
 
268
        else:
 
269
            dict_[fname].append(line_no)
 
270
 
 
271
    def _format_message(self, dict_, message):
 
272
        files = ["%s: %s" % (f, ', '.join([str(i+1) for i in lines]))
 
273
                for f, lines in dict_.items()]
 
274
        files.sort()
 
275
        return message + '\n\n    %s' % ('\n    '.join(files))
 
276
 
 
277
    def test_coding_style(self):
 
278
        """Check if bazaar code conforms to some coding style conventions.
 
279
 
 
280
        Currently we check for:
 
281
         * any tab characters
 
282
         * trailing white space
 
283
         * non-unix newlines
 
284
         * no newline at end of files
 
285
         * lines longer than 79 chars
 
286
           (only print how many files and lines are in violation)
 
287
        """
 
288
        tabs = {}
 
289
        trailing_ws = {}
 
290
        illegal_newlines = {}
 
291
        long_lines = {}
 
292
        no_newline_at_eof = []
 
293
        for fname, text in self.get_source_file_contents():
 
294
            if not self.is_our_code(fname):
 
295
                continue
 
296
            lines = text.splitlines(True)
 
297
            last_line_no = len(lines) - 1
 
298
            for line_no, line in enumerate(lines):
 
299
                if '\t' in line:
 
300
                    self._push_file(tabs, fname, line_no)
 
301
                if not line.endswith('\n') or line.endswith('\r\n'):
 
302
                    if line_no != last_line_no: # not no_newline_at_eof
 
303
                        self._push_file(illegal_newlines, fname, line_no)
 
304
                if line.endswith(' \n'):
 
305
                    self._push_file(trailing_ws, fname, line_no)
 
306
                if len(line) > 80:
 
307
                    self._push_file(long_lines, fname, line_no)
 
308
            if not lines[-1].endswith('\n'):
 
309
                no_newline_at_eof.append(fname)
 
310
        problems = []
 
311
        if tabs:
 
312
            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:'))
 
315
        if trailing_ws:
 
316
            problems.append(self._format_message(trailing_ws,
 
317
                'Trailing white space was found in the following source files:'
 
318
                ))
 
319
        if illegal_newlines:
 
320
            problems.append(self._format_message(illegal_newlines,
 
321
                'Non-unix newlines were found in the following source files:'))
 
322
        if long_lines:
 
323
            print ("There are %i lines longer than 79 characters in %i files."
 
324
                % (sum([len(lines) for f, lines in long_lines.items()]),
 
325
                    len(long_lines)))
 
326
        if no_newline_at_eof:
 
327
            no_newline_at_eof.sort()
 
328
            problems.append("The following source files doesn't have a "
 
329
                "newline at the end:"
 
330
               '\n\n    %s'
 
331
               % ('\n    '.join(no_newline_at_eof)))
 
332
        if problems:
 
333
            self.fail('\n\n'.join(problems))
 
334
 
 
335
    def test_no_asserts(self):
 
336
        """bzr shouldn't use the 'assert' statement."""
 
337
        # assert causes too much variation between -O and not, and tends to
 
338
        # give bad errors to the user
 
339
        def search(x):
 
340
            # scan down through x for assert statements, report any problems
 
341
            # this is a bit cheesy; it may get some false positives?
 
342
            if x[0] == symbol.assert_stmt:
 
343
                return True
 
344
            elif x[0] == token.NAME:
 
345
                # can't search further down
 
346
                return False
 
347
            for sub in x[1:]:
 
348
                if sub and search(sub):
 
349
                    return True
 
350
            return False
 
351
        badfiles = []
 
352
        for fname, text in self.get_source_file_contents():
 
353
            if not self.is_our_code(fname):
 
354
                continue
 
355
            ast = parser.ast2tuple(parser.suite(''.join(text)))
 
356
            if search(ast):
 
357
                badfiles.append(fname)
 
358
        if badfiles:
 
359
            self.fail(
 
360
                "these files contain an assert statement and should not:\n%s"
 
361
                % '\n'.join(badfiles))