diff --git a/bases/polylith/cli/test.py b/bases/polylith/cli/test.py index fc39ca99..b02f281a 100644 --- a/bases/polylith/cli/test.py +++ b/bases/polylith/cli/test.py @@ -16,6 +16,9 @@ def diff_command( projects: Annotated[ bool, Option(help="Projects affected by changes in tests") ] = False, + strategy: Annotated[ + str, Option(help="By 'imports' (the bricks used in tests) or by 'path' (the corresponding bricks).") + ] = "imports", ): """Shows the Polylith projects and bricks that are affected by changes in tests.""" root = repo.get_workspace_root(Path.cwd()) @@ -27,6 +30,6 @@ def diff_command( print("No matching tags or commits found in repository.") return - options = {"short": short, "bricks": bricks, "projects": projects} + options = {"short": short, "bricks": bricks, "projects": projects, "strategy": strategy} commands.test.run(root, ns, tag, options) diff --git a/components/polylith/commands/test.py b/components/polylith/commands/test.py index a160ef46..60faf04f 100644 --- a/components/polylith/commands/test.py +++ b/components/polylith/commands/test.py @@ -13,8 +13,8 @@ def get_imported_bricks_in_tests( return set().union(*brick_imports.values()) -def extract_brick_names(bricks_data: List[dict], imported_bricks: Set[str]) -> Set[str]: - return {v for b in bricks_data for v in b.values() if v in imported_bricks} +def extract_brick_names(bricks_data: List[dict], possible_bricks: Set[str]) -> Set[str]: + return {v for b in bricks_data for v in b.values() if v in possible_bricks} def get_affected_bricks( @@ -28,6 +28,18 @@ def get_affected_bricks( return bases, components +def get_related_bricks( + root: Path, ns: str, tag_name: Union[str, None], theme: str +) -> Tuple[Set[str], Set[str]]: + files = test.get_changed_files(root, tag_name) + related = test.get_related_bricks(root, ns, theme, files) + + bases = extract_brick_names(dirs.get_bases_data(root, ns), related["bases"]) + components = extract_brick_names(dirs.get_components_data(root, ns), related["components"]) + + return bases, components + + def get_affected_projects( root: Path, ns: str, bases: Set[str], components: Set[str] ) -> List[dict]: @@ -40,10 +52,27 @@ def get_affected_projects( return [p for p in projects_data if p["path"].name in names] +def parse_strategy(strategy: str) -> List[str]: + strategies = str.split(strategy, ",") + + return [str.lower(s) for s in strategies] + + def run(root: Path, ns: str, tag: str, options: dict) -> None: theme = configuration.get_theme_from_config(root) - bases, components = get_affected_bricks(root, ns, tag, theme) + strategy = parse_strategy(options["strategy"]) + + by_imports = "imports" in strategy + by_path = "path" in strategy + + fallback: Tuple[Set[str], Set[str]] = set(), set() + affected = get_affected_bricks(root, ns, tag, theme) if by_imports else fallback + related = get_related_bricks(root, ns, tag, theme) if by_path else fallback + + bases = set().union(affected[0], related[0]) + components = set().union(affected[1], related[1]) + projects_data = get_affected_projects(root, ns, bases, components) if options.get("bricks"): diff --git a/components/polylith/poetry/commands/test.py b/components/polylith/poetry/commands/test.py index 611fc0d1..15d3c006 100644 --- a/components/polylith/poetry/commands/test.py +++ b/components/polylith/poetry/commands/test.py @@ -25,6 +25,12 @@ class TestDiffCommand(Command): description="Projects affected by changes in tests", flag=True, ), + option( + long_name="strategy", + description="By 'imports' (the bricks used in tests) or by 'path' (the corresponding bricks)", + flag=False, + default="imports", + ), ] def handle(self) -> int: @@ -34,6 +40,7 @@ def handle(self) -> int: "short": self.option("short"), "bricks": self.option("bricks"), "projects": self.option("projects"), + "strategy": self.option("strategy"), } root = repo.get_workspace_root(Path.cwd()) diff --git a/components/polylith/test/__init__.py b/components/polylith/test/__init__.py index d0b7ec1f..b5ba479a 100644 --- a/components/polylith/test/__init__.py +++ b/components/polylith/test/__init__.py @@ -1,5 +1,11 @@ from polylith.test import report -from polylith.test.core import get_brick_imports_in_tests, get_changed_files +from polylith.test.core import get_brick_imports_in_tests, get_changed_files, get_related_bricks from polylith.test.tests import create_test -__all__ = ["report", "create_test", "get_brick_imports_in_tests", "get_changed_files"] +__all__ = [ + "report", + "create_test", + "get_brick_imports_in_tests", + "get_changed_files", + "get_related_bricks", +] diff --git a/components/polylith/test/core.py b/components/polylith/test/core.py index 41048ab3..dabda294 100644 --- a/components/polylith/test/core.py +++ b/components/polylith/test/core.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Union +from typing import List, Set, Union from polylith import diff, imports @@ -16,6 +16,29 @@ def is_test(root: Path, ns: str, path: Path, theme: str) -> bool: return f"/{expected}/{ns}" in file_path +def extract_parts_from_test_path(root: Path, path: Path) -> List[str]: + relative_path = str.replace(path.as_posix(), root.as_posix(), "") + parts = str.split(relative_path, "/") + + return [p for p in parts if p] + + +def extract_brick_type_from_test(root: Path, path: Path, theme: str) -> str: + parts = extract_parts_from_test_path(root, path) + + return parts[1] if theme == "loose" else parts[0] + + +def extract_brick_name_from_test(root: Path, path: Path, theme: str) -> str: + parts = extract_parts_from_test_path(root, path) + + return parts[3] if theme == "loose" else parts[1] + + +def find_tests(root: Path, ns: str, theme: str, files: List[Path]) -> Set[Path]: + return {f for f in files if is_test(root, ns, f, theme)} + + def get_changed_files(root: Path, tag_name: Union[str, None]) -> List[Path]: tag = diff.collect.get_latest_tag(root, tag_name) or tag_name @@ -28,10 +51,27 @@ def get_changed_files(root: Path, tag_name: Union[str, None]) -> List[Path]: def get_brick_imports_in_tests( root: Path, ns: str, theme: str, files: List[Path] ) -> dict: - matched = {f for f in files if is_test(root, ns, f, theme)} + matched = find_tests(root, ns, theme, files) listed_imports = [imports.list_imports(m) for m in matched] all_imports = dict(enumerate(listed_imports)) return imports.extract_brick_imports(all_imports, ns) + + +def get_related_brick(root: Path, path: Path, theme: str) -> dict: + brick_name = extract_brick_name_from_test(root, path, theme) + brick_type = extract_brick_type_from_test(root, path, theme) + + return {"name": brick_name, "type": brick_type} + + +def get_related_bricks(root: Path, ns: str, theme: str, files: List[Path]) -> dict: + matched = find_tests(root, ns, theme, files) + + bricks = [get_related_brick(root, m, theme) for m in matched] + bases = {b["name"] for b in bricks if b["type"] == "bases"} + components = {b["name"] for b in bricks if b["type"] == "components"} + + return {"bases": bases, "components": components} diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml index 742ad436..df0a4870 100644 --- a/projects/poetry_polylith_plugin/pyproject.toml +++ b/projects/poetry_polylith_plugin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "poetry-polylith-plugin" -version = "1.53.1" +version = "1.54.0" description = "A Poetry plugin that adds tooling support for the Polylith Architecture" authors = ["David Vujic"] homepage = "https://davidvujic.github.io/python-polylith-docs/" diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml index 8e9b096f..12bf7545 100644 --- a/projects/polylith_cli/pyproject.toml +++ b/projects/polylith_cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polylith-cli" -version = "1.49.1" +version = "1.50.0" description = "Python tooling support for the Polylith Architecture" authors = ['David Vujic'] homepage = "https://davidvujic.github.io/python-polylith-docs/" diff --git a/test/components/polylith/test/test_core.py b/test/components/polylith/test/test_core.py new file mode 100644 index 00000000..20099ca8 --- /dev/null +++ b/test/components/polylith/test/test_core.py @@ -0,0 +1,39 @@ +from pathlib import Path + +from polylith.test import core + + +def test_extract_brick_name_from_test() -> None: + expected = "hello_world" + root = Path.cwd() + + first_test = root / f"test/components/my_namespace/{expected}/the_test.py" + second_test = root / f"components/{expected}/my_namespace/{expected}/test/the_test.py" + + first = core.extract_brick_name_from_test(root, first_test, theme="loose") + second = core.extract_brick_name_from_test(root, second_test, theme="tdd") + + assert first == expected + assert second == expected + + +def test_extract_brick_type_from_test() -> None: + root = Path.cwd() + + loose_base_test = root / "test/bases/my_namespace/hello/the_test.py" + loose_comp_test = root / "test/components/my_namespace/world/the_test.py" + + tdd_base_test = root / "bases/hello/my_namespace/hello/test/the_test.py" + tdd_comp_test = root / "components/world/my_namespace/world/test/the_test.py" + + first = core.extract_brick_type_from_test(root, loose_base_test, theme="loose") + second = core.extract_brick_type_from_test(root, loose_comp_test, theme="loose") + + third = core.extract_brick_type_from_test(root, tdd_base_test, theme="tdd") + fourth = core.extract_brick_type_from_test(root, tdd_comp_test, theme="tdd") + + assert first == "bases" + assert second == "components" + + assert third == "bases" + assert fourth == "components"