#!/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())