diff --git a/.bazelrc b/.bazelrc index 8955ad92..d3ee43c2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,10 +1,18 @@ # Always tell why tests fail test --test_output=errors -# We use Bazel's modern dependency management system. This works for us only with Bazel >= 6.2.0 -# This is the deprecated version. "--enable_bzlmod" is the forward path. However, as Bazel < 6.0.0 does not support the -# new flag. We keep using the deprecated one until we no longer support Bazel 5. -build --experimental_enable_bzlmod=true +# We use Bazel's modern dependency management system for development. +build --enable_bzlmod=true + +# We are not yet sure if we really want to lock the bzlmod resolution down given we test with various Bazel versions +# and configurations. It seems the main benefits of the lock file are not having to reanalyze the central registry +# when working without a cached workspace and being safeguarded against changed or yanked modules in the central +# registry. Both don't matter much to us right now. +build --lockfile_mode=off + +# When working with hermetic Python toolchains, supporting the legacy runfiles layout is needlessly wasting resources. +# See https://github.com/bazelbuild/rules_python/issues/1653 +build --nolegacy_external_runfiles # Mypy integration build:mypy --aspects=@mypy_integration//:mypy.bzl%mypy_aspect diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9fefa92e..312211a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: - name: Unit tests run: bazel test -- //src/... //test/aspect:all //third_party/... - name: Execute Mypy - run: bazel build --config=mypy -- //src/... //test/aspect:all + run: bazel build --config=mypy -- //src/... //test/aspect:all //test/apply_fixes:all integration-tests-aspect: runs-on: ubuntu-22.04 diff --git a/README.md b/README.md index 20405b1d..f8121035 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ You can invoke the aspect from within a rule. This can be useful to make the execution part of a bazel build without having to manually execute the longish aspect build command. The Bazel documentation for invoking an aspect from within a rule can be found [here](https://bazel.build/rules/aspects#invoking_the_aspect_from_a_rule). -A concrete example for doing so for the DWYU aspect can be found in [a rule in the recursion test cases](test/aspect/recursion/rule.bzl). +A concrete example for doing so for the DWYU aspect can be found in [aspect integration tests](test/aspect/rule_using_aspect). # Configuring DWYU diff --git a/ruff.toml b/ruff.toml index fe787184..0934d098 100644 --- a/ruff.toml +++ b/ruff.toml @@ -40,8 +40,8 @@ select = [ ignore = [ # It is fine tha some line are longer than the length limit "E501", + # Number of function arguments is too opinionated + "PLR0913", # High false positive rate. Often a plain number is the most expressive, e.g. when checling lengths. "PLR2004", - # Using Optinal and '= None' at once is overly verbose and hurts readability - "RUF013", ] diff --git a/test/apply_fixes/BUILD b/test/apply_fixes/BUILD new file mode 100644 index 00000000..1c446536 --- /dev/null +++ b/test/apply_fixes/BUILD @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# Helper target to enable mypy +py_library( + name = "python_files", + srcs = glob(["**/*.py"]), +) diff --git a/test/apply_fixes/execution_logic.py b/test/apply_fixes/execution_logic.py index 7642da0a..40c1c4ec 100644 --- a/test/apply_fixes/execution_logic.py +++ b/test/apply_fixes/execution_logic.py @@ -6,6 +6,7 @@ from tempfile import TemporaryDirectory from typing import List, Optional +from result import Error from test_case import TestCaseBase TEST_CASES_DIR = Path("test/apply_fixes") @@ -58,29 +59,32 @@ def cleanup(test_workspace: Path) -> None: def execute_test(test: TestCaseBase, origin_workspace: Path) -> bool: - logging.info(f">>> Executing '{test.name}'") + logging.info(f">>> Test '{test.name}'") succeeded = False result = None with TemporaryDirectory() as temporary_workspace: + workspace_path = Path(temporary_workspace) try: setup_test_workspace( origin_workspace=origin_workspace, test_sources=test.test_sources, extra_workspace_file_content=test.extra_workspace_file_content, - temporary_workspace=Path(temporary_workspace), + temporary_workspace=workspace_path, ) - result = test.execute_test(Path(temporary_workspace)) + result = test.execute_test(workspace_path) except Exception: logging.exception("Test failed due to exception:") - cleanup(temporary_workspace) + cleanup(workspace_path) - if result is not None: - if not result.is_success(): - logging.info(result.error) - else: - succeeded = True + if result is None: + result = Error("No result") + + if result.is_success(): + succeeded = True + else: + logging.info(result.error) logging.info(f'<<< {"OK" if succeeded else "FAILURE"}\n') diff --git a/test/apply_fixes/test_case.py b/test/apply_fixes/test_case.py index 26528926..1a913ea0 100644 --- a/test/apply_fixes/test_case.py +++ b/test/apply_fixes/test_case.py @@ -11,7 +11,7 @@ class TestCaseBase(ABC): def __init__(self, name: str, test_sources: Path) -> None: self._name = name self._test_sources = test_sources - self._workspace = None + self._workspace = Path("") # # Interface @@ -64,25 +64,44 @@ def _create_reports( """ Create report files as input for the applying fixes script """ - cmd = ["bazel"] - if startup_args: - cmd.extend(startup_args) - cmd.extend(["build", f"--aspects=//:aspect.bzl%{aspect}", "--output_groups=dwyu"]) - if extra_args: - cmd.extend(extra_args) - cmd.append(self.test_target) - self._run_cmd(cmd=cmd, check=False) - - def _run_automatic_fix(self, extra_args: List[str] = None) -> None: + cmd_startup_args = startup_args if startup_args else [] + cmd_extra_args = extra_args if extra_args else [] + + self._run_cmd( + cmd=[ + "bazel", + *cmd_startup_args, + "build", + "--nolegacy_external_runfiles", + f"--aspects=//:aspect.bzl%{aspect}", + "--output_groups=dwyu", + *cmd_extra_args, + "--", + self.test_target, + ], + check=False, + ) + + def _run_automatic_fix(self, extra_args: Optional[List[str]] = None) -> None: """ Execute the applying fixes script for the Bazel target associated with the test case """ - cmd = ["bazel", "run", "@depend_on_what_you_use//:apply_fixes", "--", f"--workspace={self._workspace}"] - if logging.getLogger().isEnabledFor(logging.DEBUG): - cmd.append("--verbose") - if extra_args: - cmd.extend(extra_args) - self._run_cmd(cmd=cmd, check=True) + verbosity = ["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else [] + cmd_extra_args = extra_args if extra_args else [] + + self._run_cmd( + cmd=[ + "bazel", + "run", + "--nolegacy_external_runfiles", + "@depend_on_what_you_use//:apply_fixes", + "--", + f"--workspace={self._workspace}", + *verbosity, + *cmd_extra_args, + ], + check=True, + ) def _get_target_attribute(self, target: str, attribute: str) -> Set["str"]: """ @@ -114,10 +133,10 @@ def _make_unexpected_output_error(expected: str, output: str) -> Error: @staticmethod def _make_unexpected_deps_error( - expected_deps: Set[str] = None, - expected_implementation_deps: Set[str] = None, - actual_deps: Set[str] = None, - actual_implementation_deps: Set[str] = None, + expected_deps: Optional[Set[str]] = None, + expected_implementation_deps: Optional[Set[str]] = None, + actual_deps: Optional[Set[str]] = None, + actual_implementation_deps: Optional[Set[str]] = None, ) -> Error: message = "Unexpected dependencies.\n" if expected_deps: diff --git a/test/apply_fixes/tool_cli/test_use_custom_output_base.py b/test/apply_fixes/tool_cli/test_use_custom_output_base.py index 70b8832f..3969e63e 100644 --- a/test/apply_fixes/tool_cli/test_use_custom_output_base.py +++ b/test/apply_fixes/tool_cli/test_use_custom_output_base.py @@ -15,7 +15,7 @@ def execute_test_logic(self) -> Result: self._run_automatic_fix(extra_args=["--fix-unused", f"--bazel-bin={output_base}"]) target_deps = self._get_target_attribute(target=self.test_target, attribute="deps") - if (expected := set()) != target_deps: + if (expected := set()) != target_deps: # type: ignore[var-annotated] return self._make_unexpected_deps_error(expected_deps=expected, actual_deps=target_deps) else: return Success() diff --git a/test/apply_fixes/tool_cli/test_utilize_bazel_info.py b/test/apply_fixes/tool_cli/test_utilize_bazel_info.py index d1f134d2..1a3d3a58 100644 --- a/test/apply_fixes/tool_cli/test_utilize_bazel_info.py +++ b/test/apply_fixes/tool_cli/test_utilize_bazel_info.py @@ -12,7 +12,7 @@ def execute_test_logic(self) -> Result: self._run_automatic_fix(extra_args=["--fix-unused", "--use-bazel-info"]) target_deps = self._get_target_attribute(target=self.test_target, attribute="deps") - if (expected := set()) != target_deps: + if (expected := set()) != target_deps: # type: ignore[var-annotated] return self._make_unexpected_deps_error(expected_deps=expected, actual_deps=target_deps) else: return Success() diff --git a/test/apply_fixes/tool_cli/test_utilize_bazel_info_with_custom_compilation_mode.py b/test/apply_fixes/tool_cli/test_utilize_bazel_info_with_custom_compilation_mode.py index 4aaee3b8..461bfa32 100644 --- a/test/apply_fixes/tool_cli/test_utilize_bazel_info_with_custom_compilation_mode.py +++ b/test/apply_fixes/tool_cli/test_utilize_bazel_info_with_custom_compilation_mode.py @@ -12,7 +12,7 @@ def execute_test_logic(self) -> Result: self._run_automatic_fix(extra_args=["--fix-unused", "--use-bazel-info=opt"]) target_deps = self._get_target_attribute(target=self.test_target, attribute="deps") - if (expected := set()) != target_deps: + if (expected := set()) != target_deps: # type: ignore[var-annotated] return self._make_unexpected_deps_error(expected_deps=expected, actual_deps=target_deps) else: return Success() diff --git a/test/aspect/BUILD b/test/aspect/BUILD index 899f47e3..31213930 100644 --- a/test/aspect/BUILD +++ b/test/aspect/BUILD @@ -1,9 +1,31 @@ -load("@rules_python//python:defs.bzl", "py_test") +load("@rules_python//python:defs.bzl", "py_library", "py_test") py_test( - name = "execute_tests_utest", + name = "result_test", srcs = [ - "execute_tests_impl.py", - "execute_tests_utest.py", + "result.py", + "result_test.py", ], ) + +py_test( + name = "test_case_test", + srcs = [ + "test_case.py", + "test_case_test.py", + ], +) + +py_test( + name = "version_test", + srcs = [ + "version.py", + "version_test.py", + ], +) + +# Helper target to enable mypy +py_library( + name = "python_files", + srcs = glob(["**/*.py"]), +) diff --git a/test/aspect/alias/test_invalid_dependency_through_alias.py b/test/aspect/alias/test_invalid_dependency_through_alias.py new file mode 100644 index 00000000..1e38eccf --- /dev/null +++ b/test/aspect/alias/test_invalid_dependency_through_alias.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=["File='test/aspect/alias/use_a_and_b.cpp', include='test/aspect/alias/a.h'"], + ) + actual = self._run_dwyu(target="//test/aspect/alias:use_a_transitively", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/alias/test_unused_dependency_through_alias.py b/test/aspect/alias/test_unused_dependency_through_alias.py new file mode 100644 index 00000000..6bc5ba5c --- /dev/null +++ b/test/aspect/alias/test_unused_dependency_through_alias.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/alias:lib_a"]) + actual = self._run_dwyu(target="//test/aspect/alias:unused_dependency", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/alias/test_use_dependency_through_alias.py b/test/aspect/alias/test_use_dependency_through_alias.py new file mode 100644 index 00000000..b3a71fb5 --- /dev/null +++ b/test/aspect/alias/test_use_dependency_through_alias.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/alias:use_a_directly", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/aspect.bzl b/test/aspect/aspect.bzl index aaad17ce..af951ad4 100644 --- a/test/aspect/aspect.bzl +++ b/test/aspect/aspect.bzl @@ -1,3 +1,4 @@ load("//:defs.bzl", "dwyu_aspect_factory") -dwyu_default_aspect = dwyu_aspect_factory() +dwyu = dwyu_aspect_factory() +dwyu_impl_deps = dwyu_aspect_factory(use_implementation_deps = True) diff --git a/test/aspect/complex_includes/test_complex_includes.py b/test/aspect/complex_includes/test_complex_includes.py new file mode 100644 index 00000000..d25bbe42 --- /dev/null +++ b/test/aspect/complex_includes/test_complex_includes.py @@ -0,0 +1,18 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + """ + Working in external repositories has its own challenges due to some paths behaving different compared to + working inside the own workspace. Thus, we explicitly test working in an external workspace on top of normal + targets from within the main workspace. + """ + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target=["//test/aspect/complex_includes:all", "@complex_includes_test_repo//..."], + aspect=self.default_aspect, + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/custom_config/test_custom_ignore_include_paths.py b/test/aspect/custom_config/test_custom_ignore_include_paths.py new file mode 100644 index 00000000..dd51e940 --- /dev/null +++ b/test/aspect/custom_config/test_custom_ignore_include_paths.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/custom_config:use_multiple_arcane_headers", + aspect="//test/aspect/custom_config:aspect.bzl%ignore_include_paths_aspect", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/custom_config/test_extra_ignore_include_paths.py b/test/aspect/custom_config/test_extra_ignore_include_paths.py new file mode 100644 index 00000000..e6be46ea --- /dev/null +++ b/test/aspect/custom_config/test_extra_ignore_include_paths.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/custom_config:use_arcane_header_and_vector", + aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_paths_aspect", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/custom_config/test_ignore_include_patterns.py b/test/aspect/custom_config/test_ignore_include_patterns.py new file mode 100644 index 00000000..065dc967 --- /dev/null +++ b/test/aspect/custom_config/test_ignore_include_patterns.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/custom_config:use_ignored_patterns", + aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_patterns_aspect", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/custom_config/test_include_not_covered_by_patterns.py b/test/aspect/custom_config/test_include_not_covered_by_patterns.py new file mode 100644 index 00000000..d4f0e75a --- /dev/null +++ b/test/aspect/custom_config/test_include_not_covered_by_patterns.py @@ -0,0 +1,18 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='test/aspect/custom_config/use_not_ignored_header.h', include='example_substring_matching_does_not_work_here.h'" + ], + ) + actual = self._run_dwyu( + target="//test/aspect/custom_config:use_not_ignored_header", + aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_patterns_aspect", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/defines/test_processing_defines.py b/test/aspect/defines/test_processing_defines.py new file mode 100644 index 00000000..f68d8206 --- /dev/null +++ b/test/aspect/defines/test_processing_defines.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/defines:all", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/execute_tests.py b/test/aspect/execute_tests.py index 0f31962b..8386603e 100755 --- a/test/aspect/execute_tests.py +++ b/test/aspect/execute_tests.py @@ -1,15 +1,12 @@ #!/usr/bin/env python3 -import sys +import logging +from argparse import ArgumentParser +from sys import exit -from execute_tests_impl import ( - CompatibleVersions, - ExpectedResult, - TestCase, - TestCmd, - TestedVersions, - cli, - main, -) +from execution_logic import main +from test_case import CompatibleVersions, TestedVersions + +logging.basicConfig(format="%(message)s", level=logging.INFO) # Test matrix. We don't combine each Bazel version with each Python version as there is no significant benefit. We # manually define pairs which make sure each Bazel and Python version we care about is used at least once. @@ -22,10 +19,10 @@ TestedVersions(bazel="8.0.0-pre.20231030.2", python="3.12.0"), ] -# When Bazel 7.0.0 releases we have to look again at the flags and check if more flags are available VERSION_SPECIFIC_ARGS = { # We support Bazel's modern dependency management system, but it works only as desired with a recent Bazel version - "--experimental_enable_bzlmod=false": CompatibleVersions(maximum="6.1.99"), + "--experimental_enable_bzlmod=false": CompatibleVersions(before="6.0.0"), + "--enable_bzlmod=false": CompatibleVersions(minimum="6.0.0", before="6.2.0"), # We are not yet sure if we really want to lock the bzlmod resolution down given we test with various Bazel versions # and configurations. It seems the main benefits of the lock file are not having to reanalyze the central registry # when working without a cached workspace and being safeguarded against changed or yanked modules in the central @@ -38,7 +35,7 @@ "--incompatible_config_setting_private_default_visibility": CompatibleVersions(minimum="5.0.0"), "--incompatible_disable_target_provider_fields": CompatibleVersions(minimum="5.0.0"), "--incompatible_struct_has_no_methods": CompatibleVersions(minimum="5.0.0"), - "--incompatible_use_platforms_repo_for_constraints": CompatibleVersions(minimum="5.0.0", maximum="6.99.99"), + "--incompatible_use_platforms_repo_for_constraints": CompatibleVersions(minimum="5.0.0", before="7.0.0"), "--incompatible_disallow_empty_glob": CompatibleVersions(minimum="5.0.0"), "--incompatible_no_implicit_file_export": CompatibleVersions(minimum="5.0.0"), "--incompatible_use_cc_configure_from_rules_cc": CompatibleVersions(minimum="5.0.0"), @@ -59,374 +56,50 @@ # "--incompatible_disallow_struct_provider_syntax": CompatibleVersions(minimum="7.0.0"), } -DEFAULT_ASPECT = "//test/aspect:aspect.bzl%dwyu_default_aspect" -TESTS = [ - TestCase( - name="custom_config_extra_ignore_include_paths", - cmd=TestCmd( - target="//test/aspect/custom_config:use_arcane_header_and_vector", - aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_paths_aspect", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="custom_config_custom_ignore_include_paths", - cmd=TestCmd( - target="//test/aspect/custom_config:use_multiple_arcane_headers", - aspect="//test/aspect/custom_config:aspect.bzl%ignore_include_paths_aspect", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="custom_config_ignore_include_patterns", - cmd=TestCmd( - target="//test/aspect/custom_config:use_ignored_patterns", - aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_patterns_aspect", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="custom_config_include_not_covered_by_patterns", - cmd=TestCmd( - target="//test/aspect/custom_config:use_not_ignored_header", - aspect="//test/aspect/custom_config:aspect.bzl%extra_ignore_include_patterns_aspect", - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/custom_config/use_not_ignored_header.h', include='example_substring_matching_does_not_work_here.h'" - ], - ), - ), - TestCase( - name="unused_dep", - cmd=TestCmd(target="//test/aspect/unused_dep:main", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/unused_dep:foo"]), - ), - TestCase( - name="unused_private_dep", - cmd=TestCmd( - target="//test/aspect/unused_dep/implementation_deps:implementation_deps_lib", - aspect="//test/aspect/unused_dep:aspect.bzl%implementation_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=False, unused_private_deps=["//test/aspect/unused_dep:foo"]), - ), - TestCase( - name="use_transitive_deps", - cmd=TestCmd(target="//test/aspect/using_transitive_dep:main", aspect=DEFAULT_ASPECT), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/using_transitive_dep/main.cpp', include='test/aspect/using_transitive_dep/foo.h'" - ], - ), - ), - TestCase( - name="use_external_dependencies", - cmd=TestCmd(target="//test/aspect/external_repo:use_external_libs", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="proper_implementation_deps", - cmd=TestCmd( - target="//test/aspect/implementation_deps:proper_private_deps", - aspect="//test/aspect/implementation_deps:aspect.bzl%implementation_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="binary_with_implementation_deps_activated", - cmd=TestCmd( - target="//test/aspect/implementation_deps:binary_using_foo", - aspect="//test/aspect/implementation_deps:aspect.bzl%implementation_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="test_with_implementation_deps_activated", - cmd=TestCmd( - target="//test/aspect/implementation_deps:test_using_foo", - aspect="//test/aspect/implementation_deps:aspect.bzl%implementation_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="superfluous_public_dep", - cmd=TestCmd( - target="//test/aspect/implementation_deps:superfluous_public_dep", - aspect="//test/aspect/implementation_deps:aspect.bzl%implementation_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=False, deps_which_should_be_private=["//test/aspect/implementation_deps:foo"]), - ), - TestCase( - name="generated_code", - cmd=TestCmd(target="//test/aspect/generated_code:foo", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - # Make sure headers from own lib are discovered correctly - TestCase( - name="system_includes_lib", - cmd=TestCmd(target="//test/aspect/includes:includes", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="system_includes_use_lib", - cmd=TestCmd(target="//test/aspect/includes:use_includes", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - # Make sure headers from own lib are discovered correctly - TestCase( - name="virtual_includes_add_prefix_lib", - cmd=TestCmd(target="//test/aspect/virtual_includes:prefixed", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="virtual_includes_use_add_prefix_lib", - cmd=TestCmd(target="//test/aspect/virtual_includes:use_prefixed", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - # Make sure headers from own lib are discovered correctly - TestCase( - name="virtual_includes_strip_prefix_lib", - cmd=TestCmd(target="//test/aspect/virtual_includes:stripped", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="virtual_includes_use_strip_prefix_lib", - cmd=TestCmd(target="//test/aspect/virtual_includes:use_stripped", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="valid", - cmd=TestCmd(target="//test/aspect/valid:bar", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="recursive_disabled", - cmd=TestCmd(target="//test/aspect/recursion:main", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="recursive_fail_transitively", - cmd=TestCmd( - target="//test/aspect/recursion:main", aspect="//test/aspect/recursion:aspect.bzl%recursive_aspect" - ), - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]), - ), - TestCase( - name="rule_direct_success", - cmd=TestCmd(target="//test/aspect/recursion:dwyu_direct_main"), - expected=ExpectedResult(success=True), - ), - TestCase( - name="rule_recursive_failure", - cmd=TestCmd(target="//test/aspect/recursion:dwyu_recursive_main"), - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]), - ), - TestCase( - name="recursive_fail_transitively_using_implementation_deps", - cmd=TestCmd( - target="//test/aspect/recursion:use_impl_deps", - aspect="//test/aspect/recursion:aspect.bzl%recursive_impl_deps_aspect", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]), - ), - TestCase( - name="rule_recursive_failure_using_implementation_deps", - cmd=TestCmd( - target="//test/aspect/recursion:dwyu_recursive_use_impl_deps", - extra_args=["--experimental_cc_implementation_deps"], - ), - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]), - ), - TestCase( - name="complex_includes", - cmd=TestCmd(target="//test/aspect/complex_includes:all", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="complex_includes_in_ext_repo", - cmd=TestCmd(target="@complex_includes_test_repo//...", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="incompatible_platforms", - cmd=TestCmd(target="//test/aspect/platforms/...", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="invalid_dependency_through_alias", - cmd=TestCmd(target="//test/aspect/alias:use_a_transitively", aspect=DEFAULT_ASPECT), - expected=ExpectedResult( - success=False, - invalid_includes=["File='test/aspect/alias/use_a_and_b.cpp', include='test/aspect/alias/a.h'"], - ), - ), - TestCase( - name="unused_dependency_through_alias", - cmd=TestCmd(target="//test/aspect/alias:unused_dependency", aspect=DEFAULT_ASPECT), - # The aspect does not see the alias, but only the resolved actual dependency - expected=ExpectedResult(success=False, unused_public_deps=["//test/aspect/alias:lib_a"]), - ), - TestCase( - name="dependency_through_alias", - cmd=TestCmd(target="//test/aspect/alias:use_a_directly", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="no_public_or_private_files_binary_link_shared", - cmd=TestCmd(target="//test/aspect/shared_library:libfoo.so", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="relative_includes", - cmd=TestCmd(target="//test/aspect/relative_includes/...", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="processing_defines", - cmd=TestCmd( - target="//test/aspect/defines:all", - aspect=DEFAULT_ASPECT, - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="depend_on_tree_artifact", - cmd=TestCmd( - target="//test/aspect/tree_artifact:use_tree_artifact", - aspect=DEFAULT_ASPECT, - ), - expected=ExpectedResult(success=True), - ), - # FIXMe this is a brittle test as it depends on '--compilation_mode' - TestCase( - name="analyze_tree_artifact", - cmd=TestCmd( - target="//test/aspect/tree_artifact:tree_artifact_library", - aspect=DEFAULT_ASPECT, - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='bazel-out/k8-fastbuild/bin/test/aspect/tree_artifact/sources.cc/tree_lib.cc', include='test/aspect/tree_artifact/some_lib.h'" - ], - ), - ), - TestCase( - name="target_mapping_b_fail_without_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_b", - aspect=DEFAULT_ASPECT, - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/target_mapping/use_lib_b.cpp', include='test/aspect/target_mapping/libs/b.h'" - ], - unused_public_deps=["//test/aspect/target_mapping/libs:a"], - ), - ), - TestCase( - name="target_mapping_b_succeed_with_specific_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_b", - aspect="//test/aspect/target_mapping:aspect.bzl%map_specific_deps", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="target_mapping_b_succeed_with_direct_deps_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_b", - aspect="//test/aspect/target_mapping:aspect.bzl%map_direct_deps", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="target_mapping_b_succeed_with_transitive_deps_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_b", - aspect="//test/aspect/target_mapping:aspect.bzl%map_transitive_deps", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="target_mapping_c_fail_without_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_c", - aspect=DEFAULT_ASPECT, - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/target_mapping/use_lib_c.cpp', include='test/aspect/target_mapping/libs/c.h'" - ], - unused_public_deps=["//test/aspect/target_mapping/libs:a"], - ), - ), - TestCase( - name="target_mapping_c_fail_with_specific_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_c", - aspect="//test/aspect/target_mapping:aspect.bzl%map_specific_deps", - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/target_mapping/use_lib_c.cpp', include='test/aspect/target_mapping/libs/c.h'" - ], - unused_public_deps=["//test/aspect/target_mapping/libs:a"], - ), - ), - TestCase( - name="target_mapping_c_fail_with_direct_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_c", - aspect="//test/aspect/target_mapping:aspect.bzl%map_direct_deps", - ), - expected=ExpectedResult( - success=False, - invalid_includes=[ - "File='test/aspect/target_mapping/use_lib_c.cpp', include='test/aspect/target_mapping/libs/c.h'" - ], - unused_public_deps=["//test/aspect/target_mapping/libs:a"], - ), - ), - TestCase( - name="target_mapping_c_succeed_with_transitive_mapping", - cmd=TestCmd( - target="//test/aspect/target_mapping:use_lib_c", - aspect="//test/aspect/target_mapping:aspect.bzl%map_transitive_deps", - ), - expected=ExpectedResult(success=True), - ), - TestCase( - name="skip_no_dwyu_tag", - cmd=TestCmd(target="//test/aspect/skip_tags:ignored_by_default_behavior", aspect=DEFAULT_ASPECT), - expected=ExpectedResult(success=True), - ), - TestCase( - name="skip_custom_tag", - cmd=TestCmd( - target="//test/aspect/skip_tags:ignored_by_custom_tag", - aspect="//test/aspect/skip_tags:aspect.bzl%test_aspect", - ), - expected=ExpectedResult(success=True), - ), -] +def cli(): + parser = ArgumentParser() + parser.add_argument("--verbose", "-v", action="store_true", help="Show output of test runs.") + parser.add_argument( + "--bazel", + "-b", + metavar="VERSION", + help="Run tests with the specified Bazel version. Also requires setting '--python'", + ) + parser.add_argument( + "--python", + "-p", + metavar="VERSION", + help="Run tests with the specified Python version." + " Has to be one of the versions for which we register a hermetic toolchain. Also requires setting '--bazel'.", + ) + parser.add_argument("--list", "-l", action="store_true", help="List all available test cases and return.") + parser.add_argument( + "--test", + "-t", + nargs="+", + help="Run the specified test cases. Substrings will match against all test names including them.", + ) + + parsed_args = parser.parse_args() + if (parsed_args.bazel and not parsed_args.python) or (not parsed_args.bazel and parsed_args.python): + logging.error("ERROR: '--bazel' and '--python' have to be used together") + exit(1) + + return parsed_args + if __name__ == "__main__": args = cli() - if not args: - sys.exit(1) - sys.exit( - main(args=args, test_cases=TESTS, tested_versions=TESTED_VERSIONS, version_specific_args=VERSION_SPECIFIC_ARGS) + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + exit( + main( + tested_versions=TESTED_VERSIONS, + version_specific_args=VERSION_SPECIFIC_ARGS, + bazel=args.bazel, + python=args.python, + requested_tests=args.test, + list_tests=args.list, + ) ) diff --git a/test/aspect/execute_tests_impl.py b/test/aspect/execute_tests_impl.py deleted file mode 100644 index 396a76a8..00000000 --- a/test/aspect/execute_tests_impl.py +++ /dev/null @@ -1,284 +0,0 @@ -import subprocess -from argparse import ArgumentParser, Namespace -from copy import deepcopy -from dataclasses import dataclass, field -from os import environ -from pathlib import Path -from shlex import join as join_cmd -from shutil import which -from typing import Dict, List, Optional - -# Each line in the output corresponding to an error is expected to start with this -ERRORS_PREFIX = " " * 2 - -# DWYU terminal output key fragments -DWYU_FAILURE = "Result: FAILURE" -CATEGORY_INVALID_INCLUDES = "Includes which are not available from the direct dependencies" -CATEGORY_NON_PRIVATE_DEPS = "Public dependencies which are used only in private code" -CATEGORY_UNUSED_PUBLIC_DEPS = "Unused dependencies in 'deps' (none of their headers are referenced)" -CATEGORY_UNUSED_PRIVATE_DEPS = "Unused dependencies in 'implementation_deps' (none of their headers are referenced)" - - -@dataclass -class TestedVersions: - bazel: str - python: str - - -@dataclass -class TestCmd: - target: str - aspect: str = "" - extra_args: List[str] = field(default_factory=list) - - -@dataclass -class ExpectedResult: - """ - Encapsulates an expected result of a DWYU analysis and offers functions - to compare a given output to the expectations. - """ - - success: bool - invalid_includes: List[str] = field(default_factory=list) - unused_public_deps: List[str] = field(default_factory=list) - unused_private_deps: List[str] = field(default_factory=list) - deps_which_should_be_private: List[str] = field(default_factory=list) - - def matches_expectation(self, return_code: int, dwyu_output: str) -> bool: - if not self._has_correct_status(return_code=return_code, output=dwyu_output): - return False - - output_lines = dwyu_output.split("\n") - if not ExpectedResult._has_expected_errors( - expected_errors=self.invalid_includes, - error_category=CATEGORY_INVALID_INCLUDES, - output=output_lines, - ): - return False - if not ExpectedResult._has_expected_errors( - expected_errors=self.unused_public_deps, - error_category=CATEGORY_UNUSED_PUBLIC_DEPS, - output=output_lines, - ): - return False - if not ExpectedResult._has_expected_errors( - expected_errors=self.unused_private_deps, - error_category=CATEGORY_UNUSED_PRIVATE_DEPS, - output=output_lines, - ): - return False - if not ExpectedResult._has_expected_errors( - expected_errors=self.deps_which_should_be_private, - error_category=CATEGORY_NON_PRIVATE_DEPS, - output=output_lines, - ): - return False - - return True - - def _has_correct_status(self, return_code: int, output: str) -> bool: - if self.success and return_code == 0: - return True - if not self.success and return_code != 0 and DWYU_FAILURE in output: - return True - return False - - @staticmethod - def _get_error_lines(idx_category: int, output: List[str]) -> List[str]: - errors_begin = idx_category + 1 - errors_end = 0 - for i in range(errors_begin, len(output)): - if output[i].startswith(ERRORS_PREFIX): - errors_end = i + 1 - else: - break - return output[errors_begin:errors_end] - - @staticmethod - def _has_expected_errors(expected_errors: List[str], error_category: str, output: List[str]) -> bool: - if not expected_errors: - return True - - idx_category = ExpectedResult._find_line_with(lines=output, val=error_category) - if idx_category is None: - return False - - if not ExpectedResult._has_errors( - error_lines=ExpectedResult._get_error_lines(idx_category=idx_category, output=output), - expected_errors=expected_errors, - ): - return False - return True - - @staticmethod - def _find_line_with(lines: List[str], val: str) -> Optional[int]: - for idx, line in enumerate(lines): - if val in line: - return idx - return None - - @staticmethod - def _has_errors(error_lines: List[str], expected_errors: List[str]) -> bool: - if len(error_lines) != len(expected_errors): - return False - return all(any(error in line for line in error_lines) for error in expected_errors) - - -@dataclass -class CompatibleVersions: - minimum: str = "" - maximum: str = "" - - def is_compatible_to(self, version: str) -> bool: - comply_with_min_version = version >= self.minimum if self.minimum else True - comply_with_max_version = version <= self.maximum if self.maximum else True - return comply_with_min_version and comply_with_max_version - - -@dataclass -class TestCase: - name: str - cmd: TestCmd - expected: ExpectedResult - compatible_versions: CompatibleVersions = field(default_factory=CompatibleVersions) - - -@dataclass -class FailedTest: - name: str - version: TestedVersions - - -def verify_test(test: TestCase, process: subprocess.CompletedProcess, verbose: bool) -> bool: - if test.expected.matches_expectation(return_code=process.returncode, dwyu_output=process.stdout): - if verbose: - print("\n" + process.stdout + "\n") - ok_verb = "succeeded" if test.expected.success else "failed" - print(f"<<< OK '{test.name}' {ok_verb} as expected") - return True - - print("\n" + process.stdout + "\n") - print(f"<<< ERROR '{test.name}' did not behave as expected") - return False - - -def bazel_binary() -> str: - bazel = which("bazelisk") or which("bazel") - if not bazel: - raise Exception("No bazel or bazelisk binary available on your system") - return bazel - - -def make_cmd(test_cmd: TestCmd, startup_args: List[str], extra_args: List[str]) -> List[str]: - cmd = [bazel_binary(), *startup_args, "build", "--noshow_progress"] - if test_cmd.aspect: - cmd.extend([f"--aspects={test_cmd.aspect}", "--output_groups=dwyu"]) - cmd.extend(extra_args) - cmd.extend(test_cmd.extra_args) - cmd.append("--") - cmd.append(test_cmd.target) - return cmd - - -def current_workspace() -> Path: - process = subprocess.run([bazel_binary(), "info", "workspace"], capture_output=True, text=True, check=True) - return Path(process.stdout.strip()) - - -def execute_tests( - versions: List[TestedVersions], - tests: List[TestCase], - version_specific_args: Dict[str, CompatibleVersions], - verbose: bool = False, -) -> List[FailedTest]: - failed_tests = [] - test_env = deepcopy(environ) - workspace_path = current_workspace() - output_root = Path(environ["HOME"]) / ".cache" / "bazel" / workspace_path.relative_to("/") - for version in versions: - test_env["USE_BAZEL_VERSION"] = version.bazel - output_base = output_root / f"aspect_integration_tests_bazel_{version.bazel}_python_{version.python}" - output_base.mkdir(parents=True, exist_ok=True) - extra_args = [ - arg - for arg, valid_versions in version_specific_args.items() - if valid_versions.is_compatible_to(version.bazel) - ] - extra_args.append(f"--@rules_python//python/config_settings:python_version={version.python}") - - for test in tests: - if not test.compatible_versions.is_compatible_to(version.bazel): - print(f"\n--- Skip '{test.name}' due to incompatible Bazel '{version.bazel}'") - continue - print(f"\n>>> Execute '{test.name}' with Bazel {version.bazel} and Python {version.python}") - - dwyu_cmd = make_cmd(test_cmd=test.cmd, startup_args=[f"--output_base={output_base}"], extra_args=extra_args) - if verbose: - print(f"Executing cmd: {join_cmd(dwyu_cmd)}") - process = subprocess.run( - dwyu_cmd, - env=test_env, - encoding="utf-8", - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=False, - ) - - if not verify_test(test=test, process=process, verbose=verbose): - # TODO clearer naming version -> bazel_version or take TestedVersions - failed_tests.append(FailedTest(name=test.name, version=version)) - - return failed_tests - - -def cli() -> Optional[Namespace]: - parser = ArgumentParser() - parser.add_argument("--verbose", "-v", action="store_true", help="Show output of test runs.") - parser.add_argument( - "--bazel", - "-b", - metavar="VERSION", - help="Run tests with the specified Bazel version. Also requires setting '--python'", - ) - parser.add_argument( - "--python", - "-p", - metavar="VERSION", - help="Run tests with the specified Python version." - " Has to be one of the versions for which we register a hermetic toolchain. Also requires setting '--bazel'.", - ) - parser.add_argument("--test", "-t", nargs="+", help="Run the specified test cases.") - - args = parser.parse_args() - if (args.bazel and not args.python) or (not args.bazel and args.python): - print("ERROR: '--bazel' and '--python' have to be used together") - return None - - return args - - -def main( - args: Namespace, - test_cases: List[TestCase], - tested_versions: List[TestedVersions], - version_specific_args: Dict[str, CompatibleVersions], -) -> int: - versions = [TestedVersions(bazel=args.bazel, python=args.python)] if args.bazel and args.python else tested_versions - active_tests = [tc for tc in test_cases if tc.name in args.test] if args.test else test_cases - - failed_tests = execute_tests( - versions=versions, tests=active_tests, version_specific_args=version_specific_args, verbose=args.verbose - ) - - print("\n" + 30 * "#" + " SUMMARY " + 30 * "#" + "\n") - if failed_tests: - print("FAILURE\n") - print("These tests failed:") - print( - "\n".join(f"- '{t.name}' for Bazel {t.version.bazel} and Python {t.version.python}" for t in failed_tests) - ) - return 1 - - print("SUCCESS\n") - return 0 diff --git a/test/aspect/execution_logic.py b/test/aspect/execution_logic.py new file mode 100644 index 00000000..2c4afd3a --- /dev/null +++ b/test/aspect/execution_logic.py @@ -0,0 +1,98 @@ +import logging +import subprocess +from importlib.machinery import SourceFileLoader +from os import environ +from pathlib import Path +from typing import Dict, List, Optional + +from result import Error +from test_case import TestCaseBase +from version import CompatibleVersions, TestedVersions + +TEST_CASES_DIR = Path("test/aspect") + + +def execute_test(test: TestCaseBase, version: TestedVersions, output_base: Path, extra_args: List[str]) -> bool: + if not test.compatible_bazel_versions.is_compatible_to(version.bazel): + logging.info(f"--- Skip '{test.name}' due to incompatible Bazel '{version.bazel}'\n") + return True + logging.info(f">>> Test '{test.name}' with Bazel {version.bazel} and Python {version.python}") + + succeeded = False + result = None + try: + result = test.execute_test(version=version, output_base=output_base, extra_args=extra_args) + except Exception: + logging.exception("Test failed due to exception:") + + if result is None: + result = Error("No result") + + if result.is_success(): + succeeded = True + else: + logging.info(result.error) + logging.info(f'<<< {"OK" if succeeded else "FAILURE"}\n') + + return succeeded + + +def get_current_workspace() -> Path: + process = subprocess.run(["bazel", "info", "workspace"], check=True, capture_output=True, text=True) + return Path(process.stdout.strip()) + + +def file_to_test_name(test_file: Path) -> str: + return test_file.parent.name + "/" + test_file.stem.replace("test_", "", 1) + + +def main( + tested_versions: List[TestedVersions], + version_specific_args: Dict[str, CompatibleVersions], + bazel: Optional[str] = None, + python: Optional[str] = None, + requested_tests: Optional[List[str]] = None, + list_tests: bool = False, +) -> int: + workspace_path = get_current_workspace() + tests_search_dir = workspace_path / TEST_CASES_DIR + test_files = sorted([Path(x) for x in tests_search_dir.glob("*/test_*.py")]) + + if list_tests: + test_names = [file_to_test_name(test) for test in test_files] + logging.info("Available test cases:") + logging.info("\n".join(f"- {t}" for t in test_names)) + return 0 + + tests = [] + for test in test_files: + name = file_to_test_name(test) + if (requested_tests and any(requested in name for requested in requested_tests)) or (not requested_tests): + module = SourceFileLoader("", str(test.resolve())).load_module() + tests.append(module.TestCase(name)) + + versions = [TestedVersions(bazel=bazel, python=python)] if bazel and python else tested_versions + + failed_tests = [] + output_root = Path(environ["HOME"]) / ".cache" / "bazel" / workspace_path.relative_to("/") + for version in versions: + output_base = output_root / f"aspect_integration_tests_bazel_{version.bazel}_python_{version.python}" + output_base.mkdir(parents=True, exist_ok=True) + + extra_args = [ + arg + for arg, valid_versions in version_specific_args.items() + if valid_versions.is_compatible_to(version.bazel) + ] + + for test in tests: + if not execute_test(test=test, version=version, output_base=output_base, extra_args=extra_args): + failed_tests.append(f"'{test.name}' for Bazel {version.bazel} and Python {version.python}") + + logging.info(f'Running tests {"FAILED" if failed_tests else "SUCCEEDED"}') + if failed_tests: + logging.info("\nFailed tests:") + logging.info("\n".join(f"- {failed}" for failed in failed_tests)) + return 1 + + return 0 diff --git a/test/aspect/external_repo/test_external_dependencies.py b/test/aspect/external_repo/test_external_dependencies.py new file mode 100644 index 00000000..0723f988 --- /dev/null +++ b/test/aspect/external_repo/test_external_dependencies.py @@ -0,0 +1,15 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + """ + Working with external repositories has its own challenges due to some paths behaving different compared to + working inside the own workspace. This test shows DWYU can be invoked on targets using libraries from external + workspaces without failing unexpectedly. + """ + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/external_repo:use_external_libs", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/generated_code/test_generated_code.py b/test/aspect/generated_code/test_generated_code.py new file mode 100644 index 00000000..3dea249c --- /dev/null +++ b/test/aspect/generated_code/test_generated_code.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + """ + Show that the aspect properly processes generated code which lives only in the bazel output tree. + """ + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/generated_code:foo", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/implementation_deps/aspect.bzl b/test/aspect/implementation_deps/aspect.bzl deleted file mode 100644 index 46686e05..00000000 --- a/test/aspect/implementation_deps/aspect.bzl +++ /dev/null @@ -1,3 +0,0 @@ -load("//:defs.bzl", "dwyu_aspect_factory") - -implementation_deps_aspect = dwyu_aspect_factory(use_implementation_deps = True) diff --git a/test/aspect/implementation_deps/test_proper_deps.py b/test/aspect/implementation_deps/test_proper_deps.py new file mode 100644 index 00000000..3bf09764 --- /dev/null +++ b/test/aspect/implementation_deps/test_proper_deps.py @@ -0,0 +1,18 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target=[ + "//test/aspect/implementation_deps:proper_private_deps", + "//test/aspect/implementation_deps:binary_using_foo", + "//test/aspect/implementation_deps:test_using_foo", + ], + aspect="//test/aspect:aspect.bzl%dwyu_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/implementation_deps/test_superfluous_public_dep.py b/test/aspect/implementation_deps/test_superfluous_public_dep.py new file mode 100644 index 00000000..93b66080 --- /dev/null +++ b/test/aspect/implementation_deps/test_superfluous_public_dep.py @@ -0,0 +1,14 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, deps_which_should_be_private=["//test/aspect/implementation_deps:foo"]) + actual = self._run_dwyu( + target="//test/aspect/implementation_deps:superfluous_public_dep", + aspect="//test/aspect:aspect.bzl%dwyu_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/includes/test_system_includes.py b/test/aspect/includes/test_system_includes.py new file mode 100644 index 00000000..767e31cf --- /dev/null +++ b/test/aspect/includes/test_system_includes.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/includes:all", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/platforms/test_ignore_incompatible_platforms.py b/test/aspect/platforms/test_ignore_incompatible_platforms.py new file mode 100644 index 00000000..b944168f --- /dev/null +++ b/test/aspect/platforms/test_ignore_incompatible_platforms.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/platforms:all", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/recursion/BUILD b/test/aspect/recursion/BUILD index b7c29434..d7b91765 100644 --- a/test/aspect/recursion/BUILD +++ b/test/aspect/recursion/BUILD @@ -1,5 +1,3 @@ -load("//test/aspect/recursion:rule.bzl", "dwyu_rule_direct", "dwyu_rule_recursive", "dwyu_rule_recursive_with_impl_deps") - # Has a diamond dependency towards C through A and B # main specifies its dependencies correctly, but C has an error cc_binary( @@ -11,15 +9,15 @@ cc_binary( ], ) -dwyu_rule_direct( - name = "dwyu_direct_main", - deps = [":main"], +cc_library( + name = "use_impl_deps", + srcs = ["use_impl_deps.cpp"], + implementation_deps = [":a"], ) -dwyu_rule_recursive( - name = "dwyu_recursive_main", - deps = [":main"], -) +## +## Support Targets +## cc_library( name = "a", @@ -51,14 +49,3 @@ cc_library( name = "e", hdrs = ["e.h"], ) - -cc_library( - name = "use_impl_deps", - srcs = ["use_impl_deps.cpp"], - implementation_deps = [":a"], -) - -dwyu_rule_recursive_with_impl_deps( - name = "dwyu_recursive_use_impl_deps", - deps = [":use_impl_deps"], -) diff --git a/test/aspect/recursion/README.md b/test/aspect/recursion/README.md index 1c2c360c..9afa9f50 100644 --- a/test/aspect/recursion/README.md +++ b/test/aspect/recursion/README.md @@ -1,3 +1 @@ The aspect can analyze either solely the targets is being executed on or it can analyze the whole dependency tree. - -One can use a rule to perform the aspect analysis as part of a normal build. diff --git a/test/aspect/recursion/aspect.bzl b/test/aspect/recursion/aspect.bzl index 522b789f..b6df64c1 100644 --- a/test/aspect/recursion/aspect.bzl +++ b/test/aspect/recursion/aspect.bzl @@ -1,4 +1,4 @@ load("//:defs.bzl", "dwyu_aspect_factory") -recursive_aspect = dwyu_aspect_factory(recursive = True) -recursive_impl_deps_aspect = dwyu_aspect_factory(recursive = True, use_implementation_deps = True) +dwyu_recursive = dwyu_aspect_factory(recursive = True) +dwyu_recursive_impl_deps = dwyu_aspect_factory(recursive = True, use_implementation_deps = True) diff --git a/test/aspect/recursion/test_find_transitive_problem_via_deps.py b/test/aspect/recursion/test_find_transitive_problem_via_deps.py new file mode 100644 index 00000000..650027c8 --- /dev/null +++ b/test/aspect/recursion/test_find_transitive_problem_via_deps.py @@ -0,0 +1,12 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]) + actual = self._run_dwyu( + target="//test/aspect/recursion:main", aspect="//test/aspect/recursion:aspect.bzl%dwyu_recursive" + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/recursion/test_find_transitive_problem_via_iml_deps.py b/test/aspect/recursion/test_find_transitive_problem_via_iml_deps.py new file mode 100644 index 00000000..85811621 --- /dev/null +++ b/test/aspect/recursion/test_find_transitive_problem_via_iml_deps.py @@ -0,0 +1,14 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/recursion:e"]) + actual = self._run_dwyu( + target="//test/aspect/recursion:use_impl_deps", + aspect="//test/aspect/recursion:aspect.bzl%dwyu_recursive_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/relative_includes/test_relative_includes.py b/test/aspect/relative_includes/test_relative_includes.py new file mode 100644 index 00000000..d426817c --- /dev/null +++ b/test/aspect/relative_includes/test_relative_includes.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/relative_includes:all", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/result.py b/test/aspect/result.py new file mode 100644 index 00000000..12ed773b --- /dev/null +++ b/test/aspect/result.py @@ -0,0 +1,132 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import List, Optional + + +class Result(ABC): + def __init__(self, error: str = "") -> None: + self.error = error + + @abstractmethod + def is_success(self) -> bool: + pass + + +class Error(Result): + def __init__(self, error: str) -> None: + super().__init__(error) + + def is_success(self) -> bool: + return False + + +class Success(Result): + def __init__(self) -> None: + super().__init__() + + def is_success(self) -> bool: + return True + + +# Each line in the output corresponding to an error is expected to start with this +ERRORS_PREFIX = " " * 2 + +# DWYU terminal output key fragments +DWYU_FAILURE = "Result: FAILURE" +CATEGORY_INVALID_INCLUDES = "Includes which are not available from the direct dependencies" +CATEGORY_NON_PRIVATE_DEPS = "Public dependencies which are used only in private code" +CATEGORY_UNUSED_PUBLIC_DEPS = "Unused dependencies in 'deps' (none of their headers are referenced)" +CATEGORY_UNUSED_PRIVATE_DEPS = "Unused dependencies in 'implementation_deps' (none of their headers are referenced)" + + +@dataclass +class ExpectedResult: + """ + Encapsulates an expected result of a DWYU analysis and offers functions + to compare a given output to the expectations. + """ + + success: bool + invalid_includes: List[str] = field(default_factory=list) + unused_public_deps: List[str] = field(default_factory=list) + unused_private_deps: List[str] = field(default_factory=list) + deps_which_should_be_private: List[str] = field(default_factory=list) + + def matches_expectation(self, return_code: int, dwyu_output: str) -> bool: + if not self._has_correct_status(return_code=return_code, output=dwyu_output): + return False + + output_lines = dwyu_output.split("\n") + if not ExpectedResult._has_expected_errors( + expected_errors=self.invalid_includes, + error_category=CATEGORY_INVALID_INCLUDES, + output=output_lines, + ): + return False + if not ExpectedResult._has_expected_errors( + expected_errors=self.unused_public_deps, + error_category=CATEGORY_UNUSED_PUBLIC_DEPS, + output=output_lines, + ): + return False + if not ExpectedResult._has_expected_errors( + expected_errors=self.unused_private_deps, + error_category=CATEGORY_UNUSED_PRIVATE_DEPS, + output=output_lines, + ): + return False + if not ExpectedResult._has_expected_errors( + expected_errors=self.deps_which_should_be_private, + error_category=CATEGORY_NON_PRIVATE_DEPS, + output=output_lines, + ): + return False + + return True + + def _has_correct_status(self, return_code: int, output: str) -> bool: + if self.success and return_code == 0: + return True + if not self.success and return_code != 0 and DWYU_FAILURE in output: + return True + return False + + @staticmethod + def _get_error_lines(idx_category: int, output: List[str]) -> List[str]: + errors_begin = idx_category + 1 + errors_end = 0 + for i in range(errors_begin, len(output)): + if output[i].startswith(ERRORS_PREFIX): + errors_end = i + 1 + else: + break + return output[errors_begin:errors_end] + + @staticmethod + def _has_expected_errors(expected_errors: List[str], error_category: str, output: List[str]) -> bool: + if not expected_errors: + return True + + idx_category = ExpectedResult._find_line_with(lines=output, val=error_category) + if idx_category is None: + return False + + if not ExpectedResult._has_errors( + error_lines=ExpectedResult._get_error_lines(idx_category=idx_category, output=output), + expected_errors=expected_errors, + ): + return False + return True + + @staticmethod + def _find_line_with(lines: List[str], val: str) -> Optional[int]: + for idx, line in enumerate(lines): + if val in line: + return idx + return None + + @staticmethod + def _has_errors(error_lines: List[str], expected_errors: List[str]) -> bool: + if len(error_lines) != len(expected_errors): + return False + return all(any(error in line for line in error_lines) for error in expected_errors) diff --git a/test/aspect/execute_tests_utest.py b/test/aspect/result_test.py similarity index 71% rename from test/aspect/execute_tests_utest.py rename to test/aspect/result_test.py index 8946d635..3a088032 100644 --- a/test/aspect/execute_tests_utest.py +++ b/test/aspect/result_test.py @@ -1,17 +1,15 @@ import unittest -from shutil import which -from execute_tests_impl import ( +from result import ( CATEGORY_INVALID_INCLUDES, CATEGORY_NON_PRIVATE_DEPS, CATEGORY_UNUSED_PRIVATE_DEPS, CATEGORY_UNUSED_PUBLIC_DEPS, DWYU_FAILURE, ERRORS_PREFIX, - CompatibleVersions, + Error, ExpectedResult, - TestCmd, - make_cmd, + Success, ) @@ -164,84 +162,16 @@ def test_expected_fail_due_to_non_private_deps_fails_on_other_error(self): ) -class TestMakeCmd(unittest.TestCase): - @staticmethod - def _base_cmd(startup_args=None): - bazel = which("bazelisk") or which("bazel") - build_with_default_options = ["build", "--noshow_progress"] - if startup_args: - return [bazel, *startup_args, *build_with_default_options] - return [bazel, *build_with_default_options] - - def test_basic_cmd(self): - cmd = make_cmd(test_cmd=TestCmd(target="//foo:bar"), extra_args=[], startup_args=[]) - self.assertEqual(cmd, [*self._base_cmd(), "--", "//foo:bar"]) - - def test_complex_test_cmd(self): - cmd = make_cmd( - test_cmd=TestCmd( - target="//foo:bar", - aspect="//some/aspect.bzl", - extra_args=["--abc", "--cba"], - ), - extra_args=[], - startup_args=[], - ) - self.assertEqual( - cmd, - [ - *self._base_cmd(), - "--aspects=//some/aspect.bzl", - "--output_groups=dwyu", - "--abc", - "--cba", - "--", - "//foo:bar", - ], - ) - - def test_extra_args_on_top_of_test_cmd(self): - cmd = make_cmd( - test_cmd=TestCmd( - target="//foo:bar", - aspect="//some/aspect.bzl", - extra_args=["--test_related_arg"], - ), - extra_args=["--outside_arg"], - startup_args=["--some_startup_arg"], - ) - self.assertEqual( - cmd, - [ - *self._base_cmd(startup_args=["--some_startup_arg"]), - "--aspects=//some/aspect.bzl", - "--output_groups=dwyu", - "--outside_arg", - "--test_related_arg", - "--", - "//foo:bar", - ], - ) - - -class TestIsCompatibleVersion(unittest.TestCase): - def test_no_limits(self): - self.assertTrue(CompatibleVersions().is_compatible_to("1.0.0")) - - def test_above_min_version(self): - self.assertTrue(CompatibleVersions(minimum="0.9.9").is_compatible_to("1.0.0")) - - def test_below_min_version(self): - self.assertFalse(CompatibleVersions(minimum="1.1.9").is_compatible_to("1.0.0")) - - def test_below_max_version(self): - self.assertTrue(CompatibleVersions(maximum="1.1.0").is_compatible_to("1.0.0")) - - def test_above_max_version(self): - self.assertFalse(CompatibleVersions(maximum="0.9.0").is_compatible_to("1.0.0")) +class TestResult(unittest.TestCase): + def test_success(self): + unit = Success() + self.assertTrue(unit.is_success()) + self.assertEqual(unit.error, "") - def test_inside_interval(self): - self.assertTrue(CompatibleVersions(minimum="0.9.0", maximum="1.1.0").is_compatible_to("1.0.0")) + def test_error(self): + unit = Error("foo") + self.assertFalse(unit.is_success()) + self.assertEqual(unit.error, "foo") if __name__ == "__main__": diff --git a/test/aspect/rule_using_aspect/BUILD b/test/aspect/rule_using_aspect/BUILD new file mode 100644 index 00000000..3da3f05c --- /dev/null +++ b/test/aspect/rule_using_aspect/BUILD @@ -0,0 +1,51 @@ +load("//test/aspect/rule_using_aspect:rule.bzl", "dwyu_rule_direct", "dwyu_rule_recursive", "dwyu_rule_recursive_with_impl_deps") + +dwyu_rule_direct( + name = "dwyu_direct_main", + deps = [":main"], +) + +dwyu_rule_recursive( + name = "dwyu_recursive_main", + deps = [":main"], +) + +dwyu_rule_recursive_with_impl_deps( + name = "dwyu_recursive_with_impl_deps", + deps = [":use_impl_deps"], +) + +## +## Support Targets +## + +cc_binary( + name = "main", + srcs = ["main.cpp"], + deps = [":a"], +) + +cc_library( + name = "a", + hdrs = ["a.h"], + deps = [":b"], +) + +cc_library( + name = "b", + hdrs = ["b.h"], + deps = [ + ":c", # unused dependency + ], +) + +cc_library( + name = "c", + hdrs = ["c.h"], +) + +cc_library( + name = "use_impl_deps", + srcs = ["use_impl_deps.cpp"], + implementation_deps = [":a"], +) diff --git a/test/aspect/rule_using_aspect/README.md b/test/aspect/rule_using_aspect/README.md new file mode 100644 index 00000000..204a361b --- /dev/null +++ b/test/aspect/rule_using_aspect/README.md @@ -0,0 +1 @@ +One can create a rule which executes the DWYU aspect as part of a normal build ond specific targets. diff --git a/test/aspect/rule_using_aspect/a.h b/test/aspect/rule_using_aspect/a.h new file mode 100644 index 00000000..5f4cda2e --- /dev/null +++ b/test/aspect/rule_using_aspect/a.h @@ -0,0 +1,6 @@ +#ifndef A_H +#define A_H + +#include "test/aspect/rule_using_aspect/b.h" + +#endif diff --git a/test/aspect/rule_using_aspect/aspect.bzl b/test/aspect/rule_using_aspect/aspect.bzl new file mode 100644 index 00000000..b6df64c1 --- /dev/null +++ b/test/aspect/rule_using_aspect/aspect.bzl @@ -0,0 +1,4 @@ +load("//:defs.bzl", "dwyu_aspect_factory") + +dwyu_recursive = dwyu_aspect_factory(recursive = True) +dwyu_recursive_impl_deps = dwyu_aspect_factory(recursive = True, use_implementation_deps = True) diff --git a/test/aspect/rule_using_aspect/b.h b/test/aspect/rule_using_aspect/b.h new file mode 100644 index 00000000..827afc87 --- /dev/null +++ b/test/aspect/rule_using_aspect/b.h @@ -0,0 +1,6 @@ +#ifndef B_H +#define B_H + +// Do nothing + +#endif diff --git a/test/aspect/rule_using_aspect/c.h b/test/aspect/rule_using_aspect/c.h new file mode 100644 index 00000000..2265c63c --- /dev/null +++ b/test/aspect/rule_using_aspect/c.h @@ -0,0 +1,6 @@ +#ifndef C_H +#define C_H + +// Do nothing + +#endif diff --git a/test/aspect/rule_using_aspect/main.cpp b/test/aspect/rule_using_aspect/main.cpp new file mode 100644 index 00000000..b26a3be6 --- /dev/null +++ b/test/aspect/rule_using_aspect/main.cpp @@ -0,0 +1,5 @@ +#include "test/aspect/rule_using_aspect/a.h" + +int main() { + return 0; +} diff --git a/test/aspect/recursion/rule.bzl b/test/aspect/rule_using_aspect/rule.bzl similarity index 59% rename from test/aspect/recursion/rule.bzl rename to test/aspect/rule_using_aspect/rule.bzl index 2e71618e..06ed6322 100644 --- a/test/aspect/recursion/rule.bzl +++ b/test/aspect/rule_using_aspect/rule.bzl @@ -1,5 +1,5 @@ -load("//test/aspect:aspect.bzl", "dwyu_default_aspect") -load("//test/aspect/recursion:aspect.bzl", "recursive_aspect", "recursive_impl_deps_aspect") +load("//test/aspect:aspect.bzl", "dwyu") +load("//test/aspect/rule_using_aspect:aspect.bzl", "dwyu_recursive", "dwyu_recursive_impl_deps") def _dwyu_rule_impl(ctx): # gather artifacts to make sure the aspect is executed @@ -9,20 +9,20 @@ def _dwyu_rule_impl(ctx): dwyu_rule_direct = rule( implementation = _dwyu_rule_impl, attrs = { - "deps": attr.label_list(aspects = [dwyu_default_aspect]), + "deps": attr.label_list(aspects = [dwyu]), }, ) dwyu_rule_recursive = rule( implementation = _dwyu_rule_impl, attrs = { - "deps": attr.label_list(aspects = [recursive_aspect]), + "deps": attr.label_list(aspects = [dwyu_recursive]), }, ) dwyu_rule_recursive_with_impl_deps = rule( implementation = _dwyu_rule_impl, attrs = { - "deps": attr.label_list(aspects = [recursive_impl_deps_aspect]), + "deps": attr.label_list(aspects = [dwyu_recursive_impl_deps]), }, ) diff --git a/test/aspect/rule_using_aspect/test_dwyu_rule.py b/test/aspect/rule_using_aspect/test_dwyu_rule.py new file mode 100644 index 00000000..fdd0e46b --- /dev/null +++ b/test/aspect/rule_using_aspect/test_dwyu_rule.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_bazel_build(target="//test/aspect/rule_using_aspect:dwyu_direct_main") + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/rule_using_aspect/test_recursive_dwyu_rule.py b/test/aspect/rule_using_aspect/test_recursive_dwyu_rule.py new file mode 100644 index 00000000..bbcbfe4f --- /dev/null +++ b/test/aspect/rule_using_aspect/test_recursive_dwyu_rule.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/rule_using_aspect:c"]) + actual = self._run_bazel_build(target="//test/aspect/rule_using_aspect:dwyu_recursive_main") + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/rule_using_aspect/test_recursive_dwyu_rule_with_impl_deps.py b/test/aspect/rule_using_aspect/test_recursive_dwyu_rule_with_impl_deps.py new file mode 100644 index 00000000..0d4f9849 --- /dev/null +++ b/test/aspect/rule_using_aspect/test_recursive_dwyu_rule_with_impl_deps.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/rule_using_aspect:c"]) + actual = self._run_bazel_build( + target="//test/aspect/rule_using_aspect:dwyu_recursive_with_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/rule_using_aspect/use_impl_deps.cpp b/test/aspect/rule_using_aspect/use_impl_deps.cpp new file mode 100644 index 00000000..7b608f75 --- /dev/null +++ b/test/aspect/rule_using_aspect/use_impl_deps.cpp @@ -0,0 +1,5 @@ +#include "test/aspect/rule_using_aspect/a.h" + +int doSth() { + return 42; +} diff --git a/test/aspect/shared_library/README.md b/test/aspect/shared_library/README.md new file mode 100644 index 00000000..cc62735f --- /dev/null +++ b/test/aspect/shared_library/README.md @@ -0,0 +1,2 @@ +There are cases where targets have no sources due to only wrapping other targets. +DWYU shall be able to gracefully handle those by skipping them. diff --git a/test/aspect/shared_library/foo.h b/test/aspect/shared_library/foo.h index 3a737737..5d5f8f0c 100644 --- a/test/aspect/shared_library/foo.h +++ b/test/aspect/shared_library/foo.h @@ -1,6 +1 @@ -#ifndef FOO_H_ -#define FOO_H_ - int foo(); - -#endif // FOO_H_ diff --git a/test/aspect/shared_library/test_shared_library_without_srcs.py b/test/aspect/shared_library/test_shared_library_without_srcs.py new file mode 100644 index 00000000..d414cda2 --- /dev/null +++ b/test/aspect/shared_library/test_shared_library_without_srcs.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/shared_library:libfoo.so", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/skip_tags/aspect.bzl b/test/aspect/skip_tags/aspect.bzl index 12d74d82..008463c1 100644 --- a/test/aspect/skip_tags/aspect.bzl +++ b/test/aspect/skip_tags/aspect.bzl @@ -1,3 +1,3 @@ load("//:defs.bzl", "dwyu_aspect_factory") -test_aspect = dwyu_aspect_factory(skipped_tags = ["tag_marking_skipping"]) +dwyu_custom_tags = dwyu_aspect_factory(skipped_tags = ["tag_marking_skipping"]) diff --git a/test/aspect/skip_tags/test_skip_custom_tag.py b/test/aspect/skip_tags/test_skip_custom_tag.py new file mode 100644 index 00000000..723feec0 --- /dev/null +++ b/test/aspect/skip_tags/test_skip_custom_tag.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/skip_tags:ignored_by_custom_tag", + aspect="//test/aspect/skip_tags:aspect.bzl%dwyu_custom_tags", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/skip_tags/test_skip_no_dwyu_tag.py b/test/aspect/skip_tags/test_skip_no_dwyu_tag.py new file mode 100644 index 00000000..7b0d896a --- /dev/null +++ b/test/aspect/skip_tags/test_skip_no_dwyu_tag.py @@ -0,0 +1,12 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/skip_tags:ignored_by_default_behavior", aspect=self.default_aspect + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_b_with_direct_deps_mapping.py b/test/aspect/target_mapping/test_use_b_with_direct_deps_mapping.py new file mode 100644 index 00000000..155d4376 --- /dev/null +++ b/test/aspect/target_mapping/test_use_b_with_direct_deps_mapping.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_b", + aspect="//test/aspect/target_mapping:aspect.bzl%map_direct_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_b_with_specific_mapping.py b/test/aspect/target_mapping/test_use_b_with_specific_mapping.py new file mode 100644 index 00000000..95d55441 --- /dev/null +++ b/test/aspect/target_mapping/test_use_b_with_specific_mapping.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_b", + aspect="//test/aspect/target_mapping:aspect.bzl%map_specific_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_b_with_transitive_deps_mapping.py b/test/aspect/target_mapping/test_use_b_with_transitive_deps_mapping.py new file mode 100644 index 00000000..a8e652b0 --- /dev/null +++ b/test/aspect/target_mapping/test_use_b_with_transitive_deps_mapping.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_b", + aspect="//test/aspect/target_mapping:aspect.bzl%map_transitive_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_c_with_direct_deps_mapping.py b/test/aspect/target_mapping/test_use_c_with_direct_deps_mapping.py new file mode 100644 index 00000000..a120b231 --- /dev/null +++ b/test/aspect/target_mapping/test_use_c_with_direct_deps_mapping.py @@ -0,0 +1,19 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='test/aspect/target_mapping/use_lib_c.cpp', include='test/aspect/target_mapping/libs/c.h'" + ], + unused_public_deps=["//test/aspect/target_mapping/libs:a"], + ) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_c", + aspect="//test/aspect/target_mapping:aspect.bzl%map_direct_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_c_with_specific_mapping.py b/test/aspect/target_mapping/test_use_c_with_specific_mapping.py new file mode 100644 index 00000000..28b09f75 --- /dev/null +++ b/test/aspect/target_mapping/test_use_c_with_specific_mapping.py @@ -0,0 +1,19 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='test/aspect/target_mapping/use_lib_c.cpp', include='test/aspect/target_mapping/libs/c.h'" + ], + unused_public_deps=["//test/aspect/target_mapping/libs:a"], + ) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_c", + aspect="//test/aspect/target_mapping:aspect.bzl%map_specific_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/target_mapping/test_use_c_with_transitive_deps_mapping.py b/test/aspect/target_mapping/test_use_c_with_transitive_deps_mapping.py new file mode 100644 index 00000000..a2762652 --- /dev/null +++ b/test/aspect/target_mapping/test_use_c_with_transitive_deps_mapping.py @@ -0,0 +1,13 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu( + target="//test/aspect/target_mapping:use_lib_c", + aspect="//test/aspect/target_mapping:aspect.bzl%map_transitive_deps", + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/test_case.py b/test/aspect/test_case.py new file mode 100644 index 00000000..7bfd5054 --- /dev/null +++ b/test/aspect/test_case.py @@ -0,0 +1,118 @@ +import logging +import subprocess +from abc import ABC, abstractmethod +from copy import deepcopy +from os import environ +from pathlib import Path +from shlex import join as shlex_join +from shutil import which +from typing import List, Optional, Union + +from result import Error, ExpectedResult, Result, Success +from version import CompatibleVersions, TestedVersions + + +class TestCaseBase(ABC): + def __init__(self, name: str) -> None: + self._name = name + self._tested_versions = TestedVersions(bazel="", python="") + self._output_base = Path("") + self._extra_args: List[str] = [] + + # + # Interface + # + + @abstractmethod + def execute_test_logic(self) -> Result: + """ + Overwrite this to implement a concrete test case + """ + pass + + @property + def compatible_bazel_versions(self) -> CompatibleVersions: + """ + Overwrite this if the test case works only with specific Bazel versions + """ + return CompatibleVersions() + + # + # Base Implementation + # + + @property + def name(self) -> str: + return self._name + + @property + def default_aspect(self) -> str: + return "//test/aspect:aspect.bzl%dwyu" + + def execute_test(self, version: TestedVersions, output_base: Path, extra_args: List[str]) -> Result: + self._tested_versions = version + self._output_base = output_base + self._extra_args = extra_args + return self.execute_test_logic() + + @staticmethod + def _check_result(actual: subprocess.CompletedProcess, expected: ExpectedResult) -> Result: + as_expected = expected.matches_expectation(return_code=actual.returncode, dwyu_output=actual.stdout) + + log_level = logging.DEBUG if as_expected else logging.INFO + logging.log(log_level, "----- DWYU stdout -----") + logging.log(log_level, actual.stdout.strip()) + logging.log(log_level, "----- DWYU stderr -----") + logging.log(log_level, actual.stderr.strip()) + logging.log(log_level, "-----------------------") + + return Success() if as_expected else Error("DWYU did not behave as expected") + + def _run_dwyu( + self, target: Union[str, List[str]], aspect: str, extra_args: Optional[List[str]] = None + ) -> subprocess.CompletedProcess: + extra_args = extra_args if extra_args else [] + return self._run_bazel_build( + target=target, + extra_args=[ + f"--@rules_python//python/config_settings:python_version={self._tested_versions.python}", + f"--aspects={aspect}", + "--output_groups=dwyu", + *extra_args, + ], + ) + + def _run_bazel_build( + self, target: Union[str, List[str]], extra_args: Optional[List[str]] = None + ) -> subprocess.CompletedProcess: + extra_args = extra_args if extra_args else [] + targets = target if isinstance(target, list) else [target] + + test_env = deepcopy(environ) + test_env["USE_BAZEL_VERSION"] = self._tested_versions.bazel + + cmd = [ + self._bazel_binary(), + f"--output_base={self._output_base}", + # Testing over many Bazel versions does work well with a static bazelrc file including flags which might not + # be available in a some tested Bazel version. + "--noworkspace_rc", + "build", + "--experimental_convenience_symlinks=ignore", + "--noshow_progress", + "--nolegacy_external_runfiles", + *self._extra_args, + *extra_args, + "--", + *targets, + ] + logging.debug(f"Executing: {shlex_join(cmd)}\n") + + return subprocess.run(cmd, env=test_env, capture_output=True, text=True, check=False) + + @staticmethod + def _bazel_binary() -> str: + bazel = which("bazel") or which("bazelisk") + if not bazel: + raise Exception("No bazel or bazelisk binary available on your system") + return bazel diff --git a/test/aspect/test_case_test.py b/test/aspect/test_case_test.py new file mode 100644 index 00000000..0c925004 --- /dev/null +++ b/test/aspect/test_case_test.py @@ -0,0 +1,155 @@ +import unittest +from pathlib import Path +from typing import Any, List +from unittest.mock import MagicMock, Mock, patch + +from result import Error, Result, Success +from test_case import TestCaseBase +from version import TestedVersions + + +class TestCaseMock(TestCaseBase): + def __init__(self, name: str): + super().__init__(name) + self.result = Success() + self.dwyu_extra_args: List[str] = [] + self.target = "//foo:bar" + + def execute_test_logic(self) -> Result: + self._run_dwyu(target=self.target, aspect="//some:aspect", extra_args=self.dwyu_extra_args) + return self.result + + +class TestCaseTests(unittest.TestCase): + def setUp(self): + self.unit = TestCaseMock("foo") + self.unit._bazel_binary = Mock(return_value="/bazel/binary") + + @staticmethod + def get_cmd(mock: MagicMock) -> Any: + """ + We expect only a single call happened. + A call object is a tuple of (name, positional args, keyword args) and the cmd is the first positional argument. + """ + return mock.mock_calls[0][1][0] + + @staticmethod + def get_env(mock: MagicMock) -> Any: + """ + We expect only a single call happened. + A call object is a tuple of (name, positional args, keyword args) and the env is part of args. + """ + return mock.mock_calls[0][2]["env"] + + def test_get_name(self): + self.assertEqual(self.unit.name, "foo") + + def test_get_default_aspect(self): + self.assertEqual(self.unit.default_aspect, "//test/aspect:aspect.bzl%dwyu") + + @patch("subprocess.run") + def test_get_success(self, _): + result = self.unit.execute_test( + version=TestedVersions(bazel="6.4.2", python="13.37"), output_base=Path("/some/path"), extra_args=[] + ) + self.assertTrue(result.is_success()) + + @patch("subprocess.run") + def test_get_error(self, _): + self.unit.result = Error("some failure") + result = self.unit.execute_test( + version=TestedVersions(bazel="6.4.2", python="13.37"), output_base=Path("/some/path"), extra_args=[] + ) + self.assertFalse(result.is_success()) + self.assertEqual(result.error, "some failure") + + @patch("subprocess.run") + def test_dwyu_command_without_any_extra_args(self, run_mock): + self.unit.execute_test( + version=TestedVersions(bazel="6.4.2", python="13.37"), output_base=Path("/some/path"), extra_args=[] + ) + + run_mock.assert_called_once() + self.assertListEqual( + self.get_cmd(run_mock), + [ + "/bazel/binary", + "--output_base=/some/path", + "--noworkspace_rc", + "build", + "--experimental_convenience_symlinks=ignore", + "--noshow_progress", + "--nolegacy_external_runfiles", + "--@rules_python//python/config_settings:python_version=13.37", + "--aspects=//some:aspect", + "--output_groups=dwyu", + "--", + "//foo:bar", + ], + ) + self.assertEqual(self.get_env(run_mock)["USE_BAZEL_VERSION"], "6.4.2") + + @patch("subprocess.run") + def test_dwyu_command_with_global_and_dwyu_extra_args(self, run_mock): + self.unit.dwyu_extra_args = ["--some_arg=42", "--another_arg"] + self.unit.execute_test( + version=TestedVersions(bazel="6.4.2", python="13.37"), + output_base=Path("/some/path"), + extra_args=["--global_arg=23", "--another_global_arg"], + ) + + run_mock.assert_called_once() + self.assertListEqual( + self.get_cmd(run_mock), + [ + "/bazel/binary", + "--output_base=/some/path", + "--noworkspace_rc", + "build", + "--experimental_convenience_symlinks=ignore", + "--noshow_progress", + "--nolegacy_external_runfiles", + "--global_arg=23", + "--another_global_arg", + "--@rules_python//python/config_settings:python_version=13.37", + "--aspects=//some:aspect", + "--output_groups=dwyu", + "--some_arg=42", + "--another_arg", + "--", + "//foo:bar", + ], + ) + self.assertEqual(self.get_env(run_mock)["USE_BAZEL_VERSION"], "6.4.2") + + @patch("subprocess.run") + def test_dwyu_command_with_multiple_targets(self, run_mock): + self.unit.target = ["//foo:bar", "//tick:tock"] + self.unit.execute_test( + version=TestedVersions(bazel="6.4.2", python="13.37"), output_base=Path("/some/path"), extra_args=[] + ) + + run_mock.assert_called_once() + self.assertListEqual( + self.get_cmd(run_mock), + [ + "/bazel/binary", + "--output_base=/some/path", + "--noworkspace_rc", + "build", + "--experimental_convenience_symlinks=ignore", + "--noshow_progress", + "--nolegacy_external_runfiles", + "--@rules_python//python/config_settings:python_version=13.37", + "--aspects=//some:aspect", + "--output_groups=dwyu", + "--", + "//foo:bar", + "//tick:tock", + ], + ) + self.assertEqual(self.get_env(run_mock)["USE_BAZEL_VERSION"], "6.4.2") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/aspect/tree_artifact/test_invalid_tree_artifact.py b/test/aspect/tree_artifact/test_invalid_tree_artifact.py new file mode 100644 index 00000000..58e393a0 --- /dev/null +++ b/test/aspect/tree_artifact/test_invalid_tree_artifact.py @@ -0,0 +1,19 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='bazel-out/k8-fastbuild/bin/test/aspect/tree_artifact/sources.cc/tree_lib.cc', include='test/aspect/tree_artifact/some_lib.h'" + ], + ) + actual = self._run_dwyu( + target="//test/aspect/tree_artifact:tree_artifact_library", + aspect=self.default_aspect, + extra_args=["--compilation_mode=fastbuild"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/tree_artifact/test_use_tree_artifact_correctly.py b/test/aspect/tree_artifact/test_use_tree_artifact_correctly.py new file mode 100644 index 00000000..b88ca60b --- /dev/null +++ b/test/aspect/tree_artifact/test_use_tree_artifact_correctly.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/tree_artifact:use_tree_artifact", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/unused_dep/aspect.bzl b/test/aspect/unused_dep/aspect.bzl deleted file mode 100644 index 46686e05..00000000 --- a/test/aspect/unused_dep/aspect.bzl +++ /dev/null @@ -1,3 +0,0 @@ -load("//:defs.bzl", "dwyu_aspect_factory") - -implementation_deps_aspect = dwyu_aspect_factory(use_implementation_deps = True) diff --git a/test/aspect/unused_dep/test_detect_unused_dep.py b/test/aspect/unused_dep/test_detect_unused_dep.py new file mode 100644 index 00000000..e519eab7 --- /dev/null +++ b/test/aspect/unused_dep/test_detect_unused_dep.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_public_deps=["//test/aspect/unused_dep:foo"]) + actual = self._run_dwyu(target="//test/aspect/unused_dep:main", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/unused_dep/test_detect_unused_impl_dep.py b/test/aspect/unused_dep/test_detect_unused_impl_dep.py new file mode 100644 index 00000000..43060a6c --- /dev/null +++ b/test/aspect/unused_dep/test_detect_unused_impl_dep.py @@ -0,0 +1,14 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=False, unused_private_deps=["//test/aspect/unused_dep:foo"]) + actual = self._run_dwyu( + target="//test/aspect/unused_dep/implementation_deps:implementation_deps_lib", + aspect="//test/aspect:aspect.bzl%dwyu_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/using_transitive_dep/BUILD b/test/aspect/using_transitive_dep/BUILD index 956302ec..b5a5d6ce 100644 --- a/test/aspect/using_transitive_dep/BUILD +++ b/test/aspect/using_transitive_dep/BUILD @@ -14,3 +14,9 @@ cc_binary( srcs = ["main.cpp"], deps = [":bar"], ) + +cc_library( + name = "transitive_usage_through_impl_deps", + srcs = ["transitive_usage_through_impl_deps.h"], + deps = [":bar"], +) diff --git a/test/aspect/using_transitive_dep/test_detect_using_transitive_dep.py b/test/aspect/using_transitive_dep/test_detect_using_transitive_dep.py new file mode 100644 index 00000000..f59c3cd9 --- /dev/null +++ b/test/aspect/using_transitive_dep/test_detect_using_transitive_dep.py @@ -0,0 +1,15 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='test/aspect/using_transitive_dep/main.cpp', include='test/aspect/using_transitive_dep/foo.h'" + ], + ) + actual = self._run_dwyu(target="//test/aspect/using_transitive_dep:main", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/using_transitive_dep/test_detect_using_transitive_impl_dep.py b/test/aspect/using_transitive_dep/test_detect_using_transitive_impl_dep.py new file mode 100644 index 00000000..1223f05a --- /dev/null +++ b/test/aspect/using_transitive_dep/test_detect_using_transitive_impl_dep.py @@ -0,0 +1,19 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult( + success=False, + invalid_includes=[ + "File='test/aspect/using_transitive_dep/transitive_usage_through_impl_deps.h', include='test/aspect/using_transitive_dep/foo.h'" + ], + ) + actual = self._run_dwyu( + target="//test/aspect/using_transitive_dep:transitive_usage_through_impl_deps", + aspect="//test/aspect:aspect.bzl%dwyu_impl_deps", + extra_args=["--experimental_cc_implementation_deps"], + ) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/using_transitive_dep/transitive_usage_through_impl_deps.h b/test/aspect/using_transitive_dep/transitive_usage_through_impl_deps.h new file mode 100644 index 00000000..df11caf4 --- /dev/null +++ b/test/aspect/using_transitive_dep/transitive_usage_through_impl_deps.h @@ -0,0 +1,9 @@ +#include "test/aspect/using_transitive_dep/foo.h" +#include "test/aspect/using_transitive_dep/bar.h" + +int doSth() { + // ERROR: Using a function from library foo but depending only on library bar + const int answer = theAnswer(); + const int stuff = doStuff(); + return answer != stuff; +} diff --git a/test/aspect/valid/test_common_valid_case.py b/test/aspect/valid/test_common_valid_case.py new file mode 100644 index 00000000..f17e66a7 --- /dev/null +++ b/test/aspect/valid/test_common_valid_case.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/valid:bar", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/test/aspect/version.py b/test/aspect/version.py new file mode 100644 index 00000000..30f48a2c --- /dev/null +++ b/test/aspect/version.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + + +@dataclass +class TestedVersions: + bazel: str + python: str + + +@dataclass +class CompatibleVersions: + minimum: str = "" + before: str = "" + + def is_compatible_to(self, version: str) -> bool: + comply_with_min_version = version >= self.minimum if self.minimum else True + comply_with_max_version = version < self.before if self.before else True + return comply_with_min_version and comply_with_max_version diff --git a/test/aspect/version_test.py b/test/aspect/version_test.py new file mode 100644 index 00000000..7d70e9f9 --- /dev/null +++ b/test/aspect/version_test.py @@ -0,0 +1,33 @@ +import unittest + +from version import CompatibleVersions + + +class TestIsCompatibleVersion(unittest.TestCase): + def test_no_limits(self): + self.assertTrue(CompatibleVersions().is_compatible_to("1.0.0")) + + def test_above_min_version(self): + self.assertTrue(CompatibleVersions(minimum="0.9.9").is_compatible_to("1.0.0")) + + def test_exactly_min_version(self): + self.assertTrue(CompatibleVersions(minimum="1.2.0").is_compatible_to("1.2.0")) + + def test_below_min_version(self): + self.assertFalse(CompatibleVersions(minimum="1.1.9").is_compatible_to("1.0.0")) + + def test_below_max_version(self): + self.assertTrue(CompatibleVersions(before="1.1.0").is_compatible_to("1.0.0")) + + def test_exactly_max_version(self): + self.assertFalse(CompatibleVersions(before="1.2.0").is_compatible_to("1.2.0")) + + def test_above_max_version(self): + self.assertFalse(CompatibleVersions(before="0.9.0").is_compatible_to("1.0.0")) + + def test_inside_interval(self): + self.assertTrue(CompatibleVersions(minimum="0.9.0", before="1.1.0").is_compatible_to("1.0.0")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/aspect/virtual_includes/test_correctly_process_virtual_includes.py b/test/aspect/virtual_includes/test_correctly_process_virtual_includes.py new file mode 100644 index 00000000..64406856 --- /dev/null +++ b/test/aspect/virtual_includes/test_correctly_process_virtual_includes.py @@ -0,0 +1,10 @@ +from result import ExpectedResult, Result +from test_case import TestCaseBase + + +class TestCase(TestCaseBase): + def execute_test_logic(self) -> Result: + expected = ExpectedResult(success=True) + actual = self._run_dwyu(target="//test/aspect/virtual_includes:all", aspect=self.default_aspect) + + return self._check_result(actual=actual, expected=expected) diff --git a/tests.sh b/tests.sh index 59b80aea..636ba007 100755 --- a/tests.sh +++ b/tests.sh @@ -3,6 +3,11 @@ set -o errexit set -o nounset +echo "" +echo "Pre-commit checks" +echo "" +poetry run pre-commit run --all-files + echo "" echo "Execute unit tests" echo "" @@ -11,7 +16,7 @@ bazel test -- //src/... //test/aspect:all //third_party/... echo "" echo "Executing mypy" echo "" -bazel build --config=mypy -- //src/... //test/aspect:all +bazel build --config=mypy -- //src/... //test/aspect:all //test/apply_fixes:all echo "" echo "Execute integration tests - Aspect"