Skip to content
Merged
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
123 changes: 122 additions & 1 deletion test/unit/parse/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ load(
"@rules_cc//cc:defs.bzl",
"cc_library",
)
load(
"@rules_python//python:py_library.bzl",
"py_library",
)
load(
"@rules_python//python:py_test.bzl",
"py_test",
)
load(
"//:defs.bzl",
"codechecker_test",
Expand All @@ -29,9 +37,122 @@ cc_library(
)

codechecker_test(
name = "codechecker",
name = "codechecker_data",
tags = ["manual"],
targets = [
"c_target",
],
)

codechecker_test(
name = "per_file_data",
per_file = True,
tags = ["manual"],
targets = [
"c_target",
],
)

# codechecker_data is a test target, so its own $(rootpaths) resolve to the
# test launcher. This plain filegroup re-exports the analysis output files,
# letting the tests receive their paths from Bazel and pick the report
# directory (codechecker-files) instead of hard-coding it.
filegroup(
name = "report_files",
testonly = True,
srcs = [":codechecker_data"],
tags = ["manual"],
)

filegroup(
name = "per_file_report_files",
testonly = True,
srcs = [":per_file_data"],
tags = ["manual"],
)

py_library(
name = "helpers",
srcs = ["helpers.py"],
imports = ["."],
)

py_test(
name = "parse_test",
srcs = ["parse_check.py"],
args = [
"--mode=parse",
"$(rootpaths :report_files)",
],
data = [":report_files"],
local = True,
main = "parse_check.py",
tags = ["unit"],
deps = [":helpers"],
)

py_test(
name = "parse_html_test",
srcs = ["parse_check.py"],
args = [
"--mode=parse_html",
"$(rootpaths :report_files)",
],
data = [":report_files"],
local = True,
main = "parse_check.py",
tags = ["unit"],
deps = [":helpers"],
)

py_test(
name = "store_test",
size = "large",
srcs = ["store_check.py"],
args = ["$(rootpaths :report_files)"],
data = [":report_files"],
local = True,
main = "store_check.py",
tags = ["unit"],
deps = [":helpers"],
)

py_test(
name = "per_file_parse_test",
srcs = ["parse_check.py"],
args = [
"--mode=parse",
"$(rootpaths :per_file_report_files)",
],
data = [":per_file_report_files"],
local = True,
main = "parse_check.py",
tags = ["unit"],
deps = [":helpers"],
)

py_test(
name = "per_file_parse_html_test",
srcs = ["parse_check.py"],
args = [
"--mode=parse_html",
"$(rootpaths :per_file_report_files)",
],
data = [":per_file_report_files"],
local = True,
main = "parse_check.py",
tags = ["unit"],
deps = [":helpers"],
)

py_test(
name = "per_file_store_test",
size = "large",
srcs = ["store_check.py"],
args = ["$(rootpaths :per_file_report_files)"],
data = [":per_file_report_files"],
local = True,
main = "store_check.py",
tags = ["unit"],
deps = [":helpers"],
)
62 changes: 62 additions & 0 deletions test/unit/parse/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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.

"""
Shared helpers for the parse and store verification scripts.
"""

import os
import subprocess
import sys

# Basename of the directory holding the analyzer result files
REPORT_DIR_NAME = "data"


def resolve_report_data(paths: list[str]) -> str:
Comment thread
furtib marked this conversation as resolved.
"""Return the report directory that CodeChecker parse/store consume.

Bazel passes every output of the analysis target via $(rootpaths). The
reports live in a directory named "data"; depending on the rule it is
either passed directly as a directory artifact (monolithic) or implied by
the individual report files inside it (per-file).
"""
for path in paths:
# Monolithic passes the "codechecker-files" directory artifact; its
# reports live in the "data" subdirectory.
if os.path.isdir(os.path.join(path, REPORT_DIR_NAME)):
return os.path.join(path, REPORT_DIR_NAME)
# Per-file passes individual report files, e.g. .../data/foo.plist;
# derive the enclosing "data" directory from them.
parent = os.path.dirname(path)
if os.path.basename(parent) == REPORT_DIR_NAME and os.path.isdir(
parent
):
return parent

print(f"FAILED: no {REPORT_DIR_NAME} report directory in paths: {paths}")
sys.exit(1)


def run_codechecker(arguments: list[str]) -> tuple[int, str, str]:
"""Run a CodeChecker command and return (exit_code, stdout, stderr)."""
command = ["CodeChecker"] + arguments
print(f"Running: {' '.join(command)}")
result = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
)
return result.returncode, result.stdout, result.stderr
120 changes: 120 additions & 0 deletions test/unit/parse/parse_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 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.

"""
Self-contained parse verification script for Bazel py_test targets.

Runs "CodeChecker parse" on the report files produced by a codechecker_test
target that is supplied as a data dependency. Two modes are supported:

parse Parse the results and assert a bug is reported.
CodeChecker parse exits with code 2 when it finds a report,
and 0 when it finds none. The analyzed source contains a bug,
so exit code 2 is expected.

parse_html Export the results as an HTML report into a temporary
directory and assert that a non-empty index.html is produced.

The report directory is passed in by Bazel via $(rootpaths), so no path is
hard-coded and no nested Bazel invocation is required.

Usage:
python parse_check.py --mode parse <rootpaths...>
python parse_check.py --mode parse_html <rootpaths...>
"""

import argparse
import os
import sys
import tempfile
from helpers import resolve_report_data, run_codechecker


def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Verify CodeChecker parse behavior."
)
parser.add_argument(
"--mode",
required=True,
choices=["parse", "parse_html"],
help="Which parse check to run.",
)
# This argument is also found in the store_check test
# pylint: disable=duplicate-code
parser.add_argument(
"paths",
nargs="+",
help="Runfiles paths of the analysis target ($(rootpaths ...)); "
"the report directory is selected from them.",
)
return parser.parse_args()


def check_parse(report_dir: str) -> int:
"""Parse the results and assert a bug is reported (exit code 2)."""
ret, stdout, stderr = run_codechecker(["parse", report_dir])
if ret != 2:
print(
f"FAILED: Expected CodeChecker parse to report a bug "
f"(exit code 2), got {ret}"
)
print(f"stdout:\n{stdout}")
print(f"stderr:\n{stderr}")
return 1
print("PASSED: CodeChecker parse reported a bug as expected.")
return 0


def check_parse_html(report_dir: str) -> int:
"""Export an HTML report and assert a non-empty index.html is produced."""
with tempfile.TemporaryDirectory() as html_dir:
ret, stdout, stderr = run_codechecker(
["parse", report_dir, "--export", "html", "--output", html_dir]
)
# CodeChecker parse exits 2 when reports are found, 0 otherwise;
# both indicate the export ran. Anything else is a real failure.
if ret not in (0, 2):
print(
f"FAILED: CodeChecker parse --export html failed with "
f"exit code {ret}"
)
print(f"stdout:\n{stdout}")
print(f"stderr:\n{stderr}")
return 1
index_html = os.path.join(html_dir, "index.html")
if not os.path.isfile(index_html):
print(f"FAILED: HTML report not produced at {index_html}")
print(f"stdout:\n{stdout}")
print(f"stderr:\n{stderr}")
return 1
if os.path.getsize(index_html) == 0:
print(f"FAILED: HTML report is empty at {index_html}")
return 1
print("PASSED: CodeChecker parse produced a non-empty HTML report.")
return 0


def main() -> int:
"""Entry point."""
args = parse_args()
report_dir = resolve_report_data(args.paths)
if args.mode == "parse":
return check_parse(report_dir)
return check_parse_html(report_dir)


if __name__ == "__main__":
sys.exit(main())
Loading
Loading