Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

232 lines
7.8 KiB
Python

"""Synthetic test task runner — watch subprocess lifecycle and report outcomes."""
from __future__ import annotations
import re
import subprocess
import tempfile
import threading
from dataclasses import dataclass
from typing import Any
from platform.common.task_types import TaskKind, TaskRecord
from tools.interactive_shell.cli import build_opensre_cli_argv
from tools.interactive_shell.shared import allow_tool
from tools.interactive_shell.subprocess import (
SYNTHETIC_DIAG_CHARS,
SYNTHETIC_TEST_TIMEOUT_SECONDS,
SubprocessPresenter,
read_diag,
watch_subprocess_until_exit,
)
DEFAULT_SYNTHETIC_SCENARIO = "001-replication-lag"
_SYNTHETIC_SCENARIO_ID_RE = re.compile(r"^\d{3}-[a-z0-9][a-z0-9-]*$")
@dataclass(frozen=True)
class SyntheticSuiteSpec:
"""Resolved synthetic suite/scenario selection."""
suite_name: str
scenario: str
run_all: bool
display_command: str
valid: bool
def resolve_synthetic_suite(suite_name: str) -> SyntheticSuiteSpec:
suite_spec = suite_name.strip().lower()
resolved_suite_name = ""
resolved_scenario = DEFAULT_SYNTHETIC_SCENARIO
run_all = False
if suite_spec == "rds_postgres":
resolved_suite_name = "rds_postgres"
elif suite_spec == "rds_postgres:all":
resolved_suite_name = "rds_postgres"
run_all = True
elif suite_spec.startswith("rds_postgres:"):
requested_scenario = suite_spec.split(":", 1)[1].strip()
if requested_scenario and _SYNTHETIC_SCENARIO_ID_RE.fullmatch(requested_scenario):
resolved_suite_name = "rds_postgres"
resolved_scenario = requested_scenario
display_command = (
"opensre tests synthetic all"
if run_all
else f"opensre tests synthetic --scenario {resolved_scenario}"
)
return SyntheticSuiteSpec(
suite_name=resolved_suite_name,
scenario=resolved_scenario,
run_all=run_all,
display_command=display_command,
valid=resolved_suite_name == "rds_postgres",
)
def synthetic_cli_argv(spec: SyntheticSuiteSpec) -> list[str]:
argv = (
build_opensre_cli_argv(["tests", "synthetic", "all"])
if spec.run_all
else build_opensre_cli_argv(
["tests", "synthetic", "--scenario", spec.scenario],
)
)
if not argv:
return argv
return [argv[0], "-u", *argv[1:]]
def spawn_synthetic_subprocess(
spec: SyntheticSuiteSpec,
*,
env: dict[str, str],
) -> subprocess.Popen[str]:
return subprocess.Popen(
synthetic_cli_argv(spec),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
start_new_session=True,
env=env,
)
def watch_synthetic_subprocess(
task: TaskRecord,
proc: subprocess.Popen[Any],
presenter: SubprocessPresenter,
suite_name: str,
stderr_buf: tempfile.SpooledTemporaryFile[bytes], # type: ignore[type-arg]
) -> None:
session = presenter.session
def _history_text() -> str:
return f"{suite_name} task:{task.task_id}"
history_gen_when_watch_started = session.terminal.history_generation
def _record_synthetic_if_current_session(ok: bool) -> None:
if session.terminal.history_generation != history_gen_when_watch_started:
return
session.record("synthetic_test", _history_text(), ok=ok)
def _run() -> None:
output_threads: list[threading.Thread] = []
suggest_follow_up = False
try:
output_threads = presenter.start_task_output_streams(
task=task,
proc=proc,
stderr_capture=stderr_buf,
)
watch_result = watch_subprocess_until_exit(
proc,
cancel_event=task.cancel_requested,
timeout_seconds=SYNTHETIC_TEST_TIMEOUT_SECONDS,
)
if watch_result.timed_out:
task.mark_failed(f"timed out after {SYNTHETIC_TEST_TIMEOUT_SECONDS}s")
_record_synthetic_if_current_session(ok=False)
suggest_follow_up = True
return
presenter.join_task_output_streams(output_threads)
code = watch_result.exit_code
if code is None:
task.mark_failed("subprocess did not report exit code")
_record_synthetic_if_current_session(ok=False)
suggest_follow_up = True
return
if watch_result.terminated_by_watcher and watch_result.cancelled:
task.mark_cancelled()
_record_synthetic_if_current_session(ok=False)
return
if code == 0:
task.mark_completed(result="ok")
_record_synthetic_if_current_session(ok=True)
else:
diag = read_diag(stderr_buf)
error_msg = f"exit code {code}" + (f": {diag}" if diag else "")
task.mark_failed(error_msg)
_record_synthetic_if_current_session(ok=False)
suggest_follow_up = True
except Exception as exc: # noqa: BLE001
task.mark_failed(str(exc))
presenter.report_exception(
exc, context="surfaces.interactive_shell.synthetic_test.watch"
)
_record_synthetic_if_current_session(ok=False)
suggest_follow_up = True
presenter.print_error(f"synthetic watcher failed: {exc}")
finally:
presenter.join_task_output_streams(output_threads)
stderr_buf.close()
if (
suggest_follow_up
and session.terminal.history_generation == history_gen_when_watch_started
):
session.suggest_synthetic_failure_follow_up(label=suite_name)
else:
session.terminal.notify_prompt_changed()
threading.Thread(target=_run, daemon=True, name=f"synthetic-{task.task_id}").start()
def run_synthetic_test(suite_name: str, presenter: SubprocessPresenter) -> None:
session = presenter.session
spec = resolve_synthetic_suite(suite_name)
if not spec.valid:
presenter.print_error(f"unknown synthetic suite: {suite_name}")
session.record("synthetic_test", suite_name, ok=False)
return
policy = allow_tool("synthetic_test")
if not presenter.execution_allowed(
policy,
action_summary=spec.display_command,
):
session.record("synthetic_test", suite_name, ok=False)
return
presenter.print_bold_command(spec.display_command)
session.last_synthetic_observation_path = None
task = session.task_registry.create(TaskKind.SYNTHETIC_TEST, command=spec.display_command)
task.mark_running()
stderr_buf: tempfile.SpooledTemporaryFile[bytes] = tempfile.SpooledTemporaryFile( # type: ignore[type-arg]
max_size=SYNTHETIC_DIAG_CHARS * 2
)
try:
proc = spawn_synthetic_subprocess(spec, env=presenter.subprocess_env())
except Exception as exc:
stderr_buf.close()
task.mark_failed(str(exc))
presenter.report_exception(exc, context="surfaces.interactive_shell.synthetic_test.start")
presenter.print_error(f"synthetic test failed to start: {exc}")
session.record("synthetic_test", suite_name, ok=False)
return
session.record("synthetic_test", suite_name)
task.attach_process(proc)
watch_synthetic_subprocess(
task,
proc,
presenter,
f"{spec.suite_name}:{spec.scenario}",
stderr_buf,
)
presenter.print(
f"[dim]synthetic test started — task[/] [bold]{task.task_id}[/bold]. "
"[highlight]/tasks[/] [dim]to monitor,[/] "
f"[highlight]/cancel {task.task_id}[/] [dim]to stop.[/]"
)
__all__ = ["run_synthetic_test", "watch_synthetic_subprocess"]