feat(tester): add integration-first journey execution
This commit is contained in:
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import plistlib
|
||||
@@ -33,6 +34,7 @@ from typing import Any
|
||||
|
||||
COMMIT = re.compile(r"^[0-9a-fA-F]{40}$")
|
||||
PLATFORMS = {"android", "ios"}
|
||||
PREFLIGHT_ROOT = Path(__file__).resolve().parents[2] / "test-preflight" / "scripts" / "preflight.py"
|
||||
|
||||
|
||||
class EnvironmentFailure(RuntimeError):
|
||||
@@ -388,6 +390,33 @@ def write_run(root: Path, record: dict[str, Any]) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def run_test_preflight(
|
||||
config_path: Path,
|
||||
*,
|
||||
mode: str = "verify",
|
||||
yaml_path: Path | None = None,
|
||||
csv_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run the shared preflight contract before any client session work."""
|
||||
if mode not in {"verify", "prepare"}:
|
||||
raise ArtifactFailure("preflight mode 只能是 verify 或 prepare")
|
||||
if not PREFLIGHT_ROOT.is_file():
|
||||
raise EnvironmentFailure(f"找不到 test-preflight skill: {PREFLIGHT_ROOT}")
|
||||
spec = importlib.util.spec_from_file_location("pouch_test_preflight", PREFLIGHT_ROOT)
|
||||
if spec is None or spec.loader is None:
|
||||
raise EnvironmentFailure("无法加载 test-preflight skill")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
try:
|
||||
config = module.load_config(config_path)
|
||||
evidence = module.run(config, mode)
|
||||
yaml_path = yaml_path or config_path.with_name("preflight.yaml")
|
||||
csv_path = csv_path or config_path.with_name("preflight.csv")
|
||||
return module.write_evidence(evidence, yaml_path, csv_path)
|
||||
except (OSError, ValueError, module.yaml.YAMLError) as exc:
|
||||
return {"status": "blocked", "error": str(exc), "yaml": str(yaml_path or ""), "csv": str(csv_path or "")}
|
||||
|
||||
|
||||
def execute(args: argparse.Namespace) -> int:
|
||||
requested = platforms(args.platform)
|
||||
context = RuntimeContext(Path(args.root).resolve())
|
||||
@@ -417,7 +446,7 @@ def execute(args: argparse.Namespace) -> int:
|
||||
artifact_dir = Path(tempfile.mkdtemp(prefix="pouch-tester-artifacts-"))
|
||||
record["artifact_dir"] = str(artifact_dir)
|
||||
expected = catalog_identities(context.root)
|
||||
prepared: dict[str, tuple[dict[str, Any], dict[str, str], str]] = {}
|
||||
prepared: dict[str, tuple[dict[str, Any], dict[str, str] | None, str]] = {}
|
||||
failures: dict[str, str] = {}
|
||||
for platform in requested:
|
||||
result: dict[str, Any] = {"node": "tester.android" if platform == "android" else "tester.ios", "status": "preparing"}
|
||||
@@ -429,22 +458,65 @@ def execute(args: argparse.Namespace) -> int:
|
||||
wanted = expected.get(platform)
|
||||
if wanted and identity != wanted:
|
||||
raise EnvironmentFailure(f"{platform} 身份不匹配: expected={wanted} actual={identity}")
|
||||
node = android_preflight(context) if platform == "android" else ios_preflight(context)
|
||||
check_network(context, node)
|
||||
result.update({"identity": identity, "node_preflight": node, "status": "preflight-passed"})
|
||||
prepared[platform] = (result, node, identity)
|
||||
result.update({"identity": identity, "status": "artifact-ready"})
|
||||
prepared[platform] = (result, None, identity)
|
||||
except (EnvironmentFailure, ArtifactFailure) as exc:
|
||||
failures[platform] = str(exc)
|
||||
result.update({"status": "environment-fail", "failure_class": "automatic-repair-failed", "error": str(exc)})
|
||||
|
||||
# This is the hard gate shared by integration and journey callers.
|
||||
# There is no safe implicit matrix: without a caller-supplied config we
|
||||
# cannot prove HOST, fixtures, tester bindings, or node reachability.
|
||||
preflight_config = Path(args.preflight_config).expanduser() if args.preflight_config else None
|
||||
if not failures:
|
||||
if preflight_config is None:
|
||||
preflight_result = {
|
||||
"status": "blocked",
|
||||
"error": "缺少 --preflight-config;不能跳过 test-preflight",
|
||||
}
|
||||
else:
|
||||
preflight_result = run_test_preflight(
|
||||
preflight_config,
|
||||
mode=args.preflight_mode,
|
||||
yaml_path=Path(args.preflight_yaml).expanduser() if args.preflight_yaml else None,
|
||||
csv_path=Path(args.preflight_csv).expanduser() if args.preflight_csv else None,
|
||||
)
|
||||
record["preflight"] = preflight_result
|
||||
if preflight_result.get("status") != "ready":
|
||||
failures["preflight"] = str(preflight_result.get("error") or preflight_result.get("status"))
|
||||
|
||||
if not failures and args.preflight_mode == "prepare":
|
||||
# test-preflight prepare owns platform probing, installation and
|
||||
# launch; do not install/start the same artifact a second time.
|
||||
for result, _, _ in prepared.values():
|
||||
result.update({"status": "preflight-prepared"})
|
||||
elif not failures:
|
||||
for platform in requested:
|
||||
result, _, identity = prepared[platform]
|
||||
try:
|
||||
node = android_preflight(context) if platform == "android" else ios_preflight(context)
|
||||
check_network(context, node)
|
||||
result.update({"node_preflight": node, "status": "preflight-passed"})
|
||||
prepared[platform] = (result, node, identity)
|
||||
except EnvironmentFailure as exc:
|
||||
failures[platform] = str(exc)
|
||||
result.update({"status": "environment-fail", "failure_class": "automatic-repair-failed", "error": str(exc)})
|
||||
if failures and not args.partial:
|
||||
for platform in requested:
|
||||
if platform not in failures:
|
||||
record["platforms"][platform].update({"status": "not-run", "reason": "全量 preflight 闸门被另一平台阻断"})
|
||||
record.update({"status": "environment-fail", "failure_class": "automatic-repair-failed", "next_action": "修复列出的平台环境后重跑;默认不会静默降级"})
|
||||
overall = record.get("preflight", {}).get("status") if "preflight" in record else "environment-fail"
|
||||
record.update({"status": overall if overall in {"blocked", "environment-fail"} else "environment-fail", "failure_class": "test-preflight" if "preflight" in record else "automatic-repair-failed", "next_action": "修复列出的平台环境后重跑;默认不会静默降级"})
|
||||
else:
|
||||
for platform, (result, node, identity) in prepared.items():
|
||||
try:
|
||||
installed = install_android(node, Path(result["artifact"]["path"]), identity, context) if platform == "android" else install_ios(node, Path(result["artifact"]["path"]), identity, context)
|
||||
installed = (
|
||||
{"delegated": "test-preflight.prepare"}
|
||||
if args.preflight_mode == "prepare"
|
||||
else install_android(node, Path(result["artifact"]["path"]), identity, context)
|
||||
if platform == "android"
|
||||
else install_ios(node, Path(result["artifact"]["path"]), identity, context)
|
||||
)
|
||||
result.update({"status": "ready", "install": installed, "failure_class": None})
|
||||
except EnvironmentFailure as exc:
|
||||
failures[platform] = str(exc)
|
||||
@@ -476,6 +548,10 @@ def parser() -> argparse.ArgumentParser:
|
||||
run.add_argument("--root", default=".")
|
||||
run.add_argument("--worktree-root", default="")
|
||||
run.add_argument("--binding", default="")
|
||||
run.add_argument("--preflight-config", default="", help="test-preflight matrix YAML(必填,不得跳过)")
|
||||
run.add_argument("--preflight-mode", choices=("verify", "prepare"), default="verify")
|
||||
run.add_argument("--preflight-yaml", default="", help="preflight YAML 证据输出路径")
|
||||
run.add_argument("--preflight-csv", default="", help="preflight CSV 证据输出路径")
|
||||
run.set_defaults(func=execute)
|
||||
return root
|
||||
|
||||
|
||||
Reference in New Issue
Block a user