/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 breezy/tests/test_source.py

  • Committer: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""These tests are tests about the source code of breezy itself.
 
18
 
 
19
They are useful for testing code quality, checking coverage metric etc.
 
20
"""
 
21
 
 
22
import os
 
23
import parser
 
24
import re
 
25
import symbol
 
26
import sys
 
27
import token
 
28
 
 
29
from breezy import (
 
30
    osutils,
 
31
    )
 
32
import breezy.branch
 
33
from breezy.tests import (
 
34
    features,
 
35
    TestCase,
 
36
    TestSkipped,
 
37
    )
 
38
 
 
39
 
 
40
# Files which are listed here will be skipped when testing for Copyright (or
 
41
# GPL) statements.
 
42
COPYRIGHT_EXCEPTIONS = [
 
43
    'breezy/_bencode_py.py',
 
44
    'breezy/doc_generate/conf.py',
 
45
    'breezy/lsprof.py',
 
46
    ]
 
47
 
 
48
LICENSE_EXCEPTIONS = [
 
49
    'breezy/_bencode_py.py',
 
50
    'breezy/doc_generate/conf.py',
 
51
    'breezy/lsprof.py',
 
52
    ]
 
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)
 
55
# but for compatibility with previous releases, we don't want to move it.
 
56
#
 
57
# sphinx_conf is semi-autogenerated.
 
58
 
 
59
 
 
60
class TestSourceHelper(TestCase):
 
61
 
 
62
    def source_file_name(self, package):
 
63
        """Return the path of the .py file for package."""
 
64
        if getattr(sys, "frozen", None) is not None:
 
65
            raise TestSkipped("can't test sources in frozen distributions.")
 
66
        path = package.__file__
 
67
        if path[-1] in 'co':
 
68
            return path[:-1]
 
69
        else:
 
70
            return path
 
71
 
 
72
 
 
73
class TestApiUsage(TestSourceHelper):
 
74
 
 
75
    def find_occurences(self, rule, filename):
 
76
        """Find the number of occurences of rule in a file."""
 
77
        occurences = 0
 
78
        source = open(filename, 'r')
 
79
        for line in source:
 
80
            if line.find(rule) > -1:
 
81
                occurences += 1
 
82
        return occurences
 
83
 
 
84
    def test_branch_working_tree(self):
 
85
        """Test that the number of uses of working_tree in branch is stable."""
 
86
        occurences = self.find_occurences('self.working_tree()',
 
87
                                          self.source_file_name(breezy.branch))
 
88
        # do not even think of increasing this number. If you think you need to
 
89
        # increase it, then you almost certainly are doing something wrong as
 
90
        # the relationship from working_tree to branch is one way.
 
91
        # 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.
 
93
        self.assertEqual(0, occurences)
 
94
 
 
95
    def test_branch_WorkingTree(self):
 
96
        """Test that the number of uses of working_tree in branch is stable."""
 
97
        occurences = self.find_occurences('WorkingTree',
 
98
                                          self.source_file_name(breezy.branch))
 
99
        # Do not even think of increasing this number. If you think you need to
 
100
        # increase it, then you almost certainly are doing something wrong as
 
101
        # the relationship from working_tree to branch is one way.
 
102
        # As of 20070809, there are no longer any mentions at all.
 
103
        self.assertEqual(0, occurences)
 
104
 
 
105
 
 
106
class TestSource(TestSourceHelper):
 
107
 
 
108
    def get_breezy_dir(self):
 
109
        """Get the path to the root of breezy"""
 
110
        source = self.source_file_name(breezy)
 
111
        source_dir = os.path.dirname(source)
 
112
 
 
113
        # Avoid the case when breezy is packaged in a zip file
 
114
        if not os.path.isdir(source_dir):
 
115
            raise TestSkipped(
 
116
                'Cannot find breezy source directory. Expected %s'
 
117
                % source_dir)
 
118
        return source_dir
 
119
 
 
120
    def get_source_files(self, extensions=None):
 
121
        """Yield all source files for bzr and breezy
 
122
 
 
123
        :param our_files_only: If true, exclude files from included libraries
 
124
            or plugins.
 
125
        """
 
126
        breezy_dir = self.get_breezy_dir()
 
127
        if extensions is None:
 
128
            extensions = ('.py',)
 
129
 
 
130
        # This is the front-end 'bzr' script
 
131
        bzr_path = self.get_brz_path()
 
132
        yield bzr_path
 
133
 
 
134
        for root, dirs, files in os.walk(breezy_dir):
 
135
            for d in dirs:
 
136
                if d.endswith('.tmp'):
 
137
                    dirs.remove(d)
 
138
            for f in files:
 
139
                for extension in extensions:
 
140
                    if f.endswith(extension):
 
141
                        break
 
142
                else:
 
143
                    # Did not match the accepted extensions
 
144
                    continue
 
145
                yield osutils.pathjoin(root, f)
 
146
 
 
147
    def get_source_file_contents(self, extensions=None):
 
148
        for fname in self.get_source_files(extensions=extensions):
 
149
            with open(fname, 'r') as f:
 
150
                yield fname, f.read()
 
151
 
 
152
    def is_our_code(self, fname):
 
153
        """True if it's a "real" part of breezy rather than external code"""
 
154
        if '/util/' in fname or '/plugins/' in fname:
 
155
            return False
 
156
        else:
 
157
            return True
 
158
 
 
159
    def is_copyright_exception(self, fname):
 
160
        """Certain files are allowed to be different"""
 
161
        if not self.is_our_code(fname):
 
162
            return True
 
163
        for exc in COPYRIGHT_EXCEPTIONS:
 
164
            if fname.endswith(exc):
 
165
                return True
 
166
        return False
 
167
 
 
168
    def is_license_exception(self, fname):
 
169
        """Certain files are allowed to be different"""
 
170
        if not self.is_our_code(fname):
 
171
            return True
 
172
        for exc in LICENSE_EXCEPTIONS:
 
173
            if fname.endswith(exc):
 
174
                return True
 
175
        return False
 
176
 
 
177
    def test_tmpdir_not_in_source_files(self):
 
178
        """When scanning for source files, we don't descend test tempdirs"""
 
179
        for filename in self.get_source_files():
 
180
            if re.search(r'test....\.tmp', filename):
 
181
                self.fail("get_source_file() returned filename %r "
 
182
                          "from within a temporary directory"
 
183
                          % filename)
 
184
 
 
185
    def test_copyright(self):
 
186
        """Test that all .py and .pyx files have a valid copyright statement"""
 
187
        incorrect = []
 
188
 
 
189
        copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
 
190
        copyright_statement_re = re.compile(
 
191
            r'# Copyright \(C\) '  # Opening "# Copyright (C)"
 
192
            r'(\d+?)((, |-)\d+)*'  # followed by a series of dates
 
193
            r' [^ ]*')             # and then whoever.
 
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_statement_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
                         " breezy/tests/test_source.py",
 
219
                         # this is broken to prevent a false match
 
220
                         "or add '# Copyright (C)"
 
221
                         " 2007 Bazaar hackers' 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
                         " breezy/tests/test_source.py",
 
264
                         "Or add the following text to the beginning:",
 
265
                         gpl_txt]
 
266
            for fname in incorrect:
 
267
                help_text.append((' ' * 4) + fname)
 
268
 
 
269
            self.fail('\n'.join(help_text))
 
270
 
 
271
    def _push_file(self, dict_, fname, line_no):
 
272
        if fname not in dict_:
 
273
            dict_[fname] = [line_no]
 
274
        else:
 
275
            dict_[fname].append(line_no)
 
276
 
 
277
    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()])
 
280
        return message + '\n\n    %s' % ('\n    '.join(files))
 
281
 
 
282
    def test_coding_style(self):
 
283
        """Check if bazaar code conforms to some coding style conventions.
 
284
 
 
285
        Generally we expect PEP8, but we do not generally strictly enforce
 
286
        this, and there are existing files that do not comply.  The 'pep8'
 
287
        tool, available separately, will check for more cases.
 
288
 
 
289
        This test only enforces conditions that are globally true at the
 
290
        moment, and that should cause a patch to be rejected: spaces rather
 
291
        than tabs, unix newlines, and a newline at the end of the file.
 
292
        """
 
293
        tabs = {}
 
294
        illegal_newlines = {}
 
295
        no_newline_at_eof = []
 
296
        for fname, text in self.get_source_file_contents(
 
297
                extensions=('.py', '.pyx')):
 
298
            if not self.is_our_code(fname):
 
299
                continue
 
300
            lines = text.splitlines(True)
 
301
            last_line_no = len(lines) - 1
 
302
            for line_no, line in enumerate(lines):
 
303
                if '\t' in line:
 
304
                    self._push_file(tabs, fname, line_no)
 
305
                if not line.endswith('\n') or line.endswith('\r\n'):
 
306
                    if line_no != last_line_no:  # not no_newline_at_eof
 
307
                        self._push_file(illegal_newlines, fname, line_no)
 
308
            if not lines[-1].endswith('\n'):
 
309
                no_newline_at_eof.append(fname)
 
310
        problems = []
 
311
        if tabs:
 
312
            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:'))
 
315
        if illegal_newlines:
 
316
            problems.append(self._format_message(illegal_newlines,
 
317
                                                 'Non-unix newlines were found in the following source files:'))
 
318
        if no_newline_at_eof:
 
319
            no_newline_at_eof.sort()
 
320
            problems.append("The following source files doesn't have a "
 
321
                            "newline at the end:"
 
322
                            '\n\n    %s'
 
323
                            % ('\n    '.join(no_newline_at_eof)))
 
324
        if problems:
 
325
            self.fail('\n\n'.join(problems))
 
326
 
 
327
    def test_flake8(self):
 
328
        self.requireFeature(features.flake8)
 
329
        # Older versions of flake8 don't support the 'paths'
 
330
        # variable
 
331
        new_path = list(sys.path)
 
332
        new_path.insert(
 
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
 
337
        app = Application()
 
338
        app.config = u'setup.cfg'
 
339
        app.jobs = 1
 
340
 
 
341
        class Formatter(BaseFormatter):
 
342
 
 
343
            def __init__(self):
 
344
                self.errors = []
 
345
 
 
346
            def start(self):
 
347
                pass
 
348
 
 
349
            def stop(self):
 
350
                app.file_checker_manager.report()
 
351
 
 
352
            def handle(self, error):
 
353
                self.errors.append(error)
 
354
 
 
355
        app.formatter = Formatter()
 
356
        app.initialize([])
 
357
        app.run_checks()
 
358
        app.report()
 
359
        self.assertEqual(app.formatter.errors, [])
 
360
 
 
361
    def test_no_asserts(self):
 
362
        """bzr shouldn't use the 'assert' statement."""
 
363
        # assert causes too much variation between -O and not, and tends to
 
364
        # give bad errors to the user
 
365
        def search(x):
 
366
            # scan down through x for assert statements, report any problems
 
367
            # this is a bit cheesy; it may get some false positives?
 
368
            if x[0] == symbol.assert_stmt:
 
369
                return True
 
370
            elif x[0] == token.NAME:
 
371
                # can't search further down
 
372
                return False
 
373
            for sub in x[1:]:
 
374
                if sub and search(sub):
 
375
                    return True
 
376
            return False
 
377
        badfiles = []
 
378
        assert_re = re.compile(r'\bassert\b')
 
379
        for fname, text in self.get_source_file_contents():
 
380
            if not self.is_our_code(fname):
 
381
                continue
 
382
            if not assert_re.search(text):
 
383
                continue
 
384
            st = parser.suite(text)
 
385
            code = parser.st2tuple(st)
 
386
            if search(code):
 
387
                badfiles.append(fname)
 
388
        if badfiles:
 
389
            self.fail(
 
390
                "these files contain an assert statement and should not:\n%s"
 
391
                % '\n'.join(badfiles))
 
392
 
 
393
    def test_extension_exceptions(self):
 
394
        """Extension functions should propagate exceptions.
 
395
 
 
396
        Either they should return an object, have an 'except' clause, or
 
397
        have a "# cannot_raise" to indicate that we've audited them and
 
398
        defined them as not raising exceptions.
 
399
        """
 
400
        both_exc_and_no_exc = []
 
401
        missing_except = []
 
402
        common_classes = ('StaticTuple',)
 
403
        class_re = re.compile(r'^(cdef\s+)?(public\s+)?'
 
404
                              r'(api\s+)?class (\w+).*:', re.MULTILINE)
 
405
        except_re = re.compile(
 
406
            r'cdef\s+'        # start with cdef
 
407
            r'([\w *]*?)\s*'  # this is the return signature
 
408
            r'(\w+)\s*\('     # the function name
 
409
            r'[^)]*\)\s*'     # parameters
 
410
            r'(.*)\s*:'       # the except clause
 
411
            r'\s*(#\s*cannot[- _]raise)?')  # cannot raise comment
 
412
        for fname, text in self.get_source_file_contents(
 
413
                extensions=('.pyx',)):
 
414
            known_classes = {m[-1] for m in class_re.findall(text)}
 
415
            known_classes.update(common_classes)
 
416
            cdefs = except_re.findall(text)
 
417
            for sig, func, exc_clause, no_exc_comment in cdefs:
 
418
                if sig.startswith('api '):
 
419
                    sig = sig[4:]
 
420
                if not sig or sig in known_classes:
 
421
                    sig = 'object'
 
422
                if 'nogil' in exc_clause:
 
423
                    exc_clause = exc_clause.replace('nogil', '').strip()
 
424
                if exc_clause and no_exc_comment:
 
425
                    both_exc_and_no_exc.append((fname, func))
 
426
                if sig != 'object' and not (exc_clause or no_exc_comment):
 
427
                    missing_except.append((fname, func))
 
428
        error_msg = []
 
429
        if both_exc_and_no_exc:
 
430
            error_msg.append(
 
431
                'The following functions had "cannot raise" comments'
 
432
                ' but did have an except clause set:')
 
433
            for fname, func in both_exc_and_no_exc:
 
434
                error_msg.append('%s:%s' % (fname, func))
 
435
            error_msg.extend(('', ''))
 
436
        if missing_except:
 
437
            error_msg.append(
 
438
                'The following functions have fixed return types,'
 
439
                ' but no except clause.')
 
440
            error_msg.append(
 
441
                'Either add an except or append "# cannot_raise".')
 
442
            for fname, func in missing_except:
 
443
                error_msg.append('%s:%s' % (fname, func))
 
444
            error_msg.extend(('', ''))
 
445
        if error_msg:
 
446
            self.fail('\n'.join(error_msg))
 
447
 
 
448
    def test_feature_absolute_import(self):
 
449
        """Using absolute imports means avoiding unnecesary stat and
 
450
        open calls.
 
451
 
 
452
        Make sure that all non-test files have absolute imports enabled.
 
453
        """
 
454
        missing_absolute_import = []
 
455
        for fname, text in self.get_source_file_contents(
 
456
                extensions=('.py', '.pyx')):
 
457
            if "/tests/" in fname or "test_" in fname:
 
458
                # We don't really care about tests
 
459
                continue
 
460
            if "from __future__ import absolute_import" not in text:
 
461
                missing_absolute_import.append(fname)
 
462
 
 
463
        if missing_absolute_import:
 
464
            self.fail(
 
465
                'The following files do not have absolute_import enabled:\n'
 
466
                '\n' + '\n'.join(missing_absolute_import))