/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Ian Clatworthy
  • Date: 2009-03-20 04:16:24 UTC
  • mto: (4171.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 4173.
  • Revision ID: ian.clatworthy@canonical.com-20090320041624-x34ct8j7sa80bmn8
sha1 calculation done by workingtree layer now

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