交付流程 · 產物閱讀
PYexamples/quality-workflow/run_demo.py

run_demo.py

"""Run green → two expected business failures → restored green; all local."""

import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import platform
import subprocess
import sys


ROOT = Path(__file__).resolve().parent
SCENARIOS = [
    ("baseline", "none", set()),
    ("fault-duplicate-write", "duplicate-write", {"BE-PAID-REJECT", "BE-CONCURRENT"}),
    ("fault-wrong-error", "wrong-error", {"BE-PAID-REJECT"}),
    ("restored", "none", set()),
]


def main(output):
    output.mkdir(parents=True, exist_ok=True)
    manifest = {"kind": "local-teaching-execution", "product_enabled": False,
                "generated_at": datetime.now(timezone.utc).isoformat(),
                "python": platform.python_version(), "tested_surface": "HTTP API + SQLite",
                "not_tested": ["FE DOM and keyboard", "BE + FE integration", "payment provider", "production deployment"],
                "sources": {}, "runs": []}
    files = [*ROOT.glob("demo_app/*.py"), *ROOT.glob("independent/*.py"), ROOT / "run_demo.py",
             ROOT / "acceptance-plan.json", ROOT / "api-contract.json", ROOT / "acceptance.feature",
             ROOT / "product-spec.md", ROOT / "product.gitlab-ci.yml"]
    for source in sorted(files):
        manifest["sources"][str(source.relative_to(ROOT))] = hashlib.sha256(source.read_bytes()).hexdigest()
    valid = True
    for name, fault, expected_failures in SCENARIOS:
        report = output / (name + ".xml")
        process = subprocess.run([
            sys.executable, "-m", "independent.harness", "--fault", fault, "--junit", str(report.resolve()),
        ], cwd=ROOT, text=True, capture_output=True)
        try:
            summary = json.loads(process.stdout)
        except json.JSONDecodeError:
            print(process.stdout + process.stderr, file=sys.stderr)
            return 1
        expected_exit = 1 if expected_failures else 0
        matches = (process.returncode == expected_exit and set(summary["failures"]) == expected_failures
                   and not summary["errors"] and not summary["skipped"] and summary["tests"] == 5)
        valid = valid and matches
        manifest["runs"].append({"name": name, "runner_exit": process.returncode,
                                 "expected_exit": expected_exit, "expected_result_verified": matches,
                                 "junit": report.name,
                                 "junit_sha256": hashlib.sha256(report.read_bytes()).hexdigest(), **summary})
        print(f"{name}: runner exit {process.returncode}; {summary['passed']}/5 passed; "
              f"expected result {'verified' if matches else 'NOT verified'}")
    manifest["demo_verified"] = valid
    (output / "execution-manifest.json").write_text(
        json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return 0 if valid else 1


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=ROOT / "reports")
    raise SystemExit(main(parser.parse_args().output.resolve()))

靜態唯讀副本 · 可直接開啟,不需伺服器;不改變核准或執行結果