Skip to content

Commit a8a2e3d

Browse files
committed
Convert to py_test()
1 parent c4e9ece commit a8a2e3d

4 files changed

Lines changed: 213 additions & 118 deletions

File tree

test/bazel/BUILD

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,43 +27,20 @@
2727

2828
load(":foss_test.bzl", "foss_test")
2929

30-
BUILD_CONTENT = """
31-
load("@rules_codechecker//src:codechecker.bzl", "codechecker_test")
32-
load("@rules_codechecker//src:compile_commands.bzl", "compile_commands")
33-
34-
codechecker_test(
35-
name = "codechecker_test",
36-
targets = ["//{target}"],
37-
)
38-
39-
codechecker_test(
40-
name = "codechecker_per_file",
41-
targets = ["//{target}"],
42-
per_file = True,
43-
)
44-
45-
compile_commands(
46-
name = "compile_commands",
47-
targets = ["//{target}"],
48-
)
49-
"""
50-
5130
foss_test(
5231
name = "zlib",
53-
url = "https://github.com/madler/zlib/archive/5a82f71ed1dfc0bec044d9702463dbdf84ea3b71.tar.gz",
54-
build_content = BUILD_CONTENT.format(target = ":z"),
55-
targets = [
32+
url = "https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz",
33+
tests = [
5634
":codechecker_test",
5735
":compile_commands",
5836
":codechecker_per_file",
5937
],
6038
)
6139

6240
foss_test(
63-
name = "yaml_cpp",
41+
name = "yaml-cpp",
6442
url = "https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.tar.gz",
65-
build_content = BUILD_CONTENT.format(target = ":yaml-cpp"),
66-
targets = [
43+
tests = [
6744
":codechecker_test",
6845
":compile_commands",
6946
# FIXME: output 'analysis/codechecker_per_file/data/src-emit.cpp_clangsa.plist' was not created

test/bazel/foss_test.bzl

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,52 @@
1515
"""
1616
Macro for generating FOSS integration tests for rules_codechecker.
1717
18-
Each foss_test() generates a local sh_test that:
18+
Each foss_test() generates a local py_test that:
1919
1. Downloads a FOSS project into a temp directory
2020
2. Sets up a standalone Bazel project with rules_codechecker
2121
3. Runs "bazel build" on codechecker targets to verify the rules work
22+
4. Validates the outputs (compile_commands.json, codechecker artifacts)
2223
2324
Example:
2425
foss_test(
2526
name = "zlib",
2627
url = "https://github.com/madler/zlib/archive/<commit>.tar.gz",
27-
build_content = "cc_library(...)",
28-
targets = [":codechecker_test"],
28+
tests = [":codechecker_test", ":compile_commands"],
2929
)
3030
"""
3131

3232
def foss_test(
3333
name,
3434
url,
35-
build_content,
36-
targets,
35+
tests,
36+
target = None,
3737
tags = [],
38-
size = "enormous",
38+
size = "large",
3939
**kwargs):
40-
"""Generate an sh_test that runs rules_codechecker on a FOSS project.
40+
"""Generate a py_test that runs rules_codechecker on a FOSS project.
4141
4242
Args:
4343
name: Test name.
4444
url: URL to the source archive (.tar.gz).
45-
build_content: BUILD file content appended to the project.
46-
targets: Bazel targets to build inside the FOSS project.
45+
tests: Analysis targets to build (e.g. codechecker_test, compile_commands).
46+
target: The cc_library target to analyze. Defaults to ":<name>".
4747
tags: Additional test tags.
4848
size: Test size (default: enormous, as these download + run bazel).
49-
**kwargs: Forwarded to sh_test.
49+
**kwargs: Forwarded to py_test.
5050
"""
51-
native.sh_test(
51+
if target == None:
52+
target = ":" + name
53+
54+
native.py_test(
5255
name = name,
53-
srcs = ["foss_test_runner.sh"],
54-
env = {
55-
"FOSS_URL": url,
56-
"FOSS_BUILD": build_content,
57-
"FOSS_TARGETS": " ".join(targets),
58-
},
56+
srcs = ["foss_test_runner.py"],
57+
main = "foss_test_runner.py",
58+
args = [
59+
"-vvv",
60+
"--url=" + url,
61+
"--target=" + target,
62+
"--tests"
63+
] + tests,
5964
local = True,
6065
tags = ["foss", "external"] + tags,
6166
size = size,

test/bazel/foss_test_runner.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Copyright 2026 Ericsson AB
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
FOSS integration test runner for rules_codechecker.
17+
18+
Downloads a FOSS project, sets up a standalone Bazel project with
19+
rules_codechecker, builds codechecker targets, and verifies outputs.
20+
"""
21+
22+
import argparse
23+
import json
24+
import os
25+
import shutil
26+
import subprocess
27+
import sys
28+
import tarfile
29+
import tempfile
30+
import unittest
31+
from pathlib import Path
32+
33+
MODULE_TEMPLATE = """
34+
local_path_override(
35+
module_name = "rules_codechecker",
36+
path = "{rules_path}",
37+
)
38+
bazel_dep(name = "rules_codechecker")
39+
"""
40+
41+
BUILD_TEMPLATE = """
42+
load("@rules_codechecker//src:codechecker.bzl", "codechecker_test")
43+
load("@rules_codechecker//src:compile_commands.bzl", "compile_commands")
44+
45+
codechecker_test(
46+
name = "codechecker_test",
47+
targets = ["//{target}"],
48+
)
49+
50+
codechecker_test(
51+
name = "codechecker_per_file",
52+
targets = ["//{target}"],
53+
per_file = True,
54+
)
55+
56+
compile_commands(
57+
name = "compile_commands",
58+
targets = ["//{target}"],
59+
)
60+
"""
61+
62+
63+
class FossTest(unittest.TestCase):
64+
"""Base test that downloads a FOSS project and runs rules_codechecker."""
65+
66+
# Set by main()
67+
url = None
68+
target = None
69+
tests = None
70+
71+
def setUp(self):
72+
self.work_dir = Path(tempfile.mkdtemp())
73+
74+
# Resolve rules_codechecker path from the real script location
75+
script_path = Path(os.path.realpath(__file__))
76+
self.rules_path = script_path.parent.parent.parent
77+
78+
self._download_and_extract()
79+
self._setup_bazel_project()
80+
81+
def tearDown(self):
82+
if self.work_dir.exists():
83+
subprocess.run(
84+
["bazel", f"--output_base={self.work_dir / '.bazel_output'}",
85+
"shutdown"],
86+
capture_output=True,
87+
)
88+
subprocess.run(
89+
["chmod", "-R", "u+w", str(self.work_dir)],
90+
capture_output=True,
91+
)
92+
shutil.rmtree(self.work_dir, ignore_errors=True)
93+
94+
def _download_and_extract(self):
95+
archive = self.work_dir / "archive.tar.gz"
96+
subprocess.run(
97+
["wget", "-q", "-O", str(archive), self.url],
98+
check=True,
99+
)
100+
with tarfile.open(archive) as tar:
101+
members = tar.getmembers()
102+
prefix = members[0].name.split("/")[0]
103+
for m in members:
104+
m.name = m.name[len(prefix):].lstrip("/")
105+
if m.name:
106+
tar.extract(m, self.work_dir / "src")
107+
self.project_dir = self.work_dir / "src"
108+
109+
def _setup_bazel_project(self):
110+
analysis_dir = self.project_dir / "analysis"
111+
analysis_dir.mkdir()
112+
(analysis_dir / "BUILD.bazel").write_text(
113+
BUILD_TEMPLATE.format(target=self.target)
114+
)
115+
116+
(self.project_dir / "MODULE.bazel").write_text(
117+
MODULE_TEMPLATE.format(rules_path=self.rules_path)
118+
)
119+
(self.project_dir / "WORKSPACE").touch()
120+
121+
def _bazel_build(self):
122+
prefixed = [f"//analysis{t}" for t in self.tests]
123+
result = subprocess.run(
124+
["bazel",
125+
f"--output_base={self.work_dir / '.bazel_output'}",
126+
"build"] + prefixed,
127+
cwd=self.project_dir,
128+
capture_output=True,
129+
text=True,
130+
)
131+
self.assertEqual(result.returncode, 0,
132+
f"bazel build failed:\n{result.stderr}")
133+
134+
def _bazel_bin(self):
135+
result = subprocess.run(
136+
["bazel",
137+
f"--output_base={self.work_dir / '.bazel_output'}",
138+
"info", "bazel-bin"],
139+
cwd=self.project_dir,
140+
capture_output=True,
141+
text=True,
142+
)
143+
return Path(result.stdout.strip())
144+
145+
def test_build_succeeds(self):
146+
"""Verify that codechecker rules build successfully."""
147+
self._bazel_build()
148+
149+
def test_compile_commands_valid(self):
150+
"""Verify compile_commands.json is valid and non-empty."""
151+
self._bazel_build()
152+
bazel_bin = self._bazel_bin()
153+
cc_json = bazel_bin / "analysis" / "compile_commands" / "compile_commands.json"
154+
self.assertTrue(cc_json.exists(),
155+
f"compile_commands.json not found at {cc_json}")
156+
data = json.loads(cc_json.read_text())
157+
self.assertIsInstance(data, list)
158+
self.assertGreater(len(data), 0,
159+
"compile_commands.json is empty")
160+
for entry in data:
161+
self.assertIn("file", entry)
162+
self.assertIn("directory", entry)
163+
164+
def test_codechecker_outputs_exist(self):
165+
"""Verify codechecker produces expected output files."""
166+
self._bazel_build()
167+
bazel_bin = self._bazel_bin()
168+
cc_dir = bazel_bin / "analysis" / "codechecker_test"
169+
self.assertTrue(cc_dir.exists(),
170+
f"codechecker output dir not found at {cc_dir}")
171+
cc_json = cc_dir / "compile_commands.json"
172+
self.assertTrue(cc_json.exists(),
173+
"codechecker compile_commands.json not found")
174+
175+
176+
if __name__ == "__main__":
177+
parser = argparse.ArgumentParser()
178+
parser.add_argument("--url", required=True)
179+
parser.add_argument("--target", required=True)
180+
parser.add_argument("--tests", nargs="+", required=True)
181+
args, remaining = parser.parse_known_args()
182+
183+
FossTest.url = args.url
184+
FossTest.target = args.target
185+
FossTest.tests = args.tests
186+
187+
unittest.main(argv=[sys.argv[0]] + remaining)

test/bazel/foss_test_runner.sh

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)