~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-17 07:51:51 UTC
  • Revision ID: mbp@sourcefrog.net-20050517075151-b64853a6a38a6225
- export bzrlib.find_branch

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