| # 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 |
| |
| """Unit test for the extensions.diadefslib modules.""" |
| |
| # pylint: disable=redefined-outer-name |
| |
| from __future__ import annotations |
| |
| import sys |
| from collections.abc import Iterator |
| from pathlib import Path |
| |
| import pytest |
| from astroid import extract_node, nodes |
| |
| from pylint.pyreverse.diadefslib import ( |
| ClassDiadefGenerator, |
| DefaultDiadefGenerator, |
| DiaDefGenerator, |
| DiadefsHandler, |
| ) |
| from pylint.pyreverse.diagrams import DiagramEntity, Relationship |
| from pylint.pyreverse.inspector import Linker, Project |
| from pylint.testutils.pyreverse import PyreverseConfig |
| from pylint.testutils.utils import _test_cwd |
| from pylint.typing import GetProjectCallable |
| |
| HERE = Path(__file__) |
| TESTS = HERE.parent.parent |
| |
| |
| def _process_classes(classes: list[DiagramEntity]) -> list[tuple[bool, str]]: |
| """Extract class names of a list.""" |
| return sorted((isinstance(c.node, nodes.ClassDef), c.title) for c in classes) |
| |
| |
| def _process_relations( |
| relations: dict[str, list[Relationship]] |
| ) -> list[tuple[str, str, str]]: |
| """Extract relation indices from a relation list.""" |
| result = [] |
| for rel_type, rels in relations.items(): |
| for rel in rels: |
| result.append((rel_type, rel.from_object.title, rel.to_object.title)) |
| result.sort() |
| return result |
| |
| |
| @pytest.fixture |
| def HANDLER(default_config: PyreverseConfig) -> DiadefsHandler: |
| return DiadefsHandler(default_config) |
| |
| |
| @pytest.fixture(scope="module") |
| def PROJECT(get_project: GetProjectCallable) -> Iterator[Project]: |
| with _test_cwd(TESTS): |
| yield get_project("data") |
| |
| |
| def test_option_values( |
| default_config: PyreverseConfig, HANDLER: DiadefsHandler, PROJECT: Project |
| ) -> None: |
| """Test for ancestor, associated and module options.""" |
| df_h = DiaDefGenerator(Linker(PROJECT), HANDLER) |
| cl_config = default_config |
| cl_config.classes = ["Specialization"] |
| cl_h = DiaDefGenerator(Linker(PROJECT), DiadefsHandler(cl_config)) |
| assert df_h._get_levels() == (0, 0) |
| assert not df_h.module_names |
| assert cl_h._get_levels() == (-1, -1) |
| assert cl_h.module_names |
| for hndl in (df_h, cl_h): |
| hndl.config.all_ancestors = True |
| hndl.config.all_associated = True |
| hndl.config.module_names = True |
| hndl._set_default_options() |
| assert hndl._get_levels() == (-1, -1) |
| assert hndl.module_names |
| handler = DiadefsHandler(default_config) |
| df_h = DiaDefGenerator(Linker(PROJECT), handler) |
| cl_config = default_config |
| cl_config.classes = ["Specialization"] |
| cl_h = DiaDefGenerator(Linker(PROJECT), DiadefsHandler(cl_config)) |
| for hndl in (df_h, cl_h): |
| hndl.config.show_ancestors = 2 |
| hndl.config.show_associated = 1 |
| hndl.config.module_names = False |
| hndl._set_default_options() |
| assert hndl._get_levels() == (2, 1) |
| assert not hndl.module_names |
| |
| |
| def test_default_values() -> None: |
| """Test default values for package or class diagrams.""" |
| # TODO : should test difference between default values for package or class diagrams |
| |
| |
| class TestShowOptions: |
| def test_show_stdlib(self) -> None: |
| example = extract_node( |
| ''' |
| import collections |
| |
| class CustomDict(collections.OrderedDict): |
| """docstring""" |
| ''' |
| ) |
| |
| config = PyreverseConfig() |
| dd_gen = DiaDefGenerator(Linker(PROJECT), DiadefsHandler(config)) |
| |
| # Default behavior |
| assert not list(dd_gen.get_ancestors(example, 1)) |
| |
| # Show standard library enabled |
| config.show_stdlib = True |
| ancestors = list(dd_gen.get_ancestors(example, 1)) |
| assert len(ancestors) == 1 |
| assert ancestors[0].name == "OrderedDict" |
| |
| def test_show_builtin(self) -> None: |
| example = extract_node( |
| ''' |
| class CustomError(Exception): |
| """docstring""" |
| ''' |
| ) |
| |
| config = PyreverseConfig() |
| dd_gen = DiaDefGenerator(Linker(PROJECT), DiadefsHandler(config)) |
| |
| # Default behavior |
| assert not list(dd_gen.get_ancestors(example, 1)) |
| |
| # Show builtin enabled |
| config.show_builtin = True |
| ancestors = list(dd_gen.get_ancestors(example, 1)) |
| assert len(ancestors) == 1 |
| assert ancestors[0].name == "Exception" |
| |
| |
| class TestDefaultDiadefGenerator: |
| _should_rels = [ |
| ("aggregation", "DoNothing2", "Specialization"), |
| ("association", "DoNothing", "Ancestor"), |
| ("association", "DoNothing", "Specialization"), |
| ("specialization", "Specialization", "Ancestor"), |
| ] |
| |
| def test_extract_relations(self, HANDLER: DiadefsHandler, PROJECT: Project) -> None: |
| """Test extract_relations between classes.""" |
| cd = DefaultDiadefGenerator(Linker(PROJECT), HANDLER).visit(PROJECT)[1] |
| cd.extract_relationships() |
| relations = _process_relations(cd.relationships) |
| assert relations == self._should_rels |
| |
| def test_functional_relation_extraction( |
| self, default_config: PyreverseConfig, get_project: GetProjectCallable |
| ) -> None: |
| """Functional test of relations extraction; |
| different classes possibly in different modules. |
| """ |
| # XXX should be catching pyreverse environment problem but doesn't |
| # pyreverse doesn't extract the relations but this test ok |
| project = get_project("data") |
| handler = DiadefsHandler(default_config) |
| diadefs = handler.get_diadefs(project, Linker(project, tag=True)) |
| cd = diadefs[1] |
| relations = _process_relations(cd.relationships) |
| assert relations == self._should_rels |
| |
| |
| def test_known_values1(HANDLER: DiadefsHandler, PROJECT: Project) -> None: |
| dd = DefaultDiadefGenerator(Linker(PROJECT), HANDLER).visit(PROJECT) |
| assert len(dd) == 2 |
| keys = [d.TYPE for d in dd] |
| assert keys == ["package", "class"] |
| pd = dd[0] |
| assert pd.title == "packages No Name" |
| modules = sorted((isinstance(m.node, nodes.Module), m.title) for m in pd.objects) |
| assert modules == [ |
| (True, "data"), |
| (True, "data.clientmodule_test"), |
| (True, "data.nullable_pattern"), |
| (True, "data.property_pattern"), |
| (True, "data.suppliermodule_test"), |
| ] |
| cd = dd[1] |
| assert cd.title == "classes No Name" |
| classes = _process_classes(cd.objects) |
| assert classes == [ |
| (True, "Ancestor"), |
| (True, "CustomException"), |
| (True, "DoNothing"), |
| (True, "DoNothing2"), |
| (True, "DoSomething"), |
| (True, "Interface"), |
| (True, "NullablePatterns"), |
| (True, "PropertyPatterns"), |
| (True, "Specialization"), |
| ] |
| |
| |
| def test_known_values2( |
| HANDLER: DiadefsHandler, get_project: GetProjectCallable |
| ) -> None: |
| project = get_project("data.clientmodule_test") |
| dd = DefaultDiadefGenerator(Linker(project), HANDLER).visit(project) |
| assert len(dd) == 1 |
| keys = [d.TYPE for d in dd] |
| assert keys == ["class"] |
| cd = dd[0] |
| assert cd.title == "classes No Name" |
| classes = _process_classes(cd.objects) |
| assert classes == [(True, "Ancestor"), (True, "Specialization")] |
| |
| |
| def test_known_values3(HANDLER: DiadefsHandler, PROJECT: Project) -> None: |
| HANDLER.config.classes = ["Specialization"] |
| cdg = ClassDiadefGenerator(Linker(PROJECT), HANDLER) |
| special = "data.clientmodule_test.Specialization" |
| cd = cdg.class_diagram(PROJECT, special) |
| assert cd.title == special |
| classes = _process_classes(cd.objects) |
| assert classes == [ |
| (True, "data.clientmodule_test.Ancestor"), |
| (True, special), |
| (True, "data.suppliermodule_test.DoNothing"), |
| (True, "data.suppliermodule_test.DoNothing2"), |
| ] |
| |
| |
| def test_known_values4(HANDLER: DiadefsHandler, PROJECT: Project) -> None: |
| HANDLER.config.classes = ["Specialization"] |
| HANDLER.config.module_names = False |
| cd = ClassDiadefGenerator(Linker(PROJECT), HANDLER).class_diagram( |
| PROJECT, "data.clientmodule_test.Specialization" |
| ) |
| assert cd.title == "data.clientmodule_test.Specialization" |
| classes = _process_classes(cd.objects) |
| assert classes == [ |
| (True, "Ancestor"), |
| (True, "DoNothing"), |
| (True, "DoNothing2"), |
| (True, "Specialization"), |
| ] |
| |
| |
| @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires dataclasses") |
| def test_regression_dataclasses_inference( |
| HANDLER: DiadefsHandler, get_project: GetProjectCallable |
| ) -> None: |
| project_path = Path("regrtest_data") / "dataclasses_pyreverse" |
| path = get_project(str(project_path)) |
| |
| cdg = ClassDiadefGenerator(Linker(path), HANDLER) |
| special = "regrtest_data.dataclasses_pyreverse.InventoryItem" |
| cd = cdg.class_diagram(path, special) |
| assert cd.title == special |