test_file.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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
  9. def parse_python_version(ver_str: str) -> tuple[int, ...]:
  10. """Convert python version to a tuple of integers for easy comparison."""
  11. return tuple(int(digit) for digit in ver_str.split("."))
  12. class NoFileError(Exception):
  13. pass
  14. if sys.version_info >= (3, 8):
  15. from typing import TypedDict
  16. else:
  17. from typing_extensions import TypedDict
  18. class TestFileOptions(TypedDict):
  19. min_pyver: tuple[int, ...]
  20. max_pyver: tuple[int, ...]
  21. min_pyver_end_position: tuple[int, ...]
  22. requires: list[str]
  23. except_implementations: list[str]
  24. exclude_platforms: list[str]
  25. exclude_from_minimal_messages_config: bool
  26. # mypy need something literal, we can't create this dynamically from TestFileOptions
  27. POSSIBLE_TEST_OPTIONS = {
  28. "min_pyver",
  29. "max_pyver",
  30. "min_pyver_end_position",
  31. "requires",
  32. "except_implementations",
  33. "exclude_platforms",
  34. "exclude_from_minimal_messages_config",
  35. }
  36. class FunctionalTestFile:
  37. """A single functional test case file with options."""
  38. _CONVERTERS: dict[str, Callable[[str], tuple[int, ...] | list[str]]] = {
  39. "min_pyver": parse_python_version,
  40. "max_pyver": parse_python_version,
  41. "min_pyver_end_position": parse_python_version,
  42. "requires": lambda s: [i.strip() for i in s.split(",")],
  43. "except_implementations": lambda s: [i.strip() for i in s.split(",")],
  44. "exclude_platforms": lambda s: [i.strip() for i in s.split(",")],
  45. }
  46. def __init__(self, directory: str, filename: str) -> None:
  47. self._directory = directory
  48. self.base = filename.replace(".py", "")
  49. # TODO: 2.x: Deprecate FunctionalTestFile.options and related code
  50. # We should just parse these options like a normal configuration file.
  51. self.options: TestFileOptions = {
  52. "min_pyver": (2, 5),
  53. "max_pyver": (4, 0),
  54. "min_pyver_end_position": (3, 8),
  55. "requires": [],
  56. "except_implementations": [],
  57. "exclude_platforms": [],
  58. "exclude_from_minimal_messages_config": False,
  59. }
  60. self._parse_options()
  61. def __repr__(self) -> str:
  62. return f"FunctionalTest:{self.base}"
  63. def _parse_options(self) -> None:
  64. cp = configparser.ConfigParser()
  65. cp.add_section("testoptions")
  66. try:
  67. cp.read(self.option_file)
  68. except NoFileError:
  69. pass
  70. for name, value in cp.items("testoptions"):
  71. conv = self._CONVERTERS.get(name, lambda v: v)
  72. assert (
  73. name in POSSIBLE_TEST_OPTIONS
  74. ), f"[testoptions]' can only contains one of {POSSIBLE_TEST_OPTIONS} and had '{name}'"
  75. self.options[name] = conv(value) # type: ignore[literal-required]
  76. @property
  77. def option_file(self) -> str:
  78. return self._file_type(".rc")
  79. @property
  80. def module(self) -> str:
  81. package = basename(self._directory)
  82. return ".".join([package, self.base])
  83. @property
  84. def expected_output(self) -> str:
  85. return self._file_type(".txt", check_exists=False)
  86. @property
  87. def source(self) -> str:
  88. return self._file_type(".py")
  89. def _file_type(self, ext: str, check_exists: bool = True) -> str:
  90. name = join(self._directory, self.base + ext)
  91. if not check_exists or exists(name):
  92. return name
  93. raise NoFileError(f"Cannot find '{name}'.")