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 |