1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
# 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()
|