feat(plugins): add managed runtime SDK and release 2.4.5
This commit is contained in:
1 parent
506e3bf1c5
commit
2a3bbaa3fb
45 files changed
+3348
-98
No files matched your search
@@ -0,0 +1,212 @@
|
||||
"""Run opt-in, offline SDK fault regressions in temporary directories only."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import psutil
|
||||
|
||||
from artemis.plugins.evidence import decode_receipt, persist_attachments
|
||||
from artemis.plugins.models import PluginCallResult
|
||||
from artemis.plugins.sdk import (
|
||||
FileVerificationError,
|
||||
ManagedProcessError,
|
||||
ProcessTimedOut,
|
||||
run_process,
|
||||
snapshot_file,
|
||||
)
|
||||
|
||||
PROBE = Path(__file__).with_name("_sdk_probe.py").resolve()
|
||||
|
||||
|
||||
def _require(condition, message):
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _owned_process(marker: Path, started: float):
|
||||
if not marker.exists():
|
||||
return None
|
||||
pid = json.loads(marker.read_text("utf-8"))["pid"]
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
if process.status() == psutil.STATUS_ZOMBIE:
|
||||
return None
|
||||
_require(process.create_time() >= started - 1, "测试 PID 的启动时间不匹配")
|
||||
_require(str(PROBE) in process.cmdline(), "测试 PID 的脚本身份不匹配")
|
||||
return process
|
||||
except psutil.NoSuchProcess:
|
||||
return None
|
||||
|
||||
|
||||
async def _tree_case(root: Path, mode: str):
|
||||
marker = root / f"{mode}.json"
|
||||
started = time.time()
|
||||
task = asyncio.create_task(
|
||||
run_process(
|
||||
[
|
||||
sys.executable,
|
||||
"-I",
|
||||
"-B",
|
||||
str(PROBE),
|
||||
"tree",
|
||||
str(marker),
|
||||
"exit" if mode == "normal" else "linger",
|
||||
],
|
||||
cwd=root,
|
||||
timeout=2 if mode == "timeout" else 10,
|
||||
)
|
||||
)
|
||||
try:
|
||||
if mode == "cancel":
|
||||
async with asyncio.timeout(5):
|
||||
while not marker.exists() and not task.done():
|
||||
await asyncio.sleep(0.02)
|
||||
_require(marker.exists(), "取消探针没有启动")
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("取消没有传播")
|
||||
elif mode == "timeout":
|
||||
try:
|
||||
await task
|
||||
except ProcessTimedOut:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("超时没有生效")
|
||||
else:
|
||||
result = await task
|
||||
_require(result.returncode == 0, "模拟工具异常退出")
|
||||
_require(marker.exists(), "后代进程探针没有启动")
|
||||
async with asyncio.timeout(3):
|
||||
while _owned_process(marker, started) is not None:
|
||||
await asyncio.sleep(0.02)
|
||||
finally:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
with suppress(asyncio.CancelledError, ManagedProcessError):
|
||||
await task
|
||||
# Failing implementations must not leave the fault suite's own sleeper alive.
|
||||
process = _owned_process(marker, started)
|
||||
if process is not None:
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
async def run_checks() -> dict:
|
||||
checks = []
|
||||
with tempfile.TemporaryDirectory(prefix="artemis-sdk-faults-") as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "synthetic.rom"
|
||||
approved = b"approved firmware bytes\x00\xff"
|
||||
source.write_bytes(approved)
|
||||
digest = hashlib.sha256(approved).hexdigest()
|
||||
snapshot = snapshot_file(source, digest)
|
||||
for replacement in (b"X" * len(approved), b"", b"Y" * (len(approved) + 10)):
|
||||
source.write_bytes(replacement)
|
||||
_require(b"".join(snapshot.chunks(3)) == approved, "快照受原文件修改影响")
|
||||
try:
|
||||
snapshot_file(source, digest)
|
||||
except FileVerificationError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("摘要不符仍被接收")
|
||||
_require(all(part.readonly for part in snapshot.chunks()), "分块内容可被修改")
|
||||
checks.append("file_replacement_truncation_growth_and_digest")
|
||||
result = await run_process(
|
||||
[sys.executable, "-I", "-B", str(PROBE), "echo"],
|
||||
cwd=root,
|
||||
timeout=5,
|
||||
input_data=snapshot.data,
|
||||
)
|
||||
_require(
|
||||
result.stdout == approved and result.stderr == b"synthetic-stderr",
|
||||
"二进制输入输出不一致",
|
||||
)
|
||||
checks.append("binary_input_and_separate_output")
|
||||
failed = await run_process(
|
||||
[sys.executable, "-I", "-B", str(PROBE), "failure"], cwd=root, timeout=5
|
||||
)
|
||||
_require(failed.returncode == 7, "非零退出码被隐藏")
|
||||
checks.append("nonzero_returncode")
|
||||
for mode in ("normal", "timeout", "cancel"):
|
||||
await _tree_case(root, mode)
|
||||
checks.append(f"descendants_reclaimed_on_{mode}")
|
||||
try:
|
||||
await run_process(
|
||||
[sys.executable, "-I", "-B", str(PROBE), "overflow"],
|
||||
cwd=root,
|
||||
timeout=5,
|
||||
max_output_bytes=1024,
|
||||
)
|
||||
except ManagedProcessError as exc:
|
||||
_require(exc.error_code == "process_output_limit", "输出超限错误类型不正确")
|
||||
else:
|
||||
raise AssertionError("输出超限没有停止")
|
||||
checks.append("output_limit_stops_process")
|
||||
trace = root / "synthetic-trace"
|
||||
artifacts = trace / "artifacts/plugins/sdk-faults"
|
||||
artifacts.mkdir(parents=True)
|
||||
sample = artifacts / "sample.txt"
|
||||
sample.write_bytes(b"synthetic evidence")
|
||||
receipt = {"name": sample.name, "sha256": hashlib.sha256(sample.read_bytes()).hexdigest()}
|
||||
_require(len(persist_attachments(trace, "sdk-faults", [receipt])) == 1, "附件未被回读")
|
||||
for invalid in ({**receipt, "sha256": "0" * 64}, {**receipt, "name": "../outside.txt"}):
|
||||
try:
|
||||
persist_attachments(trace, "sdk-faults", [invalid])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("证据摘要不符或路径穿越未拒绝")
|
||||
checks.append("evidence_hash_and_path_rejection")
|
||||
for execution in ("partial", "unknown"):
|
||||
outcome = PluginCallResult(
|
||||
status="success",
|
||||
structured_content={
|
||||
"artemis": {
|
||||
"version": 1,
|
||||
"execution": execution,
|
||||
"side_effect": "possible",
|
||||
"retryable": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
decode_receipt(outcome)
|
||||
_require(
|
||||
not outcome.retryable and outcome.requires_observation, "不确定写操作被允许重试"
|
||||
)
|
||||
checks.append("partial_unknown_never_authorize_retries")
|
||||
return {
|
||||
"status": "passed",
|
||||
"scope": "sdk_faults_only",
|
||||
"python": sys.version.split()[0],
|
||||
"platform": sys.platform,
|
||||
"checks": checks,
|
||||
"real_device_actions": False,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--json-output", type=Path)
|
||||
args = parser.parse_args()
|
||||
result = asyncio.run(run_checks())
|
||||
if args.json_output:
|
||||
args.json_output.write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2) + "\n", "utf-8"
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
print("仅验证 SDK 自身;插件仍需对真实入口做故障注入、证据与任务验收。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user