configuration_test.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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. """Utility functions for configuration testing."""
  5. from __future__ import annotations
  6. import copy
  7. import json
  8. import logging
  9. import unittest.mock
  10. from pathlib import Path
  11. from typing import Any
  12. from pylint.lint import Run
  13. # We use Any in this typing because the configuration contains real objects and constants
  14. # that could be a lot of things.
  15. ConfigurationValue = Any
  16. PylintConfiguration = dict[str, ConfigurationValue]
  17. def get_expected_or_default(
  18. tested_configuration_file: str | Path,
  19. suffix: str,
  20. default: str,
  21. ) -> str:
  22. """Return the expected value from the file if it exists, or the given default."""
  23. expected = default
  24. path = Path(tested_configuration_file)
  25. expected_result_path = path.parent / f"{path.stem}.{suffix}"
  26. if expected_result_path.exists():
  27. with open(expected_result_path, encoding="utf8") as f:
  28. expected = f.read()
  29. # logging is helpful to realize your file is not taken into
  30. # account after a misspelling of the file name. The output of the
  31. # program is checked during the test so printing messes with the result.
  32. logging.info("%s exists.", expected_result_path)
  33. else:
  34. logging.info("%s not found, using '%s'.", expected_result_path, default)
  35. return expected
  36. EXPECTED_CONF_APPEND_KEY = "functional_append"
  37. EXPECTED_CONF_REMOVE_KEY = "functional_remove"
  38. def get_expected_configuration(
  39. configuration_path: str, default_configuration: PylintConfiguration
  40. ) -> PylintConfiguration:
  41. """Get the expected parsed configuration of a configuration functional test."""
  42. result = copy.deepcopy(default_configuration)
  43. config_as_json = get_expected_or_default(
  44. configuration_path, suffix="result.json", default="{}"
  45. )
  46. to_override = json.loads(config_as_json)
  47. for key, value in to_override.items():
  48. if key == EXPECTED_CONF_APPEND_KEY:
  49. for fkey, fvalue in value.items():
  50. result[fkey] += fvalue
  51. elif key == EXPECTED_CONF_REMOVE_KEY:
  52. for fkey, fvalue in value.items():
  53. new_value = []
  54. for old_value in result[fkey]:
  55. if old_value not in fvalue:
  56. new_value.append(old_value)
  57. result[fkey] = new_value
  58. else:
  59. result[key] = value
  60. return result
  61. def get_related_files(
  62. tested_configuration_file: str | Path, suffix_filter: str
  63. ) -> list[Path]:
  64. """Return all the file related to a test conf file ending with a suffix."""
  65. conf_path = Path(tested_configuration_file)
  66. return [
  67. p
  68. for p in conf_path.parent.iterdir()
  69. if str(p.stem).startswith(conf_path.stem) and str(p).endswith(suffix_filter)
  70. ]
  71. def get_expected_output(
  72. configuration_path: str | Path, user_specific_path: Path
  73. ) -> tuple[int, str]:
  74. """Get the expected output of a functional test."""
  75. exit_code = 0
  76. msg = (
  77. "we expect a single file of the form 'filename.32.out' where 'filename' represents "
  78. "the name of the configuration file, and '32' the expected error code."
  79. )
  80. possible_out_files = get_related_files(configuration_path, suffix_filter="out")
  81. if len(possible_out_files) > 1:
  82. logging.error(
  83. "Too much .out files for %s %s.",
  84. configuration_path,
  85. msg,
  86. )
  87. return -1, "out file is broken"
  88. if not possible_out_files:
  89. # logging is helpful to see what the expected exit code is and why.
  90. # The output of the program is checked during the test so printing
  91. # messes with the result.
  92. logging.info(".out file does not exists, so the expected exit code is 0")
  93. return 0, ""
  94. path = possible_out_files[0]
  95. try:
  96. exit_code = int(str(path.stem).rsplit(".", maxsplit=1)[-1])
  97. except Exception as e: # pylint: disable=broad-except
  98. logging.error(
  99. "Wrong format for .out file name for %s %s: %s",
  100. configuration_path,
  101. msg,
  102. e,
  103. )
  104. return -1, "out file is broken"
  105. output = get_expected_or_default(
  106. configuration_path, suffix=f"{exit_code}.out", default=""
  107. )
  108. logging.info(
  109. "Output exists for %s so the expected exit code is %s",
  110. configuration_path,
  111. exit_code,
  112. )
  113. return exit_code, output.format(
  114. abspath=configuration_path,
  115. relpath=Path(configuration_path).relative_to(user_specific_path),
  116. )
  117. def run_using_a_configuration_file(
  118. configuration_path: Path | str, file_to_lint: str = __file__
  119. ) -> Run:
  120. """Simulate a run with a configuration without really launching the checks."""
  121. configuration_path = str(configuration_path)
  122. args = ["--rcfile", configuration_path, file_to_lint]
  123. # Do not actually run checks, that could be slow. We don't mock
  124. # `PyLinter.check`: it calls `PyLinter.initialize` which is
  125. # needed to properly set up messages inclusion/exclusion
  126. # in `_msg_states`, used by `is_message_enabled`.
  127. check = "pylint.lint.pylinter.check_parallel"
  128. with unittest.mock.patch(check):
  129. runner = Run(args, exit=False)
  130. return runner