| 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 |