Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / pylint / testutils.py @ 745

History | View | Annotate | Download (13.2 KB)

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

    
19
import collections
20
import contextlib
21
import functools
22
from glob import glob
23
import os
24
from os import linesep, getcwd, sep
25
from os.path import abspath, basename, dirname, isdir, join, splitext
26
import sys
27
import re
28
import unittest
29
import tempfile
30
import tokenize
31

    
32
import six
33
from six.moves import StringIO
34

    
35
import astroid
36
from pylint import checkers
37
from pylint.utils import PyLintASTWalker
38
from pylint.reporters import BaseReporter
39
from pylint.interfaces import IReporter
40
from pylint.lint import PyLinter
41

    
42

    
43

    
44
# Utils
45

    
46
SYS_VERS_STR = '%d%d%d' % sys.version_info[:3]
47
TITLE_UNDERLINES = ['', '=', '-', '.']
48
PREFIX = abspath(dirname(__file__))
49
PY3K = sys.version_info[0] == 3
50

    
51
def fix_path():
52
    sys.path.insert(0, PREFIX)
53

    
54
def get_tests_info(input_dir, msg_dir, prefix, suffix):
55
    """get python input examples and output messages
56

57
    We use following conventions for input files and messages:
58
    for different inputs:
59
        test for python  >= x.y    ->  input   =  <name>_pyxy.py
60
        test for python  <  x.y    ->  input   =  <name>_py_xy.py
61
    for one input and different messages:
62
        message for python >=  x.y ->  message =  <name>_pyxy.txt
63
        lower versions             ->  message with highest num
64
    """
65
    result = []
66
    for fname in glob(join(input_dir, prefix + '*' + suffix)):
67
        infile = basename(fname)
68
        fbase = splitext(infile)[0]
69
        # filter input files :
70
        pyrestr = fbase.rsplit('_py', 1)[-1] # like _26 or 26
71
        if pyrestr.isdigit(): # '24', '25'...
72
            if SYS_VERS_STR < pyrestr:
73
                continue
74
        if pyrestr.startswith('_') and  pyrestr[1:].isdigit():
75
            # skip test for higher python versions
76
            if SYS_VERS_STR >= pyrestr[1:]:
77
                continue
78
        messages = glob(join(msg_dir, fbase + '*.txt'))
79
        # the last one will be without ext, i.e. for all or upper versions:
80
        if messages:
81
            for outfile in sorted(messages, reverse=True):
82
                py_rest = outfile.rsplit('_py', 1)[-1][:-4]
83
                if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
84
                    break
85
        else:
86
            # This will provide an error message indicating the missing filename.
87
            outfile = join(msg_dir, fbase + '.txt')
88
        result.append((infile, outfile))
89
    return result
90

    
91

    
92
class TestReporter(BaseReporter):
93
    """reporter storing plain text messages"""
94

    
95
    __implements__ = IReporter
96

    
97
    def __init__(self): # pylint: disable=super-init-not-called
98

    
99
        self.message_ids = {}
100
        self.reset()
101
        self.path_strip_prefix = getcwd() + sep
102

    
103
    def reset(self):
104
        self.out = StringIO()
105
        self.messages = []
106

    
107
    def add_message(self, msg_id, location, msg):
108
        """manage message of different type and in the context of path """
109
        _, _, obj, line, _ = location
110
        self.message_ids[msg_id] = 1
111
        if obj:
112
            obj = ':%s' % obj
113
        sigle = msg_id[0]
114
        if PY3K and linesep != '\n':
115
            # 2to3 writes os.linesep instead of using
116
            # the previosly used line separators
117
            msg = msg.replace('\r\n', '\n')
118
        self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg))
119

    
120
    def finalize(self):
121
        self.messages.sort()
122
        for msg in self.messages:
123
            print(msg, file=self.out)
124
        result = self.out.getvalue()
125
        self.reset()
126
        return result
127

    
128
    def display_reports(self, layout):
129
        """ignore layouts"""
130

    
131
    _display = None
132

    
133

    
134
class Message(collections.namedtuple('Message',
135
                                     ['msg_id', 'line', 'node', 'args'])):
136
    def __new__(cls, msg_id, line=None, node=None, args=None):
137
        return tuple.__new__(cls, (msg_id, line, node, args))
138

    
139

    
140
class UnittestLinter(object):
141
    """A fake linter class to capture checker messages."""
142
    # pylint: disable=unused-argument, no-self-use
143

    
144
    def __init__(self):
145
        self._messages = []
146
        self.stats = {}
147

    
148
    def release_messages(self):
149
        try:
150
            return self._messages
151
        finally:
152
            self._messages = []
153

    
154
    def add_message(self, msg_id, line=None, node=None, args=None,
155
                    confidence=None):
156
        self._messages.append(Message(msg_id, line, node, args))
157

    
158
    def is_message_enabled(self, *unused_args):
159
        return True
160

    
161
    def add_stats(self, **kwargs):
162
        for name, value in six.iteritems(kwargs):
163
            self.stats[name] = value
164
        return self.stats
165

    
166
    @property
167
    def options_providers(self):
168
        return linter.options_providers
169

    
170
def set_config(**kwargs):
171
    """Decorator for setting config values on a checker."""
172
    def _wrapper(fun):
173
        @functools.wraps(fun)
174
        def _forward(self):
175
            for key, value in six.iteritems(kwargs):
176
                setattr(self.checker.config, key, value)
177
            if isinstance(self, CheckerTestCase):
178
                # reopen checker in case, it may be interested in configuration change
179
                self.checker.open()
180
            fun(self)
181

    
182
        return _forward
183
    return _wrapper
184

    
185

    
186
class CheckerTestCase(unittest.TestCase):
187
    """A base testcase class for unittesting individual checker classes."""
188
    CHECKER_CLASS = None
189
    CONFIG = {}
190

    
191
    def setUp(self):
192
        self.linter = UnittestLinter()
193
        self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
194
        for key, value in six.iteritems(self.CONFIG):
195
            setattr(self.checker.config, key, value)
196
        self.checker.open()
197

    
198
    @contextlib.contextmanager
199
    def assertNoMessages(self):
200
        """Assert that no messages are added by the given method."""
201
        with self.assertAddsMessages():
202
            yield
203

    
204
    @contextlib.contextmanager
205
    def assertAddsMessages(self, *messages):
206
        """Assert that exactly the given method adds the given messages.
207

208
        The list of messages must exactly match *all* the messages added by the
209
        method. Additionally, we check to see whether the args in each message can
210
        actually be substituted into the message string.
211
        """
212
        yield
213
        got = self.linter.release_messages()
214
        msg = ('Expected messages did not match actual.\n'
215
               'Expected:\n%s\nGot:\n%s' % ('\n'.join(repr(m) for m in messages),
216
                                            '\n'.join(repr(m) for m in got)))
217
        self.assertEqual(list(messages), got, msg)
218

    
219
    def walk(self, node):
220
        """recursive walk on the given node"""
221
        walker = PyLintASTWalker(linter)
222
        walker.add_checker(self.checker)
223
        walker.walk(node)
224

    
225

    
226
# Init
227
test_reporter = TestReporter()
228
linter = PyLinter()
229
linter.set_reporter(test_reporter)
230
linter.config.persistent = 0
231
checkers.initialize(linter)
232

    
233
if linesep != '\n':
234
    LINE_RGX = re.compile(linesep)
235
    def ulines(string):
236
        return LINE_RGX.sub('\n', string)
237
else:
238
    def ulines(string):
239
        return string
240

    
241
INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$')
242

    
243
def exception_str(self, ex): # pylint: disable=unused-argument
244
    """function used to replace default __str__ method of exception instances"""
245
    return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args))
246

    
247
# Test classes
248

    
249
class LintTestUsingModule(unittest.TestCase):
250
    INPUT_DIR = None
251
    DEFAULT_PACKAGE = 'input'
252
    package = DEFAULT_PACKAGE
253
    linter = linter
254
    module = None
255
    depends = None
256
    output = None
257
    _TEST_TYPE = 'module'
258
    maxDiff = None
259

    
260
    def shortDescription(self):
261
        values = {'mode' : self._TEST_TYPE,
262
                  'input': self.module,
263
                  'pkg':   self.package,
264
                  'cls':   self.__class__.__name__}
265

    
266
        if self.package == self.DEFAULT_PACKAGE:
267
            msg = '%(mode)s test of input file "%(input)s" (%(cls)s)'
268
        else:
269
            msg = '%(mode)s test of input file "%(input)s" in "%(pkg)s" (%(cls)s)'
270
        return msg % values
271

    
272
    def test_functionality(self):
273
        tocheck = [self.package+'.'+self.module]
274
        # pylint: disable=not-an-iterable; can't handle boolean checks for now
275
        if self.depends:
276
            tocheck += [self.package+'.%s' % name.replace('.py', '')
277
                        for name, _ in self.depends]
278
        self._test(tocheck)
279

    
280
    def _check_result(self, got):
281
        self.assertMultiLineEqual(self._get_expected().strip()+'\n',
282
                                  got.strip()+'\n')
283

    
284
    def _test(self, tocheck):
285
        if INFO_TEST_RGX.match(self.module):
286
            self.linter.enable('I')
287
        else:
288
            self.linter.disable('I')
289
        try:
290
            self.linter.check(tocheck)
291
        except Exception as ex:
292
            # need finalization to restore a correct state
293
            self.linter.reporter.finalize()
294
            ex.file = tocheck
295
            print(ex)
296
            ex.__str__ = exception_str
297
            raise
298
        self._check_result(self.linter.reporter.finalize())
299

    
300
    def _has_output(self):
301
        return not self.module.startswith('func_noerror_')
302

    
303
    def _get_expected(self):
304
        if self._has_output() and self.output:
305
            with open(self.output, 'U') as fobj:
306
                return fobj.read().strip() + '\n'
307
        else:
308
            return ''
309

    
310
class LintTestUsingFile(LintTestUsingModule):
311

    
312
    _TEST_TYPE = 'file'
313

    
314
    def test_functionality(self):
315
        importable = join(self.INPUT_DIR, self.module)
316
        # python also prefers packages over simple modules.
317
        if not isdir(importable):
318
            importable += '.py'
319
        tocheck = [importable]
320
        # pylint: disable=not-an-iterable; can't handle boolean checks for now
321
        if self.depends:
322
            tocheck += [join(self.INPUT_DIR, name) for name, _ in self.depends]
323
        self._test(tocheck)
324

    
325
class LintTestUpdate(LintTestUsingModule):
326

    
327
    _TEST_TYPE = 'update'
328

    
329
    def _check_result(self, got):
330
        if self._has_output():
331
            try:
332
                expected = self._get_expected()
333
            except IOError:
334
                expected = ''
335
            if got != expected:
336
                with open(self.output, 'w') as fobj:
337
                    fobj.write(got)
338

    
339
# Callback
340

    
341
def cb_test_gen(base_class):
342
    def call(input_dir, msg_dir, module_file, messages_file, dependencies):
343
        # pylint: disable=no-init
344
        class LintTC(base_class):
345
            module = module_file.replace('.py', '')
346
            output = messages_file
347
            depends = dependencies or None
348
            INPUT_DIR = input_dir
349
            MSG_DIR = msg_dir
350
        return LintTC
351
    return call
352

    
353
# Main function
354

    
355
def make_tests(input_dir, msg_dir, filter_rgx, callbacks):
356
    """generate tests classes from test info
357

358
    return the list of generated test classes
359
    """
360
    if filter_rgx:
361
        is_to_run = re.compile(filter_rgx).search
362
    else:
363
        is_to_run = lambda x: 1
364
    tests = []
365
    for module_file, messages_file in (
366
            get_tests_info(input_dir, msg_dir, 'func_', '')
367
    ):
368
        if not is_to_run(module_file) or module_file.endswith(('.pyc', "$py.class")):
369
            continue
370
        base = module_file.replace('func_', '').replace('.py', '')
371

    
372
        dependencies = get_tests_info(input_dir, msg_dir, base, '.py')
373

    
374
        for callback in callbacks:
375
            test = callback(input_dir, msg_dir, module_file, messages_file,
376
                            dependencies)
377
            if test:
378
                tests.append(test)
379
    return tests
380

    
381
def tokenize_str(code):
382
    return list(tokenize.generate_tokens(StringIO(code).readline))
383

    
384
@contextlib.contextmanager
385
def create_tempfile(content=None):
386
    """Create a new temporary file.
387

388
    If *content* parameter is given, then it will be written
389
    in the temporary file, before passing it back.
390
    This is a context manager and should be used with a *with* statement.
391
    """
392
    # Can't use tempfile.NamedTemporaryFile here
393
    # because on Windows the file must be closed before writing to it,
394
    # see http://bugs.python.org/issue14243
395
    file_handle, tmp = tempfile.mkstemp()
396
    if content:
397
        if sys.version_info >= (3, 0):
398
            # erff
399
            os.write(file_handle, bytes(content, 'ascii'))
400
        else:
401
            os.write(file_handle, content)
402
    try:
403
        yield tmp
404
    finally:
405
        os.close(file_handle)
406
        os.remove(tmp)
407

    
408
@contextlib.contextmanager
409
def create_file_backed_module(code):
410
    """Create an astroid module for the given code, backed by a real file."""
411
    with create_tempfile() as temp:
412
        module = astroid.parse(code)
413
        module.file = temp
414
        yield module