Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_functional.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
  3. # Copyright (c) 2014 Google, Inc.
  4. # Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
  5. # Copyright (c) 2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  6. # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
  7. # Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
  8. # Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
  9. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  10. # For details: https://github.com/PyCQA/pylint/blob/master/COPYING
  11. """Functional full-module tests for PyLint."""
  12. import csv
  13. import collections
  14. import io
  15. import operator
  16. import os
  17. import re
  18. import sys
  19. import platform
  20. import six
  21. from six.moves import configparser
  22. import pytest
  23. from pylint import checkers
  24. from pylint import interfaces
  25. from pylint import lint
  26. from pylint import reporters
  27. class test_dialect(csv.excel):
  28. if sys.version_info[0] < 3:
  29. delimiter = b':'
  30. lineterminator = b'\n'
  31. else:
  32. delimiter = ':'
  33. lineterminator = '\n'
  34. csv.register_dialect('test', test_dialect)
  35. class NoFileError(Exception):
  36. pass
  37. # Notes:
  38. # - for the purpose of this test, the confidence levels HIGH and UNDEFINED
  39. # are treated as the same.
  40. # TODOs
  41. # - implement exhaustivity tests
  42. # If message files should be updated instead of checked.
  43. UPDATE = False
  44. class OutputLine(collections.namedtuple('OutputLine',
  45. ['symbol', 'lineno', 'object', 'msg', 'confidence'])):
  46. @classmethod
  47. def from_msg(cls, msg):
  48. return cls(
  49. msg.symbol, msg.line, msg.obj or '', msg.msg.replace("\r\n", "\n"),
  50. msg.confidence.name
  51. if msg.confidence != interfaces.UNDEFINED else interfaces.HIGH.name)
  52. @classmethod
  53. def from_csv(cls, row):
  54. confidence = row[4] if len(row) == 5 else interfaces.HIGH.name
  55. return cls(row[0], int(row[1]), row[2], row[3], confidence)
  56. def to_csv(self):
  57. if self.confidence == interfaces.HIGH.name:
  58. return self[:-1]
  59. else:
  60. return self
  61. # Common sub-expressions.
  62. _MESSAGE = {'msg': r'[a-z][a-z\-]+'}
  63. # Matches a #,
  64. # - followed by a comparison operator and a Python version (optional),
  65. # - followed by an line number with a +/- (optional),
  66. # - followed by a list of bracketed message symbols.
  67. # Used to extract expected messages from testdata files.
  68. _EXPECTED_RE = re.compile(
  69. r'\s*#\s*(?:(?P<line>[+-]?[0-9]+):)?'
  70. r'(?:(?P<op>[><=]+) *(?P<version>[0-9.]+):)?'
  71. r'\s*\[(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)\]' % _MESSAGE)
  72. def parse_python_version(str):
  73. return tuple(int(digit) for digit in str.split('.'))
  74. class FunctionalTestReporter(reporters.BaseReporter):
  75. def handle_message(self, msg):
  76. self.messages.append(msg)
  77. def on_set_current_module(self, module, filepath):
  78. self.messages = []
  79. def display_reports(self, layout):
  80. """Ignore layouts."""
  81. class FunctionalTestFile(object):
  82. """A single functional test case file with options."""
  83. _CONVERTERS = {
  84. 'min_pyver': parse_python_version,
  85. 'max_pyver': parse_python_version,
  86. 'requires': lambda s: s.split(',')
  87. }
  88. def __init__(self, directory, filename):
  89. self._directory = directory
  90. self.base = filename.replace('.py', '')
  91. self.options = {
  92. 'min_pyver': (2, 5),
  93. 'max_pyver': (4, 0),
  94. 'requires': [],
  95. 'except_implementations': [],
  96. }
  97. self._parse_options()
  98. def _parse_options(self):
  99. cp = configparser.ConfigParser()
  100. cp.add_section('testoptions')
  101. try:
  102. cp.read(self.option_file)
  103. except NoFileError:
  104. pass
  105. for name, value in cp.items('testoptions'):
  106. conv = self._CONVERTERS.get(name, lambda v: v)
  107. self.options[name] = conv(value)
  108. @property
  109. def option_file(self):
  110. return self._file_type('.rc')
  111. @property
  112. def module(self):
  113. package = os.path.basename(self._directory)
  114. return '.'.join([package, self.base])
  115. @property
  116. def expected_output(self):
  117. return self._file_type('.txt', check_exists=False)
  118. @property
  119. def source(self):
  120. return self._file_type('.py')
  121. def _file_type(self, ext, check_exists=True):
  122. name = os.path.join(self._directory, self.base + ext)
  123. if not check_exists or os.path.exists(name):
  124. return name
  125. else:
  126. raise NoFileError
  127. _OPERATORS = {
  128. '>': operator.gt,
  129. '<': operator.lt,
  130. '>=': operator.ge,
  131. '<=': operator.le,
  132. }
  133. def parse_expected_output(stream):
  134. return [OutputLine.from_csv(row) for row in csv.reader(stream, 'test')]
  135. def get_expected_messages(stream):
  136. """Parses a file and get expected messages.
  137. :param stream: File-like input stream.
  138. :returns: A dict mapping line,msg-symbol tuples to the count on this line.
  139. """
  140. messages = collections.Counter()
  141. for i, line in enumerate(stream):
  142. match = _EXPECTED_RE.search(line)
  143. if match is None:
  144. continue
  145. line = match.group('line')
  146. if line is None:
  147. line = i + 1
  148. elif line.startswith('+') or line.startswith('-'):
  149. line = i + 1 + int(line)
  150. else:
  151. line = int(line)
  152. version = match.group('version')
  153. op = match.group('op')
  154. if version:
  155. required = parse_python_version(version)
  156. if not _OPERATORS[op](sys.version_info, required):
  157. continue
  158. for msg_id in match.group('msgs').split(','):
  159. messages[line, msg_id.strip()] += 1
  160. return messages
  161. def multiset_difference(left_op, right_op):
  162. """Takes two multisets and compares them.
  163. A multiset is a dict with the cardinality of the key as the value.
  164. :param left_op: The expected entries.
  165. :param right_op: Actual entries.
  166. :returns: The two multisets of missing and unexpected messages.
  167. """
  168. missing = left_op.copy()
  169. missing.subtract(right_op)
  170. unexpected = {}
  171. for key, value in list(six.iteritems(missing)):
  172. if value <= 0:
  173. missing.pop(key)
  174. if value < 0:
  175. unexpected[key] = -value
  176. return missing, unexpected
  177. class LintModuleTest(object):
  178. maxDiff = None
  179. def __init__(self, test_file):
  180. test_reporter = FunctionalTestReporter()
  181. self._linter = lint.PyLinter()
  182. self._linter.set_reporter(test_reporter)
  183. self._linter.config.persistent = 0
  184. checkers.initialize(self._linter)
  185. self._linter.disable('I')
  186. try:
  187. self._linter.read_config_file(test_file.option_file)
  188. self._linter.load_config_file()
  189. except NoFileError:
  190. pass
  191. self._test_file = test_file
  192. def setUp(self):
  193. if self._should_be_skipped_due_to_version():
  194. pytest.skip( 'Test cannot run with Python %s.' % (sys.version.split(' ')[0],))
  195. missing = []
  196. for req in self._test_file.options['requires']:
  197. try:
  198. __import__(req)
  199. except ImportError:
  200. missing.append(req)
  201. if missing:
  202. pytest.skip('Requires %s to be present.' % (','.join(missing),))
  203. if self._test_file.options['except_implementations']:
  204. implementations = [
  205. item.strip() for item in
  206. self._test_file.options['except_implementations'].split(",")
  207. ]
  208. implementation = platform.python_implementation()
  209. if implementation in implementations:
  210. pytest.skip(
  211. 'Test cannot run with Python implementation %r'
  212. % (implementation, ))
  213. def _should_be_skipped_due_to_version(self):
  214. return (sys.version_info < self._test_file.options['min_pyver'] or
  215. sys.version_info > self._test_file.options['max_pyver'])
  216. def __str__(self):
  217. return "%s (%s.%s)" % (self._test_file.base, self.__class__.__module__,
  218. self.__class__.__name__)
  219. def _open_expected_file(self):
  220. return open(self._test_file.expected_output)
  221. def _open_source_file(self):
  222. if self._test_file.base == "invalid_encoded_data":
  223. return open(self._test_file.source)
  224. else:
  225. return io.open(self._test_file.source, encoding="utf8")
  226. def _get_expected(self):
  227. with self._open_source_file() as fobj:
  228. expected_msgs = get_expected_messages(fobj)
  229. if expected_msgs:
  230. with self._open_expected_file() as fobj:
  231. expected_output_lines = parse_expected_output(fobj)
  232. else:
  233. expected_output_lines = []
  234. return expected_msgs, expected_output_lines
  235. def _get_received(self):
  236. messages = self._linter.reporter.messages
  237. messages.sort(key=lambda m: (m.line, m.symbol, m.msg))
  238. received_msgs = collections.Counter()
  239. received_output_lines = []
  240. for msg in messages:
  241. received_msgs[msg.line, msg.symbol] += 1
  242. received_output_lines.append(OutputLine.from_msg(msg))
  243. return received_msgs, received_output_lines
  244. def _runTest(self):
  245. self._linter.check([self._test_file.module])
  246. expected_messages, expected_text = self._get_expected()
  247. received_messages, received_text = self._get_received()
  248. if expected_messages != received_messages:
  249. msg = ['Wrong results for file "%s":' % (self._test_file.base)]
  250. missing, unexpected = multiset_difference(expected_messages,
  251. received_messages)
  252. if missing:
  253. msg.append('\nExpected in testdata:')
  254. msg.extend(' %3d: %s' % msg for msg in sorted(missing))
  255. if unexpected:
  256. msg.append('\nUnexpected in testdata:')
  257. msg.extend(' %3d: %s' % msg for msg in sorted(unexpected))
  258. pytest.fail('\n'.join(msg))
  259. self._check_output_text(expected_messages, expected_text, received_text)
  260. def _split_lines(self, expected_messages, lines):
  261. emitted, omitted = [], []
  262. for msg in lines:
  263. if (msg[1], msg[0]) in expected_messages:
  264. emitted.append(msg)
  265. else:
  266. omitted.append(msg)
  267. return emitted, omitted
  268. def _check_output_text(self, expected_messages, expected_lines,
  269. received_lines):
  270. assert self._split_lines(expected_messages, expected_lines)[0] == \
  271. received_lines, self._test_file.base
  272. class LintModuleOutputUpdate(LintModuleTest):
  273. def _open_expected_file(self):
  274. try:
  275. return super(LintModuleOutputUpdate, self)._open_expected_file()
  276. except IOError:
  277. return io.StringIO()
  278. def _check_output_text(self, expected_messages, expected_lines,
  279. received_lines):
  280. if not expected_messages:
  281. return
  282. emitted, remaining = self._split_lines(expected_messages, expected_lines)
  283. if emitted != received_lines:
  284. remaining.extend(received_lines)
  285. remaining.sort(key=lambda m: (m[1], m[0], m[3]))
  286. with open(self._test_file.expected_output, 'w') as fobj:
  287. writer = csv.writer(fobj, dialect='test')
  288. for line in remaining:
  289. writer.writerow(line.to_csv())
  290. def get_tests():
  291. input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
  292. 'functional')
  293. suite = []
  294. for fname in os.listdir(input_dir):
  295. if fname != '__init__.py' and fname.endswith('.py'):
  296. suite.append(FunctionalTestFile(input_dir, fname))
  297. return suite
  298. TESTS = get_tests()
  299. TESTS_NAMES = [t.base for t in TESTS]
  300. @pytest.mark.parametrize("test_file", TESTS, ids=TESTS_NAMES)
  301. def test_functional(test_file):
  302. LintTest = LintModuleOutputUpdate(test_file) if UPDATE else LintModuleTest(test_file)
  303. LintTest.setUp()
  304. LintTest._runTest()
  305. if __name__ == '__main__':
  306. if '-u' in sys.argv:
  307. UPDATE = True
  308. sys.argv.remove('-u')
  309. pytest.main(sys.argv)