From a97bd92145ae065f12dd9fec505e52f37e786c62 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 19 Aug 2026 11:47:05 +0200 Subject: [PATCH 1/2] Migrate external_repo tests to bazel Add local = True --- .bazelignore | 1 - BUILD | 3 +- test/unit/external_repository/BUILD | 69 ++-- test/unit/external_repository/MODULE.bazel | 33 -- test/unit/external_repository/WORKSPACE | 1 - .../external_repository/external_test.bzl | 116 ++++++ .../external_test_runner.py | 332 ++++++++++++++++++ .../external_repository/test_external_repo.py | 174 --------- .../third_party/my_lib/BUILD | 25 -- .../third_party/my_lib/MODULE.bazel | 23 -- .../third_party/my_lib/WORKSPACE | 13 - .../third_party/my_lib/include/nothing.h | 16 - 12 files changed, 483 insertions(+), 323 deletions(-) delete mode 100644 test/unit/external_repository/MODULE.bazel delete mode 100644 test/unit/external_repository/WORKSPACE create mode 100644 test/unit/external_repository/external_test.bzl create mode 100644 test/unit/external_repository/external_test_runner.py delete mode 100644 test/unit/external_repository/test_external_repo.py delete mode 100644 test/unit/external_repository/third_party/my_lib/BUILD delete mode 100644 test/unit/external_repository/third_party/my_lib/MODULE.bazel delete mode 100644 test/unit/external_repository/third_party/my_lib/WORKSPACE delete mode 100644 test/unit/external_repository/third_party/my_lib/include/nothing.h diff --git a/.bazelignore b/.bazelignore index a99234f3..16f17034 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,2 +1 @@ .ci/micromamba -test/unit/external_repository diff --git a/BUILD b/BUILD index ef48882c..a3e37856 100644 --- a/BUILD +++ b/BUILD @@ -41,11 +41,12 @@ toolchain( toolchain_type = ":toolchain_type", ) -# Repository root marker, used by buildifier and pylint tests +# Repository root marker, used by the buildifier, pylint, and external_repository tests exports_files( ["MODULE.bazel"], visibility = [ "//test/buildifier:__pkg__", "//test/pylint:__pkg__", + "//test/unit/external_repository:__pkg__", ], ) diff --git a/test/unit/external_repository/BUILD b/test/unit/external_repository/BUILD index 3cc1d319..bf0cc878 100644 --- a/test/unit/external_repository/BUILD +++ b/test/unit/external_repository/BUILD @@ -1,4 +1,4 @@ -# Copyright 2023 Ericsson AB +# Copyright 2026 Ericsson AB # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,49 +13,46 @@ # limitations under the License. load( - "@rules_cc//cc:defs.bzl", - "cc_binary", - "cc_library", -) -load( - "@rules_codechecker//:defs.bzl", - "codechecker_test", - "compile_commands", + "//test/unit/external_repository:external_test.bzl", + "external_test", ) -# This is the important bit -- not only do we have an external dependency here -# in the form of external_lib, but is also an implementation_dep. This is a -# tricky edge case we need to handle. -cc_library( - name = "intermediate_lib", - srcs = ["intermediate.cpp"], - hdrs = ["intermediate.h"], - implementation_deps = ["@external_lib//:lib"], - visibility = ["//visibility:public"], +exports_files([ + "external_test_runner.py", + "main.cc", + "intermediate.cpp", + "intermediate.h", +]) + +external_test( + name = "compile_commands_isystem_test", + action = "build", + contains = [ + "-isystem external/external_lib[~+]/include", + "-isystem bazel-out/k8-fastbuild/bin/external/external_lib[~+]/include", + ], + output_file = "bazel-bin/compile_commands_isystem/compile_commands.json", + target = ":compile_commands_isystem", ) -cc_binary( - name = "isystem_impl_dep", - srcs = ["main.cc"], - deps = [":intermediate_lib"], +external_test( + name = "codechecker_external_deps_test", + target = ":codechecker_external_deps", ) -compile_commands( - name = "compile_commands_isystem", - targets = [ - ":isystem_impl_dep", - ], +external_test( + name = "per_file_external_deps_test", + target = ":per_file_external_deps", ) -codechecker_test( - name = "codechecker_external_deps", - tags = ["manual"], - targets = ["isystem_impl_dep"], +external_test( + name = "codechecker_external_include_paths_test", + extra_flags = ["--features=external_include_paths"], + target = ":codechecker_external_deps", ) -codechecker_test( - name = "per_file_external_deps", - per_file = True, - tags = ["manual"], - targets = ["isystem_impl_dep"], +external_test( + name = "per_file_external_include_paths_test", + extra_flags = ["--features=external_include_paths"], + target = ":per_file_external_deps", ) diff --git a/test/unit/external_repository/MODULE.bazel b/test/unit/external_repository/MODULE.bazel deleted file mode 100644 index 80b5c822..00000000 --- a/test/unit/external_repository/MODULE.bazel +++ /dev/null @@ -1,33 +0,0 @@ -""" -Copyright 2023 Ericsson AB - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -Test project for using headers thought implementation_deps, -from external repositories -""" - -local_path_override( - module_name = "rules_codechecker", - path = "../../../", -) - -bazel_dep(name = "rules_codechecker") - -local_path_override( - module_name = "external_lib", - path = "third_party/my_lib", -) - -bazel_dep(name = "external_lib") -bazel_dep(name = "rules_cc", version = "0.2.17") diff --git a/test/unit/external_repository/WORKSPACE b/test/unit/external_repository/WORKSPACE deleted file mode 100644 index be13a358..00000000 --- a/test/unit/external_repository/WORKSPACE +++ /dev/null @@ -1 +0,0 @@ -# This file is mandatory for old Bazel versions diff --git a/test/unit/external_repository/external_test.bzl b/test/unit/external_repository/external_test.bzl new file mode 100644 index 00000000..ce2c32fe --- /dev/null +++ b/test/unit/external_repository/external_test.bzl @@ -0,0 +1,116 @@ +# Copyright 2026 Ericsson AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Macro for external repository integration tests. + +Each external_test() generates a py_test that: + 1. Creates a temporary Bazel workspace with external dependencies + 2. Runs a bazel command inside it + 3. Asserts on exit code and optionally on output file contents + +Example: + external_test( + name = "codechecker_external_deps_test", + target = ":codechecker_external_deps", + ) + + external_test( + name = "compile_commands_test", + action = "build", + target = ":compile_commands_isystem", + output_file = "bazel-bin/compile_commands_isystem/compile_commands.json", + contains = ["-isystem external/external_lib"], + ) +""" + +load("@rules_python//python:py_test.bzl", "py_test") + +# Source files that get copied into the generated workspace root +_SRCS = [ + "//test/unit/external_repository:main.cc", + "//test/unit/external_repository:intermediate.cpp", + "//test/unit/external_repository:intermediate.h", +] + +def external_test( + name, + target, + action = "test", + extra_flags = [], + expected_exit_code = 0, + output_file = None, + contains = None, + tags = [], + size = "large", + **kwargs): + """Generate a py_test that runs a bazel command in a temp workspace. + + Args: + name: Test name. + target: Bazel target to act on (e.g. ":codechecker_external_deps"). + action: Bazel action to run (default: "test"). + extra_flags: Additional flags to pass to the bazel command. + expected_exit_code: Expected exit code (default: 0). + output_file: Optional relative path to an output file to check. + contains: Optional list of regex patterns to find in the output file. + tags: Additional test tags. + size: Test size (default: large). + **kwargs: Forwarded to py_test. + """ + if type(contains) == "string": + contains = [contains] + if type(extra_flags) == "string": + extra_flags = [extra_flags] + + python_args = [ + "--repo_root", + "$(rootpath //:MODULE.bazel)", + "--action", + action, + "--target", + target, + "--expected_exit_code", + str(expected_exit_code), + ] + + # Source files + python_args.append("--srcs") + for src in _SRCS: + python_args.append("$(rootpath {})".format(src)) + + # Output file assertions + if output_file: + python_args.extend(["--output_file", output_file]) + if contains: + python_args.append("--contains") + python_args.extend(["'{}'".format(pat) for pat in contains]) + + # Extra flags go after -- to avoid argparse confusion with --flags + if extra_flags: + python_args.append("--") + python_args.extend(extra_flags) + + py_test( + name = name, + srcs = ["//test/unit/external_repository:external_test_runner.py"], + main = "//test/unit/external_repository:external_test_runner.py", + args = python_args, + data = _SRCS + ["//:MODULE.bazel"], + # No other way to ensure integrated bazel can find the repository + local = True, + size = size, + tags = tags, + **kwargs + ) diff --git a/test/unit/external_repository/external_test_runner.py b/test/unit/external_repository/external_test_runner.py new file mode 100644 index 00000000..5861e057 --- /dev/null +++ b/test/unit/external_repository/external_test_runner.py @@ -0,0 +1,332 @@ +# Copyright 2026 Ericsson AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test runner for external repository tests. + +This script creates a temporary Bazel workspace, copies source files into it, +runs a bazel command, and asserts on the result. It is designed to be invoked +by the external_test() Bazel macro. +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile + +MODULE_BAZEL_CONTENT = """\ +\"\"\"Test workspace for external repository integration test.\"\"\" + +local_path_override( + module_name = "rules_codechecker", + path = "{repo_root}", +) + +bazel_dep(name = "rules_codechecker") + +local_path_override( + module_name = "external_lib", + path = "third_party/my_lib", +) + +bazel_dep(name = "external_lib") +bazel_dep(name = "rules_cc", version = "0.2.17") +""" + +BUILD_CONTENT = """\ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") +load( + "@rules_codechecker//:defs.bzl", + "codechecker_test", + "compile_commands", +) + +cc_library( + name = "intermediate_lib", + srcs = ["intermediate.cpp"], + hdrs = ["intermediate.h"], + implementation_deps = ["@external_lib//:lib"], + visibility = ["//visibility:public"], +) + +cc_binary( + name = "isystem_impl_dep", + srcs = ["main.cc"], + deps = [":intermediate_lib"], +) + +compile_commands( + name = "compile_commands_isystem", + targets = [":isystem_impl_dep"], +) + +codechecker_test( + name = "codechecker_external_deps", + tags = ["manual"], + targets = ["isystem_impl_dep"], +) + +codechecker_test( + name = "per_file_external_deps", + per_file = True, + tags = ["manual"], + targets = ["isystem_impl_dep"], +) +""" + +THIRD_PARTY_BUILD_CONTENT = """\ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "lib", + hdrs = ["include/nothing.h"], + includes = ["include"], + visibility = ["//visibility:public"], +) +""" + +THIRD_PARTY_MODULE_CONTENT = """\ +module(name = "external_lib") + +bazel_dep(name = "rules_cc", version = "0.2.17") +""" + +THIRD_PARTY_HEADER_CONTENT = """\ +#pragma once +// Just a dummy header +""" + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Run a bazel command in a generated workspace" + ) + parser.add_argument( + "--repo_root", + required=True, + help="Path to a file in the repo root (MODULE.bazel)", + ) + parser.add_argument( + "--action", + required=True, + choices=["build", "test"], + help="Bazel action to run", + ) + parser.add_argument( + "--target", + required=True, + help="Bazel target (e.g. ':codechecker_external_deps')", + ) + parser.add_argument( + "extra_flags", + nargs="*", + default=[], + help="Extra flags to pass to bazel (after --)", + ) + parser.add_argument( + "--expected_exit_code", + type=int, + default=0, + help="Expected exit code from the bazel command", + ) + parser.add_argument( + "--output_file", + help="Relative path to output file to check (from workspace)", + ) + parser.add_argument( + "--contains", + nargs="*", + default=[], + help="Regex patterns that must appear in the output file", + ) + parser.add_argument( + "--srcs", + nargs="*", + default=[], + help="Source files to copy into the workspace root", + ) + return parser.parse_args() + + +def resolve_repo_root(module_bazel_path): + """Resolve the repo root from the MODULE.bazel runfiles path.""" + resolved = os.path.realpath(module_bazel_path) + return os.path.dirname(resolved) + + +def copy_file(src, dst): + """Copy a file, creating parent directories as needed.""" + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copy2(src, dst) + + +def write_file(path, content): + """Write content to a file, creating parent directories.""" + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def setup_workspace(tmpdir, repo_root, srcs): + """Set up the temporary workspace with all needed files.""" + # Write MODULE.bazel + module_content = MODULE_BAZEL_CONTENT.format(repo_root=repo_root) + write_file(os.path.join(tmpdir, "MODULE.bazel"), module_content) + + # Write empty WORKSPACE + write_file( + os.path.join(tmpdir, "WORKSPACE"), + "# This file is mandatory for old Bazel versions\n", + ) + + # Write BUILD + write_file(os.path.join(tmpdir, "BUILD"), BUILD_CONTENT) + + # Copy source files into workspace root + for src_path in srcs: + real_src = os.path.realpath(src_path) + basename = os.path.basename(real_src) + copy_file(real_src, os.path.join(tmpdir, basename)) + + # Write third_party/my_lib files + tp_dir = os.path.join(tmpdir, "third_party", "my_lib") + write_file( + os.path.join(tp_dir, "BUILD"), THIRD_PARTY_BUILD_CONTENT + ) + write_file( + os.path.join(tp_dir, "MODULE.bazel"), + THIRD_PARTY_MODULE_CONTENT, + ) + write_file( + os.path.join(tp_dir, "include", "nothing.h"), + THIRD_PARTY_HEADER_CONTENT, + ) + + # Copy .bazelversion if it exists -- bazelisk support + bazelversion = os.path.join(repo_root, ".bazelversion") + if os.path.exists(bazelversion): + shutil.copy2( + bazelversion, os.path.join(tmpdir, ".bazelversion") + ) + + +def run_bazel(tmpdir, action, target, extra_flags): + """Run a bazel command in the workspace directory.""" + cmd = [ + "bazel", + action, + target, + "--experimental_cc_implementation_deps", + "--enable_bzlmod", + ] + extra_flags + print(f"Running: {' '.join(cmd)}") + print(f"In directory: {tmpdir}") + result = subprocess.run( + cmd, + cwd=tmpdir, + capture_output=True, + check=False, + text=True, + ) + print(f"Exit code: {result.returncode}") + if result.stdout: + print(f"stdout:\n{result.stdout}") + if result.stderr: + print(f"stderr:\n{result.stderr}") + return result + + +def check_output_file(tmpdir, output_file, patterns): + """Check that all patterns appear in the output file.""" + filepath = os.path.join(tmpdir, output_file) + if not os.path.exists(filepath): + print(f"FAIL: Output file does not exist: {filepath}") + # List what's in bazel-bin to help debug + bazel_bin = os.path.join(tmpdir, "bazel-bin") + if os.path.exists(bazel_bin): + print("Contents of bazel-bin:") + for root, _, files in os.walk(bazel_bin): + for f in files: + print(f" {os.path.join(root, f)}") + sys.exit(1) + + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + for pattern in patterns: + if not re.search(pattern, content): + print( + f"FAIL: Pattern not found in {output_file}: " + f"{pattern}" + ) + print(f"File content:\n{content}") + sys.exit(1) + print(f"PASS: Found pattern: {pattern}") + + +def cleanup(tmpdir): + """Shut down bazel server to release file locks.""" + subprocess.run( + ["bazel", "shutdown"], + cwd=tmpdir, + capture_output=True, + check=False, + ) + + +def main(): + """Main entry point.""" + args = parse_args() + + repo_root = resolve_repo_root(args.repo_root) + print(f"Resolved repo root: {repo_root}") + + tmpdir = tempfile.mkdtemp(prefix="external_test_") + print(f"Created temp workspace: {tmpdir}") + + try: + setup_workspace(tmpdir, repo_root, args.srcs) + + result = run_bazel( + tmpdir, args.action, args.target, args.extra_flags + ) + + if result.returncode != args.expected_exit_code: + print( + f"FAIL: Expected exit code {args.expected_exit_code}, " + f"got {result.returncode}" + ) + sys.exit(1) + print( + f"PASS: Exit code matches expected: " + f"{args.expected_exit_code}" + ) + + if args.output_file and args.contains: + check_output_file( + tmpdir, args.output_file, args.contains + ) + + print("ALL CHECKS PASSED") + finally: + cleanup(tmpdir) + shutil.rmtree(tmpdir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/test/unit/external_repository/test_external_repo.py b/test/unit/external_repository/test_external_repo.py deleted file mode 100644 index 0e924227..00000000 --- a/test/unit/external_repository/test_external_repo.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2023 Ericsson AB -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test external repositories with codechecker -""" - -import logging -import os -import re -import shutil -import unittest -from typing import final -from common.base import TestBase - - -class TestImplDepExternalDep(TestBase): - """Test external repositories with codechecker""" - - # Set working directory - __test_path__ = os.path.dirname(os.path.abspath(__file__)) - BAZEL_BIN_DIR = os.path.join("bazel-bin") - BAZEL_TESTLOGS_DIR = os.path.join("bazel-testlogs") - BAZEL_VERSION: str = "" - - @final - @classmethod - def setUpClass(cls): - """ - Copy bazelversion from main, otherwise bazelisk will download the latest - bazel version. - """ - # The folder bazel-external_repository - # created by bazel during bazel build/test - # contains a copy of this test file - # and the unittest test discovery finds it. - # This is why, it is imperative that these directories get cleared - cls.run_command("bazel clean") - super().setUpClass() - try: - shutil.copy("../../../.bazelversion", ".bazelversion") - shutil.copy( - "../../../.bazelversion", "third_party/my_lib/.bazelversion" - ) - # If no such file exists assume user doesn't use bazelisk - # This file is not needed - except FileNotFoundError: - logging.debug("No bazel version set, using system default") - _, stdout, _ = cls.run_command("bazel version --gnu_format") - match = re.search(r"bazel\s+([\d\.]+)", stdout, re.MULTILINE) - if match: - cls.BAZEL_VERSION = match.group(1) - else: - raise RuntimeError(f"Bazel version not found: {stdout}") - logging.debug("Using Bazel %s", cls.BAZEL_VERSION) - - @final - @classmethod - def tearDownClass(cls): - """Remove bazelversion from this test""" - super().tearDownClass() - # The folder bazel-external_repository contains this script - # and the unittest test discovery finds it. - # This is why, it is imperative that these directories get cleared - cls.run_command("bazel clean") - try: - os.remove(".bazelversion") - # If no such file exists assume user doesn't use bazelisk - # This file was not needed - except FileNotFoundError: - pass - try: - os.remove("third_party/my_lib/.bazelversion") - # If no such file exists assume user doesn't use bazelisk - # This file was not needed - except FileNotFoundError: - pass - - def test_compile_commands_external_lib(self): - """ - Test: bazel build :compile_commands_isystem " - "--experimental_cc_implementation_deps --enable_bzlmod - """ - ret, _, stderr = self.run_command( - "bazel build :compile_commands_isystem " - "--experimental_cc_implementation_deps --enable_bzlmod" - ) - self.assertEqual(ret, 0, stderr) - comp_json_file = os.path.join( - self.BAZEL_BIN_DIR, # pyright: ignore - "compile_commands_isystem", - "compile_commands.json", - ) - - if self.BAZEL_VERSION.startswith("7"): # type: ignore - pattern1 = "-isystem external/external_lib~/include" - pattern2 = ( - "-isystem bazel-out/k8-fastbuild/bin/external/" - "external_lib~/include" - ) - else: - pattern1 = r"-isystem external/external_lib\+/include" - pattern2 = ( - r"-isystem " - r"bazel-out/k8-fastbuild/bin/external/external_lib\+/include" - ) - - self.assertTrue(self.contains_regex_in_file(comp_json_file, pattern1)) - self.assertTrue(self.contains_regex_in_file(comp_json_file, pattern2)) - - def test_codechecker_external_lib(self): - """ - Test: bazel build :codechecker_external_deps - --experimental_cc_implementation_deps --enable_bzlmod - """ - ret, _, stderr = self.run_command( - "bazel build :codechecker_external_deps " - "--experimental_cc_implementation_deps --enable_bzlmod" - ) - self.assertEqual(ret, 0, stderr) - - def test_per_file_external_lib(self): - """Test: bazel build :per_file_external_deps " - "--experimental_cc_implementation_deps""" - ret, _, stderr = self.run_command( - "bazel build :per_file_external_deps " - "--experimental_cc_implementation_deps --enable_bzlmod" - ) - self.assertEqual(ret, 0, stderr) - - def test_codechecker_external_include_paths(self): - """ - Enabling external_include_paths will make bazel pass external - include paths with -isystem. - Test: bazel test :codechecker_external_deps - --experimental_cc_implementation_deps --enable_bzlmod - --features=external_include_paths - """ - ret, _, stderr = self.run_command( - "bazel test :codechecker_external_deps " - "--experimental_cc_implementation_deps --enable_bzlmod " - "--features=external_include_paths" - ) - self.assertEqual(ret, 0, stderr) - - def test_per_file_external_include_paths(self): - """ - Enabling external_include_paths will make bazel pass external - include paths with -isystem. - Test: bazel test :per_file_external_deps - --experimental_cc_implementation_deps - --features=external_include_paths - """ - ret, _, stderr = self.run_command( - "bazel test :per_file_external_deps " - "--experimental_cc_implementation_deps --enable_bzlmod " - "--features=external_include_paths" - ) - self.assertEqual(ret, 0, stderr) - - -if __name__ == "__main__": - unittest.main(buffer=True) diff --git a/test/unit/external_repository/third_party/my_lib/BUILD b/test/unit/external_repository/third_party/my_lib/BUILD deleted file mode 100644 index b84d3094..00000000 --- a/test/unit/external_repository/third_party/my_lib/BUILD +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2023 Ericsson AB -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -load( - "@rules_cc//cc:defs.bzl", - "cc_library", -) - -cc_library( - name = "lib", - hdrs = ["include/nothing.h"], - # This attribute, combined with being an external repo, triggers -isystem - includes = ["include"], - visibility = ["//visibility:public"], -) diff --git a/test/unit/external_repository/third_party/my_lib/MODULE.bazel b/test/unit/external_repository/third_party/my_lib/MODULE.bazel deleted file mode 100644 index ecffc980..00000000 --- a/test/unit/external_repository/third_party/my_lib/MODULE.bazel +++ /dev/null @@ -1,23 +0,0 @@ -""" -Copyright 2023 Ericsson AB - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -Dummy external lib for tests -""" - -module( - name = "external_lib", -) - -bazel_dep(name = "rules_cc", version = "0.2.17") diff --git a/test/unit/external_repository/third_party/my_lib/WORKSPACE b/test/unit/external_repository/third_party/my_lib/WORKSPACE deleted file mode 100644 index 78bab5f1..00000000 --- a/test/unit/external_repository/third_party/my_lib/WORKSPACE +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2023 Ericsson AB -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/test/unit/external_repository/third_party/my_lib/include/nothing.h b/test/unit/external_repository/third_party/my_lib/include/nothing.h deleted file mode 100644 index e26b9847..00000000 --- a/test/unit/external_repository/third_party/my_lib/include/nothing.h +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2023 Ericsson AB -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#pragma once -// Just a dummy header From fe16a2a4ca7e9dfc3f7d08b906f7724f5164184d Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 30 Sep 2026 16:14:20 +0200 Subject: [PATCH 2/2] Review comment fixes --- test/unit/external_repository/BUILD | 13 +++- .../external_repository/external_test.bzl | 30 ++------ .../external_test_runner.py | 77 ++++--------------- 3 files changed, 32 insertions(+), 88 deletions(-) diff --git a/test/unit/external_repository/BUILD b/test/unit/external_repository/BUILD index bf0cc878..9b53fe3d 100644 --- a/test/unit/external_repository/BUILD +++ b/test/unit/external_repository/BUILD @@ -19,11 +19,18 @@ load( exports_files([ "external_test_runner.py", - "main.cc", - "intermediate.cpp", - "intermediate.h", ]) +# Source files made available to the inner build via the test runfiles +filegroup( + name = "external_test_srcs", + srcs = [ + "intermediate.cpp", + "intermediate.h", + "main.cc", + ], +) + external_test( name = "compile_commands_isystem_test", action = "build", diff --git a/test/unit/external_repository/external_test.bzl b/test/unit/external_repository/external_test.bzl index ce2c32fe..0e2ec590 100644 --- a/test/unit/external_repository/external_test.bzl +++ b/test/unit/external_repository/external_test.bzl @@ -21,35 +21,29 @@ Each external_test() generates a py_test that: 3. Asserts on exit code and optionally on output file contents Example: - external_test( - name = "codechecker_external_deps_test", - target = ":codechecker_external_deps", - ) - external_test( name = "compile_commands_test", - action = "build", target = ":compile_commands_isystem", + action = "build", + extra_flags = ["--features=external_include_paths"], output_file = "bazel-bin/compile_commands_isystem/compile_commands.json", contains = ["-isystem external/external_lib"], + tags = ["manual"], + size = "large", ) """ load("@rules_python//python:py_test.bzl", "py_test") -# Source files that get copied into the generated workspace root -_SRCS = [ - "//test/unit/external_repository:main.cc", - "//test/unit/external_repository:intermediate.cpp", - "//test/unit/external_repository:intermediate.h", -] +# Source files made available to the inner build via the test runfiles, +# alongside the runner. Declared as a filegroup in the BUILD file. +_SRCS = "//test/unit/external_repository:external_test_srcs" def external_test( name, target, action = "test", extra_flags = [], - expected_exit_code = 0, output_file = None, contains = None, tags = [], @@ -62,7 +56,6 @@ def external_test( target: Bazel target to act on (e.g. ":codechecker_external_deps"). action: Bazel action to run (default: "test"). extra_flags: Additional flags to pass to the bazel command. - expected_exit_code: Expected exit code (default: 0). output_file: Optional relative path to an output file to check. contains: Optional list of regex patterns to find in the output file. tags: Additional test tags. @@ -81,15 +74,8 @@ def external_test( action, "--target", target, - "--expected_exit_code", - str(expected_exit_code), ] - # Source files - python_args.append("--srcs") - for src in _SRCS: - python_args.append("$(rootpath {})".format(src)) - # Output file assertions if output_file: python_args.extend(["--output_file", output_file]) @@ -107,7 +93,7 @@ def external_test( srcs = ["//test/unit/external_repository:external_test_runner.py"], main = "//test/unit/external_repository:external_test_runner.py", args = python_args, - data = _SRCS + ["//:MODULE.bazel"], + data = [_SRCS, "//:MODULE.bazel"], # No other way to ensure integrated bazel can find the repository local = True, size = size, diff --git a/test/unit/external_repository/external_test_runner.py b/test/unit/external_repository/external_test_runner.py index 5861e057..4ab5ef0d 100644 --- a/test/unit/external_repository/external_test_runner.py +++ b/test/unit/external_repository/external_test_runner.py @@ -26,7 +26,6 @@ import shutil import subprocess import sys -import tempfile MODULE_BAZEL_CONTENT = """\ \"\"\"Test workspace for external repository integration test.\"\"\" @@ -138,12 +137,6 @@ def parse_args(): default=[], help="Extra flags to pass to bazel (after --)", ) - parser.add_argument( - "--expected_exit_code", - type=int, - default=0, - help="Expected exit code from the bazel command", - ) parser.add_argument( "--output_file", help="Relative path to output file to check (from workspace)", @@ -154,12 +147,6 @@ def parse_args(): default=[], help="Regex patterns that must appear in the output file", ) - parser.add_argument( - "--srcs", - nargs="*", - default=[], - help="Source files to copy into the workspace root", - ) return parser.parse_args() @@ -169,12 +156,6 @@ def resolve_repo_root(module_bazel_path): return os.path.dirname(resolved) -def copy_file(src, dst): - """Copy a file, creating parent directories as needed.""" - os.makedirs(os.path.dirname(dst), exist_ok=True) - shutil.copy2(src, dst) - - def write_file(path, content): """Write content to a file, creating parent directories.""" os.makedirs(os.path.dirname(path), exist_ok=True) @@ -182,32 +163,18 @@ def write_file(path, content): f.write(content) -def setup_workspace(tmpdir, repo_root, srcs): +def setup_workspace(tmpdir, repo_root): """Set up the temporary workspace with all needed files.""" # Write MODULE.bazel module_content = MODULE_BAZEL_CONTENT.format(repo_root=repo_root) write_file(os.path.join(tmpdir, "MODULE.bazel"), module_content) - # Write empty WORKSPACE - write_file( - os.path.join(tmpdir, "WORKSPACE"), - "# This file is mandatory for old Bazel versions\n", - ) - # Write BUILD write_file(os.path.join(tmpdir, "BUILD"), BUILD_CONTENT) - # Copy source files into workspace root - for src_path in srcs: - real_src = os.path.realpath(src_path) - basename = os.path.basename(real_src) - copy_file(real_src, os.path.join(tmpdir, basename)) - # Write third_party/my_lib files tp_dir = os.path.join(tmpdir, "third_party", "my_lib") - write_file( - os.path.join(tp_dir, "BUILD"), THIRD_PARTY_BUILD_CONTENT - ) + write_file(os.path.join(tp_dir, "BUILD"), THIRD_PARTY_BUILD_CONTENT) write_file( os.path.join(tp_dir, "MODULE.bazel"), THIRD_PARTY_MODULE_CONTENT, @@ -220,9 +187,7 @@ def setup_workspace(tmpdir, repo_root, srcs): # Copy .bazelversion if it exists -- bazelisk support bazelversion = os.path.join(repo_root, ".bazelversion") if os.path.exists(bazelversion): - shutil.copy2( - bazelversion, os.path.join(tmpdir, ".bazelversion") - ) + shutil.copy2(bazelversion, os.path.join(tmpdir, ".bazelversion")) def run_bazel(tmpdir, action, target, extra_flags): @@ -231,8 +196,6 @@ def run_bazel(tmpdir, action, target, extra_flags): "bazel", action, target, - "--experimental_cc_implementation_deps", - "--enable_bzlmod", ] + extra_flags print(f"Running: {' '.join(cmd)}") print(f"In directory: {tmpdir}") @@ -270,10 +233,7 @@ def check_output_file(tmpdir, output_file, patterns): for pattern in patterns: if not re.search(pattern, content): - print( - f"FAIL: Pattern not found in {output_file}: " - f"{pattern}" - ) + print(f"FAIL: Pattern not found in {output_file}: " f"{pattern}") print(f"File content:\n{content}") sys.exit(1) print(f"PASS: Found pattern: {pattern}") @@ -296,36 +256,27 @@ def main(): repo_root = resolve_repo_root(args.repo_root) print(f"Resolved repo root: {repo_root}") - tmpdir = tempfile.mkdtemp(prefix="external_test_") - print(f"Created temp workspace: {tmpdir}") + # Create the workspace next to the source files, in the runfiles directory. + tmpdir = os.path.dirname(os.path.abspath(__file__)) + print(f"Using workspace: {tmpdir}") try: - setup_workspace(tmpdir, repo_root, args.srcs) + setup_workspace(tmpdir, repo_root) - result = run_bazel( - tmpdir, args.action, args.target, args.extra_flags - ) + result = run_bazel(tmpdir, args.action, args.target, args.extra_flags) - if result.returncode != args.expected_exit_code: - print( - f"FAIL: Expected exit code {args.expected_exit_code}, " - f"got {result.returncode}" - ) + if result.returncode != 0: + print(f"FAIL: Expected exit code 0, got {result.returncode}") sys.exit(1) - print( - f"PASS: Exit code matches expected: " - f"{args.expected_exit_code}" - ) + print("PASS: Exit code matches expected: 0") if args.output_file and args.contains: - check_output_file( - tmpdir, args.output_file, args.contains - ) + check_output_file(tmpdir, args.output_file, args.contains) print("ALL CHECKS PASSED") finally: + # Shut down the inner bazel server to release locks on its output base. cleanup(tmpdir) - shutil.rmtree(tmpdir, ignore_errors=True) if __name__ == "__main__":