| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/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 |
| |
| |
| @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_verbose(_: mock.MagicMock, capsys: CaptureFixture[str]) -> None: |
| """Test the --verbose flag.""" |
| with pytest.raises(SystemExit): |
| # we have to catch the SystemExit so the test execution does not stop |
| main.Run(["--verbose", TEST_DATA_DIR]) |
| assert "parsing" in capsys.readouterr().out |
| |
| |
| @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), |
| ("show_stdlib", 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 |