1140 lines
42 KiB
Python
1140 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: flatbuffers",
|
|
)
|
|
FUZZTEST_OPTIONAL_CACHE_MARKERS = (
|
|
"FUZZTEST_BUILD_TESTING:BOOL=OFF",
|
|
"FUZZTEST_BUILD_FLATBUFFERS:BOOL=OFF",
|
|
)
|
|
FUZZTEST_OPTIONAL_FORBIDDEN_TARGETS = (
|
|
"protobuf::libprotobuf",
|
|
"flatbuffers",
|
|
)
|
|
FUZZTEST_OPTIONAL_SCAN_TERMS = (
|
|
"protobuf",
|
|
"libprotobuf",
|
|
"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-owned protobuf/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=(
|
|
"-DCC_ENABLE_FUZZTEST=ON",
|
|
"-DCC_ENABLE_TESTING=OFF",
|
|
"-DCC_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 `CC_ENABLE_FUZZTEST=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",
|
|
"-DCC_ENABLE_TESTING=ON",
|
|
"-DCC_ENABLE_BENCHMARKS=OFF",
|
|
"-DCC_ENABLE_FUZZTEST=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-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",
|
|
"cc_project: fuzz targets skipped",
|
|
]
|
|
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++",
|
|
"-DCC_ENABLE_TESTING=OFF",
|
|
"-DCC_ENABLE_BENCHMARKS=OFF",
|
|
"-DCC_ENABLE_FUZZTEST=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++",
|
|
"-DCC_ENABLE_TESTING=OFF",
|
|
"-DCC_ENABLE_BENCHMARKS=OFF",
|
|
"-DCC_ENABLE_FUZZTEST=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 FuzzTest-owned protobuf/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 FuzzTest-owned protobuf/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 FuzzTest-owned protobuf/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())
|