14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""These tests are tests about the source code of breezy itself.
17
"""These tests are tests about the source code of brzlib itself.
19
19
They are useful for testing code quality, checking coverage metric etc.
40
39
# Files which are listed here will be skipped when testing for Copyright (or
42
41
COPYRIGHT_EXCEPTIONS = [
43
'breezy/_bencode_py.py',
44
'breezy/doc_generate/conf.py',
42
'brzlib/_bencode_py.py',
43
'brzlib/doc_generate/conf.py',
48
47
LICENSE_EXCEPTIONS = [
49
'breezy/_bencode_py.py',
50
'breezy/doc_generate/conf.py',
48
'brzlib/_bencode_py.py',
49
'brzlib/doc_generate/conf.py',
53
# Technically, 'breezy/lsprof.py' should be 'breezy/util/lsprof.py',
54
# (we do not check breezy/util/, since that is code bundled from elsewhere)
52
# Technically, 'brzlib/lsprof.py' should be 'brzlib/util/lsprof.py',
53
# (we do not check brzlib/util/, since that is code bundled from elsewhere)
55
54
# but for compatibility with previous releases, we don't want to move it.
57
56
# sphinx_conf is semi-autogenerated.
75
74
def find_occurences(self, rule, filename):
76
75
"""Find the number of occurences of rule in a file."""
78
source = open(filename, 'r')
77
source = file(filename, 'r')
79
78
for line in source:
80
79
if line.find(rule) > -1:
84
83
def test_branch_working_tree(self):
85
84
"""Test that the number of uses of working_tree in branch is stable."""
86
85
occurences = self.find_occurences('self.working_tree()',
87
self.source_file_name(breezy.branch))
86
self.source_file_name(brzlib.branch))
88
87
# do not even think of increasing this number. If you think you need to
89
88
# increase it, then you almost certainly are doing something wrong as
90
89
# the relationship from working_tree to branch is one way.
91
90
# Note that this is an exact equality so that when the number drops,
92
# it is not given a buffer but rather has this test updated immediately.
91
#it is not given a buffer but rather has this test updated immediately.
93
92
self.assertEqual(0, occurences)
95
94
def test_branch_WorkingTree(self):
96
95
"""Test that the number of uses of working_tree in branch is stable."""
97
96
occurences = self.find_occurences('WorkingTree',
98
self.source_file_name(breezy.branch))
97
self.source_file_name(brzlib.branch))
99
98
# Do not even think of increasing this number. If you think you need to
100
99
# increase it, then you almost certainly are doing something wrong as
101
100
# the relationship from working_tree to branch is one way.
106
105
class TestSource(TestSourceHelper):
108
def get_breezy_dir(self):
109
"""Get the path to the root of breezy"""
110
source = self.source_file_name(breezy)
107
def get_brzlib_dir(self):
108
"""Get the path to the root of brzlib"""
109
source = self.source_file_name(brzlib)
111
110
source_dir = os.path.dirname(source)
113
# Avoid the case when breezy is packaged in a zip file
112
# Avoid the case when brzlib is packaged in a zip file
114
113
if not os.path.isdir(source_dir):
115
114
raise TestSkipped(
116
'Cannot find breezy source directory. Expected %s'
115
'Cannot find brzlib source directory. Expected %s'
118
117
return source_dir
120
119
def get_source_files(self, extensions=None):
121
"""Yield all source files for bzr and breezy
120
"""Yield all source files for bzr and brzlib
123
122
:param our_files_only: If true, exclude files from included libraries
126
breezy_dir = self.get_breezy_dir()
125
brzlib_dir = self.get_brzlib_dir()
127
126
if extensions is None:
128
127
extensions = ('.py',)
131
130
bzr_path = self.get_brz_path()
134
for root, dirs, files in os.walk(breezy_dir):
133
for root, dirs, files in os.walk(brzlib_dir):
136
135
if d.endswith('.tmp'):
147
146
def get_source_file_contents(self, extensions=None):
148
147
for fname in self.get_source_files(extensions=extensions):
149
with open(fname, 'r') as f:
150
yield fname, f.read()
148
f = open(fname, 'rb')
152
155
def is_our_code(self, fname):
153
"""True if it's a "real" part of breezy rather than external code"""
156
"""True if it's a "real" part of brzlib rather than external code"""
154
157
if '/util/' in fname or '/plugins/' in fname:
159
162
def is_copyright_exception(self, fname):
160
163
"""Certain files are allowed to be different"""
161
164
if not self.is_our_code(fname):
165
# We don't ask that external utilities or plugins be
163
168
for exc in COPYRIGHT_EXCEPTIONS:
164
169
if fname.endswith(exc):
189
194
copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
190
copyright_statement_re = re.compile(
195
copyright_canonical_re = re.compile(
191
196
r'# Copyright \(C\) ' # Opening "# Copyright (C)"
192
r'(\d+?)((, |-)\d+)*' # followed by a series of dates
193
r' [^ ]*') # and then whoever.
197
r'(\d+)(, \d+)*' # followed by a series of dates
198
r'.*Canonical Ltd') # and containing 'Canonical Ltd'.
195
200
for fname, text in self.get_source_file_contents(
196
201
extensions=('.py', '.pyx')):
197
202
if self.is_copyright_exception(fname):
199
match = copyright_statement_re.search(text)
204
match = copyright_canonical_re.search(text)
201
206
match = copyright_re.search(text)
216
221
"Please either add them to the list of"
217
222
" COPYRIGHT_EXCEPTIONS in"
218
" breezy/tests/test_source.py",
223
" brzlib/tests/test_source.py",
219
224
# this is broken to prevent a false match
220
225
"or add '# Copyright (C)"
221
" 2007 Bazaar hackers' to these files:",
226
" 2007 Canonical Ltd' to these files:",
224
229
for fname, comment in incorrect:
225
230
help_text.append(fname)
226
231
help_text.append((' ' * 4) + comment)
261
266
"Please either add them to the list of"
262
267
" LICENSE_EXCEPTIONS in"
263
" breezy/tests/test_source.py",
268
" brzlib/tests/test_source.py",
264
269
"Or add the following text to the beginning:",
266
271
for fname in incorrect:
275
280
dict_[fname].append(line_no)
277
282
def _format_message(self, dict_, message):
278
files = sorted(["%s: %s" % (f, ', '.join([str(i + 1) for i in lines]))
279
for f, lines in dict_.items()])
283
files = ["%s: %s" % (f, ', '.join([str(i + 1) for i in lines]))
284
for f, lines in dict_.items()]
280
286
return message + '\n\n %s' % ('\n '.join(files))
282
288
def test_coding_style(self):
312
318
problems.append(self._format_message(tabs,
313
'Tab characters were found in the following source files.'
314
'\nThey should either be replaced by "\\t" or by spaces:'))
319
'Tab characters were found in the following source files.'
320
'\nThey should either be replaced by "\\t" or by spaces:'))
315
321
if illegal_newlines:
316
322
problems.append(self._format_message(illegal_newlines,
317
'Non-unix newlines were found in the following source files:'))
323
'Non-unix newlines were found in the following source files:'))
318
324
if no_newline_at_eof:
319
325
no_newline_at_eof.sort()
320
326
problems.append("The following source files doesn't have a "
321
"newline at the end:"
323
% ('\n '.join(no_newline_at_eof)))
327
"newline at the end:"
329
% ('\n '.join(no_newline_at_eof)))
325
331
self.fail('\n\n'.join(problems))
327
def test_flake8(self):
328
self.requireFeature(features.flake8)
329
# Older versions of flake8 don't support the 'paths'
331
new_path = list(sys.path)
333
0, os.path.join(os.path.dirname(__file__), '..', '..', 'tools'))
334
self.overrideAttr(sys, 'path', new_path)
335
from flake8.main.application import Application
336
from flake8.formatting.base import BaseFormatter
338
app.config = u'setup.cfg'
341
class Formatter(BaseFormatter):
350
app.file_checker_manager.report()
352
def handle(self, error):
353
self.errors.append(error)
355
app.formatter = Formatter()
359
self.assertEqual(app.formatter.errors, [])
361
333
def test_no_asserts(self):
362
334
"""bzr shouldn't use the 'assert' statement."""
363
335
# assert causes too much variation between -O and not, and tends to
400
371
both_exc_and_no_exc = []
401
372
missing_except = []
402
common_classes = ('StaticTuple',)
403
373
class_re = re.compile(r'^(cdef\s+)?(public\s+)?'
404
374
r'(api\s+)?class (\w+).*:', re.MULTILINE)
375
extern_class_re = re.compile(r'## extern cdef class (\w+)',
405
377
except_re = re.compile(
406
378
r'cdef\s+' # start with cdef
407
379
r'([\w *]*?)\s*' # this is the return signature
411
383
r'\s*(#\s*cannot[- _]raise)?') # cannot raise comment
412
384
for fname, text in self.get_source_file_contents(
413
385
extensions=('.pyx',)):
414
known_classes = {m[-1] for m in class_re.findall(text)}
415
known_classes.update(common_classes)
386
known_classes = set([m[-1] for m in class_re.findall(text)])
387
known_classes.update(extern_class_re.findall(text))
416
388
cdefs = except_re.findall(text)
417
389
for sig, func, exc_clause, no_exc_comment in cdefs:
418
390
if sig.startswith('api '):
444
416
error_msg.extend(('', ''))
446
418
self.fail('\n'.join(error_msg))
420
def test_feature_absolute_import(self):
421
"""Using absolute imports means avoiding unnecesary stat and
424
Make sure that all non-test files have absolute imports enabled.
426
missing_absolute_import = []
427
for fname, text in self.get_source_file_contents(
428
extensions=('.py', )):
429
if "/tests/" in fname or "test_" in fname:
430
# We don't really care about tests
432
if not "from __future__ import absolute_import" in text:
433
missing_absolute_import.append(fname)
435
if missing_absolute_import:
437
'The following files do not have absolute_import enabled:\n'
438
'\n' + '\n'.join(missing_absolute_import))