find_functional_tests.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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 os
  6. from collections.abc import Iterator
  7. from pathlib import Path
  8. from pylint.testutils.functional.test_file import FunctionalTestFile
  9. REASONABLY_DISPLAYABLE_VERTICALLY = 40
  10. """'Wet finger' number of python files that are reasonable to have in a functional test
  11. directory.
  12. 'Wet finger' as in 'in my settings there are precisely this many'.
  13. Initially the total number of files then we started counting only the python files to
  14. avoid moving a lot of files.
  15. """
  16. IGNORED_PARENT_DIRS = {
  17. "deprecated_relative_import",
  18. "ext",
  19. "regression",
  20. "regression_02",
  21. "used_02",
  22. }
  23. """Direct parent directories that should be ignored."""
  24. IGNORED_PARENT_PARENT_DIRS = {
  25. "docparams",
  26. "deprecated_relative_import",
  27. "ext",
  28. }
  29. """Parents of direct parent directories that should be ignored."""
  30. def get_functional_test_files_from_directory(
  31. input_dir: Path | str,
  32. max_file_per_directory: int = REASONABLY_DISPLAYABLE_VERTICALLY,
  33. ) -> list[FunctionalTestFile]:
  34. """Get all functional tests in the input_dir."""
  35. suite = []
  36. _check_functional_tests_structure(Path(input_dir), max_file_per_directory)
  37. for dirpath, dirnames, filenames in os.walk(input_dir):
  38. if dirpath.endswith("__pycache__"):
  39. continue
  40. dirnames.sort()
  41. filenames.sort()
  42. for filename in filenames:
  43. if filename != "__init__.py" and filename.endswith(".py"):
  44. suite.append(FunctionalTestFile(dirpath, filename))
  45. return suite
  46. def _check_functional_tests_structure(
  47. directory: Path, max_file_per_directory: int
  48. ) -> None:
  49. """Check if test directories follow correct file/folder structure.
  50. Ignore underscored directories or files.
  51. """
  52. if Path(directory).stem.startswith("_"):
  53. return
  54. files: set[Path] = set()
  55. dirs: set[Path] = set()
  56. def _get_files_from_dir(
  57. path: Path, violations: list[tuple[Path, int]]
  58. ) -> list[Path]:
  59. """Return directories and files from a directory and handles violations."""
  60. files_without_leading_underscore = list(
  61. p
  62. for p in path.iterdir()
  63. if not (p.stem.startswith("_") or (p.is_file() and p.suffix != ".py"))
  64. )
  65. if len(files_without_leading_underscore) > max_file_per_directory:
  66. violations.append((path, len(files_without_leading_underscore)))
  67. return files_without_leading_underscore
  68. def walk(path: Path) -> Iterator[Path]:
  69. violations: list[tuple[Path, int]] = []
  70. violations_msgs: set[str] = set()
  71. parent_dir_files = _get_files_from_dir(path, violations)
  72. error_msg = (
  73. "The following directory contains too many functional tests files:\n"
  74. )
  75. for _file_or_dir in parent_dir_files:
  76. if _file_or_dir.is_dir():
  77. yield _file_or_dir.resolve()
  78. try:
  79. yield from walk(_file_or_dir)
  80. except AssertionError as e:
  81. violations_msgs.add(str(e).replace(error_msg, ""))
  82. else:
  83. yield _file_or_dir.resolve()
  84. if violations or violations_msgs:
  85. _msg = error_msg
  86. for offending_file, number in violations:
  87. _msg += f"- {offending_file}: {number} when the max is {max_file_per_directory}\n"
  88. for error_msg in violations_msgs:
  89. _msg += error_msg
  90. raise AssertionError(_msg)
  91. # Collect all sub-directories and files in directory
  92. for file_or_dir in walk(directory):
  93. if file_or_dir.is_dir():
  94. dirs.add(file_or_dir)
  95. elif file_or_dir.suffix == ".py":
  96. files.add(file_or_dir)
  97. directory_does_not_exists: list[tuple[Path, Path]] = []
  98. misplaced_file: list[Path] = []
  99. for file in files:
  100. possible_dir = file.parent / file.stem.split("_")[0]
  101. if possible_dir.exists():
  102. directory_does_not_exists.append((file, possible_dir))
  103. # Exclude some directories as they follow a different structure
  104. if (
  105. not len(file.parent.stem) == 1 # First letter sub-directories
  106. and file.parent.stem not in IGNORED_PARENT_DIRS
  107. and file.parent.parent.stem not in IGNORED_PARENT_PARENT_DIRS
  108. ):
  109. if not file.stem.startswith(file.parent.stem):
  110. misplaced_file.append(file)
  111. if directory_does_not_exists or misplaced_file:
  112. msg = "The following functional tests are disorganized:\n"
  113. for file, possible_dir in directory_does_not_exists:
  114. msg += (
  115. f"- In '{directory}', '{file.relative_to(directory)}' "
  116. f"should go in '{possible_dir.relative_to(directory)}'\n"
  117. )
  118. for file in misplaced_file:
  119. msg += (
  120. f"- In '{directory}', {file.relative_to(directory)} should go in a directory"
  121. f" that starts with the first letters"
  122. f" of '{file.stem}' (not '{file.parent.stem}')\n"
  123. )
  124. raise AssertionError(msg)