#!/usr/bin/env python3 """Behavioral tests for project_guard.py using isolated temporary projects.""" import json import os import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) import project_guard # noqa: E402 def make_reference(root: Path, name: str = "CV1841CP-REF_dualos") -> Path: reference = root / name uboot = reference / "configs" / "uboot" uboot.mkdir(parents=True) (reference / "buildEnvConfig.sh").write_text( "EP_CHIP=CV1841CP\nEP_SDK_VER=v6.3.2\nEP_FLASH_TYPE=nand\n", encoding="utf-8", ) (uboot / "cvi_board_init.c").write_text("int cvi_board_init(void) { return 0; }\n", encoding="utf-8") generated = reference / "build" generated.mkdir() (generated / "artifact.bin").write_bytes(b"generated") (generated / "external-cache-link").symlink_to("/outside/ignored-build-cache") (reference / "debug.log").write_text("ignored\n", encoding="utf-8") (reference / "imgs").symlink_to(project_guard.expected_imgs_target(name)) return reference def test_prepare_verify_finalize(root: Path) -> None: reference_name = "CV1841CP-REF_dualos" target_name = "CV1841CP-NEW_dualos" reference = make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] assert staging.is_dir() assert not (staging / "build").exists() assert not (staging / "debug.log").exists() assert (staging / "imgs").is_symlink() assert os.readlink(staging / "imgs") == project_guard.expected_imgs_target(target_name) manifest_path = staging / "doc" / "bsp_adaptation" / "validation" / "clone_manifest.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) assert manifest["reference_project"] == reference_name assert manifest["imgs_retargeted"] is True relative_config = "configs/uboot/cvi_board_init.c" (staging / relative_config).write_text("int cvi_board_init(void) { return 1; }\n", encoding="utf-8") report = staging / "doc" / "bsp_adaptation" / "adaptation_report.md" report.write_text("# BSP adaptation\n\nStatic validation passed.\n", encoding="utf-8") verified = project_guard.verify_project( reference_name, staging.name, target_name, [relative_config], root=root, ) assert verified["passed"] is True, verified assert relative_config in verified["changes"]["modified"] assert "imgs" in verified["changes"]["modified"] finalized = project_guard.finalize_project( reference_name, staging.name, target_name, [relative_config], root=root, ) target = root / target_name assert finalized["target_path"] == str(target) assert target.is_dir() and not staging.exists() assert reference.is_dir() final_manifest = json.loads( (target / "doc" / "bsp_adaptation" / "validation" / "clone_manifest.json").read_text( encoding="utf-8" ) ) assert final_manifest["final_path"] == str(target) handoff = target / "doc" / "bsp_adaptation" / "handoff" / "psp_handoff.json" handoff.parent.mkdir(parents=True) handoff.write_text('{"schema_version": 1}\n', encoding="utf-8") audited = project_guard.audit_final_project(target_name, root=root) assert audited["passed"] is True, audited assert audited["reference_drifted_since_prepare"] is False (target / "buildEnvConfig.sh").write_text("EP_CHIP=CV1842HP\n", encoding="utf-8") violated = project_guard.audit_final_project(target_name, root=root) assert violated["passed"] is False assert violated["violations"] == ["buildEnvConfig.sh"] def test_unapproved_delta_is_rejected(root: Path) -> None: reference_name = "CV1841CP-REF2_dualos" target_name = "CV1841CP-NEW2_dualos" make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] (staging / "buildEnvConfig.sh").write_text("EP_CHIP=CV1842HP\n", encoding="utf-8") (staging / "doc" / "bsp_adaptation" / "adaptation_report.md").write_text( "not final\n", encoding="utf-8" ) verified = project_guard.verify_project( reference_name, staging.name, target_name, ["configs/uboot/cvi_board_init.c"], root=root, ) assert verified["passed"] is False assert verified["violations"] == ["buildEnvConfig.sh"] try: project_guard.finalize_project( reference_name, staging.name, target_name, ["configs/uboot/cvi_board_init.c"], root=root, ) except project_guard.GuardError as exc: assert "未通过" in str(exc) else: raise AssertionError("finalize should reject a failed delta report") def test_external_link_and_credentials_are_rejected(root: Path) -> None: external = make_reference(root, "CV1815JA-EXTERNAL") (external / "imgs").unlink() (external / "other-link").symlink_to("/outside/emxres/shared") try: project_guard.prepare_project(external.name, "CV1815JA-NEW", root=root) except project_guard.GuardError as exc: assert "外部绝对软链接" in str(exc) else: raise AssertionError("unexpected external symlink should be rejected") credential = make_reference(root, "CV1815JA-CREDENTIAL") (credential / ".env").write_text("TOKEN=secret\n", encoding="utf-8") try: project_guard.prepare_project(credential.name, "CV1815JA-CLEAN", root=root) except project_guard.GuardError as exc: assert "疑似凭据" in str(exc) else: raise AssertionError("credential-like files should be rejected") def test_changes_after_verify_are_rejected(root: Path) -> None: reference_name = "CV1841CP-REF3_dualos" target_name = "CV1841CP-NEW3_dualos" make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] report = staging / "doc" / "bsp_adaptation" / "adaptation_report.md" report.write_text("# verified\n", encoding="utf-8") verified = project_guard.verify_project( reference_name, staging.name, target_name, [], root=root, ) assert verified["passed"] is True (staging / "buildEnvConfig.sh").write_text("EP_CHIP=CV1842HP\n", encoding="utf-8") try: project_guard.finalize_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "未通过" in str(exc) else: raise AssertionError("finalize should reject post-verification changes") def test_reference_drift_is_rejected(root: Path) -> None: reference_name = "CV1841CP-REF4_dualos" target_name = "CV1841CP-NEW4_dualos" reference = make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] (reference / "buildEnvConfig.sh").write_text("EP_CHIP=CV1842HP\n", encoding="utf-8") try: project_guard.verify_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "参考项目发生变化" in str(exc) else: raise AssertionError("verify should reject reference drift after prepare") def test_staging_credentials_and_excluded_outputs_are_rejected(root: Path) -> None: reference_name = "CV1815JA-REF5" target_name = "CV1815JA-NEW5" make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] (staging / ".env").write_text("TOKEN=secret\n", encoding="utf-8") try: project_guard.verify_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "疑似凭据" in str(exc) else: raise AssertionError("verify should reject credentials added during editing") (staging / ".env").unlink() (staging / "logs").mkdir() (staging / "logs" / "build.txt").write_text("generated\n", encoding="utf-8") try: project_guard.verify_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "被排除" in str(exc) else: raise AssertionError("verify should reject excluded outputs added during editing") def test_excluded_output_added_after_verify_is_rejected(root: Path) -> None: reference_name = "CV1815JA-REF6" target_name = "CV1815JA-NEW6" make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] (staging / "doc" / "bsp_adaptation" / "adaptation_report.md").write_text( "# verified\n", encoding="utf-8" ) verified = project_guard.verify_project( reference_name, staging.name, target_name, [], root=root ) assert verified["passed"] is True (staging / "logs").mkdir() (staging / "logs" / "late.txt").write_text("late\n", encoding="utf-8") try: project_guard.finalize_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "被排除" in str(exc) else: raise AssertionError("finalize should reject excluded output added after verify") def test_tampered_delta_report_is_not_trusted(root: Path) -> None: reference_name = "CV1815JA-REF7" target_name = "CV1815JA-NEW7" make_reference(root, reference_name) prepared = project_guard.prepare_project(reference_name, target_name, root=root) staging = root / prepared["staging_directory"] (staging / "doc" / "bsp_adaptation" / "adaptation_report.md").write_text( "# report\n", encoding="utf-8" ) (staging / "buildEnvConfig.sh").write_text("EP_CHIP=CV1842HP\n", encoding="utf-8") verified = project_guard.verify_project( reference_name, staging.name, target_name, [], root=root ) assert verified["passed"] is False delta_path = staging / "doc" / "bsp_adaptation" / "validation" / "project_delta.json" tampered = json.loads(delta_path.read_text(encoding="utf-8")) tampered["passed"] = True tampered["violations"] = [] project_guard.write_json(delta_path, tampered) try: project_guard.finalize_project(reference_name, staging.name, target_name, [], root=root) except project_guard.GuardError as exc: assert "未通过" in str(exc) else: raise AssertionError("finalize should recompute instead of trusting a tampered delta report") def test_allow_paths_must_be_exact() -> None: try: project_guard.normalize_allow_paths(["configs/**"]) except project_guard.GuardError as exc: assert "不允许通配符" in str(exc) else: raise AssertionError("wildcard allow paths should be rejected") def test_audit_requires_frozen_baseline(root: Path) -> None: target = root / "CV1841CP-UNMANAGED_dualos" target.mkdir() try: project_guard.audit_final_project(target.name, root=root) except project_guard.GuardError as exc: assert "clone_manifest" in str(exc) else: raise AssertionError("audit should reject an unmanaged project") def main() -> int: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) / "EmxRes" root.mkdir() test_prepare_verify_finalize(root) test_unapproved_delta_is_rejected(root) test_external_link_and_credentials_are_rejected(root) test_changes_after_verify_are_rejected(root) test_reference_drift_is_rejected(root) test_staging_credentials_and_excluded_outputs_are_rejected(root) test_excluded_output_added_after_verify_is_rejected(root) test_tampered_delta_report_is_not_trusted(root) test_audit_requires_frozen_baseline(root) test_allow_paths_must_be_exact() print("Project guard behavioral tests passed") return 0 if __name__ == "__main__": raise SystemExit(main())