22ef3e60b9053376a6f216dea05202d4ecff6a78
[samba.git] / python / samba / tests / source.py
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2011
3 #
4 # Loosely based on bzrlib's test_source.py
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 """Source level Python tests."""
21
22 import errno
23 import os
24 import re
25 import warnings
26
27 from samba.tests import (
28     TestCase,
29     )
30
31
32 def get_python_source_files():
33     """Iterate over all Python source files."""
34     library_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "samba"))
35     assert os.path.isdir(library_dir), library_dir
36
37     for root, dirs, files in os.walk(library_dir):
38         for f in files:
39             if f.endswith(".py"):
40                 yield os.path.abspath(os.path.join(root, f))
41
42     bindir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "bin"))
43     assert os.path.isdir(bindir), bindir
44     for f in os.listdir(bindir):
45         p = os.path.abspath(os.path.join(bindir, f))
46         if not os.path.islink(p):
47             continue
48         target = os.readlink(p)
49         if os.path.dirname(target).endswith("scripting/bin"):
50             yield p
51     wafsambadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "buildtools", "wafsamba"))
52     assert os.path.isdir(wafsambadir), wafsambadir
53     for root, dirs, files in os.walk(wafsambadir):
54         for f in files:
55             if f.endswith(".py"):
56                 yield os.path.abspath(os.path.join(root, f))
57
58
59 def get_source_file_contents():
60     """Iterate over the contents of all python files."""
61     for fname in get_python_source_files():
62         try:
63             f = open(fname, 'rb')
64         except IOError, e:
65             if e.errno == errno.ENOENT:
66                 warnings.warn("source file %s broken link?" % fname)
67                 continue
68             else:
69                 raise
70         try:
71             text = f.read()
72         finally:
73             f.close()
74         yield fname, text
75
76
77 class TestSource(TestCase):
78
79     def test_copyright(self):
80         """Test that all Python files have a valid copyright statement."""
81         incorrect = []
82
83         copyright_re = re.compile('#\\s*copyright.*(?=\n)', re.I)
84
85         for fname, text in get_source_file_contents():
86             if fname.endswith("ms_schema.py"):
87                 # FIXME: Not sure who holds copyright on ms_schema.py
88                 continue
89             if "wafsamba" in fname:
90                 # FIXME: No copyright headers in wafsamba
91                 continue
92             match = copyright_re.search(text)
93             if not match:
94                 incorrect.append((fname, 'no copyright line found\n'))
95
96         if incorrect:
97             help_text = [
98                 "Some files have missing or incorrect copyright"
99                 " statements.", ""]
100             for fname, comment in incorrect:
101                 help_text.append(fname)
102                 help_text.append((' ' * 4) + comment)
103
104             self.fail('\n'.join(help_text))
105
106     def test_gpl(self):
107         """Test that all .py files have a GPL disclaimer."""
108         incorrect = []
109
110         gpl_txt = """
111 # This program is free software; you can redistribute it and/or modify
112 # it under the terms of the GNU General Public License as published by
113 # the Free Software Foundation; either version 3 of the License, or
114 # (at your option) any later version.
115 #
116 # This program is distributed in the hope that it will be useful,
117 # but WITHOUT ANY WARRANTY; without even the implied warranty of
118 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
119 # GNU General Public License for more details.
120 #
121 # You should have received a copy of the GNU General Public License
122 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
123 """
124         gpl_re = re.compile(re.escape(gpl_txt), re.MULTILINE)
125
126         for fname, text in get_source_file_contents():
127             if "wafsamba" in fname:
128                 # FIXME: License to wafsamba hasn't been clarified yet
129                 continue
130             if fname.endswith("/python/samba/subunit/run.py"):
131                 # Imported from subunit/testtools, which are dual
132                 # Apache2/BSD-3.
133                 continue
134             if not gpl_re.search(text):
135                 incorrect.append(fname)
136
137         if incorrect:
138             help_text = ['Some files have missing or incomplete GPL statement',
139                          gpl_txt]
140             for fname in incorrect:
141                 help_text.append((' ' * 4) + fname)
142
143             self.fail('\n'.join(help_text))
144
145     def _push_file(self, dict_, fname, line_no):
146         if fname not in dict_:
147             dict_[fname] = [line_no]
148         else:
149             dict_[fname].append(line_no)
150
151     def _format_message(self, dict_, message):
152         files = ["%s: %s" % (f, ', '.join([str(i + 1) for i in lines]))
153                 for f, lines in dict_.items()]
154         files.sort()
155         return message + '\n\n    %s' % ('\n    '.join(files))
156
157     def _iter_source_files_lines(self):
158         for fname, text in get_source_file_contents():
159             lines = text.splitlines(True)
160             last_line_no = len(lines) - 1
161             for line_no, line in enumerate(lines):
162                 yield fname, line_no, line
163
164     def test_no_tabs(self):
165         """Check that there are no tabs in Python files."""
166         tabs = {}
167         for fname, line_no, line in self._iter_source_files_lines():
168             if '\t' in line:
169                 self._push_file(tabs, fname, line_no)
170         if tabs:
171             self.fail(self._format_message(tabs,
172                 'Tab characters were found in the following source files.'
173                 '\nThey should either be replaced by "\\t" or by spaces:'))
174
175     def test_unix_newlines(self):
176         """Check for unix new lines."""
177         illegal_newlines = {}
178         for fname, line_no, line in self._iter_source_files_lines():
179             if not line.endswith('\n') or line.endswith('\r\n'):
180                 self._push_file(illegal_newlines, fname, line_no)
181         if illegal_newlines:
182             self.fail(self._format_message(illegal_newlines,
183                 'Non-unix newlines were found in the following source files:'))
184
185     def test_trailing_whitespace(self):
186         """Check that there is not trailing whitespace in Python files."""
187         trailing_whitespace = {}
188         for fname, line_no, line in self._iter_source_files_lines():
189             if line.rstrip("\n").endswith(" "):
190                 self._push_file(trailing_whitespace, fname, line_no)
191         if trailing_whitespace:
192             self.fail(self._format_message(trailing_whitespace,
193                 'Trailing whitespace was found in the following source files.'))
194
195     def test_shebang_lines(self):
196         """Check that files with shebang lines and only those are executable."""
197         files_with_shebang = {}
198         files_without_shebang= {}
199         for fname, line_no, line in self._iter_source_files_lines():
200             if line_no >= 1:
201                 continue
202             executable = (os.stat(fname).st_mode & 0111)
203             has_shebang = line.startswith("#!")
204             if has_shebang and not executable:
205                 self._push_file(files_with_shebang, fname, line_no)
206             if not has_shebang and executable:
207                 self._push_file(files_without_shebang, fname, line_no)
208         if files_with_shebang:
209             self.fail(self._format_message(files_with_shebang,
210                       'Files with shebang line that are not executable:'))
211         if files_without_shebang:
212             self.fail(self._format_message(files_without_shebang,
213                       'Files without shebang line that are executable:'))