summaryrefslogtreecommitdiff
path: root/pylint/testutils/configuration_test.py
blob: fc0842be63007e9b682be3c375e37d8c21d3da23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 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
from typing import Any, Dict, Tuple, Union
from unittest.mock import Mock

from pylint.lint import Run

USER_SPECIFIC_PATH = str(Path(__file__).parent.parent.parent)
# 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(
    tested_configuration_file: str, suffix: str, default: ConfigurationValue
) -> str:
    """Return the expected value from the file if it exists, or the given default."""

    def get_path_according_to_suffix() -> Path:
        path = Path(tested_configuration_file)
        return path.parent / f"{path.stem}.{suffix}"

    expected = default
    expected_result_path = get_path_according_to_suffix()
    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


def get_expected_output(configuration_path: str) -> Tuple[int, str]:
    """Get the expected output of a functional test."""

    def get_relative_path(path: str) -> str:
        """Get the relative path we want without the user specific path"""
        # [1:] is to remove the opening '/'
        return path.split(USER_SPECIFIC_PATH)[1][1:]

    output = get_expected_or_default(configuration_path, suffix="out", default="")
    if output:
        # 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(
            "Output exists for %s so the expected exit code is 2", configuration_path
        )
        exit_code = 2
    else:
        logging.info(".out file does not exists, so the expected exit code is 0")
        exit_code = 0
    relpath = get_relative_path(configuration_path)
    return exit_code, output.format(abspath=configuration_path, relpath=relpath)


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