~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_source.py

  • Committer: Ian Clatworthy
  • Date: 2009-01-19 02:24:15 UTC
  • mto: This revision was merged to the branch mainline in revision 3944.
  • Revision ID: ian.clatworthy@canonical.com-20090119022415-mo0mcfeiexfktgwt
apply jam's log --short fix (Ian Clatworthy)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
2
2
#   Authors: Robert Collins <robert.collins@canonical.com>
 
3
#            and others
3
4
#
4
5
# This program is free software; you can redistribute it and/or modify
5
6
# it under the terms of the GNU General Public License as published by
22
23
 
23
24
# import system imports here
24
25
import os
 
26
import parser
 
27
import re
 
28
from cStringIO import StringIO
 
29
import symbol
25
30
import sys
 
31
import token
26
32
 
27
33
#import bzrlib specific imports here
28
 
from bzrlib.tests import TestCase
 
34
from bzrlib import (
 
35
    diff,
 
36
    osutils,
 
37
    patiencediff,
 
38
    textfile,
 
39
    )
29
40
import bzrlib.branch
30
 
 
31
 
 
32
 
class TestApiUsage(TestCase):
 
41
from bzrlib.tests import (
 
42
    KnownFailure,
 
43
    TestCase,
 
44
    TestSkipped,
 
45
    )
 
46
from bzrlib.workingtree import WorkingTree
 
47
 
 
48
 
 
49
# Files which are listed here will be skipped when testing for Copyright (or
 
50
# GPL) statements.
 
51
COPYRIGHT_EXCEPTIONS = ['bzrlib/lsprof.py']
 
52
 
 
53
LICENSE_EXCEPTIONS = ['bzrlib/lsprof.py']
 
54
# Technically, 'bzrlib/lsprof.py' should be 'bzrlib/util/lsprof.py',
 
55
# (we do not check bzrlib/util/, since that is code bundled from elsewhere)
 
56
# but for compatibility with previous releases, we don't want to move it.
 
57
 
 
58
 
 
59
def check_coding_style(old_filename, oldlines, new_filename, newlines, to_file,
 
60
                  allow_binary=False, sequence_matcher=None,
 
61
                  path_encoding='utf8'):
 
62
    """text_differ to be passed to diff.DiffText, which checks code style """
 
63
    if allow_binary is False:
 
64
        textfile.check_text_lines(oldlines)
 
65
        textfile.check_text_lines(newlines)
 
66
 
 
67
    if sequence_matcher is None:
 
68
        sequence_matcher = patiencediff.PatienceSequenceMatcher
 
69
 
 
70
    started = [False] #trick to access parent scoped variable
 
71
    def start_if_needed():
 
72
        if not started[0]:
 
73
            to_file.write('+++ %s\n' % new_filename)
 
74
            started[0] = True
 
75
 
 
76
    def check_newlines(j1, j2):
 
77
        for i, line in enumerate(newlines[j1:j2]):
 
78
            bad_ws_match = re.match(r'^(([\t]*)(.*?)([\t ]*))(\r?\n)?$', line)
 
79
            if bad_ws_match:
 
80
                line_content = bad_ws_match.group(1)
 
81
                has_leading_tabs = bool(bad_ws_match.group(2))
 
82
                has_trailing_whitespace = bool(bad_ws_match.group(4))
 
83
                if has_leading_tabs:
 
84
                    start_if_needed()
 
85
                    to_file.write('line %i has leading tabs: "%s"\n'% (
 
86
                        i+1+j1, line_content))
 
87
                if has_trailing_whitespace:
 
88
                    start_if_needed()
 
89
                    to_file.write('line %i has trailing whitespace: "%s"\n'% (
 
90
                        i+1+j1, line_content))
 
91
                if len(line_content) > 79:
 
92
                    print (
 
93
                        '\nFile %s\nline %i is longer than 79 characters:'
 
94
                        '\n"%s"'% (new_filename, i+1+j1, line_content))
 
95
 
 
96
    for group in sequence_matcher(None, oldlines, newlines
 
97
            ).get_grouped_opcodes(0):
 
98
        for tag, i1, i2, j1, j2 in group:
 
99
            if tag == 'replace' or tag == 'insert':
 
100
                check_newlines(j1, j2)
 
101
 
 
102
    if len(newlines) == j2 and not newlines[j2-1].endswith('\n'):
 
103
        start_if_needed()
 
104
        to_file.write("\\ No newline at end of file\n")
 
105
 
 
106
 
 
107
class TestSourceHelper(TestCase):
 
108
 
 
109
    def source_file_name(self, package):
 
110
        """Return the path of the .py file for package."""
 
111
        if getattr(sys, "frozen", None) is not None:
 
112
            raise TestSkipped("can't test sources in frozen distributions.")
 
113
        path = package.__file__
 
114
        if path[-1] in 'co':
 
115
            return path[:-1]
 
116
        else:
 
117
            return path
 
118
 
 
119
 
 
120
class TestApiUsage(TestSourceHelper):
33
121
 
34
122
    def find_occurences(self, rule, filename):
35
123
        """Find the number of occurences of rule in a file."""
40
128
                occurences += 1
41
129
        return occurences
42
130
 
43
 
    def source_file_name(self, package):
44
 
        """Return the path of the .py file for package."""
45
 
        path = package.__file__
46
 
        if path[-1] in 'co':
47
 
            return path[:-1]
48
 
        else:
49
 
            return path
50
 
 
51
131
    def test_branch_working_tree(self):
52
132
        """Test that the number of uses of working_tree in branch is stable."""
53
133
        occurences = self.find_occurences('self.working_tree()',
63
143
        """Test that the number of uses of working_tree in branch is stable."""
64
144
        occurences = self.find_occurences('WorkingTree',
65
145
                                          self.source_file_name(bzrlib.branch))
66
 
        # do not even think of increasing this number. If you think you need to
 
146
        # Do not even think of increasing this number. If you think you need to
67
147
        # increase it, then you almost certainly are doing something wrong as
68
148
        # the relationship from working_tree to branch is one way.
69
 
        # This number should be 4 (import NoWorkingTree and WorkingTree, 
70
 
        # raise NoWorkingTree from working_tree(), and construct a working tree
71
 
        # there) but a merge that regressed this was done before this test was
72
 
        # written. Note that this is an exact equality so that when the number
73
 
        # drops, it is not given a buffer but rather this test updated
74
 
        # immediately.
75
 
        self.assertEqual(4, occurences)
 
149
        # As of 20070809, there are no longer any mentions at all.
 
150
        self.assertEqual(0, occurences)
 
151
 
 
152
 
 
153
class TestSource(TestSourceHelper):
 
154
 
 
155
    def get_bzrlib_dir(self):
 
156
        """Get the path to the root of bzrlib"""
 
157
        source = self.source_file_name(bzrlib)
 
158
        source_dir = os.path.dirname(source)
 
159
 
 
160
        # Avoid the case when bzrlib is packaged in a zip file
 
161
        if not os.path.isdir(source_dir):
 
162
            raise TestSkipped('Cannot find bzrlib source directory. Expected %s'
 
163
                              % source_dir)
 
164
        return source_dir
 
165
 
 
166
    def get_source_files(self):
 
167
        """Yield all source files for bzr and bzrlib
 
168
        
 
169
        :param our_files_only: If true, exclude files from included libraries
 
170
            or plugins.
 
171
        """
 
172
        bzrlib_dir = self.get_bzrlib_dir()
 
173
 
 
174
        # This is the front-end 'bzr' script
 
175
        bzr_path = self.get_bzr_path()
 
176
        yield bzr_path
 
177
 
 
178
        for root, dirs, files in os.walk(bzrlib_dir):
 
179
            for d in dirs:
 
180
                if d.endswith('.tmp'):
 
181
                    dirs.remove(d)
 
182
            for f in files:
 
183
                if not f.endswith('.py'):
 
184
                    continue
 
185
                yield osutils.pathjoin(root, f)
 
186
 
 
187
    def get_source_file_contents(self):
 
188
        for fname in self.get_source_files():
 
189
            f = open(fname, 'rb')
 
190
            try:
 
191
                text = f.read()
 
192
            finally:
 
193
                f.close()
 
194
            yield fname, text
 
195
 
 
196
    def is_our_code(self, fname):
 
197
        """Return true if it's a "real" part of bzrlib rather than external code"""
 
198
        if '/util/' in fname or '/plugins/' in fname:
 
199
            return False
 
200
        else:
 
201
            return True
 
202
 
 
203
    def is_copyright_exception(self, fname):
 
204
        """Certain files are allowed to be different"""
 
205
        if not self.is_our_code(fname):
 
206
            # We don't ask that external utilities or plugins be
 
207
            # (C) Canonical Ltd
 
208
            return True
 
209
        for exc in COPYRIGHT_EXCEPTIONS:
 
210
            if fname.endswith(exc):
 
211
                return True
 
212
        return False
 
213
 
 
214
    def is_license_exception(self, fname):
 
215
        """Certain files are allowed to be different"""
 
216
        if not self.is_our_code(fname):
 
217
            return True
 
218
        for exc in LICENSE_EXCEPTIONS:
 
219
            if fname.endswith(exc):
 
220
                return True
 
221
        return False
 
222
 
 
223
    def test_tmpdir_not_in_source_files(self):
 
224
        """When scanning for source files, we don't descend test tempdirs"""
 
225
        for filename in self.get_source_files():
 
226
            if re.search(r'test....\.tmp', filename):
 
227
                self.fail("get_source_file() returned filename %r "
 
228
                          "from within a temporary directory"
 
229
                          % filename)
 
230
 
 
231
    def test_copyright(self):
 
232
        """Test that all .py files have a valid copyright statement"""
 
233
        # These are files which contain a different copyright statement
 
234
        # and that is okay.
 
235
        incorrect = []
 
236
 
 
237
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
 
238
        copyright_canonical_re = re.compile(
 
239
            r'# Copyright \(C\) ' # Opening "# Copyright (C)"
 
240
            r'(\d+)(, \d+)*' # Followed by a series of dates
 
241
            r'.*Canonical Ltd' # And containing 'Canonical Ltd'
 
242
            )
 
243
 
 
244
        for fname, text in self.get_source_file_contents():
 
245
            if self.is_copyright_exception(fname):
 
246
                continue
 
247
            match = copyright_canonical_re.search(text)
 
248
            if not match:
 
249
                match = copyright_re.search(text)
 
250
                if match:
 
251
                    incorrect.append((fname, 'found: %s' % (match.group(),)))
 
252
                else:
 
253
                    incorrect.append((fname, 'no copyright line found\n'))
 
254
            else:
 
255
                if 'by Canonical' in match.group():
 
256
                    incorrect.append((fname,
 
257
                        'should not have: "by Canonical": %s'
 
258
                        % (match.group(),)))
 
259
 
 
260
        if incorrect:
 
261
            help_text = ["Some files have missing or incorrect copyright"
 
262
                         " statements.",
 
263
                         "",
 
264
                         "Please either add them to the list of"
 
265
                         " COPYRIGHT_EXCEPTIONS in"
 
266
                         " bzrlib/tests/test_source.py",
 
267
                         # this is broken to prevent a false match
 
268
                         "or add '# Copyright (C)"
 
269
                         " 2007 Canonical Ltd' to these files:",
 
270
                         "",
 
271
                        ]
 
272
            for fname, comment in incorrect:
 
273
                help_text.append(fname)
 
274
                help_text.append((' '*4) + comment)
 
275
 
 
276
            self.fail('\n'.join(help_text))
 
277
 
 
278
    def test_gpl(self):
 
279
        """Test that all .py files have a GPL disclaimer"""
 
280
        incorrect = []
 
281
 
 
282
        gpl_txt = """
 
283
# This program is free software; you can redistribute it and/or modify
 
284
# it under the terms of the GNU General Public License as published by
 
285
# the Free Software Foundation; either version 2 of the License, or
 
286
# (at your option) any later version.
 
287
#
 
288
# This program is distributed in the hope that it will be useful,
 
289
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
290
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
291
# GNU General Public License for more details.
 
292
#
 
293
# You should have received a copy of the GNU General Public License
 
294
# along with this program; if not, write to the Free Software
 
295
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
296
"""
 
297
        gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
 
298
 
 
299
        for fname, text in self.get_source_file_contents():
 
300
            if self.is_license_exception(fname):
 
301
                continue
 
302
            if not gpl_re.search(text):
 
303
                incorrect.append(fname)
 
304
 
 
305
        if incorrect:
 
306
            help_text = ['Some files have missing or incomplete GPL statement',
 
307
                         "",
 
308
                         "Please either add them to the list of"
 
309
                         " LICENSE_EXCEPTIONS in"
 
310
                         " bzrlib/tests/test_source.py",
 
311
                         "Or add the following text to the beginning:",
 
312
                         gpl_txt
 
313
                        ]
 
314
            for fname in incorrect:
 
315
                help_text.append((' '*4) + fname)
 
316
 
 
317
            self.fail('\n'.join(help_text))
 
318
 
 
319
    def test_no_tabs(self):
 
320
        """bzrlib source files should not contain any tab characters."""
 
321
        incorrect = []
 
322
 
 
323
        for fname, text in self.get_source_file_contents():
 
324
            if not self.is_our_code(fname):
 
325
                continue
 
326
            if '\t' in text:
 
327
                incorrect.append(fname)
 
328
 
 
329
        if incorrect:
 
330
            self.fail('Tab characters were found in the following source files.'
 
331
              '\nThey should either be replaced by "\\t" or by spaces:'
 
332
              '\n\n    %s'
 
333
              % ('\n    '.join(incorrect)))
 
334
 
 
335
    def test_coding_style(self):
 
336
        """ Check if bazaar code conforms to some coding style conventions.
 
337
 
 
338
        Currently we check all .py files for:
 
339
         * new trailing white space
 
340
         * new leading tabs
 
341
         * new long lines (give warning only)
 
342
         * no newline at end of files
 
343
        """
 
344
        bzr_dir = osutils.dirname(self.get_bzrlib_dir())
 
345
        try:
 
346
            wt = WorkingTree.open(bzr_dir)
 
347
        except:
 
348
            raise TestSkipped(
 
349
                'Could not open bazaar working tree %s'
 
350
                % bzr_dir)
 
351
        diff_output = StringIO()
 
352
        wt.lock_read()
 
353
        try:
 
354
            new_tree = wt
 
355
            old_tree = new_tree.basis_tree()
 
356
 
 
357
            old_tree.lock_read()
 
358
            new_tree.lock_read()
 
359
            try:
 
360
                iterator = new_tree.iter_changes(old_tree)
 
361
                for (file_id, paths, changed_content, versioned, parent,
 
362
                    name, kind, executable) in iterator:
 
363
                    if (changed_content and paths[1].endswith('.py')):
 
364
                        if kind == ('file', 'file'):
 
365
                            diff_text = diff.DiffText(old_tree, new_tree,
 
366
                                to_file=diff_output,
 
367
                                text_differ=check_coding_style)
 
368
                            diff_text.diff(file_id, paths[0], paths[1],
 
369
                                kind[0], kind[1])
 
370
                        else:
 
371
                            check_coding_style(name[0], (), name[1],
 
372
                                new_tree.get_file(file_id).readlines(),
 
373
                                diff_output)
 
374
            finally:
 
375
                old_tree.unlock()
 
376
                new_tree.unlock()
 
377
        finally:
 
378
            wt.unlock()
 
379
        if len(diff_output.getvalue()) > 0:
 
380
            self.fail("Unacceptable coding style:\n" + diff_output.getvalue())
 
381
 
 
382
    def test_no_asserts(self):
 
383
        """bzr shouldn't use the 'assert' statement."""
 
384
        # assert causes too much variation between -O and not, and tends to
 
385
        # give bad errors to the user
 
386
        def search(x):
 
387
            # scan down through x for assert statements, report any problems
 
388
            # this is a bit cheesy; it may get some false positives?
 
389
            if x[0] == symbol.assert_stmt:
 
390
                return True
 
391
            elif x[0] == token.NAME:
 
392
                # can't search further down
 
393
                return False
 
394
            for sub in x[1:]:
 
395
                if sub and search(sub):
 
396
                    return True
 
397
            return False
 
398
        badfiles = []
 
399
        for fname, text in self.get_source_file_contents():
 
400
            if not self.is_our_code(fname):
 
401
                continue
 
402
            ast = parser.ast2tuple(parser.suite(''.join(text)))
 
403
            if search(ast):
 
404
                badfiles.append(fname)
 
405
        if badfiles:
 
406
            self.fail(
 
407
                "these files contain an assert statement and should not:\n%s"
 
408
                % '\n'.join(badfiles))