| Index: third_party/pylint/testutils.py
|
| ===================================================================
|
| --- third_party/pylint/testutils.py (revision 0)
|
| +++ third_party/pylint/testutils.py (working copy)
|
| @@ -0,0 +1,386 @@
|
| +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
| +# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
| +#
|
| +# This program is free software; you can redistribute it and/or modify it under
|
| +# the terms of the GNU General Public License as published by the Free Software
|
| +# Foundation; either version 2 of the License, or (at your option) any later
|
| +# version.
|
| +#
|
| +# This program is distributed in the hope that it will be useful, but WITHOUT
|
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
| +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
| +#
|
| +# You should have received a copy of the GNU General Public License along with
|
| +# this program; if not, write to the Free Software Foundation, Inc.,
|
| +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
| +"""functional/non regression tests for pylint"""
|
| +from __future__ import with_statement
|
| +
|
| +import collections
|
| +import contextlib
|
| +import functools
|
| +import sys
|
| +import re
|
| +
|
| +from glob import glob
|
| +from os import linesep
|
| +from os.path import abspath, basename, dirname, isdir, join, splitext
|
| +from cStringIO import StringIO
|
| +
|
| +from logilab.common import testlib
|
| +
|
| +from pylint import checkers
|
| +from pylint.utils import PyLintASTWalker
|
| +from pylint.reporters import BaseReporter
|
| +from pylint.interfaces import IReporter
|
| +from pylint.lint import PyLinter
|
| +
|
| +
|
| +# Utils
|
| +
|
| +SYS_VERS_STR = '%d%d%d' % sys.version_info[:3]
|
| +TITLE_UNDERLINES = ['', '=', '-', '.']
|
| +PREFIX = abspath(dirname(__file__))
|
| +PY3K = sys.version_info[0] == 3
|
| +
|
| +def fix_path():
|
| + sys.path.insert(0, PREFIX)
|
| +
|
| +def get_tests_info(input_dir, msg_dir, prefix, suffix):
|
| + """get python input examples and output messages
|
| +
|
| + We use following conventions for input files and messages:
|
| + for different inputs:
|
| + test for python >= x.y -> input = <name>_pyxy.py
|
| + test for python < x.y -> input = <name>_py_xy.py
|
| + for one input and different messages:
|
| + message for python >= x.y -> message = <name>_pyxy.txt
|
| + lower versions -> message with highest num
|
| + """
|
| + result = []
|
| + for fname in glob(join(input_dir, prefix + '*' + suffix)):
|
| + infile = basename(fname)
|
| + fbase = splitext(infile)[0]
|
| + # filter input files :
|
| + pyrestr = fbase.rsplit('_py', 1)[-1] # like _26 or 26
|
| + if pyrestr.isdigit(): # '24', '25'...
|
| + if SYS_VERS_STR < pyrestr:
|
| + continue
|
| + if pyrestr.startswith('_') and pyrestr[1:].isdigit():
|
| + # skip test for higher python versions
|
| + if SYS_VERS_STR >= pyrestr[1:]:
|
| + continue
|
| + messages = glob(join(msg_dir, fbase + '*.txt'))
|
| + # the last one will be without ext, i.e. for all or upper versions:
|
| + if messages:
|
| + for outfile in sorted(messages, reverse=True):
|
| + py_rest = outfile.rsplit('_py', 1)[-1][:-4]
|
| + if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
|
| + break
|
| + else:
|
| + # This will provide an error message indicating the missing filename.
|
| + outfile = join(msg_dir, fbase + '.txt')
|
| + result.append((infile, outfile))
|
| + return result
|
| +
|
| +
|
| +class TestReporter(BaseReporter):
|
| + """reporter storing plain text messages"""
|
| +
|
| + __implements____ = IReporter
|
| +
|
| + def __init__(self):
|
| + self.message_ids = {}
|
| + self.reset()
|
| +
|
| + def reset(self):
|
| + self.out = StringIO()
|
| + self.messages = []
|
| +
|
| + def add_message(self, msg_id, location, msg):
|
| + """manage message of different type and in the context of path """
|
| + _, _, obj, line, _ = location
|
| + self.message_ids[msg_id] = 1
|
| + if obj:
|
| + obj = ':%s' % obj
|
| + sigle = msg_id[0]
|
| + if PY3K and linesep != '\n':
|
| + # 2to3 writes os.linesep instead of using
|
| + # the previosly used line separators
|
| + msg = msg.replace('\r\n', '\n')
|
| + self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg))
|
| +
|
| + def finalize(self):
|
| + self.messages.sort()
|
| + for msg in self.messages:
|
| + print >> self.out, msg
|
| + result = self.out.getvalue()
|
| + self.reset()
|
| + return result
|
| +
|
| + def display_results(self, layout):
|
| + """ignore layouts"""
|
| +
|
| +
|
| +if sys.version_info < (2, 6):
|
| + class Message(tuple):
|
| + def __new__(cls, msg_id, line=None, node=None, args=None):
|
| + return tuple.__new__(cls, (msg_id, line, node, args))
|
| +
|
| + @property
|
| + def msg_id(self):
|
| + return self[0]
|
| + @property
|
| + def line(self):
|
| + return self[1]
|
| + @property
|
| + def node(self):
|
| + return self[2]
|
| + @property
|
| + def args(self):
|
| + return self[3]
|
| +
|
| +
|
| +else:
|
| + class Message(collections.namedtuple('Message',
|
| + ['msg_id', 'line', 'node', 'args'])):
|
| + def __new__(cls, msg_id, line=None, node=None, args=None):
|
| + return tuple.__new__(cls, (msg_id, line, node, args))
|
| +
|
| +
|
| +class UnittestLinter(object):
|
| + """A fake linter class to capture checker messages."""
|
| +
|
| + def __init__(self):
|
| + self._messages = []
|
| + self.stats = {}
|
| +
|
| + def release_messages(self):
|
| + try:
|
| + return self._messages
|
| + finally:
|
| + self._messages = []
|
| +
|
| + def add_message(self, msg_id, line=None, node=None, args=None):
|
| + self._messages.append(Message(msg_id, line, node, args))
|
| +
|
| + def is_message_enabled(self, *unused_args):
|
| + return True
|
| +
|
| + def add_stats(self, **kwargs):
|
| + for name, value in kwargs.iteritems():
|
| + self.stats[name] = value
|
| + return self.stats
|
| +
|
| + @property
|
| + def options_providers(self):
|
| + return linter.options_providers
|
| +
|
| +def set_config(**kwargs):
|
| + """Decorator for setting config values on a checker."""
|
| + def _Wrapper(fun):
|
| + @functools.wraps(fun)
|
| + def _Forward(self):
|
| + for key, value in kwargs.iteritems():
|
| + setattr(self.checker.config, key, value)
|
| + if isinstance(self, CheckerTestCase):
|
| + # reopen checker in case, it may be interested in configuration change
|
| + self.checker.open()
|
| + fun(self)
|
| +
|
| + return _Forward
|
| + return _Wrapper
|
| +
|
| +
|
| +class CheckerTestCase(testlib.TestCase):
|
| + """A base testcase class for unittesting individual checker classes."""
|
| + CHECKER_CLASS = None
|
| + CONFIG = {}
|
| +
|
| + def setUp(self):
|
| + self.linter = UnittestLinter()
|
| + self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
|
| + for key, value in self.CONFIG.iteritems():
|
| + setattr(self.checker.config, key, value)
|
| + self.checker.open()
|
| +
|
| + @contextlib.contextmanager
|
| + def assertNoMessages(self):
|
| + """Assert that no messages are added by the given method."""
|
| + with self.assertAddsMessages():
|
| + yield
|
| +
|
| + @contextlib.contextmanager
|
| + def assertAddsMessages(self, *messages):
|
| + """Assert that exactly the given method adds the given messages.
|
| +
|
| + The list of messages must exactly match *all* the messages added by the
|
| + method. Additionally, we check to see whether the args in each message can
|
| + actually be substituted into the message string.
|
| + """
|
| + yield
|
| + got = self.linter.release_messages()
|
| + msg = ('Expected messages did not match actual.\n'
|
| + 'Expected:\n%s\nGot:\n%s' % ('\n'.join(repr(m) for m in messages),
|
| + '\n'.join(repr(m) for m in got)))
|
| + self.assertEqual(list(messages), got, msg)
|
| +
|
| + def walk(self, node):
|
| + """recursive walk on the given node"""
|
| + walker = PyLintASTWalker(linter)
|
| + walker.add_checker(self.checker)
|
| + walker.walk(node)
|
| +
|
| +
|
| +# Init
|
| +test_reporter = TestReporter()
|
| +linter = PyLinter()
|
| +linter.set_reporter(test_reporter)
|
| +linter.config.persistent = 0
|
| +checkers.initialize(linter)
|
| +linter.global_set_option('required-attributes', ('__revision__',))
|
| +
|
| +if linesep != '\n':
|
| + LINE_RGX = re.compile(linesep)
|
| + def ulines(string):
|
| + return LINE_RGX.sub('\n', string)
|
| +else:
|
| + def ulines(string):
|
| + return string
|
| +
|
| +INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$')
|
| +
|
| +def exception_str(self, ex):
|
| + """function used to replace default __str__ method of exception instances"""
|
| + return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args))
|
| +
|
| +# Test classes
|
| +
|
| +class LintTestUsingModule(testlib.TestCase):
|
| + INPUT_DIR = None
|
| + DEFAULT_PACKAGE = 'input'
|
| + package = DEFAULT_PACKAGE
|
| + linter = linter
|
| + module = None
|
| + depends = None
|
| + output = None
|
| + _TEST_TYPE = 'module'
|
| +
|
| + def shortDescription(self):
|
| + values = {'mode' : self._TEST_TYPE,
|
| + 'input': self.module,
|
| + 'pkg': self.package,
|
| + 'cls': self.__class__.__name__}
|
| +
|
| + if self.package == self.DEFAULT_PACKAGE:
|
| + msg = '%(mode)s test of input file "%(input)s" (%(cls)s)'
|
| + else:
|
| + msg = '%(mode)s test of input file "%(input)s" in "%(pkg)s" (%(cls)s)'
|
| + return msg % values
|
| +
|
| + def test_functionality(self):
|
| + tocheck = [self.package+'.'+self.module]
|
| + if self.depends:
|
| + tocheck += [self.package+'.%s' % name.replace('.py', '')
|
| + for name, _ in self.depends]
|
| + self._test(tocheck)
|
| +
|
| + def _check_result(self, got):
|
| + self.assertMultiLineEqual(self._get_expected().strip()+'\n',
|
| + got.strip()+'\n')
|
| +
|
| + def _test(self, tocheck):
|
| + if INFO_TEST_RGX.match(self.module):
|
| + self.linter.enable('I')
|
| + else:
|
| + self.linter.disable('I')
|
| + try:
|
| + self.linter.check(tocheck)
|
| + except Exception, ex:
|
| + # need finalization to restore a correct state
|
| + self.linter.reporter.finalize()
|
| + ex.file = tocheck
|
| + print ex
|
| + ex.__str__ = exception_str
|
| + raise
|
| + self._check_result(self.linter.reporter.finalize())
|
| +
|
| + def _has_output(self):
|
| + return not self.module.startswith('func_noerror_')
|
| +
|
| + def _get_expected(self):
|
| + if self._has_output() and self.output:
|
| + with open(self.output, 'U') as fobj:
|
| + return fobj.read().strip() + '\n'
|
| + else:
|
| + return ''
|
| +
|
| +class LintTestUsingFile(LintTestUsingModule):
|
| +
|
| + _TEST_TYPE = 'file'
|
| +
|
| + def test_functionality(self):
|
| + importable = join(self.INPUT_DIR, self.module)
|
| + # python also prefers packages over simple modules.
|
| + if not isdir(importable):
|
| + importable += '.py'
|
| + tocheck = [importable]
|
| + if self.depends:
|
| + tocheck += [join(self.INPUT_DIR, name) for name, _file in self.depends]
|
| + self._test(tocheck)
|
| +
|
| +class LintTestUpdate(LintTestUsingModule):
|
| +
|
| + _TEST_TYPE = 'update'
|
| +
|
| + def _check_result(self, got):
|
| + if self._has_output():
|
| + try:
|
| + expected = self._get_expected()
|
| + except IOError:
|
| + expected = ''
|
| + if got != expected:
|
| + with open(self.output, 'w') as fobj:
|
| + fobj.write(got)
|
| +
|
| +# Callback
|
| +
|
| +def cb_test_gen(base_class):
|
| + def call(input_dir, msg_dir, module_file, messages_file, dependencies):
|
| + class LintTC(base_class):
|
| + module = module_file.replace('.py', '')
|
| + output = messages_file
|
| + depends = dependencies or None
|
| + tags = testlib.Tags(('generated', 'pylint_input_%s' % module))
|
| + INPUT_DIR = input_dir
|
| + MSG_DIR = msg_dir
|
| + return LintTC
|
| + return call
|
| +
|
| +# Main function
|
| +
|
| +def make_tests(input_dir, msg_dir, filter_rgx, callbacks):
|
| + """generate tests classes from test info
|
| +
|
| + return the list of generated test classes
|
| + """
|
| + if filter_rgx:
|
| + is_to_run = re.compile(filter_rgx).search
|
| + else:
|
| + is_to_run = lambda x: 1
|
| + tests = []
|
| + for module_file, messages_file in (
|
| + get_tests_info(input_dir, msg_dir, 'func_', '')
|
| + ):
|
| + if not is_to_run(module_file):
|
| + continue
|
| + base = module_file.replace('func_', '').replace('.py', '')
|
| +
|
| + dependencies = get_tests_info(input_dir, msg_dir, base, '.py')
|
| +
|
| + for callback in callbacks:
|
| + test = callback(input_dir, msg_dir, module_file, messages_file,
|
| + dependencies)
|
| + if test:
|
| + tests.append(test)
|
| + return tests
|
|
|