OLD | NEW |
| (Empty) |
1 # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). | |
2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr | |
3 # | |
4 # This program is free software; you can redistribute it and/or modify it under | |
5 # the terms of the GNU General Public License as published by the Free Software | |
6 # Foundation; either version 2 of the License, or (at your option) any later | |
7 # version. | |
8 # | |
9 # This program is distributed in the hope that it will be useful, but WITHOUT | |
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | |
12 # | |
13 # You should have received a copy of the GNU General Public License along with | |
14 # this program; if not, write to the Free Software Foundation, Inc., | |
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
16 """functional/non regression tests for pylint""" | |
17 from __future__ import with_statement | |
18 | |
19 import collections | |
20 import contextlib | |
21 import functools | |
22 import sys | |
23 import re | |
24 | |
25 from glob import glob | |
26 from os import linesep | |
27 from os.path import abspath, basename, dirname, isdir, join, splitext | |
28 from cStringIO import StringIO | |
29 | |
30 from logilab.common import testlib | |
31 | |
32 from pylint import checkers | |
33 from pylint.utils import PyLintASTWalker | |
34 from pylint.reporters import BaseReporter | |
35 from pylint.interfaces import IReporter | |
36 from pylint.lint import PyLinter | |
37 | |
38 | |
39 # Utils | |
40 | |
41 SYS_VERS_STR = '%d%d%d' % sys.version_info[:3] | |
42 TITLE_UNDERLINES = ['', '=', '-', '.'] | |
43 PREFIX = abspath(dirname(__file__)) | |
44 PY3K = sys.version_info[0] == 3 | |
45 | |
46 def fix_path(): | |
47 sys.path.insert(0, PREFIX) | |
48 | |
49 def get_tests_info(input_dir, msg_dir, prefix, suffix): | |
50 """get python input examples and output messages | |
51 | |
52 We use following conventions for input files and messages: | |
53 for different inputs: | |
54 test for python >= x.y -> input = <name>_pyxy.py | |
55 test for python < x.y -> input = <name>_py_xy.py | |
56 for one input and different messages: | |
57 message for python >= x.y -> message = <name>_pyxy.txt | |
58 lower versions -> message with highest num | |
59 """ | |
60 result = [] | |
61 for fname in glob(join(input_dir, prefix + '*' + suffix)): | |
62 infile = basename(fname) | |
63 fbase = splitext(infile)[0] | |
64 # filter input files : | |
65 pyrestr = fbase.rsplit('_py', 1)[-1] # like _26 or 26 | |
66 if pyrestr.isdigit(): # '24', '25'... | |
67 if SYS_VERS_STR < pyrestr: | |
68 continue | |
69 if pyrestr.startswith('_') and pyrestr[1:].isdigit(): | |
70 # skip test for higher python versions | |
71 if SYS_VERS_STR >= pyrestr[1:]: | |
72 continue | |
73 messages = glob(join(msg_dir, fbase + '*.txt')) | |
74 # the last one will be without ext, i.e. for all or upper versions: | |
75 if messages: | |
76 for outfile in sorted(messages, reverse=True): | |
77 py_rest = outfile.rsplit('_py', 1)[-1][:-4] | |
78 if py_rest.isdigit() and SYS_VERS_STR >= py_rest: | |
79 break | |
80 else: | |
81 # This will provide an error message indicating the missing filename
. | |
82 outfile = join(msg_dir, fbase + '.txt') | |
83 result.append((infile, outfile)) | |
84 return result | |
85 | |
86 | |
87 class TestReporter(BaseReporter): | |
88 """reporter storing plain text messages""" | |
89 | |
90 __implements____ = IReporter | |
91 | |
92 def __init__(self): | |
93 self.message_ids = {} | |
94 self.reset() | |
95 | |
96 def reset(self): | |
97 self.out = StringIO() | |
98 self.messages = [] | |
99 | |
100 def add_message(self, msg_id, location, msg): | |
101 """manage message of different type and in the context of path """ | |
102 _, _, obj, line, _ = location | |
103 self.message_ids[msg_id] = 1 | |
104 if obj: | |
105 obj = ':%s' % obj | |
106 sigle = msg_id[0] | |
107 if PY3K and linesep != '\n': | |
108 # 2to3 writes os.linesep instead of using | |
109 # the previosly used line separators | |
110 msg = msg.replace('\r\n', '\n') | |
111 self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg)) | |
112 | |
113 def finalize(self): | |
114 self.messages.sort() | |
115 for msg in self.messages: | |
116 print >> self.out, msg | |
117 result = self.out.getvalue() | |
118 self.reset() | |
119 return result | |
120 | |
121 def display_results(self, layout): | |
122 """ignore layouts""" | |
123 | |
124 | |
125 if sys.version_info < (2, 6): | |
126 class Message(tuple): | |
127 def __new__(cls, msg_id, line=None, node=None, args=None): | |
128 return tuple.__new__(cls, (msg_id, line, node, args)) | |
129 | |
130 @property | |
131 def msg_id(self): | |
132 return self[0] | |
133 @property | |
134 def line(self): | |
135 return self[1] | |
136 @property | |
137 def node(self): | |
138 return self[2] | |
139 @property | |
140 def args(self): | |
141 return self[3] | |
142 | |
143 | |
144 else: | |
145 class Message(collections.namedtuple('Message', | |
146 ['msg_id', 'line', 'node', 'args'])): | |
147 def __new__(cls, msg_id, line=None, node=None, args=None): | |
148 return tuple.__new__(cls, (msg_id, line, node, args)) | |
149 | |
150 | |
151 class UnittestLinter(object): | |
152 """A fake linter class to capture checker messages.""" | |
153 | |
154 def __init__(self): | |
155 self._messages = [] | |
156 self.stats = {} | |
157 | |
158 def release_messages(self): | |
159 try: | |
160 return self._messages | |
161 finally: | |
162 self._messages = [] | |
163 | |
164 def add_message(self, msg_id, line=None, node=None, args=None): | |
165 self._messages.append(Message(msg_id, line, node, args)) | |
166 | |
167 def is_message_enabled(self, *unused_args): | |
168 return True | |
169 | |
170 def add_stats(self, **kwargs): | |
171 for name, value in kwargs.iteritems(): | |
172 self.stats[name] = value | |
173 return self.stats | |
174 | |
175 @property | |
176 def options_providers(self): | |
177 return linter.options_providers | |
178 | |
179 def set_config(**kwargs): | |
180 """Decorator for setting config values on a checker.""" | |
181 def _Wrapper(fun): | |
182 @functools.wraps(fun) | |
183 def _Forward(self): | |
184 for key, value in kwargs.iteritems(): | |
185 setattr(self.checker.config, key, value) | |
186 if isinstance(self, CheckerTestCase): | |
187 # reopen checker in case, it may be interested in configuration
change | |
188 self.checker.open() | |
189 fun(self) | |
190 | |
191 return _Forward | |
192 return _Wrapper | |
193 | |
194 | |
195 class CheckerTestCase(testlib.TestCase): | |
196 """A base testcase class for unittesting individual checker classes.""" | |
197 CHECKER_CLASS = None | |
198 CONFIG = {} | |
199 | |
200 def setUp(self): | |
201 self.linter = UnittestLinter() | |
202 self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-cal
lable | |
203 for key, value in self.CONFIG.iteritems(): | |
204 setattr(self.checker.config, key, value) | |
205 self.checker.open() | |
206 | |
207 @contextlib.contextmanager | |
208 def assertNoMessages(self): | |
209 """Assert that no messages are added by the given method.""" | |
210 with self.assertAddsMessages(): | |
211 yield | |
212 | |
213 @contextlib.contextmanager | |
214 def assertAddsMessages(self, *messages): | |
215 """Assert that exactly the given method adds the given messages. | |
216 | |
217 The list of messages must exactly match *all* the messages added by the | |
218 method. Additionally, we check to see whether the args in each message c
an | |
219 actually be substituted into the message string. | |
220 """ | |
221 yield | |
222 got = self.linter.release_messages() | |
223 msg = ('Expected messages did not match actual.\n' | |
224 'Expected:\n%s\nGot:\n%s' % ('\n'.join(repr(m) for m in messages)
, | |
225 '\n'.join(repr(m) for m in got))) | |
226 self.assertEqual(list(messages), got, msg) | |
227 | |
228 def walk(self, node): | |
229 """recursive walk on the given node""" | |
230 walker = PyLintASTWalker(linter) | |
231 walker.add_checker(self.checker) | |
232 walker.walk(node) | |
233 | |
234 | |
235 # Init | |
236 test_reporter = TestReporter() | |
237 linter = PyLinter() | |
238 linter.set_reporter(test_reporter) | |
239 linter.config.persistent = 0 | |
240 checkers.initialize(linter) | |
241 linter.global_set_option('required-attributes', ('__revision__',)) | |
242 | |
243 if linesep != '\n': | |
244 LINE_RGX = re.compile(linesep) | |
245 def ulines(string): | |
246 return LINE_RGX.sub('\n', string) | |
247 else: | |
248 def ulines(string): | |
249 return string | |
250 | |
251 INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$') | |
252 | |
253 def exception_str(self, ex): | |
254 """function used to replace default __str__ method of exception instances""" | |
255 return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args)) | |
256 | |
257 # Test classes | |
258 | |
259 class LintTestUsingModule(testlib.TestCase): | |
260 INPUT_DIR = None | |
261 DEFAULT_PACKAGE = 'input' | |
262 package = DEFAULT_PACKAGE | |
263 linter = linter | |
264 module = None | |
265 depends = None | |
266 output = None | |
267 _TEST_TYPE = 'module' | |
268 | |
269 def shortDescription(self): | |
270 values = {'mode' : self._TEST_TYPE, | |
271 'input': self.module, | |
272 'pkg': self.package, | |
273 'cls': self.__class__.__name__} | |
274 | |
275 if self.package == self.DEFAULT_PACKAGE: | |
276 msg = '%(mode)s test of input file "%(input)s" (%(cls)s)' | |
277 else: | |
278 msg = '%(mode)s test of input file "%(input)s" in "%(pkg)s" (%(cls)s
)' | |
279 return msg % values | |
280 | |
281 def test_functionality(self): | |
282 tocheck = [self.package+'.'+self.module] | |
283 if self.depends: | |
284 tocheck += [self.package+'.%s' % name.replace('.py', '') | |
285 for name, _ in self.depends] | |
286 self._test(tocheck) | |
287 | |
288 def _check_result(self, got): | |
289 self.assertMultiLineEqual(self._get_expected().strip()+'\n', | |
290 got.strip()+'\n') | |
291 | |
292 def _test(self, tocheck): | |
293 if INFO_TEST_RGX.match(self.module): | |
294 self.linter.enable('I') | |
295 else: | |
296 self.linter.disable('I') | |
297 try: | |
298 self.linter.check(tocheck) | |
299 except Exception, ex: | |
300 # need finalization to restore a correct state | |
301 self.linter.reporter.finalize() | |
302 ex.file = tocheck | |
303 print ex | |
304 ex.__str__ = exception_str | |
305 raise | |
306 self._check_result(self.linter.reporter.finalize()) | |
307 | |
308 def _has_output(self): | |
309 return not self.module.startswith('func_noerror_') | |
310 | |
311 def _get_expected(self): | |
312 if self._has_output() and self.output: | |
313 with open(self.output, 'U') as fobj: | |
314 return fobj.read().strip() + '\n' | |
315 else: | |
316 return '' | |
317 | |
318 class LintTestUsingFile(LintTestUsingModule): | |
319 | |
320 _TEST_TYPE = 'file' | |
321 | |
322 def test_functionality(self): | |
323 importable = join(self.INPUT_DIR, self.module) | |
324 # python also prefers packages over simple modules. | |
325 if not isdir(importable): | |
326 importable += '.py' | |
327 tocheck = [importable] | |
328 if self.depends: | |
329 tocheck += [join(self.INPUT_DIR, name) for name, _file in self.depen
ds] | |
330 self._test(tocheck) | |
331 | |
332 class LintTestUpdate(LintTestUsingModule): | |
333 | |
334 _TEST_TYPE = 'update' | |
335 | |
336 def _check_result(self, got): | |
337 if self._has_output(): | |
338 try: | |
339 expected = self._get_expected() | |
340 except IOError: | |
341 expected = '' | |
342 if got != expected: | |
343 with open(self.output, 'w') as fobj: | |
344 fobj.write(got) | |
345 | |
346 # Callback | |
347 | |
348 def cb_test_gen(base_class): | |
349 def call(input_dir, msg_dir, module_file, messages_file, dependencies): | |
350 class LintTC(base_class): | |
351 module = module_file.replace('.py', '') | |
352 output = messages_file | |
353 depends = dependencies or None | |
354 tags = testlib.Tags(('generated', 'pylint_input_%s' % module)) | |
355 INPUT_DIR = input_dir | |
356 MSG_DIR = msg_dir | |
357 return LintTC | |
358 return call | |
359 | |
360 # Main function | |
361 | |
362 def make_tests(input_dir, msg_dir, filter_rgx, callbacks): | |
363 """generate tests classes from test info | |
364 | |
365 return the list of generated test classes | |
366 """ | |
367 if filter_rgx: | |
368 is_to_run = re.compile(filter_rgx).search | |
369 else: | |
370 is_to_run = lambda x: 1 | |
371 tests = [] | |
372 for module_file, messages_file in ( | |
373 get_tests_info(input_dir, msg_dir, 'func_', '') | |
374 ): | |
375 if not is_to_run(module_file): | |
376 continue | |
377 base = module_file.replace('func_', '').replace('.py', '') | |
378 | |
379 dependencies = get_tests_info(input_dir, msg_dir, base, '.py') | |
380 | |
381 for callback in callbacks: | |
382 test = callback(input_dir, msg_dir, module_file, messages_file, | |
383 dependencies) | |
384 if test: | |
385 tests.append(test) | |
386 return tests | |
OLD | NEW |