186 lines
7.1 KiB
Python
186 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Shared execution policy for tester's integration-first runs.
|
|
|
|
This module intentionally has no browser, mobile, deployment, or API driver.
|
|
Those adapters belong to the caller. It only controls the phase gate and the
|
|
stable five-state testcase result propagation so a caller cannot create a
|
|
journey session before preflight and integration have passed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Iterable, Mapping
|
|
from typing import Any
|
|
|
|
from failure_feedback import collect_issues
|
|
|
|
RESULTS = ("pass", "fail", "environment-fail", "skipped", "not-run")
|
|
PHASES = ("preflight", "integration", "journey")
|
|
|
|
|
|
def classify_integration(*, ok: bool, service_reachable: bool = True) -> str:
|
|
"""Separate a contract assertion failure from an unavailable service."""
|
|
if not service_reachable:
|
|
return "environment-fail"
|
|
return "pass" if ok else "fail"
|
|
|
|
|
|
def _case_id(case: Mapping[str, Any] | str) -> str:
|
|
if isinstance(case, Mapping):
|
|
return str(case.get("id") or "")
|
|
return str(case)
|
|
|
|
|
|
def _not_run(
|
|
cases: Iterable[Mapping[str, Any] | str],
|
|
reason: str,
|
|
*,
|
|
blocked_by: Iterable[str] = (),
|
|
) -> list[dict[str, Any]]:
|
|
rows: list[dict[str, str]] = []
|
|
blockers = [str(item) for item in blocked_by if str(item)]
|
|
for case in cases:
|
|
row: dict[str, Any] = {"id": _case_id(case), "result": "not-run", "notes": reason}
|
|
if blockers:
|
|
row["blocked_by"] = blockers
|
|
if isinstance(case, Mapping):
|
|
row["test_type"] = str(case.get("test_type") or "")
|
|
row["suite"] = str(
|
|
case.get("client_suite")
|
|
or case.get("integration_suite")
|
|
or case.get("suite")
|
|
or ""
|
|
)
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
def _dependencies(
|
|
case: Mapping[str, Any] | str,
|
|
*,
|
|
default: Iterable[str] = (),
|
|
) -> list[str]:
|
|
if not isinstance(case, Mapping):
|
|
return [str(item) for item in default]
|
|
declared = case.get("depends_on", case.get("dependencies"))
|
|
if declared is None:
|
|
return [str(item) for item in default]
|
|
if isinstance(declared, str):
|
|
return [declared]
|
|
if isinstance(declared, Iterable):
|
|
return [str(item) for item in declared if str(item)]
|
|
raise ValueError(f"depends_on 必须是字符串或列表: {_case_id(case)}")
|
|
|
|
|
|
def _failed_dependencies(dependencies: Iterable[str], results: Mapping[str, Mapping[str, Any]]) -> list[str]:
|
|
return [
|
|
dependency
|
|
for dependency in dependencies
|
|
if str((results.get(dependency) or {}).get("result"))
|
|
in {"fail", "environment-fail", "not-run"}
|
|
]
|
|
|
|
|
|
def _issue_structures(results: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
issues = collect_issues(results)
|
|
return {"defects": issues, "test_issues": [dict(issue) for issue in issues]}
|
|
|
|
|
|
def run_integration_first(
|
|
*,
|
|
preflight_status: str,
|
|
integration_cases: Iterable[Mapping[str, Any] | str],
|
|
journey_cases: Iterable[Mapping[str, Any] | str],
|
|
execute_integration: Callable[[Mapping[str, Any] | str], str | Mapping[str, Any]],
|
|
create_journey_session: Callable[[], Any],
|
|
execute_journey: Callable[[Any, Mapping[str, Any] | str], str | Mapping[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""Execute integration cases before creating any journey session.
|
|
|
|
``preflight_status`` must be the status emitted by test-preflight. A
|
|
blocked/environment-fail gate records only ``not-run`` testcase rows and
|
|
never invokes either adapter. An integration failure stops the phase and
|
|
propagates ``not-run`` to all journey cases.
|
|
"""
|
|
integration = list(integration_cases)
|
|
journey = list(journey_cases)
|
|
if preflight_status != "ready":
|
|
reason = f"test-preflight={preflight_status},未创建客户端 session"
|
|
result_rows = _not_run(integration, reason, blocked_by=("test-preflight",)) + _not_run(journey, reason, blocked_by=("test-preflight",))
|
|
return {
|
|
"status": preflight_status,
|
|
"phase": "preflight",
|
|
"session_created": False,
|
|
"results": result_rows,
|
|
**_issue_structures(result_rows),
|
|
}
|
|
|
|
results: list[dict[str, Any]] = []
|
|
results_by_id: dict[str, dict[str, Any]] = {}
|
|
for case in integration:
|
|
case_id = _case_id(case)
|
|
dependencies = _dependencies(case)
|
|
blocked_by = _failed_dependencies(dependencies, results_by_id)
|
|
if blocked_by:
|
|
rows = _not_run(
|
|
[case],
|
|
"依赖的 integration 失败,未执行",
|
|
blocked_by=blocked_by,
|
|
)
|
|
results.extend(rows)
|
|
results_by_id[case_id] = rows[0]
|
|
continue
|
|
outcome = execute_integration(case)
|
|
row = dict(outcome) if isinstance(outcome, Mapping) else {"result": outcome}
|
|
row["id"] = _case_id(case)
|
|
if isinstance(case, Mapping):
|
|
row.setdefault("test_type", str(case.get("test_type") or "integration"))
|
|
row.setdefault("suite", str(case.get("integration_suite") or case.get("suite") or ""))
|
|
result = str(row.get("result") or "not-run")
|
|
if result not in RESULTS:
|
|
raise ValueError(f"未知 integration 结果: {row['id']}={result}")
|
|
row["result"] = result
|
|
results.append(row)
|
|
results_by_id[case_id] = row
|
|
|
|
integration_ids = [_case_id(case) for case in integration]
|
|
has_runnable_journey = any(
|
|
not _failed_dependencies(_dependencies(case, default=integration_ids), results_by_id)
|
|
for case in journey
|
|
)
|
|
session = create_journey_session() if has_runnable_journey else None
|
|
for case in journey:
|
|
dependencies = _dependencies(case, default=integration_ids)
|
|
blocked_by = _failed_dependencies(dependencies, results_by_id)
|
|
if blocked_by:
|
|
row = _not_run(
|
|
[case],
|
|
"依赖的用例失败,journey 未执行",
|
|
blocked_by=blocked_by,
|
|
)[0]
|
|
results.append(row)
|
|
results_by_id[_case_id(case)] = row
|
|
continue
|
|
outcome = execute_journey(session, case)
|
|
row = dict(outcome) if isinstance(outcome, Mapping) else {"result": outcome}
|
|
row["id"] = _case_id(case)
|
|
if isinstance(case, Mapping):
|
|
row.setdefault("test_type", str(case.get("test_type") or "journey"))
|
|
row.setdefault("suite", str(case.get("client_suite") or case.get("suite") or ""))
|
|
result = str(row.get("result") or "not-run")
|
|
if result not in RESULTS:
|
|
raise ValueError(f"未知 journey 结果: {row['id']}={result}")
|
|
row["result"] = result
|
|
results.append(row)
|
|
results_by_id[_case_id(case)] = row
|
|
status = "fail" if any(row["result"] == "fail" for row in results) else (
|
|
"environment-fail" if any(row["result"] == "environment-fail" for row in results) else "pass"
|
|
)
|
|
return {
|
|
"status": status,
|
|
"phase": "journey" if journey else "integration",
|
|
"session_created": has_runnable_journey,
|
|
"results": results,
|
|
**_issue_structures(results),
|
|
}
|