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
|