2021-11-12 21:19:56 +00:00
|
|
|
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
|
|
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
|
|
|
|
|
|
|
|
"""Utility functions for configuration testing."""
|
|
|
|
import copy
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import unittest
|
|
|
|
from pathlib import Path
|
2021-11-22 12:27:14 +00:00
|
|
|
from typing import Any, Dict, List, Tuple, Union
|
2021-11-12 21:19:56 +00:00
|
|
|
from unittest.mock import Mock
|
|
|
|
|
|
|
|
from pylint.lint import Run
|
|
|
|
|
|
|
|
# We use Any in this typing because the configuration contains real objects and constants
|
|
|
|
# that could be a lot of things.
|
|
|
|
ConfigurationValue = Any
|
|
|
|
PylintConfiguration = Dict[str, ConfigurationValue]
|
|
|
|
|
|
|
|
|
|
|
|
def get_expected_or_default(
|
2021-11-22 12:27:14 +00:00
|
|
|
tested_configuration_file: Union[str, Path],
|
|
|
|
suffix: str,
|
|
|
|
default: ConfigurationValue,
|
2021-11-12 21:19:56 +00:00
|
|
|
) -> str:
|
|
|
|
"""Return the expected value from the file if it exists, or the given default."""
|
|
|
|
expected = default
|
2021-11-22 12:27:14 +00:00
|
|
|
path = Path(tested_configuration_file)
|
|
|
|
expected_result_path = path.parent / f"{path.stem}.{suffix}"
|
2021-11-12 21:19:56 +00:00
|
|
|
if expected_result_path.exists():
|
|
|
|
with open(expected_result_path, encoding="utf8") as f:
|
|
|
|
expected = f.read()
|
|
|
|
# logging is helpful to realize your file is not taken into
|
|
|
|
# account after a misspell of the file name. The output of the
|
|
|
|
# program is checked during the test so printing messes with the result.
|
|
|
|
logging.info("%s exists.", expected_result_path)
|
|
|
|
else:
|
|
|
|
logging.info("%s not found, using '%s'.", expected_result_path, default)
|
|
|
|
return expected
|
|
|
|
|
|
|
|
|
|
|
|
EXPECTED_CONF_APPEND_KEY = "functional_append"
|
|
|
|
EXPECTED_CONF_REMOVE_KEY = "functional_remove"
|
|
|
|
|
|
|
|
|
|
|
|
def get_expected_configuration(
|
|
|
|
configuration_path: str, default_configuration: PylintConfiguration
|
|
|
|
) -> PylintConfiguration:
|
|
|
|
"""Get the expected parsed configuration of a configuration functional test"""
|
|
|
|
result = copy.deepcopy(default_configuration)
|
|
|
|
config_as_json = get_expected_or_default(
|
|
|
|
configuration_path, suffix="result.json", default="{}"
|
|
|
|
)
|
|
|
|
to_override = json.loads(config_as_json)
|
|
|
|
for key, value in to_override.items():
|
|
|
|
if key == EXPECTED_CONF_APPEND_KEY:
|
|
|
|
for fkey, fvalue in value.items():
|
|
|
|
result[fkey] += fvalue
|
|
|
|
elif key == EXPECTED_CONF_REMOVE_KEY:
|
|
|
|
for fkey, fvalue in value.items():
|
|
|
|
new_value = []
|
|
|
|
for old_value in result[fkey]:
|
|
|
|
if old_value not in fvalue:
|
|
|
|
new_value.append(old_value)
|
|
|
|
result[fkey] = new_value
|
|
|
|
else:
|
|
|
|
result[key] = value
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2021-11-22 12:27:14 +00:00
|
|
|
def get_related_files(
|
|
|
|
tested_configuration_file: Union[str, Path], suffix_filter: str
|
|
|
|
) -> List[Path]:
|
|
|
|
"""Return all the file related to a test conf file endind with a suffix."""
|
|
|
|
conf_path = Path(tested_configuration_file)
|
|
|
|
return [
|
|
|
|
p
|
|
|
|
for p in conf_path.parent.iterdir()
|
|
|
|
if str(p.stem).startswith(conf_path.stem) and str(p).endswith(suffix_filter)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-11-14 08:16:56 +00:00
|
|
|
def get_expected_output(
|
2021-11-22 12:27:14 +00:00
|
|
|
configuration_path: Union[str, Path], user_specific_path: Path
|
2021-11-14 08:16:56 +00:00
|
|
|
) -> Tuple[int, str]:
|
2021-11-12 21:19:56 +00:00
|
|
|
"""Get the expected output of a functional test."""
|
2021-11-22 12:27:14 +00:00
|
|
|
exit_code = 0
|
|
|
|
msg = (
|
|
|
|
"we expect a single file of the form 'filename.32.out' where 'filename' represents "
|
|
|
|
"the name of the configuration file, and '32' the expected error code."
|
|
|
|
)
|
|
|
|
possible_out_files = get_related_files(configuration_path, suffix_filter="out")
|
|
|
|
if len(possible_out_files) > 1:
|
|
|
|
logging.error(
|
|
|
|
"Too much .out files for %s %s.",
|
|
|
|
configuration_path,
|
|
|
|
msg,
|
|
|
|
)
|
|
|
|
return -1, "out file is broken"
|
|
|
|
if not possible_out_files:
|
2021-11-12 21:19:56 +00:00
|
|
|
# logging is helpful to see what the expected exit code is and why.
|
|
|
|
# The output of the program is checked during the test so printing
|
|
|
|
# messes with the result.
|
|
|
|
logging.info(".out file does not exists, so the expected exit code is 0")
|
2021-11-22 12:27:14 +00:00
|
|
|
return 0, ""
|
|
|
|
path = possible_out_files[0]
|
|
|
|
try:
|
|
|
|
exit_code = int(str(path.stem).rsplit(".", maxsplit=1)[-1])
|
|
|
|
except Exception as e: # pylint: disable=broad-except
|
|
|
|
logging.error(
|
|
|
|
"Wrong format for .out file name for %s %s: %s",
|
|
|
|
configuration_path,
|
|
|
|
msg,
|
|
|
|
e,
|
|
|
|
)
|
|
|
|
return -1, "out file is broken"
|
|
|
|
|
|
|
|
output = get_expected_or_default(
|
|
|
|
configuration_path, suffix=f"{exit_code}.out", default=""
|
|
|
|
)
|
|
|
|
logging.info(
|
|
|
|
"Output exists for %s so the expected exit code is %s",
|
|
|
|
configuration_path,
|
|
|
|
exit_code,
|
|
|
|
)
|
2021-11-12 21:19:56 +00:00
|
|
|
return exit_code, output.format(
|
|
|
|
abspath=configuration_path,
|
2021-11-14 08:16:56 +00:00
|
|
|
relpath=Path(configuration_path).relative_to(user_specific_path),
|
2021-11-12 21:19:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def run_using_a_configuration_file(
|
|
|
|
configuration_path: Union[Path, str], file_to_lint: str = __file__
|
|
|
|
) -> Tuple[Mock, Mock, Run]:
|
|
|
|
"""Simulate a run with a configuration without really launching the checks."""
|
|
|
|
configuration_path = str(configuration_path)
|
|
|
|
args = ["--rcfile", configuration_path, file_to_lint]
|
|
|
|
# We do not capture the `SystemExit` as then the `runner` variable
|
|
|
|
# would not be accessible outside the `with` block.
|
|
|
|
with unittest.mock.patch("sys.exit") as mocked_exit:
|
|
|
|
# Do not actually run checks, that could be slow. We don't mock
|
|
|
|
# `Pylinter.check`: it calls `Pylinter.initialize` which is
|
|
|
|
# needed to properly set up messages inclusion/exclusion
|
|
|
|
# in `_msg_states`, used by `is_message_enabled`.
|
|
|
|
check = "pylint.lint.pylinter.check_parallel"
|
|
|
|
with unittest.mock.patch(check) as mocked_check_parallel:
|
|
|
|
runner = Run(args)
|
|
|
|
return mocked_exit, mocked_check_parallel, runner
|