Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .ci/mise/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 2 additions & 20 deletions BUILD
Original file line number Diff line number Diff line change
@@ -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",
)
3 changes: 1 addition & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions test/bazel/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
Comment on lines +45 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to only happen in mise.
This does not happen in any CI or when simply running it locally.
I do not agree with disabling this for the sake of mise passing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a big bug to fix! In the test or in mise.
We MUST NOT accept "works for me" excuses.

Copy link
Copy Markdown
Contributor

@Szelethus Szelethus Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. That is likely a bug in mise that is irreproducible locally or in CI. I think its unreasonable to support it like this. We landed mise (#132 (review)) and micromamba (#167 (review)) on the condition that we will see how they can assist testing, but will not unconditionally fix or hack around related breakages until they are proven to be reliable.

url = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz",
)
70 changes: 70 additions & 0 deletions test/bazel/foss_test.bzl
Original file line number Diff line number Diff line change
@@ -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/<commit>.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 ":<name>".
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
)
187 changes: 187 additions & 0 deletions test/bazel/foss_test_runner.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more readable like this.

Suggested change
self.rules_path = script_path.parent.parent.parent
self.rules_path = script_path.parents[2]


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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need this; Bazel will remove the sandbox at the end of the run.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this probably should be done different way...
But what do you mean by "Bazel will remove the sandbox at the end of the run"?
Do you mean bazel clean?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I may have said it wrong.
I meant that Bazel will run it in a clean state each time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I thought this was teardownClass.


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()

Comment on lines +116 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to test Bazel 6 without the bzlmod system too!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, Bazel 6 is not supported in this proto yet

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)
1 change: 1 addition & 0 deletions test/unit/generated_files/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cc_binary(
"genrule_header_consumer.cc",
":genrule_header.h",
],
copts = ["-Wno-unused-variable"],
)

codechecker_test(
Expand Down
1 change: 1 addition & 0 deletions test/unit/legacy/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ cc_library(
cc_library(
name = "test_lib",
srcs = ["src/lib.cc"],
copts = ["-Wno-unused-variable"],
)

# Test defect in CTU mode
Expand Down
1 change: 1 addition & 0 deletions test/unit/virtual_include/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ cc_library(
cc_library(
name = "virtual_include",
srcs = ["source.cc"],
copts = ["-Wno-return-type"],
deps = ["test_inc"],
)

Expand Down
Loading