Files
cpp-project-template/scripts/dev_check.py
T
2026-05-18 09:41:16 +08:00

1142 lines
42 KiB
Python

#!/usr/bin/env python3
"""Developer checks for the C++ template project."""
from __future__ import annotations
# pyright: reportAny=false
import argparse
import pathlib
from collections.abc import Callable, Sequence
import shutil
import subprocess
import tempfile
from typing import cast
from lib import cmake_fixture_runner, common, network_guard
PROJECT_ROOT = common.find_project_root(pathlib.Path(__file__))
DEFAULT_EVIDENCE_DIR = PROJECT_ROOT / ".sisyphus" / "evidence" / "no-network"
DEFAULT_BUILD_ROOT = PROJECT_ROOT / "build" / "no-network"
DEFAULT_FUZZ_EVIDENCE_DIR = PROJECT_ROOT / ".sisyphus" / "evidence" / "fuzz-no-network"
DEFAULT_INSTALL_CONSUMER_EVIDENCE_DIR = PROJECT_ROOT / ".sisyphus" / "evidence" / "install-consumer"
DEFAULT_FUZZTEST_OPTIONAL_EVIDENCE_DIR = PROJECT_ROOT / ".sisyphus" / "evidence" / "fuzztest-optional-features"
FUZZTEST_LANE_PACKAGES = (
"abseil",
"re2",
"googletest-gmock-1.17.x",
"antlr4-runtime",
"patched-fuzztest",
)
FUZZTEST_ARCHIVES = (
network_guard.ArchiveSpec(
"abseil",
PROJECT_ROOT / "3rd" / "archives" / "abseil-cpp-20260107.1.tar.gz",
"4314e2a7cbac89cac25a2f2322870f343d81579756ceff7f431803c2c9090195",
),
network_guard.ArchiveSpec(
"re2",
PROJECT_ROOT / "3rd" / "archives" / "re2-2025-11-05.tar.gz",
"87f6029d2f6de8aa023654240a03ada90e876ce9a4676e258dd01ea4c26ffd67",
),
network_guard.ArchiveSpec(
"googletest-gmock-1.17.x",
PROJECT_ROOT / "3rd" / "archives" / "googletest-1.17.0.tar.gz",
"65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c",
),
network_guard.ArchiveSpec(
"antlr4-runtime",
PROJECT_ROOT / "3rd" / "archives" / "antlr4-4.13.2.tar.gz",
"9f18272a9b32b622835a3365f850dd1063d60f5045fb1e12ce475ae6e18a35bb",
),
network_guard.ArchiveSpec(
"patched-fuzztest",
PROJECT_ROOT / "3rd" / "archives" / "fuzztest-2026-02-19.tar.gz",
"1c6e04065eb988e2c99613369db8294aa58429d392bf479740b237f1255204ef",
),
)
FUZZTEST_REQUIRED_MARKERS = (
"fuzz lane: materializing patched FuzzTest",
"patches: 1 file(s)",
"patched source materialized:",
"fuzz lane: archive verified, patches applied",
"fuzz lane: FuzzTest 2026-02-19 loaded and patched successfully",
"fuzz lane: abseil-cpp 20260107.1 loaded from local archive",
"fuzz lane: re2 2025-11-05 loaded from local archive",
"fuzz lane: antlr4_runtime 4.13.2 loaded from local archive",
"fuzz lane: GTest/GMock 1.17.0 loaded from local archive",
)
FUZZTEST_OPTIONAL_REQUIRED_MARKERS = (
"T36 guard: FUZZTEST_BUILD_TESTING=OFF",
"T36 guard: FUZZTEST_BUILD_FLATBUFFERS=OFF",
"T36 guard: optional target absent: protobuf::libprotobuf",
"T36 guard: optional target absent: nlohmann_json::nlohmann_json",
"T36 guard: optional target absent: flatbuffers",
)
FUZZTEST_OPTIONAL_CACHE_MARKERS = (
"FUZZTEST_BUILD_TESTING:BOOL=OFF",
"FUZZTEST_BUILD_FLATBUFFERS:BOOL=OFF",
)
FUZZTEST_OPTIONAL_FORBIDDEN_TARGETS = (
"protobuf::libprotobuf",
"nlohmann_json::nlohmann_json",
"flatbuffers",
)
FUZZTEST_OPTIONAL_SCAN_TERMS = (
"protobuf",
"libprotobuf",
"nlohmann",
"flatbuffers",
)
STATUS_PASS = "PASS"
STATUS_FAIL = "FAIL"
STATUS_DEFERRED = "DEFERRED"
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.fast:
return run_fast(args)
if not hasattr(args, "func"):
parser.print_help()
return 0
func = cast(Callable[[argparse.Namespace], int], args.func)
return func(args)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run local developer verification checks.",
)
_ = parser.add_argument(
"--fast",
action="store_true",
help="Run the fast developer check sequence: format, debug configure/build/test, clang-tidy.",
)
_ = parser.add_argument(
"--dry-run",
action="store_true",
help="Print the selected command sequence without executing it.",
)
subparsers = parser.add_subparsers(dest="command")
default_parser = subparsers.add_parser(
"no-network-default",
help="Configure, and optionally build, with invalid proxies and an isolated CPM cache.",
)
_ = default_parser.add_argument("--build", action="store_true", help="Run cmake --build after configure succeeds.")
_ = default_parser.add_argument("--build-type", default="Debug", help="CMake build type, default: Debug.")
_ = default_parser.add_argument(
"--build-dir",
type=pathlib.Path,
default=DEFAULT_BUILD_ROOT / "debug",
help="Clean build directory to use for the check.",
)
_ = default_parser.add_argument(
"--cpm-cache-dir",
type=pathlib.Path,
default=DEFAULT_BUILD_ROOT / "cpm-cache",
help="Clean isolated CPM source cache directory.",
)
_ = default_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=DEFAULT_EVIDENCE_DIR,
help="Directory for command evidence logs.",
)
_ = default_parser.add_argument(
"--cmake-arg",
action="append",
default=[],
help="Additional CMake configure argument. Repeat for multiple arguments.",
)
_ = default_parser.set_defaults(func=run_no_network_default)
archive_parser = subparsers.add_parser(
"no-network-missing-archive",
help="Check the local archive validation path used before CMake dependency materialization.",
)
_ = archive_parser.add_argument("--name", default="example-dependency", help="Dependency/archive label.")
_ = archive_parser.add_argument(
"--archive",
type=pathlib.Path,
help="Archive path to require. Omit to report deferred until T13/T14 metadata exists.",
)
_ = archive_parser.add_argument("--sha256", help="Optional expected archive sha256.")
_ = archive_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=DEFAULT_EVIDENCE_DIR,
help="Directory for evidence logs.",
)
_ = archive_parser.set_defaults(func=run_missing_archive_check)
fuzztest_parser = subparsers.add_parser(
"no-network-fuzztest-lane",
help="Verify the FuzzTest lane with invalid proxies and an isolated CPM cache.",
)
_ = fuzztest_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=DEFAULT_FUZZ_EVIDENCE_DIR,
)
_ = fuzztest_parser.add_argument(
"--build-dir",
type=pathlib.Path,
default=DEFAULT_BUILD_ROOT / "fuzztest-lane",
help="Clean build directory to use for the FuzzTest no-network lane.",
)
_ = fuzztest_parser.add_argument(
"--cpm-cache-dir",
type=pathlib.Path,
default=DEFAULT_BUILD_ROOT / "fuzztest-cpm-cache",
help="Clean isolated CPM source cache directory for the FuzzTest lane.",
)
_ = fuzztest_parser.add_argument(
"--build-type",
default="Debug",
help="CMake build type, default: Debug.",
)
_ = fuzztest_parser.add_argument(
"--build-target",
default="calc_fuzz",
help="Build target that forces FuzzTest materialization, default: calc_fuzz.",
)
_ = fuzztest_parser.set_defaults(func=run_fuzztest_lane_check)
cmake_helper_parser = subparsers.add_parser(
"cmake-helper-fixtures",
help="Run CMake helper API positive/negative fixture configure tests.",
)
_ = cmake_helper_parser.add_argument(
"--subset",
nargs="*",
default=None,
help="Run only these fixture case IDs (e.g. 01-cc-project-top-level). Omit for all.",
)
_ = cmake_helper_parser.add_argument(
"--no-positive",
action="store_true",
help="Skip positive fixtures.",
)
_ = cmake_helper_parser.add_argument(
"--no-negative",
action="store_true",
help="Skip negative fixtures.",
)
_ = cmake_helper_parser.add_argument(
"--no-subproject",
action="store_true",
help="Skip subproject fixtures.",
)
_ = cmake_helper_parser.set_defaults(func=run_cmake_helper_fixtures)
install_consumer_parser = subparsers.add_parser(
"install-consumer",
help="Run install-consumer QA: install fixture package, build/run separate consumer.",
)
_ = install_consumer_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=DEFAULT_INSTALL_CONSUMER_EVIDENCE_DIR,
help="Directory for install-consumer evidence logs.",
)
_ = install_consumer_parser.set_defaults(func=run_install_consumer)
gtest_isolation_parser = subparsers.add_parser(
"gtest-isolation",
help="T35: Verify GTest lane isolation (normal=1.16, fuzz=1.17, conflict rejection).",
)
_ = gtest_isolation_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=PROJECT_ROOT / ".sisyphus" / "evidence" / "gtest-isolation",
help="Directory for gtest-isolation evidence logs.",
)
_ = gtest_isolation_parser.add_argument(
"--build-dir",
type=pathlib.Path,
default=PROJECT_ROOT / "build" / "gtest-isolation",
help="Build directory for gtest-isolation checks.",
)
_ = gtest_isolation_parser.set_defaults(func=run_gtest_isolation)
fuzztest_optional_parser = subparsers.add_parser(
"fuzztest-optional-features",
help="T36: Verify FuzzTest protobuf/nlohmann_json/flatbuffers optional paths stay disabled.",
)
_ = fuzztest_optional_parser.add_argument(
"--log-dir",
type=pathlib.Path,
default=DEFAULT_FUZZTEST_OPTIONAL_EVIDENCE_DIR,
help="Directory for fuzztest-optional-features evidence logs.",
)
_ = fuzztest_optional_parser.add_argument(
"--build-dir",
type=pathlib.Path,
default=PROJECT_ROOT / "build" / "fuzztest-optional-features",
help="Build directory for fuzztest-optional-features checks.",
)
_ = fuzztest_optional_parser.set_defaults(func=run_fuzztest_optional_features)
return parser
def run_fast(args: argparse.Namespace) -> int:
if args.command is not None:
print(f"{STATUS_FAIL}: --fast cannot be combined with subcommand {args.command}")
return 2
plans = fast_check_plan(PROJECT_ROOT)
print("fast developer check sequence")
if args.dry_run:
common.print_plan(plans)
print(f"{STATUS_PASS}: dry-run planned fast checks with active clang-tidy enforcement")
return 0
return common.run_plans(plans, dry_run=False)
def fast_check_plan(project_root: pathlib.Path) -> tuple[common.CommandPlan, ...]:
build_dir = project_root / "build" / "debug"
scripts_dir = project_root / "scripts"
return (
common.CommandPlan(
label="format-check",
command=common.python_command(scripts_dir / "format.py", "--check"),
cwd=project_root,
),
common.CommandPlan(
label="debug-configure",
command=(
"cmake",
"-S",
str(project_root),
"-B",
str(build_dir),
"-DCMAKE_BUILD_TYPE=Debug",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
),
cwd=project_root,
required_tool="cmake",
),
common.CommandPlan(
label="debug-build",
command=("cmake", "--build", str(build_dir)),
cwd=project_root,
required_tool="cmake",
),
common.CommandPlan(
label="debug-test",
command=("ctest", "--test-dir", str(build_dir), "--output-on-failure"),
cwd=project_root,
required_tool="ctest",
),
common.CommandPlan(
label="clang-tidy",
command=common.python_command(scripts_dir / "clang_tidy.py", "--build-dir", str(build_dir)),
cwd=project_root,
),
# Fast subset of cmake helper fixtures: positive only, no subproject.
*cmake_fixture_runner.build_cmake_plans(
project_root,
include_positive=True,
include_negative=False,
include_subproject=False,
),
)
def run_no_network_default(args: argparse.Namespace) -> int:
config = network_guard.CMakeNoNetworkConfig(
source_dir=PROJECT_ROOT,
build_dir=args.build_dir.resolve(),
cpm_cache_dir=args.cpm_cache_dir.resolve(),
build_type=args.build_type,
extra_cmake_args=tuple(args.cmake_arg),
)
env = network_guard.invalid_proxy_environment()
network_guard.prepare_cmake_run(config)
print("no-network default configure")
print(f"build_dir: {config.build_dir}")
print(f"cpm_cache_dir: {config.cpm_cache_dir}")
print(f"proxy: {network_guard.INVALID_PROXY_URL}")
configure_result = network_guard.run_logged_command(
network_guard.cmake_configure_command(config),
cwd=PROJECT_ROOT,
env=env,
log_path=args.log_dir / "default-configure.log",
)
_print_command_summary("configure", configure_result)
if configure_result.returncode != 0:
print(f"{STATUS_FAIL}: configure failed under invalid-proxy no-network harness")
return configure_result.returncode
if not args.build:
print(f"{STATUS_PASS}: configure completed with invalid proxies and isolated CPM cache")
return 0
build_result = network_guard.run_logged_command(
network_guard.cmake_build_command(config),
cwd=PROJECT_ROOT,
env=env,
log_path=args.log_dir / "default-build.log",
)
_print_command_summary("build", build_result)
if build_result.returncode != 0:
print(f"{STATUS_FAIL}: build failed under invalid-proxy no-network harness")
return build_result.returncode
print(f"{STATUS_PASS}: configure and build completed with invalid proxies and isolated CPM cache")
return 0
def run_missing_archive_check(args: argparse.Namespace) -> int:
args.log_dir.mkdir(parents=True, exist_ok=True)
log_name = "missing-archive-negative.log" if args.archive is not None else "missing-archive-deferred.log"
log_path = args.log_dir / log_name
if args.archive is None:
spec = network_guard.ArchiveSpec(
name=args.name,
path=(PROJECT_ROOT / "3rd" / "archives" / "__missing_archive_negative_probe__.tar.gz").resolve(),
sha256=args.sha256,
)
try:
network_guard.verify_archive(spec)
except network_guard.LocalInputError as exc:
message = f"{STATUS_PASS}: missing archive rejected locally for {args.name}: {exc}"
log_path.write_text(message + "\n", encoding="utf-8")
print(message)
return 0
message = f"{STATUS_FAIL}: synthetic missing archive unexpectedly verified for {args.name}: {spec.path}"
log_path.write_text(message + "\n", encoding="utf-8")
print(message)
return 1
spec = network_guard.ArchiveSpec(name=args.name, path=args.archive.resolve(), sha256=args.sha256)
try:
network_guard.verify_archive(spec)
except network_guard.LocalInputError as exc:
message = f"{STATUS_FAIL}: {exc}"
log_path.write_text(message + "\n", encoding="utf-8")
print(message)
return 1
message = f"{STATUS_PASS}: local archive verified for {args.name}: {spec.path}"
log_path.write_text(message + "\n", encoding="utf-8")
print(message)
return 0
def run_fuzztest_lane_check(args: argparse.Namespace) -> int:
log_dir = args.log_dir.resolve()
log_dir.mkdir(parents=True, exist_ok=True)
archive_log = log_dir / "fuzztest-archives.log"
try:
_verify_fuzztest_archives(FUZZTEST_ARCHIVES, archive_log)
except network_guard.LocalInputError as exc:
print(f"{STATUS_FAIL}: {exc}")
return 1
config = network_guard.CMakeNoNetworkConfig(
source_dir=PROJECT_ROOT,
build_dir=args.build_dir.resolve(),
cpm_cache_dir=args.cpm_cache_dir.resolve(),
build_type=args.build_type,
extra_cmake_args=(
"-DCPP_TEMPLATE_FUZZ_LANE=ON",
"-DCPP_TEMPLATE_ENABLE_TESTS=OFF",
"-DCPP_TEMPLATE_ENABLE_BENCHMARKS=OFF",
"-DCMAKE_CXX_STANDARD=17",
"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",
),
)
env = network_guard.invalid_proxy_environment()
network_guard.prepare_cmake_run(config)
print("no-network FuzzTest lane")
print(f"build_dir: {config.build_dir}")
print(f"cpm_cache_dir: {config.cpm_cache_dir}")
print(f"proxy: {network_guard.INVALID_PROXY_URL}")
print(f"build_target: {args.build_target}")
configure_result = network_guard.run_logged_command(
network_guard.cmake_configure_command(config),
cwd=PROJECT_ROOT,
env=env,
log_path=log_dir / "fuzztest-configure.log",
)
_append_fuzztest_harness_evidence(log_dir / "fuzztest-configure.log", config, env)
_print_command_summary("configure", configure_result)
if configure_result.returncode != 0:
print(f"{STATUS_FAIL}: FuzzTest lane configure failed under invalid-proxy no-network harness")
return configure_result.returncode
missing_markers = _missing_fuzztest_markers(configure_result.stdout + "\n" + configure_result.stderr)
marker_log = log_dir / "fuzztest-marker-check.log"
if missing_markers:
marker_log.write_text(
"missing required FuzzTest no-network proof markers:\n"
+ "\n".join(f"- {marker}" for marker in missing_markers)
+ "\n",
encoding="utf-8",
)
print(f"{STATUS_FAIL}: configure log is missing FuzzTest no-network proof markers")
return 1
marker_log.write_text(
"required FuzzTest no-network proof markers found:\n"
+ "\n".join(f"- {marker}" for marker in FUZZTEST_REQUIRED_MARKERS)
+ "\n",
encoding="utf-8",
)
build_result = network_guard.run_logged_command(
network_guard.cmake_build_command(config, targets=(args.build_target,)),
cwd=PROJECT_ROOT,
env=env,
log_path=log_dir / "fuzztest-build.log",
)
_print_command_summary("build", build_result)
if build_result.returncode != 0:
print(f"{STATUS_FAIL}: FuzzTest lane build failed under invalid-proxy no-network harness")
return build_result.returncode
negative_status = _run_fuzztest_missing_archive_negative(log_dir)
if negative_status != 0:
return negative_status
_write_fuzztest_readme(log_dir, config, args.build_target)
package_list = ", ".join(FUZZTEST_LANE_PACKAGES)
print(f"{STATUS_PASS}: FuzzTest no-network lane verified local archives/patches only for {package_list}")
return 0
def _verify_fuzztest_archives(specs: Sequence[network_guard.ArchiveSpec], log_path: pathlib.Path) -> None:
lines = ["FuzzTest lane local archive preflight"]
for spec in specs:
lines.append(f"verifying archive: {spec.name}: {spec.path}")
network_guard.verify_archive(spec)
lines.append(f"verified archive: {spec.name}: {spec.path}")
_ = log_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def _missing_fuzztest_markers(output: str) -> tuple[str, ...]:
return tuple(marker for marker in FUZZTEST_REQUIRED_MARKERS if marker not in output)
def _run_fuzztest_missing_archive_negative(log_dir: pathlib.Path) -> int:
with tempfile.TemporaryDirectory(prefix="cpp-template-fuzz-no-network-") as temp_dir_name:
temp_root = pathlib.Path(temp_dir_name) / "repo"
_ = shutil.copytree(
PROJECT_ROOT,
temp_root,
ignore=shutil.ignore_patterns(".git", "build", ".cache", ".codegraph"),
)
missing_spec = FUZZTEST_ARCHIVES[0]
temp_archive = temp_root / missing_spec.path.relative_to(PROJECT_ROOT)
temp_archive.unlink()
temp_spec = network_guard.ArchiveSpec(
name=missing_spec.name,
path=temp_archive,
sha256=missing_spec.sha256,
)
log_path = log_dir / "fuzztest-missing-archive-negative.log"
try:
network_guard.verify_archive(temp_spec)
except network_guard.LocalInputError as exc:
_ = log_path.write_text(
"\n".join(
(
"negative missing-archive check",
f"removed_archive: {temp_archive}",
"phase: local archive preflight before CMake configure",
f"result: expected local failure: {exc}",
"",
)
),
encoding="utf-8",
)
print(f"negative_missing_archive: expected local failure for {missing_spec.name}")
return 0
_ = log_path.write_text(
"\n".join(
(
"negative missing-archive check",
f"removed_archive: {temp_archive}",
"result: unexpected success; local archive preflight did not fail",
"",
)
),
encoding="utf-8",
)
print(f"{STATUS_FAIL}: missing archive negative check unexpectedly succeeded")
return 1
def _append_fuzztest_harness_evidence(
log_path: pathlib.Path,
config: network_guard.CMakeNoNetworkConfig,
env: dict[str, str],
) -> None:
with log_path.open("a", encoding="utf-8") as handle:
_ = handle.write("\nT34 harness evidence:\n")
_ = handle.write(f"HTTP_PROXY={env.get('HTTP_PROXY', '')}\n")
_ = handle.write(f"HTTPS_PROXY={env.get('HTTPS_PROXY', '')}\n")
_ = handle.write(f"ALL_PROXY={env.get('ALL_PROXY', '')}\n")
_ = handle.write(f"NO_PROXY={env.get('NO_PROXY', '')}\n")
_ = handle.write(f"CPM_SOURCE_CACHE={config.cpm_cache_dir}\n")
def _write_fuzztest_readme(
log_dir: pathlib.Path,
config: network_guard.CMakeNoNetworkConfig,
build_target: str,
) -> None:
readme = log_dir / "README.md"
_ = readme.write_text(
"\n".join(
(
"# T34 FuzzTest no-network evidence",
"",
"Generated by `python3 scripts/dev_check.py no-network-fuzztest-lane`.",
"",
"## Contract",
"",
"- Uses invalid HTTP/HTTPS/ALL proxies instead of firewall or sudo controls.",
"- Uses a fresh build directory and isolated empty `CPM_SOURCE_CACHE`.",
"- Verifies committed fuzz-lane archives before configure.",
"- Configures `CPP_TEMPLATE_FUZZ_LANE=ON` and builds the fuzz smoke target.",
"- Runs a negative temp-copy missing-archive check without mutating real archives.",
"",
"## Paths",
"",
f"- build_dir: `{config.build_dir}`",
f"- cpm_cache_dir: `{config.cpm_cache_dir}`",
f"- build_target: `{build_target}`",
"",
"## Logs",
"",
"- `fuzztest-archives.log`",
"- `fuzztest-configure.log`",
"- `fuzztest-marker-check.log`",
"- `fuzztest-build.log`",
"- `fuzztest-missing-archive-negative.log`",
"",
)
),
encoding="utf-8",
)
def run_cmake_helper_fixtures(args: argparse.Namespace) -> int:
subset = tuple(args.subset) if args.subset else None
results = cmake_fixture_runner.run_all_fixtures(
PROJECT_ROOT,
dry_run=args.dry_run,
subset=subset,
include_positive=not args.no_positive,
include_negative=not args.no_negative,
include_subproject=not args.no_subproject,
)
return cmake_fixture_runner.summarize_results(results)
def run_install_consumer(args: argparse.Namespace) -> int:
results = cmake_fixture_runner.run_all_install_consumers(
PROJECT_ROOT,
dry_run=args.dry_run,
log_dir=args.log_dir,
)
return cmake_fixture_runner.summarize_install_consumer_results(results)
def _run_gtest_isolation_positive_debug(
project_root: pathlib.Path, build_dir: pathlib.Path, log_dir: pathlib.Path,
) -> int:
"""Positive check: debug lane loads only GTest 1.16.0."""
debug_build = build_dir / "positive-debug"
if debug_build.is_dir():
shutil.rmtree(debug_build)
debug_build.mkdir(parents=True, exist_ok=True)
cmd = (
"cmake", "-S", str(project_root), "-B", str(debug_build),
"-DCMAKE_BUILD_TYPE=Debug",
"-DCMAKE_CXX_STANDARD=14",
"-DCPP_TEMPLATE_ENABLE_TESTS=ON",
"-DCPP_TEMPLATE_ENABLE_BENCHMARKS=OFF",
"-DCPP_TEMPLATE_FUZZ_LANE=OFF",
)
result = subprocess.run(list(cmd), capture_output=True, text=True, check=False)
log_content = (
f"command: {' '.join(cmd)}\n"
f"returncode: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}"
f"--- stderr ---\n{result.stderr}"
)
_ = (log_dir / "positive-debug-configure.log").write_text(log_content, encoding="utf-8")
if result.returncode != 0:
print(f"{STATUS_FAIL}: positive-debug configure failed")
return 1
output = result.stdout + result.stderr
required_markers = [
"normal lane: GTest/GMock 1.16.0 loaded",
"T35 guard: CPP_TEMPLATE_GTEST_LANE_ACTIVE=normal-lane",
]
missing = [m for m in required_markers if m not in output]
if missing:
msg = f"{STATUS_FAIL}: positive-debug missing markers: {missing}"
print(msg)
_ = (log_dir / "positive-debug-markers.log").write_text(
"missing markers:\n" + "\n".join(f"- {m}" for m in missing) + "\n",
encoding="utf-8",
)
return 1
# Verify no fuzz-lane markers leaked.
forbidden = ["fuzz lane: GTest/GMock 1.17"]
leaked = [m for m in forbidden if m in output]
if leaked:
msg = f"{STATUS_FAIL}: positive-debug has forbidden fuzz markers: {leaked}"
print(msg)
return 1
_ = (log_dir / "positive-debug-markers.log").write_text(
"required markers found:\n" + "\n".join(f"- {m}" for m in required_markers) + "\n",
encoding="utf-8",
)
print(f"{STATUS_PASS}: positive-debug — GTest 1.16.0 only, no fuzz leakage")
return 0
def _run_gtest_isolation_positive_fuzz(
project_root: pathlib.Path, build_dir: pathlib.Path, log_dir: pathlib.Path,
) -> int:
"""Positive check: fuzz lane loads only GTest 1.17.0."""
fuzz_build = build_dir / "positive-fuzz"
if fuzz_build.is_dir():
shutil.rmtree(fuzz_build)
fuzz_build.mkdir(parents=True, exist_ok=True)
cmd = (
"cmake", "-S", str(project_root), "-B", str(fuzz_build),
"-DCMAKE_BUILD_TYPE=Debug",
"-DCMAKE_CXX_STANDARD=17",
"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",
"-DCPP_TEMPLATE_ENABLE_TESTS=OFF",
"-DCPP_TEMPLATE_ENABLE_BENCHMARKS=OFF",
"-DCPP_TEMPLATE_FUZZ_LANE=ON",
)
result = subprocess.run(list(cmd), capture_output=True, text=True, check=False)
log_content = (
f"command: {' '.join(cmd)}\n"
f"returncode: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}"
f"--- stderr ---\n{result.stderr}"
)
_ = (log_dir / "positive-fuzz-configure.log").write_text(log_content, encoding="utf-8")
if result.returncode != 0:
print(f"{STATUS_FAIL}: positive-fuzz configure failed")
return 1
output = result.stdout + result.stderr
required_markers = [
"fuzz lane: GTest/GMock 1.17.0 loaded",
"T35 guard: CPP_TEMPLATE_GTEST_LANE_ACTIVE=fuzz-lane",
]
missing = [m for m in required_markers if m not in output]
if missing:
msg = f"{STATUS_FAIL}: positive-fuzz missing markers: {missing}"
print(msg)
_ = (log_dir / "positive-fuzz-markers.log").write_text(
"missing markers:\n" + "\n".join(f"- {m}" for m in missing) + "\n",
encoding="utf-8",
)
return 1
# Verify no normal-lane GTest 1.16 markers leaked.
forbidden = ["normal lane: GTest/GMock 1.16"]
leaked = [m for m in forbidden if m in output]
if leaked:
msg = f"{STATUS_FAIL}: positive-fuzz has forbidden normal markers: {leaked}"
print(msg)
return 1
_ = (log_dir / "positive-fuzz-markers.log").write_text(
"required markers found:\n" + "\n".join(f"- {m}" for m in required_markers) + "\n",
encoding="utf-8",
)
print(f"{STATUS_PASS}: positive-fuzz — GTest 1.17.0 only, no normal leakage")
return 0
def _run_gtest_isolation_negative_conflict(
project_root: pathlib.Path, build_dir: pathlib.Path, log_dir: pathlib.Path,
) -> int:
"""Negative check: conflict fixture must fail configure."""
fixture_dir = (
project_root / "cmake" / "tests" / "fixtures"
/ "negative" / "fuzz-smoke" / "gtest-isolation"
)
conflict_build = build_dir / "negative-conflict"
if conflict_build.is_dir():
shutil.rmtree(conflict_build)
conflict_build.mkdir(parents=True, exist_ok=True)
cmd = (
"cmake", "-S", str(fixture_dir), "-B", str(conflict_build),
f"-DCC_PROJECT_ROOT={project_root}",
)
result = subprocess.run(list(cmd), capture_output=True, text=True, check=False)
log_content = (
f"command: {' '.join(cmd)}\n"
f"returncode: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}"
f"--- stderr ---\n{result.stderr}"
)
_ = (log_dir / "negative-conflict-configure.log").write_text(log_content, encoding="utf-8")
expected_diag = "Build tree already has normal-lane"
combined = result.stderr + result.stdout
if result.returncode == 0:
msg = f"{STATUS_FAIL}: negative-conflict expected configure failure but got 0"
print(msg)
return 1
if expected_diag not in combined:
msg = (
f"{STATUS_FAIL}: negative-conflict failed but diagnostic "
f"substring not found. expected: {expected_diag!r}"
)
print(msg)
return 1
negative_result = (
"negative conflict check: PASS\n"
f"expected diagnostic: {expected_diag}\n"
f"returncode: {result.returncode}\n"
)
_ = (log_dir / "negative-conflict-result.log").write_text(
negative_result,
encoding="utf-8",
)
print(f"{STATUS_PASS}: negative-conflict — configure failed with expected diagnostic")
return 0
def run_gtest_isolation(args: argparse.Namespace) -> int:
"""T35: Verify GTest lane isolation — normal 1.16, fuzz 1.17, conflict rejection."""
log_dir = args.log_dir.resolve()
log_dir.mkdir(parents=True, exist_ok=True)
build_dir = args.build_dir.resolve()
print("T35 GTest lane isolation checks")
print(f"log_dir: {log_dir}")
print(f"build_dir: {build_dir}")
# Phase 1: Positive debug — only GTest 1.16.0.
status = _run_gtest_isolation_positive_debug(PROJECT_ROOT, build_dir, log_dir)
if status != 0:
return status
# Phase 2: Positive fuzz — only GTest 1.17.0.
status = _run_gtest_isolation_positive_fuzz(PROJECT_ROOT, build_dir, log_dir)
if status != 0:
return status
# Phase 3: Negative conflict — must fail configure.
status = _run_gtest_isolation_negative_conflict(PROJECT_ROOT, build_dir, log_dir)
if status != 0:
return status
# Write summary README.
summary = "\n".join((
"# T35 GTest lane isolation evidence",
"",
"Generated by `python3 scripts/dev_check.py gtest-isolation`.",
"",
"## Contract",
"",
"- Normal debug lane (`normal-lane`) loads only GTest/GMock 1.16.0.",
"- Fuzz lane (`fuzz-lane`) loads only GTest/GMock 1.17.0.",
"- Attempting to load both in one build tree fails at configure time.",
"",
"## Checks",
"",
"1. positive-debug: configure with testing ON, fuzz OFF → GTest 1.16.0 only.",
"2. positive-fuzz: configure with fuzz ON, testing OFF → GTest 1.17.0 only.",
"3. negative-conflict: conflict fixture → configure fails with guard message.",
"",
))
_ = (log_dir / "README.md").write_text(summary, encoding="utf-8")
print(f"{STATUS_PASS}: T35 GTest lane isolation — all checks passed")
return 0
def _run_fuzztest_optional_positive(
project_root: pathlib.Path, build_dir: pathlib.Path, log_dir: pathlib.Path,
) -> int:
"""Positive check: fuzz lane keeps optional FuzzTest dependency features disabled."""
positive_build = build_dir / "positive-fuzz"
if positive_build.is_dir():
shutil.rmtree(positive_build)
positive_build.mkdir(parents=True, exist_ok=True)
cmd = (
"cmake", "-S", str(project_root), "-B", str(positive_build),
"-DCMAKE_BUILD_TYPE=Debug",
"-DCMAKE_CXX_STANDARD=17",
"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",
"-DCPP_TEMPLATE_ENABLE_TESTS=OFF",
"-DCPP_TEMPLATE_ENABLE_BENCHMARKS=OFF",
"-DCPP_TEMPLATE_FUZZ_LANE=ON",
)
result = subprocess.run(list(cmd), capture_output=True, text=True, check=False)
log_content = (
f"command: {' '.join(cmd)}\n"
f"returncode: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}"
f"--- stderr ---\n{result.stderr}"
)
_ = (log_dir / "configure.log").write_text(log_content, encoding="utf-8")
if result.returncode != 0:
print(f"{STATUS_FAIL}: fuzztest-optional positive configure failed")
return result.returncode
output = result.stdout + result.stderr
missing_markers = [marker for marker in FUZZTEST_OPTIONAL_REQUIRED_MARKERS if marker not in output]
marker_log = log_dir / "marker-check.log"
if missing_markers:
_ = marker_log.write_text(
"missing T36 configure markers:\n" + "\n".join(f"- {marker}" for marker in missing_markers) + "\n",
encoding="utf-8",
)
print(f"{STATUS_FAIL}: fuzztest-optional configure log is missing T36 markers")
return 1
_ = marker_log.write_text(
"required T36 configure markers found:\n"
+ "\n".join(f"- {marker}" for marker in FUZZTEST_OPTIONAL_REQUIRED_MARKERS)
+ "\n",
encoding="utf-8",
)
cache_status = _check_fuzztest_optional_cache(positive_build, log_dir / "cache-check.log")
if cache_status != 0:
return cache_status
scan_status = _scan_fuzztest_optional_patched_tree(positive_build, log_dir / "patched-tree-scan.log")
if scan_status != 0:
return scan_status
print(f"{STATUS_PASS}: positive-fuzz — optional FuzzTest dependency features are disabled")
return 0
def _check_fuzztest_optional_cache(build_dir: pathlib.Path, log_path: pathlib.Path) -> int:
cache_file = build_dir / "CMakeCache.txt"
if not cache_file.is_file():
_ = log_path.write_text(f"missing cache file: {cache_file}\n", encoding="utf-8")
print(f"{STATUS_FAIL}: CMakeCache.txt not found for fuzztest-optional check")
return 1
cache_content = cache_file.read_text(encoding="utf-8", errors="replace")
missing = [marker for marker in FUZZTEST_OPTIONAL_CACHE_MARKERS if marker not in cache_content]
if missing:
_ = log_path.write_text(
"missing required cache markers:\n" + "\n".join(f"- {marker}" for marker in missing) + "\n",
encoding="utf-8",
)
print(f"{STATUS_FAIL}: fuzztest-optional cache markers missing")
return 1
_ = log_path.write_text(
"required cache markers found:\n"
+ "\n".join(f"- {marker}" for marker in FUZZTEST_OPTIONAL_CACHE_MARKERS)
+ "\n",
encoding="utf-8",
)
return 0
def _scan_fuzztest_optional_patched_tree(build_dir: pathlib.Path, log_path: pathlib.Path) -> int:
patched_source = build_dir / "_fuzztest_patched_src"
if not patched_source.is_dir():
_ = log_path.write_text(f"missing patched source tree: {patched_source}\n", encoding="utf-8")
print(f"{STATUS_FAIL}: patched FuzzTest source tree missing")
return 1
files_to_scan = (
patched_source / "CMakeLists.txt",
patched_source / "cmake" / "BuildDependencies.cmake",
patched_source / "domain_tests" / "CMakeLists.txt",
)
unexpected: list[str] = []
lines: list[str] = [f"patched_source: {patched_source}"]
for path in files_to_scan:
if not path.is_file():
lines.append(f"missing expected scan file: {path.relative_to(patched_source)}")
unexpected.append(f"missing scan file: {path}")
continue
relative = path.relative_to(patched_source)
lines.append(f"scanning: {relative}")
for line_number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1):
lowered = line.lower()
if not any(term.lower() in lowered for term in FUZZTEST_OPTIONAL_SCAN_TERMS):
continue
classification = _classify_fuzztest_optional_scan_line(relative, line)
lines.append(f"{relative}:{line_number}: {classification}: {line.strip()}")
if classification == "UNEXPECTED_ACTIVE_OPTIONAL_PATH":
unexpected.append(f"{relative}:{line_number}: {line.strip()}")
if unexpected:
lines.append("unexpected active optional dependency paths:")
lines.extend(f"- {entry}" for entry in unexpected)
_ = log_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"{STATUS_FAIL}: patched FuzzTest tree contains active optional dependency paths")
return 1
lines.append("result: no active protobuf/nlohmann_json/flatbuffers acquisition or usage paths detected")
_ = log_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return 0
def _classify_fuzztest_optional_scan_line(relative: pathlib.Path, line: str) -> str:
normalized = relative.as_posix()
stripped = line.strip()
if stripped.startswith("#"):
return "allowed-comment"
if "FUZZTEST_BUILD_TESTING" in line or "FUZZTEST_BUILD_FLATBUFFERS" in line:
return "allowed-disabled-option"
if normalized == "domain_tests/CMakeLists.txt":
return "allowed-disabled-by-FUZZTEST_BUILD_TESTING"
if "not required" in line.lower() or "forbid" in line.lower() or "disable" in line.lower():
return "allowed-diagnostic"
if stripped.startswith("if (TARGET ") and any(target in line for target in FUZZTEST_OPTIONAL_FORBIDDEN_TARGETS):
return "allowed-forbidden-target-guard"
return "UNEXPECTED_ACTIVE_OPTIONAL_PATH"
def _run_fuzztest_optional_negative_conflict(
project_root: pathlib.Path, build_dir: pathlib.Path, log_dir: pathlib.Path,
) -> int:
fixture_dir = (
project_root / "cmake" / "tests" / "fixtures"
/ "negative" / "fuzz-smoke" / "fuzztest-optional-features"
)
conflict_build = build_dir / "negative-forbidden-target"
if conflict_build.is_dir():
shutil.rmtree(conflict_build)
conflict_build.mkdir(parents=True, exist_ok=True)
cmd = (
"cmake", "-S", str(fixture_dir), "-B", str(conflict_build),
f"-DCC_PROJECT_ROOT={project_root}",
)
result = subprocess.run(list(cmd), capture_output=True, text=True, check=False)
log_content = (
f"command: {' '.join(cmd)}\n"
f"returncode: {result.returncode}\n"
f"--- stdout ---\n{result.stdout}"
f"--- stderr ---\n{result.stderr}"
)
_ = (log_dir / "negative-configure.log").write_text(log_content, encoding="utf-8")
expected_terms = (
"FuzzTest patched mode forbids optional dependency target:",
"protobuf::libprotobuf",
)
combined = result.stderr + result.stdout
if result.returncode == 0:
print(f"{STATUS_FAIL}: fuzztest-optional negative fixture expected configure failure but got 0")
return 1
if not all(term in combined for term in expected_terms):
print(f"{STATUS_FAIL}: fuzztest-optional negative fixture missing expected diagnostic")
return 1
_ = (log_dir / "negative-result.log").write_text(
"".join((
"negative forbidden-target check: PASS\n",
"expected diagnostic terms:\n",
*(f"- {term}\n" for term in expected_terms),
f"returncode: {result.returncode}\n",
)),
encoding="utf-8",
)
print(f"{STATUS_PASS}: negative-forbidden-target — configure failed with expected diagnostic")
return 0
def run_fuzztest_optional_features(args: argparse.Namespace) -> int:
"""T36: Verify optional FuzzTest protobuf/nlohmann_json/flatbuffers paths stay disabled."""
log_dir = args.log_dir.resolve()
log_dir.mkdir(parents=True, exist_ok=True)
build_dir = args.build_dir.resolve()
print("T36 FuzzTest optional feature disable checks")
print(f"log_dir: {log_dir}")
print(f"build_dir: {build_dir}")
status = _run_fuzztest_optional_positive(PROJECT_ROOT, build_dir, log_dir)
if status != 0:
return status
status = _run_fuzztest_optional_negative_conflict(PROJECT_ROOT, build_dir, log_dir)
if status != 0:
return status
summary = "\n".join((
"# T36 FuzzTest optional feature disable evidence",
"",
"Generated by `python3 scripts/dev_check.py fuzztest-optional-features`.",
"",
"## Contract",
"",
"- Fuzz lane configures with upstream FuzzTest testing and flatbuffers support disabled.",
"- `CMakeCache.txt` contains `FUZZTEST_BUILD_TESTING:BOOL=OFF` and `FUZZTEST_BUILD_FLATBUFFERS:BOOL=OFF`.",
"- Configure output contains T36 target-absence markers after patched FuzzTest is added.",
"- The generated patched FuzzTest tree has no active protobuf/nlohmann_json/flatbuffers acquisition or usage paths.",
"- A negative fixture that predefines `protobuf::libprotobuf` fails locally with the forbidden-target guard.",
"",
"## Logs",
"",
"- `configure.log`",
"- `marker-check.log`",
"- `cache-check.log`",
"- `patched-tree-scan.log`",
"- `negative-configure.log`",
"- `negative-result.log`",
"",
))
_ = (log_dir / "README.md").write_text(summary, encoding="utf-8")
print(f"{STATUS_PASS}: T36 FuzzTest optional feature disable checks passed")
return 0
def _print_command_summary(label: str, result: network_guard.CommandResult) -> None:
print(f"{label}_command: {network_guard.format_command(result.command)}")
print(f"{label}_returncode: {result.returncode}")
if __name__ == "__main__":
raise SystemExit(main())