test_file.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import configparser
  6. import sys
  7. from collections.abc import Callable
  8. from os.path import basename, exists, join, split
  9. from pathlib import Path
  10. from typing import Final, TypedDict
  11. _CURRENT_VERSION: Final = sys.version_info[:2]
  12. def parse_python_version(ver_str: str) -> tuple[int, ...]:
  13. """Convert python version to a tuple of integers for easy comparison."""
  14. return tuple(int(digit) for digit in ver_str.split("."))
  15. class NoFileError(Exception):
  16. pass
  17. class TestFileOptions(TypedDict):
  18. min_pyver: tuple[int, ...]
  19. max_pyver: tuple[int, ...]
  20. min_pyver_end_position: tuple[int, ...]
  21. requires: list[str]
  22. except_implementations: list[str]
  23. exclude_platforms: list[str]
  24. exclude_from_minimal_messages_config: bool
  25. # mypy need something literal, we can't create this dynamically from TestFileOptions
  26. POSSIBLE_TEST_OPTIONS = {
  27. "min_pyver",
  28. "max_pyver",
  29. "min_pyver_end_position",
  30. "requires",
  31. "except_implementations",
  32. "exclude_platforms",
  33. "exclude_from_minimal_messages_config",
  34. }
  35. class FunctionalTestFile:
  36. """A single functional test case file with options."""
  37. _CONVERTERS: dict[str, Callable[[str], tuple[int, ...] | list[str]]] = {
  38. "min_pyver": parse_python_version,
  39. "max_pyver": parse_python_version,
  40. "min_pyver_end_position": parse_python_version,
  41. "requires": lambda s: [i.strip() for i in s.split(",")],
  42. "except_implementations": lambda s: [i.strip() for i in s.split(",")],
  43. "exclude_platforms": lambda s: [i.strip() for i in s.split(",")],
  44. }
  45. def __init__(self, directory: str, filename: str) -> None:
  46. self._directory = directory
  47. self.base = filename.replace(".py", "")
  48. # TODO:4.0: Deprecate FunctionalTestFile.options and related code
  49. # We should just parse these options like a normal configuration file.
  50. self.options: TestFileOptions = {
  51. "min_pyver": (2, 5),
  52. "max_pyver": (4, 0),
  53. "min_pyver_end_position": (3, 8),
  54. "requires": [],
  55. "except_implementations": [],
  56. "exclude_platforms": [],
  57. "exclude_from_minimal_messages_config": False,
  58. }
  59. self._parse_options()
  60. def __repr__(self) -> str:
  61. return f"FunctionalTest:{self.base}"
  62. def _parse_options(self) -> None:
  63. cp = configparser.ConfigParser()
  64. cp.add_section("testoptions")
  65. try:
  66. cp.read(self.option_file)
  67. except NoFileError:
  68. pass
  69. for name, value in cp.items("testoptions"):
  70. conv = self._CONVERTERS.get(name, lambda v: v)
  71. assert (
  72. name in POSSIBLE_TEST_OPTIONS
  73. ), f"[testoptions]' can only contains one of {POSSIBLE_TEST_OPTIONS} and had '{name}'"
  74. self.options[name] = conv(value) # type: ignore[literal-required]
  75. @property
  76. def option_file(self) -> str:
  77. return self._file_type(".rc")
  78. @property
  79. def module(self) -> str:
  80. package = basename(self._directory)
  81. return ".".join([package, self.base])
  82. @property
  83. def expected_output(self) -> str:
  84. files = [
  85. p.stem
  86. for p in Path(self._directory).glob(f"{split(self.base)[-1]}.[0-9]*.txt")
  87. ]
  88. output_options = [
  89. (int(version[0]), int(version[1:]))
  90. for s in files
  91. if (version := s.rpartition(".")[2]).isalnum()
  92. ]
  93. for opt in sorted(output_options, reverse=True):
  94. if _CURRENT_VERSION >= opt:
  95. str_opt = "".join([str(s) for s in opt])
  96. return join(self._directory, f"{self.base}.{str_opt}.txt")
  97. return join(self._directory, self.base + ".txt")
  98. @property
  99. def source(self) -> str:
  100. return self._file_type(".py")
  101. def _file_type(self, ext: str, check_exists: bool = True) -> str:
  102. name = join(self._directory, self.base + ext)
  103. if not check_exists or exists(name):
  104. return name
  105. raise NoFileError(f"Cannot find '{name}'.")