| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt |
| |
| """Functional/non regression tests for pylint.""" |
| |
| from __future__ import annotations |
| |
| import re |
| import sys |
| from os.path import abspath, dirname, join |
| |
| import pytest |
| |
| from pylint.testutils import UPDATE_FILE, UPDATE_OPTION, _get_tests_info, linter |
| from pylint.testutils.reporter_for_tests import GenericTestReporter |
| from pylint.testutils.utils import _test_cwd |
| |
| TESTS_DIR = dirname(abspath(__file__)) |
| INPUT_DIR = join(TESTS_DIR, "input") |
| MSG_DIR = join(TESTS_DIR, "messages") |
| |
| |
| FILTER_RGX = None |
| INFO_TEST_RGX = re.compile(r"^func_i\d\d\d\d$") |
| |
| |
| def exception_str( |
| self: Exception, ex: Exception # pylint: disable=unused-argument |
| ) -> str: |
| """Function used to replace default __str__ method of exception instances |
| This function is not typed because it is legacy code. |
| """ |
| return f"in {ex.file}\n:: {', '.join(ex.args)}" # type: ignore[attr-defined] # Defined in the caller |
| |
| |
| class LintTestUsingModule: |
| INPUT_DIR: str | None = None |
| DEFAULT_PACKAGE = "input" |
| package = DEFAULT_PACKAGE |
| linter = linter |
| module: str | None = None |
| depends: list[tuple[str, str]] | None = None |
| output: str | None = None |
| |
| def _test_functionality(self) -> None: |
| tocheck = [self.package + "." + self.module] if self.module else [] |
| if self.depends: |
| tocheck += [ |
| self.package + f".{name.replace('.py', '')}" for name, _ in self.depends |
| ] |
| # given that TESTS_DIR could be treated as a namespace package |
| # when under the current directory, cd to it so that "tests." is not |
| # prepended to module names in the output of cyclic-import |
| with _test_cwd(TESTS_DIR): |
| self._test(tocheck) |
| |
| def _check_result(self, got: str) -> None: |
| error_msg = ( |
| f"Wrong output for '{self.output}':\n" |
| "You can update the expected output automatically with: '" |
| f"python tests/test_func.py {UPDATE_OPTION}'\n\n" |
| ) |
| assert self._get_expected() == got, error_msg |
| |
| def _test(self, tocheck: list[str]) -> None: |
| if self.module and INFO_TEST_RGX.match(self.module): |
| self.linter.enable("I") |
| else: |
| self.linter.disable("I") |
| try: |
| self.linter.check(tocheck) |
| except Exception as ex: |
| print(f"Exception: {ex} in {tocheck}:: {', '.join(ex.args)}") |
| # This is legacy code we're trying to remove, not worth it to type correctly |
| ex.file = tocheck # type: ignore[attr-defined] |
| print(ex) |
| # This is legacy code we're trying to remove, not worth it to type correctly |
| ex.__str__ = exception_str # type: ignore[assignment] |
| raise |
| assert isinstance(self.linter.reporter, GenericTestReporter) |
| self._check_result(self.linter.reporter.finalize()) |
| |
| def _has_output(self) -> bool: |
| return isinstance(self.module, str) and not self.module.startswith( |
| "func_noerror_" |
| ) |
| |
| def _get_expected(self) -> str: |
| if self._has_output() and self.output: |
| with open(self.output, encoding="utf-8") as fobj: |
| return fobj.read().strip() + "\n" |
| else: |
| return "" |
| |
| |
| class LintTestUpdate(LintTestUsingModule): |
| def _check_result(self, got: str) -> None: |
| if not self._has_output(): |
| return |
| try: |
| expected = self._get_expected() |
| except OSError: |
| expected = "" |
| if got != expected: |
| with open(self.output or "", "w", encoding="utf-8") as f: |
| f.write(got) |
| |
| |
| def gen_tests( |
| filter_rgx: str | re.Pattern[str] | None, |
| ) -> list[tuple[str, str, list[tuple[str, str]]]]: |
| if filter_rgx: |
| is_to_run = re.compile(filter_rgx).search |
| else: |
| is_to_run = ( # noqa: E731, We're going to throw all this anyway |
| lambda x: 1 # type: ignore[assignment] # pylint: disable=unnecessary-lambda-assignment |
| ) |
| tests: list[tuple[str, str, list[tuple[str, str]]]] = [] |
| 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", "$py.class")): |
| continue |
| base = module_file.replace(".py", "").split("_")[1] |
| dependencies = _get_tests_info(INPUT_DIR, MSG_DIR, base, ".py") |
| tests.append((module_file, messages_file, dependencies)) |
| if UPDATE_FILE.exists(): |
| return tests |
| assert len(tests) < 13, "Please do not add new test cases here." + "\n".join( |
| str(k) for k in tests if not k[2] |
| ) |
| return tests |
| |
| |
| TEST_WITH_EXPECTED_DEPRECATION = ["func_excess_escapes.py"] |
| |
| |
| @pytest.mark.parametrize( |
| "module_file,messages_file,dependencies", |
| gen_tests(FILTER_RGX), |
| ids=[o[0] for o in gen_tests(FILTER_RGX)], |
| ) |
| def test_functionality( |
| module_file: str, |
| messages_file: str, |
| dependencies: list[tuple[str, str]], |
| recwarn: pytest.WarningsRecorder, |
| ) -> None: |
| __test_functionality(module_file, messages_file, dependencies) |
| if recwarn.list: |
| if module_file in TEST_WITH_EXPECTED_DEPRECATION and sys.version_info.minor > 5: |
| assert any( |
| "invalid escape sequence" in str(i.message) |
| for i in recwarn.list |
| if issubclass(i.category, DeprecationWarning) |
| ) |
| |
| |
| def __test_functionality( |
| module_file: str, messages_file: str, dependencies: list[tuple[str, str]] |
| ) -> None: |
| lint_test = LintTestUpdate() if UPDATE_FILE.exists() else LintTestUsingModule() |
| lint_test.module = module_file.replace(".py", "") |
| lint_test.output = messages_file |
| lint_test.depends = dependencies or None |
| lint_test.INPUT_DIR = INPUT_DIR |
| lint_test._test_functionality() |
| |
| |
| if __name__ == "__main__": |
| if UPDATE_OPTION in sys.argv: |
| UPDATE_FILE.touch() |
| sys.argv.remove(UPDATE_OPTION) |
| if len(sys.argv) > 1: |
| FILTER_RGX = sys.argv[1] |
| del sys.argv[1] |
| try: |
| pytest.main(sys.argv) |
| finally: |
| if UPDATE_FILE.exists(): |
| UPDATE_FILE.unlink() |