summaryrefslogtreecommitdiff
path: root/tests/pyreverse/test_main.py
blob: 86401e05be94aba181f7847c4be42da73ae2f6a7 (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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# 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
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt

"""Unittest for the main module."""

from __future__ import annotations

import os
import sys
from collections.abc import Iterator
from typing import Any
from unittest import mock

import pytest
from _pytest.capture import CaptureFixture
from _pytest.fixtures import SubRequest

from pylint.lint import augmented_sys_path, discover_package_path
from pylint.pyreverse import main

TEST_DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "data"))
PROJECT_ROOT_DIR = os.path.abspath(os.path.join(TEST_DATA_DIR, ".."))


@pytest.fixture(name="mock_subprocess")
def mock_utils_subprocess() -> Iterator[mock.MagicMock]:
    with mock.patch("pylint.pyreverse.utils.subprocess") as mock_subprocess:
        yield mock_subprocess


@pytest.fixture
def mock_graphviz(mock_subprocess: mock.MagicMock) -> Iterator[None]:
    mock_subprocess.run.return_value = mock.Mock(
        stderr=(
            'Format: "XYZ" not recognized. Use one of: '
            "bmp canon cgimage cmap cmapx cmapx_np dot dot_json eps exr fig gd "
            "gd2 gif gv icns ico imap imap_np ismap jp2 jpe jpeg jpg json json0 "
            "mp pct pdf pic pict plain plain-ext png pov ps ps2 psd sgi svg svgz "
            "tga tif tiff tk vdx vml vmlz vrml wbmp webp xdot xdot1.2 xdot1.4 xdot_json"
        )
    )
    with mock.patch("pylint.pyreverse.utils.shutil") as mock_shutil:
        mock_shutil.which.return_value = "/usr/bin/dot"
        yield


@pytest.fixture(params=[PROJECT_ROOT_DIR, TEST_DATA_DIR])
def setup_path(request: SubRequest) -> Iterator[None]:
    current_sys_path = list(sys.path)
    sys.path[:] = []
    current_dir = os.getcwd()
    os.chdir(request.param)
    yield
    os.chdir(current_dir)
    sys.path[:] = current_sys_path


@pytest.mark.usefixtures("setup_path")
def test_project_root_in_sys_path() -> None:
    """Test the context manager adds the project root directory to sys.path.
    This should happen when pyreverse is run from any directory
    """
    with augmented_sys_path([discover_package_path(TEST_DATA_DIR, [])]):
        assert sys.path == [PROJECT_ROOT_DIR]


@mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.writer")
@pytest.mark.usefixtures("mock_graphviz")
def test_graphviz_supported_image_format(
    mock_writer: mock.MagicMock, capsys: CaptureFixture[str]
) -> None:
    """Test that Graphviz is used if the image format is supported."""
    with pytest.raises(SystemExit) as wrapped_sysexit:
        # we have to catch the SystemExit so the test execution does not stop
        main.Run(["-o", "png", TEST_DATA_DIR])
    # Check that the right info message is shown to the user
    assert (
        "Format png is not supported natively. Pyreverse will try to generate it using Graphviz..."
        in capsys.readouterr().out
    )
    # Check that pyreverse actually made the call to create the diagram and we exit cleanly
    mock_writer.DiagramWriter().write.assert_called_once()
    assert wrapped_sysexit.value.code == 0


@mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.writer")
@pytest.mark.usefixtures("mock_graphviz")
def test_graphviz_cant_determine_supported_formats(
    mock_writer: mock.MagicMock, mock_subprocess: mock.MagicMock, capsys: CaptureFixture
) -> None:
    """Test that Graphviz is used if the image format is supported."""
    mock_subprocess.run.return_value.stderr = "..."
    with pytest.raises(SystemExit) as wrapped_sysexit:
        # we have to catch the SystemExit so the test execution does not stop
        main.Run(["-o", "png", TEST_DATA_DIR])
    # Check that the right info message is shown to the user
    assert (
        "Unable to determine Graphviz supported output formats."
        in capsys.readouterr().out
    )
    # Check that pyreverse actually made the call to create the diagram and we exit cleanly
    mock_writer.DiagramWriter().write.assert_called_once()
    assert wrapped_sysexit.value.code == 0


@mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.writer", new=mock.MagicMock())
@pytest.mark.usefixtures("mock_graphviz")
def test_graphviz_unsupported_image_format(capsys: CaptureFixture) -> None:
    """Test that Graphviz is used if the image format is supported."""
    with pytest.raises(SystemExit) as wrapped_sysexit:
        # we have to catch the SystemExit so the test execution does not stop
        main.Run(["-o", "somethingElse", TEST_DATA_DIR])
    # Check that the right info messages are shown to the user
    stdout = capsys.readouterr().out
    assert (
        "Format somethingElse is not supported natively. Pyreverse will try to generate it using Graphviz..."
        in stdout
    )
    assert "Format somethingElse is not supported by Graphviz. It supports:" in stdout
    # Check that we exited with the expected error code
    assert wrapped_sysexit.value.code == 32


@pytest.mark.parametrize(
    ("arg", "expected_default"),
    [
        ("mode", "PUB_ONLY"),
        ("classes", []),
        ("show_ancestors", None),
        ("all_ancestors", None),
        ("show_associated", None),
        ("all_associated", None),
        ("show_builtin", 0),
        ("module_names", None),
        ("output_format", "dot"),
        ("colorized", 0),
        ("max_color_depth", 2),
        ("ignore_list", ("CVS",)),
        ("project", ""),
        ("output_directory", ""),
    ],
)
@mock.patch("pylint.pyreverse.main.Run.run", new=mock.MagicMock())
@mock.patch("pylint.pyreverse.main.sys.exit", new=mock.MagicMock())
def test_command_line_arguments_defaults(arg: str, expected_default: Any) -> None:
    """Test that the default arguments of all options are correct."""
    run = main.Run([TEST_DATA_DIR])  # type: ignore[var-annotated]
    assert getattr(run.config, arg) == expected_default


@mock.patch("pylint.pyreverse.main.writer")
def test_command_line_arguments_yes_no(
    mock_writer: mock.MagicMock,  # pylint: disable=unused-argument
) -> None:
    """Regression test for the --module-names option.

    Make sure that we support --module-names=yes syntax instead
    of using it as a flag.
    """
    with pytest.raises(SystemExit) as wrapped_sysexit:
        main.Run(["--module-names=yes", TEST_DATA_DIR])
    assert wrapped_sysexit.value.code == 0


@mock.patch("pylint.pyreverse.main.writer")
@mock.patch("pylint.pyreverse.main.sys.exit", new=mock.MagicMock())
def test_class_command(
    mock_writer: mock.MagicMock,  # pylint: disable=unused-argument
) -> None:
    """Regression test for the --class option.

    Make sure that we append multiple --class arguments to one option destination.
    """
    runner = main.Run(  # type: ignore[var-annotated]
        [
            "--class",
            "data.clientmodule_test.Ancestor",
            "--class",
            "data.property_pattern.PropertyPatterns",
            TEST_DATA_DIR,
        ]
    )
    assert "data.clientmodule_test.Ancestor" in runner.config.classes
    assert "data.property_pattern.PropertyPatterns" in runner.config.classes


def test_version_info(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
) -> None:
    """Test that it is possible to display the version information."""
    test_full_version = "1.2.3.4"
    monkeypatch.setattr(main.constants, "full_version", test_full_version)  # type: ignore[attr-defined]
    with pytest.raises(SystemExit):
        main.Run(["--version"])
    out, _ = capsys.readouterr()
    assert "pyreverse is included in pylint" in out
    assert test_full_version in out