diff --git a/.ci/mise/config.toml b/.ci/mise/config.toml index 6691f135..94c26626 100644 --- a/.ci/mise/config.toml +++ b/.ci/mise/config.toml @@ -4,10 +4,10 @@ experimental = true [tools] # Environment tools: -"bazel" = "6.5" +"bazel" = "7.7" "python" = "3.11" "pipx" = "latest" -"pipx:codechecker" = "6.26" +"pipx:codechecker" = "6.27.3" # Clang tools: "conda:clang" = "latest" diff --git a/BUILD b/BUILD index 2e308d79..f295490a 100644 --- a/BUILD +++ b/BUILD @@ -1,31 +1,13 @@ -load("@aspect_rules_lint//format:defs.bzl", "format_test") load("@buildifier_prebuilt//:rules.bzl", "buildifier_test") buildifier_test( - name = "buildifier_native", - diff_command = "diff -u", + name = "buildifier", exclude_patterns = [ "./.git/*", ], lint_mode = "warn", + lint_warnings = ["all"], mode = "diff", no_sandbox = True, workspace = "//:WORKSPACE", ) - -format_test( - name = "format_test", - # Temporary workaround for not being able to use -diff_command - env = ["BUILDIFIER_DIFF='diff -u'"], - no_sandbox = True, - # TODO: extend with pylint - starlark = "@buildifier_prebuilt//:buildifier", - starlark_check_args = [ - "-lint=warn", - "-warnings=all", - "-mode=diff", - # -u will always get passed to buildifier not diff_command - #"-diff_command=\"diff -u\"", - ], - workspace = "//:WORKSPACE", -) diff --git a/MODULE.bazel b/MODULE.bazel index eb740bd0..50e2f42b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -21,10 +21,9 @@ bazel_dep(name = "rules_cc", version = "0.2.3") bazel_dep( name = "buildifier_prebuilt", - version = "6.4.0", + version = "7.3.1", dev_dependency = True, ) -bazel_dep(name = "aspect_rules_lint", version = "1.11.0", dev_dependency = True) codechecker_extension = use_extension( "//src:tools.bzl", diff --git a/test/bazel/BUILD b/test/bazel/BUILD new file mode 100644 index 00000000..3eac3c29 --- /dev/null +++ b/test/bazel/BUILD @@ -0,0 +1,49 @@ +# 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. + +# ----------------------------------------------------------------------- +# FOSS integration tests for rules_codechecker +# +# Each test downloads a real FOSS project, sets up a standalone Bazel +# project with rules_codechecker, and verifies the rules execute +# successfully via "bazel build". +# +# Run all: bazel test //test/bazel/... +# Run one: bazel test //test/bazel:zlib +# +# To add a new project: add a foss_test() call below. +# ----------------------------------------------------------------------- + +load(":foss_test.bzl", "foss_test") + +foss_test( + name = "zlib", + tests = [ + ":codechecker_per_file", + ":codechecker_test", + ":compile_commands", + ], + url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz", +) + +foss_test( + name = "yaml-cpp", + tests = [ + ":codechecker_test", + ":compile_commands", + # FIXME: output 'analysis/codechecker_per_file/data/src-emit.cpp_clangsa.plist' was not created + # ":codechecker_per_file", + ], + url = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz", +) diff --git a/test/bazel/foss_test.bzl b/test/bazel/foss_test.bzl new file mode 100644 index 00000000..41f7d492 --- /dev/null +++ b/test/bazel/foss_test.bzl @@ -0,0 +1,70 @@ +# 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 generating FOSS integration tests for rules_codechecker. + +Each foss_test() generates a local py_test that: + 1. Downloads a FOSS project into a temp directory + 2. Sets up a standalone Bazel project with rules_codechecker + 3. Runs "bazel build" on codechecker targets to verify the rules work + 4. Validates the outputs (compile_commands.json, codechecker artifacts) + +Example: + foss_test( + name = "zlib", + url = "https://github.com/madler/zlib/archive/.tar.gz", + tests = [":codechecker_test", ":compile_commands"], + ) +""" + +load("@rules_python//python:defs.bzl", "py_test") + +def foss_test( + name, + url, + tests, + target = None, + tags = [], + size = "large", + **kwargs): + """Generate a py_test that runs rules_codechecker on a FOSS project. + + Args: + name: Test name. + url: URL to the source archive (.tar.gz). + tests: Analysis targets to build (e.g. codechecker_test, compile_commands). + target: The cc_library target to analyze. Defaults to ":". + tags: Additional test tags. + size: Test size (default: enormous, as these download + run bazel). + **kwargs: Forwarded to py_test. + """ + if target == None: + target = ":" + name + + py_test( + name = name, + srcs = ["foss_test_runner.py"], + main = "foss_test_runner.py", + args = [ + "-vvv", + "--url=" + url, + "--target=" + target, + "--tests", + ] + tests, + local = True, + tags = ["foss", "external"] + tags, + size = size, + **kwargs + ) diff --git a/test/bazel/foss_test_runner.py b/test/bazel/foss_test_runner.py new file mode 100644 index 00000000..8ba9c92e --- /dev/null +++ b/test/bazel/foss_test_runner.py @@ -0,0 +1,187 @@ +# 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. + +""" +FOSS integration test runner for rules_codechecker. + +Downloads a FOSS project, sets up a standalone Bazel project with +rules_codechecker, builds codechecker targets, and verifies outputs. +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tarfile +import tempfile +import unittest +from pathlib import Path + +MODULE_TEMPLATE = """ +local_path_override( + module_name = "rules_codechecker", + path = "{rules_path}", +) +bazel_dep(name = "rules_codechecker") +""" + +BUILD_TEMPLATE = """ +load("@rules_codechecker//src:codechecker.bzl", "codechecker_test") +load("@rules_codechecker//src:compile_commands.bzl", "compile_commands") + +codechecker_test( + name = "codechecker_test", + targets = ["//{target}"], +) + +codechecker_test( + name = "codechecker_per_file", + targets = ["//{target}"], + per_file = True, +) + +compile_commands( + name = "compile_commands", + targets = ["//{target}"], +) +""" + + +class FossTest(unittest.TestCase): + """Base test that downloads a FOSS project and runs rules_codechecker.""" + + # Set by main() + url = None + target = None + tests = None + + def setUp(self): + self.work_dir = Path(tempfile.mkdtemp()) + + # Resolve rules_codechecker path from the real script location + script_path = Path(os.path.realpath(__file__)) + self.rules_path = script_path.parent.parent.parent + + self._download_and_extract() + self._setup_bazel_project() + + def tearDown(self): + if self.work_dir.exists(): + subprocess.run( + ["bazel", f"--output_base={self.work_dir / '.bazel_output'}", + "shutdown"], + capture_output=True, + ) + subprocess.run( + ["chmod", "-R", "u+w", str(self.work_dir)], + capture_output=True, + ) + shutil.rmtree(self.work_dir, ignore_errors=True) + + def _download_and_extract(self): + archive = self.work_dir / "archive.tar.gz" + subprocess.run( + ["wget", "-q", "-O", str(archive), self.url], + check=True, + ) + with tarfile.open(archive) as tar: + members = tar.getmembers() + prefix = members[0].name.split("/")[0] + for m in members: + m.name = m.name[len(prefix):].lstrip("/") + if m.name: + tar.extract(m, self.work_dir / "src") + self.project_dir = self.work_dir / "src" + + def _setup_bazel_project(self): + analysis_dir = self.project_dir / "analysis" + analysis_dir.mkdir() + (analysis_dir / "BUILD.bazel").write_text( + BUILD_TEMPLATE.format(target=self.target) + ) + + (self.project_dir / "MODULE.bazel").write_text( + MODULE_TEMPLATE.format(rules_path=self.rules_path) + ) + (self.project_dir / "WORKSPACE").touch() + + def _bazel_build(self): + prefixed = [f"//analysis{t}" for t in self.tests] + result = subprocess.run( + ["bazel", + f"--output_base={self.work_dir / '.bazel_output'}", + "build"] + prefixed, + cwd=self.project_dir, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, + f"bazel build failed:\n{result.stderr}") + + def _bazel_bin(self): + result = subprocess.run( + ["bazel", + f"--output_base={self.work_dir / '.bazel_output'}", + "info", "bazel-bin"], + cwd=self.project_dir, + capture_output=True, + text=True, + ) + return Path(result.stdout.strip()) + + def test_build_succeeds(self): + """Verify that codechecker rules build successfully.""" + self._bazel_build() + + def test_compile_commands_valid(self): + """Verify compile_commands.json is valid and non-empty.""" + self._bazel_build() + bazel_bin = self._bazel_bin() + cc_json = bazel_bin / "analysis" / "compile_commands" / "compile_commands.json" + self.assertTrue(cc_json.exists(), + f"compile_commands.json not found at {cc_json}") + data = json.loads(cc_json.read_text()) + self.assertIsInstance(data, list) + self.assertGreater(len(data), 0, + "compile_commands.json is empty") + for entry in data: + self.assertIn("file", entry) + self.assertIn("directory", entry) + + def test_codechecker_outputs_exist(self): + """Verify codechecker produces expected output files.""" + self._bazel_build() + bazel_bin = self._bazel_bin() + cc_dir = bazel_bin / "analysis" / "codechecker_test" + self.assertTrue(cc_dir.exists(), + f"codechecker output dir not found at {cc_dir}") + cc_json = cc_dir / "compile_commands.json" + self.assertTrue(cc_json.exists(), + "codechecker compile_commands.json not found") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--url", required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--tests", nargs="+", required=True) + args, remaining = parser.parse_known_args() + + FossTest.url = args.url + FossTest.target = args.target + FossTest.tests = args.tests + + unittest.main(argv=[sys.argv[0]] + remaining) diff --git a/test/unit/generated_files/BUILD b/test/unit/generated_files/BUILD index b072d754..449af206 100644 --- a/test/unit/generated_files/BUILD +++ b/test/unit/generated_files/BUILD @@ -40,6 +40,7 @@ cc_binary( "genrule_header_consumer.cc", ":genrule_header.h", ], + copts = ["-Wno-unused-variable"], ) codechecker_test( diff --git a/test/unit/legacy/BUILD b/test/unit/legacy/BUILD index ac0c9ef2..cdea4b99 100644 --- a/test/unit/legacy/BUILD +++ b/test/unit/legacy/BUILD @@ -46,6 +46,7 @@ cc_library( cc_library( name = "test_lib", srcs = ["src/lib.cc"], + copts = ["-Wno-unused-variable"], ) # Test defect in CTU mode diff --git a/test/unit/virtual_include/BUILD b/test/unit/virtual_include/BUILD index 7aef09f4..f8f2ccca 100644 --- a/test/unit/virtual_include/BUILD +++ b/test/unit/virtual_include/BUILD @@ -34,6 +34,7 @@ cc_library( cc_library( name = "virtual_include", srcs = ["source.cc"], + copts = ["-Wno-return-type"], deps = ["test_inc"], )