blob: 99805d160ebb3239824c5400714956f0ef51bf2c [file] [log] [blame]
# 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()