| OLD | NEW |
| 1 # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). | 1 # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). |
| 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr | 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # | 3 # |
| 4 # This program is free software; you can redistribute it and/or modify it under | 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 | 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 | 6 # Foundation; either version 2 of the License, or (at your option) any later |
| 7 # version. | 7 # version. |
| 8 # | 8 # |
| 9 # This program is distributed in the hope that it will be useful, but WITHOUT | 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 | 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. | 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License along with | 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., | 14 # this program; if not, write to the Free Software Foundation, Inc., |
| 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 16 """functional/non regression tests for pylint""" | 16 """functional/non regression tests for pylint""" |
| 17 from __future__ import with_statement | 17 from __future__ import print_function |
| 18 | 18 |
| 19 import collections | 19 import collections |
| 20 import contextlib | 20 import contextlib |
| 21 import functools | 21 import functools |
| 22 import os |
| 22 import sys | 23 import sys |
| 23 import re | 24 import re |
| 25 import unittest |
| 26 import tempfile |
| 27 import tokenize |
| 24 | 28 |
| 25 from glob import glob | 29 from glob import glob |
| 26 from os import linesep | 30 from os import linesep, getcwd, sep |
| 27 from os.path import abspath, basename, dirname, isdir, join, splitext | 31 from os.path import abspath, basename, dirname, isdir, join, splitext |
| 28 from cStringIO import StringIO | |
| 29 | 32 |
| 30 from logilab.common import testlib | 33 from astroid import test_utils |
| 31 | 34 |
| 32 from pylint import checkers | 35 from pylint import checkers |
| 33 from pylint.utils import PyLintASTWalker | 36 from pylint.utils import PyLintASTWalker |
| 34 from pylint.reporters import BaseReporter | 37 from pylint.reporters import BaseReporter |
| 35 from pylint.interfaces import IReporter | 38 from pylint.interfaces import IReporter |
| 36 from pylint.lint import PyLinter | 39 from pylint.lint import PyLinter |
| 37 | 40 |
| 41 import six |
| 42 from six.moves import StringIO |
| 43 |
| 38 | 44 |
| 39 # Utils | 45 # Utils |
| 40 | 46 |
| 41 SYS_VERS_STR = '%d%d%d' % sys.version_info[:3] | 47 SYS_VERS_STR = '%d%d%d' % sys.version_info[:3] |
| 42 TITLE_UNDERLINES = ['', '=', '-', '.'] | 48 TITLE_UNDERLINES = ['', '=', '-', '.'] |
| 43 PREFIX = abspath(dirname(__file__)) | 49 PREFIX = abspath(dirname(__file__)) |
| 44 PY3K = sys.version_info[0] == 3 | 50 PY3K = sys.version_info[0] == 3 |
| 45 | 51 |
| 46 def fix_path(): | 52 def fix_path(): |
| 47 sys.path.insert(0, PREFIX) | 53 sys.path.insert(0, PREFIX) |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 82 outfile = join(msg_dir, fbase + '.txt') | 88 outfile = join(msg_dir, fbase + '.txt') |
| 83 result.append((infile, outfile)) | 89 result.append((infile, outfile)) |
| 84 return result | 90 return result |
| 85 | 91 |
| 86 | 92 |
| 87 class TestReporter(BaseReporter): | 93 class TestReporter(BaseReporter): |
| 88 """reporter storing plain text messages""" | 94 """reporter storing plain text messages""" |
| 89 | 95 |
| 90 __implements____ = IReporter | 96 __implements____ = IReporter |
| 91 | 97 |
| 92 def __init__(self): | 98 def __init__(self): # pylint: disable=super-init-not-called |
| 99 |
| 93 self.message_ids = {} | 100 self.message_ids = {} |
| 94 self.reset() | 101 self.reset() |
| 102 self.path_strip_prefix = getcwd() + sep |
| 95 | 103 |
| 96 def reset(self): | 104 def reset(self): |
| 97 self.out = StringIO() | 105 self.out = StringIO() |
| 98 self.messages = [] | 106 self.messages = [] |
| 99 | 107 |
| 100 def add_message(self, msg_id, location, msg): | 108 def add_message(self, msg_id, location, msg): |
| 101 """manage message of different type and in the context of path """ | 109 """manage message of different type and in the context of path """ |
| 102 _, _, obj, line, _ = location | 110 _, _, obj, line, _ = location |
| 103 self.message_ids[msg_id] = 1 | 111 self.message_ids[msg_id] = 1 |
| 104 if obj: | 112 if obj: |
| 105 obj = ':%s' % obj | 113 obj = ':%s' % obj |
| 106 sigle = msg_id[0] | 114 sigle = msg_id[0] |
| 107 if PY3K and linesep != '\n': | 115 if PY3K and linesep != '\n': |
| 108 # 2to3 writes os.linesep instead of using | 116 # 2to3 writes os.linesep instead of using |
| 109 # the previosly used line separators | 117 # the previosly used line separators |
| 110 msg = msg.replace('\r\n', '\n') | 118 msg = msg.replace('\r\n', '\n') |
| 111 self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg)) | 119 self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg)) |
| 112 | 120 |
| 113 def finalize(self): | 121 def finalize(self): |
| 114 self.messages.sort() | 122 self.messages.sort() |
| 115 for msg in self.messages: | 123 for msg in self.messages: |
| 116 print >> self.out, msg | 124 print(msg, file=self.out) |
| 117 result = self.out.getvalue() | 125 result = self.out.getvalue() |
| 118 self.reset() | 126 self.reset() |
| 119 return result | 127 return result |
| 120 | 128 |
| 121 def display_results(self, layout): | 129 def display_results(self, layout): |
| 122 """ignore layouts""" | 130 """ignore layouts""" |
| 123 | 131 |
| 124 | 132 |
| 125 if sys.version_info < (2, 6): | 133 class Message(collections.namedtuple('Message', |
| 126 class Message(tuple): | 134 ['msg_id', 'line', 'node', 'args'])): |
| 127 def __new__(cls, msg_id, line=None, node=None, args=None): | 135 def __new__(cls, msg_id, line=None, node=None, args=None): |
| 128 return tuple.__new__(cls, (msg_id, line, node, args)) | 136 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 | 137 |
| 150 | 138 |
| 151 class UnittestLinter(object): | 139 class UnittestLinter(object): |
| 152 """A fake linter class to capture checker messages.""" | 140 """A fake linter class to capture checker messages.""" |
| 141 # pylint: disable=unused-argument, no-self-use |
| 153 | 142 |
| 154 def __init__(self): | 143 def __init__(self): |
| 155 self._messages = [] | 144 self._messages = [] |
| 156 self.stats = {} | 145 self.stats = {} |
| 157 | 146 |
| 158 def release_messages(self): | 147 def release_messages(self): |
| 159 try: | 148 try: |
| 160 return self._messages | 149 return self._messages |
| 161 finally: | 150 finally: |
| 162 self._messages = [] | 151 self._messages = [] |
| 163 | 152 |
| 164 def add_message(self, msg_id, line=None, node=None, args=None): | 153 def add_message(self, msg_id, line=None, node=None, args=None, |
| 154 confidence=None): |
| 165 self._messages.append(Message(msg_id, line, node, args)) | 155 self._messages.append(Message(msg_id, line, node, args)) |
| 166 | 156 |
| 167 def is_message_enabled(self, *unused_args): | 157 def is_message_enabled(self, *unused_args): |
| 168 return True | 158 return True |
| 169 | 159 |
| 170 def add_stats(self, **kwargs): | 160 def add_stats(self, **kwargs): |
| 171 for name, value in kwargs.iteritems(): | 161 for name, value in six.iteritems(kwargs): |
| 172 self.stats[name] = value | 162 self.stats[name] = value |
| 173 return self.stats | 163 return self.stats |
| 174 | 164 |
| 175 @property | 165 @property |
| 176 def options_providers(self): | 166 def options_providers(self): |
| 177 return linter.options_providers | 167 return linter.options_providers |
| 178 | 168 |
| 179 def set_config(**kwargs): | 169 def set_config(**kwargs): |
| 180 """Decorator for setting config values on a checker.""" | 170 """Decorator for setting config values on a checker.""" |
| 181 def _Wrapper(fun): | 171 def _Wrapper(fun): |
| 182 @functools.wraps(fun) | 172 @functools.wraps(fun) |
| 183 def _Forward(self): | 173 def _Forward(self): |
| 184 for key, value in kwargs.iteritems(): | 174 for key, value in six.iteritems(kwargs): |
| 185 setattr(self.checker.config, key, value) | 175 setattr(self.checker.config, key, value) |
| 186 if isinstance(self, CheckerTestCase): | 176 if isinstance(self, CheckerTestCase): |
| 187 # reopen checker in case, it may be interested in configuration
change | 177 # reopen checker in case, it may be interested in configuration
change |
| 188 self.checker.open() | 178 self.checker.open() |
| 189 fun(self) | 179 fun(self) |
| 190 | 180 |
| 191 return _Forward | 181 return _Forward |
| 192 return _Wrapper | 182 return _Wrapper |
| 193 | 183 |
| 194 | 184 |
| 195 class CheckerTestCase(testlib.TestCase): | 185 class CheckerTestCase(unittest.TestCase): |
| 196 """A base testcase class for unittesting individual checker classes.""" | 186 """A base testcase class for unittesting individual checker classes.""" |
| 197 CHECKER_CLASS = None | 187 CHECKER_CLASS = None |
| 198 CONFIG = {} | 188 CONFIG = {} |
| 199 | 189 |
| 200 def setUp(self): | 190 def setUp(self): |
| 201 self.linter = UnittestLinter() | 191 self.linter = UnittestLinter() |
| 202 self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-cal
lable | 192 self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-cal
lable |
| 203 for key, value in self.CONFIG.iteritems(): | 193 for key, value in six.iteritems(self.CONFIG): |
| 204 setattr(self.checker.config, key, value) | 194 setattr(self.checker.config, key, value) |
| 205 self.checker.open() | 195 self.checker.open() |
| 206 | 196 |
| 207 @contextlib.contextmanager | 197 @contextlib.contextmanager |
| 208 def assertNoMessages(self): | 198 def assertNoMessages(self): |
| 209 """Assert that no messages are added by the given method.""" | 199 """Assert that no messages are added by the given method.""" |
| 210 with self.assertAddsMessages(): | 200 with self.assertAddsMessages(): |
| 211 yield | 201 yield |
| 212 | 202 |
| 213 @contextlib.contextmanager | 203 @contextlib.contextmanager |
| (...skipping 29 matching lines...) Expand all Loading... |
| 243 if linesep != '\n': | 233 if linesep != '\n': |
| 244 LINE_RGX = re.compile(linesep) | 234 LINE_RGX = re.compile(linesep) |
| 245 def ulines(string): | 235 def ulines(string): |
| 246 return LINE_RGX.sub('\n', string) | 236 return LINE_RGX.sub('\n', string) |
| 247 else: | 237 else: |
| 248 def ulines(string): | 238 def ulines(string): |
| 249 return string | 239 return string |
| 250 | 240 |
| 251 INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$') | 241 INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$') |
| 252 | 242 |
| 253 def exception_str(self, ex): | 243 def exception_str(self, ex): # pylint: disable=unused-argument |
| 254 """function used to replace default __str__ method of exception instances""" | 244 """function used to replace default __str__ method of exception instances""" |
| 255 return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args)) | 245 return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args)) |
| 256 | 246 |
| 257 # Test classes | 247 # Test classes |
| 258 | 248 |
| 259 class LintTestUsingModule(testlib.TestCase): | 249 class LintTestUsingModule(unittest.TestCase): |
| 260 INPUT_DIR = None | 250 INPUT_DIR = None |
| 261 DEFAULT_PACKAGE = 'input' | 251 DEFAULT_PACKAGE = 'input' |
| 262 package = DEFAULT_PACKAGE | 252 package = DEFAULT_PACKAGE |
| 263 linter = linter | 253 linter = linter |
| 264 module = None | 254 module = None |
| 265 depends = None | 255 depends = None |
| 266 output = None | 256 output = None |
| 267 _TEST_TYPE = 'module' | 257 _TEST_TYPE = 'module' |
| 258 maxDiff = None |
| 268 | 259 |
| 269 def shortDescription(self): | 260 def shortDescription(self): |
| 270 values = {'mode' : self._TEST_TYPE, | 261 values = {'mode' : self._TEST_TYPE, |
| 271 'input': self.module, | 262 'input': self.module, |
| 272 'pkg': self.package, | 263 'pkg': self.package, |
| 273 'cls': self.__class__.__name__} | 264 'cls': self.__class__.__name__} |
| 274 | 265 |
| 275 if self.package == self.DEFAULT_PACKAGE: | 266 if self.package == self.DEFAULT_PACKAGE: |
| 276 msg = '%(mode)s test of input file "%(input)s" (%(cls)s)' | 267 msg = '%(mode)s test of input file "%(input)s" (%(cls)s)' |
| 277 else: | 268 else: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 289 self.assertMultiLineEqual(self._get_expected().strip()+'\n', | 280 self.assertMultiLineEqual(self._get_expected().strip()+'\n', |
| 290 got.strip()+'\n') | 281 got.strip()+'\n') |
| 291 | 282 |
| 292 def _test(self, tocheck): | 283 def _test(self, tocheck): |
| 293 if INFO_TEST_RGX.match(self.module): | 284 if INFO_TEST_RGX.match(self.module): |
| 294 self.linter.enable('I') | 285 self.linter.enable('I') |
| 295 else: | 286 else: |
| 296 self.linter.disable('I') | 287 self.linter.disable('I') |
| 297 try: | 288 try: |
| 298 self.linter.check(tocheck) | 289 self.linter.check(tocheck) |
| 299 except Exception, ex: | 290 except Exception as ex: |
| 300 # need finalization to restore a correct state | 291 # need finalization to restore a correct state |
| 301 self.linter.reporter.finalize() | 292 self.linter.reporter.finalize() |
| 302 ex.file = tocheck | 293 ex.file = tocheck |
| 303 print ex | 294 print(ex) |
| 304 ex.__str__ = exception_str | 295 ex.__str__ = exception_str |
| 305 raise | 296 raise |
| 306 self._check_result(self.linter.reporter.finalize()) | 297 self._check_result(self.linter.reporter.finalize()) |
| 307 | 298 |
| 308 def _has_output(self): | 299 def _has_output(self): |
| 309 return not self.module.startswith('func_noerror_') | 300 return not self.module.startswith('func_noerror_') |
| 310 | 301 |
| 311 def _get_expected(self): | 302 def _get_expected(self): |
| 312 if self._has_output() and self.output: | 303 if self._has_output() and self.output: |
| 313 with open(self.output, 'U') as fobj: | 304 with open(self.output, 'U') as fobj: |
| (...skipping 26 matching lines...) Expand all Loading... |
| 340 except IOError: | 331 except IOError: |
| 341 expected = '' | 332 expected = '' |
| 342 if got != expected: | 333 if got != expected: |
| 343 with open(self.output, 'w') as fobj: | 334 with open(self.output, 'w') as fobj: |
| 344 fobj.write(got) | 335 fobj.write(got) |
| 345 | 336 |
| 346 # Callback | 337 # Callback |
| 347 | 338 |
| 348 def cb_test_gen(base_class): | 339 def cb_test_gen(base_class): |
| 349 def call(input_dir, msg_dir, module_file, messages_file, dependencies): | 340 def call(input_dir, msg_dir, module_file, messages_file, dependencies): |
| 341 # pylint: disable=no-init |
| 350 class LintTC(base_class): | 342 class LintTC(base_class): |
| 351 module = module_file.replace('.py', '') | 343 module = module_file.replace('.py', '') |
| 352 output = messages_file | 344 output = messages_file |
| 353 depends = dependencies or None | 345 depends = dependencies or None |
| 354 tags = testlib.Tags(('generated', 'pylint_input_%s' % module)) | |
| 355 INPUT_DIR = input_dir | 346 INPUT_DIR = input_dir |
| 356 MSG_DIR = msg_dir | 347 MSG_DIR = msg_dir |
| 357 return LintTC | 348 return LintTC |
| 358 return call | 349 return call |
| 359 | 350 |
| 360 # Main function | 351 # Main function |
| 361 | 352 |
| 362 def make_tests(input_dir, msg_dir, filter_rgx, callbacks): | 353 def make_tests(input_dir, msg_dir, filter_rgx, callbacks): |
| 363 """generate tests classes from test info | 354 """generate tests classes from test info |
| 364 | 355 |
| 365 return the list of generated test classes | 356 return the list of generated test classes |
| 366 """ | 357 """ |
| 367 if filter_rgx: | 358 if filter_rgx: |
| 368 is_to_run = re.compile(filter_rgx).search | 359 is_to_run = re.compile(filter_rgx).search |
| 369 else: | 360 else: |
| 370 is_to_run = lambda x: 1 | 361 is_to_run = lambda x: 1 |
| 371 tests = [] | 362 tests = [] |
| 372 for module_file, messages_file in ( | 363 for module_file, messages_file in ( |
| 373 get_tests_info(input_dir, msg_dir, 'func_', '') | 364 get_tests_info(input_dir, msg_dir, 'func_', '') |
| 374 ): | 365 ): |
| 375 if not is_to_run(module_file): | 366 if not is_to_run(module_file) or module_file.endswith('.pyc'): |
| 376 continue | 367 continue |
| 377 base = module_file.replace('func_', '').replace('.py', '') | 368 base = module_file.replace('func_', '').replace('.py', '') |
| 378 | 369 |
| 379 dependencies = get_tests_info(input_dir, msg_dir, base, '.py') | 370 dependencies = get_tests_info(input_dir, msg_dir, base, '.py') |
| 380 | 371 |
| 381 for callback in callbacks: | 372 for callback in callbacks: |
| 382 test = callback(input_dir, msg_dir, module_file, messages_file, | 373 test = callback(input_dir, msg_dir, module_file, messages_file, |
| 383 dependencies) | 374 dependencies) |
| 384 if test: | 375 if test: |
| 385 tests.append(test) | 376 tests.append(test) |
| 386 return tests | 377 return tests |
| 378 |
| 379 def tokenize_str(code): |
| 380 return list(tokenize.generate_tokens(StringIO(code).readline)) |
| 381 |
| 382 @contextlib.contextmanager |
| 383 def create_tempfile(content=None): |
| 384 """Create a new temporary file. |
| 385 |
| 386 If *content* parameter is given, then it will be written |
| 387 in the temporary file, before passing it back. |
| 388 This is a context manager and should be used with a *with* statement. |
| 389 """ |
| 390 # Can't use tempfile.NamedTemporaryFile here |
| 391 # because on Windows the file must be closed before writing to it, |
| 392 # see http://bugs.python.org/issue14243 |
| 393 fd, tmp = tempfile.mkstemp() |
| 394 if content: |
| 395 if sys.version_info >= (3, 0): |
| 396 # erff |
| 397 os.write(fd, bytes(content, 'ascii')) |
| 398 else: |
| 399 os.write(fd, content) |
| 400 try: |
| 401 yield tmp |
| 402 finally: |
| 403 os.close(fd) |
| 404 os.remove(tmp) |
| 405 |
| 406 @contextlib.contextmanager |
| 407 def create_file_backed_module(code): |
| 408 """Create an astroid module for the given code, backed by a real file.""" |
| 409 with create_tempfile() as temp: |
| 410 module = test_utils.build_module(code) |
| 411 module.file = temp |
| 412 yield module |
| OLD | NEW |