1
# Copyright (C) 2005-2010 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""These tests are tests about the source code of bzrlib itself.
19
They are useful for testing code quality, checking coverage metric etc.
22
# import system imports here
30
#import bzrlib specific imports here
35
from bzrlib.tests import (
41
# Files which are listed here will be skipped when testing for Copyright (or
43
COPYRIGHT_EXCEPTIONS = [
44
'bzrlib/_bencode_py.py',
45
'bzrlib/doc_generate/conf.py',
49
LICENSE_EXCEPTIONS = [
50
'bzrlib/_bencode_py.py',
51
'bzrlib/doc_generate/conf.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.
58
# sphinx_conf is semi-autogenerated.
61
class TestSourceHelper(TestCase):
63
def source_file_name(self, package):
64
"""Return the path of the .py file for package."""
65
if getattr(sys, "frozen", None) is not None:
66
raise TestSkipped("can't test sources in frozen distributions.")
67
path = package.__file__
74
class TestApiUsage(TestSourceHelper):
76
def find_occurences(self, rule, filename):
77
"""Find the number of occurences of rule in a file."""
79
source = file(filename, 'r')
81
if line.find(rule) > -1:
85
def test_branch_working_tree(self):
86
"""Test that the number of uses of working_tree in branch is stable."""
87
occurences = self.find_occurences('self.working_tree()',
88
self.source_file_name(bzrlib.branch))
89
# do not even think of increasing this number. If you think you need to
90
# increase it, then you almost certainly are doing something wrong as
91
# the relationship from working_tree to branch is one way.
92
# Note that this is an exact equality so that when the number drops,
93
#it is not given a buffer but rather has this test updated immediately.
94
self.assertEqual(0, occurences)
96
def test_branch_WorkingTree(self):
97
"""Test that the number of uses of working_tree in branch is stable."""
98
occurences = self.find_occurences('WorkingTree',
99
self.source_file_name(bzrlib.branch))
100
# Do not even think of increasing this number. If you think you need to
101
# increase it, then you almost certainly are doing something wrong as
102
# the relationship from working_tree to branch is one way.
103
# As of 20070809, there are no longer any mentions at all.
104
self.assertEqual(0, occurences)
107
class TestSource(TestSourceHelper):
109
def get_bzrlib_dir(self):
110
"""Get the path to the root of bzrlib"""
111
source = self.source_file_name(bzrlib)
112
source_dir = os.path.dirname(source)
114
# Avoid the case when bzrlib is packaged in a zip file
115
if not os.path.isdir(source_dir):
116
raise TestSkipped('Cannot find bzrlib source directory. Expected %s'
120
def get_source_files(self, extensions=None):
121
"""Yield all source files for bzr and bzrlib
123
:param our_files_only: If true, exclude files from included libraries
126
bzrlib_dir = self.get_bzrlib_dir()
127
if extensions is None:
128
extensions = ('.py',)
130
# This is the front-end 'bzr' script
131
bzr_path = self.get_bzr_path()
134
for root, dirs, files in os.walk(bzrlib_dir):
136
if d.endswith('.tmp'):
139
for extension in extensions:
140
if f.endswith(extension):
143
# Did not match the accepted extensions
145
yield osutils.pathjoin(root, f)
147
def get_source_file_contents(self, extensions=None):
148
for fname in self.get_source_files(extensions=extensions):
149
f = open(fname, 'rb')
156
def is_our_code(self, fname):
157
"""Return true if it's a "real" part of bzrlib rather than external code"""
158
if '/util/' in fname or '/plugins/' in fname:
163
def is_copyright_exception(self, fname):
164
"""Certain files are allowed to be different"""
165
if not self.is_our_code(fname):
166
# We don't ask that external utilities or plugins be
169
for exc in COPYRIGHT_EXCEPTIONS:
170
if fname.endswith(exc):
174
def is_license_exception(self, fname):
175
"""Certain files are allowed to be different"""
176
if not self.is_our_code(fname):
178
for exc in LICENSE_EXCEPTIONS:
179
if fname.endswith(exc):
183
def test_tmpdir_not_in_source_files(self):
184
"""When scanning for source files, we don't descend test tempdirs"""
185
for filename in self.get_source_files():
186
if re.search(r'test....\.tmp', filename):
187
self.fail("get_source_file() returned filename %r "
188
"from within a temporary directory"
191
def test_copyright(self):
192
"""Test that all .py and .pyx files have a valid copyright statement"""
195
copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
196
copyright_canonical_re = re.compile(
197
r'# Copyright \(C\) ' # Opening "# Copyright (C)"
198
r'(\d+)(, \d+)*' # Followed by a series of dates
199
r'.*Canonical Ltd' # And containing 'Canonical Ltd'
202
for fname, text in self.get_source_file_contents(
203
extensions=('.py', '.pyx')):
204
if self.is_copyright_exception(fname):
206
match = copyright_canonical_re.search(text)
208
match = copyright_re.search(text)
210
incorrect.append((fname, 'found: %s' % (match.group(),)))
212
incorrect.append((fname, 'no copyright line found\n'))
214
if 'by Canonical' in match.group():
215
incorrect.append((fname,
216
'should not have: "by Canonical": %s'
220
help_text = ["Some files have missing or incorrect copyright"
223
"Please either add them to the list of"
224
" COPYRIGHT_EXCEPTIONS in"
225
" bzrlib/tests/test_source.py",
226
# this is broken to prevent a false match
227
"or add '# Copyright (C)"
228
" 2007 Canonical Ltd' to these files:",
231
for fname, comment in incorrect:
232
help_text.append(fname)
233
help_text.append((' '*4) + comment)
235
self.fail('\n'.join(help_text))
238
"""Test that all .py and .pyx files have a GPL disclaimer."""
242
# This program is free software; you can redistribute it and/or modify
243
# it under the terms of the GNU General Public License as published by
244
# the Free Software Foundation; either version 2 of the License, or
245
# (at your option) any later version.
247
# This program is distributed in the hope that it will be useful,
248
# but WITHOUT ANY WARRANTY; without even the implied warranty of
249
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
250
# GNU General Public License for more details.
252
# You should have received a copy of the GNU General Public License
253
# along with this program; if not, write to the Free Software
254
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
256
gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
258
for fname, text in self.get_source_file_contents(
259
extensions=('.py', '.pyx')):
260
if self.is_license_exception(fname):
262
if not gpl_re.search(text):
263
incorrect.append(fname)
266
help_text = ['Some files have missing or incomplete GPL statement',
268
"Please either add them to the list of"
269
" LICENSE_EXCEPTIONS in"
270
" bzrlib/tests/test_source.py",
271
"Or add the following text to the beginning:",
274
for fname in incorrect:
275
help_text.append((' '*4) + fname)
277
self.fail('\n'.join(help_text))
279
def _push_file(self, dict_, fname, line_no):
280
if fname not in dict_:
281
dict_[fname] = [line_no]
283
dict_[fname].append(line_no)
285
def _format_message(self, dict_, message):
286
files = ["%s: %s" % (f, ', '.join([str(i+1) for i in lines]))
287
for f, lines in dict_.items()]
289
return message + '\n\n %s' % ('\n '.join(files))
291
def test_coding_style(self):
292
"""Check if bazaar code conforms to some coding style conventions.
294
Currently we assert that the following is not present:
297
* no newline at end of files
299
Print how many files have
300
* trailing white space
301
* lines longer than 79 chars
305
illegal_newlines = {}
307
no_newline_at_eof = []
308
for fname, text in self.get_source_file_contents(
309
extensions=('.py', '.pyx')):
310
if not self.is_our_code(fname):
312
lines = text.splitlines(True)
313
last_line_no = len(lines) - 1
314
for line_no, line in enumerate(lines):
316
self._push_file(tabs, fname, line_no)
317
if not line.endswith('\n') or line.endswith('\r\n'):
318
if line_no != last_line_no: # not no_newline_at_eof
319
self._push_file(illegal_newlines, fname, line_no)
320
if line.endswith(' \n'):
321
self._push_file(trailing_ws, fname, line_no)
323
self._push_file(long_lines, fname, line_no)
324
if not lines[-1].endswith('\n'):
325
no_newline_at_eof.append(fname)
328
problems.append(self._format_message(tabs,
329
'Tab characters were found in the following source files.'
330
'\nThey should either be replaced by "\\t" or by spaces:'))
332
print ("There are %i lines with trailing white space in %i files."
333
% (sum([len(lines) for f, lines in trailing_ws.items()]),
336
problems.append(self._format_message(illegal_newlines,
337
'Non-unix newlines were found in the following source files:'))
339
print ("There are %i lines longer than 79 characters in %i files."
340
% (sum([len(lines) for f, lines in long_lines.items()]),
342
if no_newline_at_eof:
343
no_newline_at_eof.sort()
344
problems.append("The following source files doesn't have a "
345
"newline at the end:"
347
% ('\n '.join(no_newline_at_eof)))
349
self.fail('\n\n'.join(problems))
351
def test_no_asserts(self):
352
"""bzr shouldn't use the 'assert' statement."""
353
# assert causes too much variation between -O and not, and tends to
354
# give bad errors to the user
356
# scan down through x for assert statements, report any problems
357
# this is a bit cheesy; it may get some false positives?
358
if x[0] == symbol.assert_stmt:
360
elif x[0] == token.NAME:
361
# can't search further down
364
if sub and search(sub):
368
assert_re = re.compile(r'\bassert\b')
369
for fname, text in self.get_source_file_contents():
370
if not self.is_our_code(fname):
372
if not assert_re.search(text):
374
ast = parser.ast2tuple(parser.suite(text))
376
badfiles.append(fname)
379
"these files contain an assert statement and should not:\n%s"
380
% '\n'.join(badfiles))
382
def test_extension_exceptions(self):
383
"""Extension functions should propagate exceptions.
385
Either they should return an object, have an 'except' clause, or have a
386
"# cannot_raise" to indicate that we've audited them and defined them as not
389
both_exc_and_no_exc = []
391
class_re = re.compile(r'^(cdef\s+)?(public\s+)?'
392
r'(api\s+)?class (\w+).*:', re.MULTILINE)
393
extern_class_re = re.compile(r'## extern cdef class (\w+)',
395
except_re = re.compile(r'cdef\s+' # start with cdef
396
r'([\w *]*?)\s*' # this is the return signature
397
r'(\w+)\s*\(' # the function name
398
r'[^)]*\)\s*' # parameters
399
r'(.*)\s*:' # the except clause
400
r'\s*(#\s*cannot[- _]raise)?' # cannot raise comment
402
for fname, text in self.get_source_file_contents(
403
extensions=('.pyx',)):
404
known_classes = set([m[-1] for m in class_re.findall(text)])
405
known_classes.update(extern_class_re.findall(text))
406
cdefs = except_re.findall(text)
407
for sig, func, exc_clause, no_exc_comment in cdefs:
408
if sig.startswith('api '):
410
if not sig or sig in known_classes:
412
if 'nogil' in exc_clause:
413
exc_clause = exc_clause.replace('nogil', '').strip()
414
if exc_clause and no_exc_comment:
415
both_exc_and_no_exc.append((fname, func))
416
if sig != 'object' and not (exc_clause or no_exc_comment):
417
missing_except.append((fname, func))
419
if both_exc_and_no_exc:
420
error_msg.append('The following functions had "cannot raise" comments'
421
' but did have an except clause set:')
422
for fname, func in both_exc_and_no_exc:
423
error_msg.append('%s:%s' % (fname, func))
424
error_msg.extend(('', ''))
426
error_msg.append('The following functions have fixed return types,'
427
' but no except clause.')
428
error_msg.append('Either add an except or append "# cannot_raise".')
429
for fname, func in missing_except:
430
error_msg.append('%s:%s' % (fname, func))
431
error_msg.extend(('', ''))
433
self.fail('\n'.join(error_msg))