~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-04-01 15:14:38 UTC
  • mfrom: (4202.3.2 bzr-send-tweak)
  • Revision ID: pqm@pqm.ubuntu.com-20090401151438-hqulqoazddtacbls
(andrew) Don't use get_revision_xml when writing a bundle,
        instead get all the revisions together.

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))