You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			413 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			413 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
# 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 print_function
 | 
						|
 | 
						|
import collections
 | 
						|
import contextlib
 | 
						|
import functools
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import re
 | 
						|
import unittest
 | 
						|
import tempfile
 | 
						|
import tokenize
 | 
						|
 | 
						|
from glob import glob
 | 
						|
from os import linesep, getcwd, sep
 | 
						|
from os.path import abspath, basename, dirname, isdir, join, splitext
 | 
						|
 | 
						|
from astroid import test_utils
 | 
						|
 | 
						|
from pylint import checkers
 | 
						|
from pylint.utils import PyLintASTWalker
 | 
						|
from pylint.reporters import BaseReporter
 | 
						|
from pylint.interfaces import IReporter
 | 
						|
from pylint.lint import PyLinter
 | 
						|
 | 
						|
import six
 | 
						|
from six.moves import StringIO
 | 
						|
 | 
						|
 | 
						|
# 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): # pylint: disable=super-init-not-called
 | 
						|
 | 
						|
        self.message_ids = {}
 | 
						|
        self.reset()
 | 
						|
        self.path_strip_prefix = getcwd() + sep
 | 
						|
 | 
						|
    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(msg, file=self.out)
 | 
						|
        result = self.out.getvalue()
 | 
						|
        self.reset()
 | 
						|
        return result
 | 
						|
 | 
						|
    def display_results(self, layout):
 | 
						|
        """ignore layouts"""
 | 
						|
 | 
						|
 | 
						|
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."""
 | 
						|
    # pylint: disable=unused-argument, no-self-use
 | 
						|
 | 
						|
    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,
 | 
						|
                    confidence=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 six.iteritems(kwargs):
 | 
						|
            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 six.iteritems(kwargs):
 | 
						|
                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(unittest.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 six.iteritems(self.CONFIG):
 | 
						|
            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): # pylint: disable=unused-argument
 | 
						|
    """function used to replace default __str__ method of exception instances"""
 | 
						|
    return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args))
 | 
						|
 | 
						|
# Test classes
 | 
						|
 | 
						|
class LintTestUsingModule(unittest.TestCase):
 | 
						|
    INPUT_DIR = None
 | 
						|
    DEFAULT_PACKAGE = 'input'
 | 
						|
    package = DEFAULT_PACKAGE
 | 
						|
    linter = linter
 | 
						|
    module = None
 | 
						|
    depends = None
 | 
						|
    output = None
 | 
						|
    _TEST_TYPE = 'module'
 | 
						|
    maxDiff = None
 | 
						|
 | 
						|
    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 as 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):
 | 
						|
        # pylint: disable=no-init
 | 
						|
        class LintTC(base_class):
 | 
						|
            module = module_file.replace('.py', '')
 | 
						|
            output = messages_file
 | 
						|
            depends = dependencies or None
 | 
						|
            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) or module_file.endswith('.pyc'):
 | 
						|
            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
 | 
						|
 | 
						|
def tokenize_str(code):
 | 
						|
    return list(tokenize.generate_tokens(StringIO(code).readline))
 | 
						|
 | 
						|
@contextlib.contextmanager
 | 
						|
def create_tempfile(content=None):
 | 
						|
    """Create a new temporary file.
 | 
						|
 | 
						|
    If *content* parameter is given, then it will be written
 | 
						|
    in the temporary file, before passing it back.
 | 
						|
    This is a context manager and should be used with a *with* statement.
 | 
						|
    """
 | 
						|
    # Can't use tempfile.NamedTemporaryFile here
 | 
						|
    # because on Windows the file must be closed before writing to it,
 | 
						|
    # see http://bugs.python.org/issue14243
 | 
						|
    fd, tmp = tempfile.mkstemp()
 | 
						|
    if content:
 | 
						|
        if sys.version_info >= (3, 0):
 | 
						|
            # erff
 | 
						|
            os.write(fd, bytes(content, 'ascii'))
 | 
						|
        else:
 | 
						|
            os.write(fd, content)
 | 
						|
    try:
 | 
						|
        yield tmp
 | 
						|
    finally:
 | 
						|
        os.close(fd)
 | 
						|
        os.remove(tmp)
 | 
						|
 | 
						|
@contextlib.contextmanager
 | 
						|
def create_file_backed_module(code):
 | 
						|
    """Create an astroid module for the given code, backed by a real file."""
 | 
						|
    with create_tempfile() as temp:
 | 
						|
        module = test_utils.build_module(code)
 | 
						|
        module.file = temp
 | 
						|
        yield module
 |