-
Notifications
You must be signed in to change notification settings - Fork 4
Prototype Bazel-native FOSS tests #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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", | ||
| ) |
| 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", | ||
| ], | ||
| url = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz", | ||
| ) | ||
| 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 | ||
| ) |
| 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is more readable like this.
Suggested change
|
||||||
|
|
||||||
| 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) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this probably should be done different way...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I may have said it wrong.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to test Bazel 6 without the bzlmod system too!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||||||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.