P28-01-RESTART: 現HEADの性能基準:途中保存・別PID再開の再現
This commit is contained in:
+473
@@ -0,0 +1,473 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RESTART evidence validation; typed state comparisons execute in Godot."""
|
||||
import ast
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[6]
|
||||
BASE = "docs/plans/original-completion-2026-10-02/execution/p28-01/"
|
||||
DEPENDENCIES = {
|
||||
"tools/run_all_tests.py": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4",
|
||||
"tools/regression/owned_outer.py": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7",
|
||||
"tools/regression/owned_diagnostic_process.py": "e31e93c6e2c5ea46a7d954ef8bc0b4caec1c08b86b8aef19e93b52ef24993087",
|
||||
"tools/regression/p09_owned_engine_process.py": "c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037",
|
||||
BASE + "collector/collect_p28_01.py": "a43e1f963ef8505ddd5acf8bf747d9bfac22a028cbb805c0b964ee11b9b40a14",
|
||||
BASE + "collector/check_p28_01_report.py": "6c0851d498ca80bdc120fad047b072255f48713f63114521f3d4dfc902b2512b",
|
||||
}
|
||||
LOCAL = {BASE + "restart/probe/" + n for n in
|
||||
("restart_pair.py", "check_restart.py", "test_restart.py", "restart_probe.gd")}
|
||||
PHASES = ["import", "parse-base", "parse-wrapper", "R-writer", "R-reader", "O-writer", "O-reader"]
|
||||
CROSS = [
|
||||
{"condition": "原本条件とUI操作が一致", "applicable": False, "reference": "collector C1/C2",
|
||||
"reason": "既存collectorのUI経路受入を参照。このtaskは再開の比較を担当する。"},
|
||||
{"condition": "Runtimeの実利用・会計・行動へ接続", "applicable": False, "reference": "collector C2/C3",
|
||||
"reason": "既存collectorの実Runtime観測を参照。製品の接続は変更しない。"},
|
||||
{"condition": "途中保存・再起動で再現", "applicable": True, "reference": "RS1/RS2/RS3/RS4/RS5",
|
||||
"reason": "別PIDのR/O writerとreaderで適用する。"},
|
||||
{"condition": "異常/境界/中断を検証", "applicable": False, "reference": "collector C5/C6/C7",
|
||||
"reason": "横断受入は既存collectorを参照。再開固有の異常と境界はRS5で検証する。"},
|
||||
{"condition": "関連回帰PASS", "applicable": False, "reference": "collector C8",
|
||||
"reason": "既存collectorの正式回帰証拠を参照。workerは全回帰を実行しない。"},
|
||||
{"condition": "要件別ログとソースハッシュを保存", "applicable": False, "reference": "collector C6/C7/C9",
|
||||
"reason": "既存collectorの証拠契約を参照。再開のraw結合はRS7で保存する。"},
|
||||
]
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def pin(path):
|
||||
path = Path(path)
|
||||
require(path.is_file() and not path.is_symlink(), "missing/symlink evidence")
|
||||
return {"sha256": sha(path), "bytes": path.stat().st_size}
|
||||
|
||||
|
||||
def check_dependencies(root=ROOT):
|
||||
for name, expected in DEPENDENCIES.items():
|
||||
require(sha(root / name) == expected, "dependency SHA: " + name)
|
||||
|
||||
|
||||
def check_imports(text, external=None):
|
||||
tree = ast.parse(text)
|
||||
if external is not None:
|
||||
require(external in DEPENDENCIES, "external AST closure")
|
||||
# The explicitly forbidden run_all_tests.main imports are not executed.
|
||||
tree.body = [node for node in tree.body if not (
|
||||
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "main")]
|
||||
allowed = {Path(p).stem for p in set(DEPENDENCIES) | LOCAL}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
names = [node.module] if isinstance(node, ast.ImportFrom) else [a.name for a in node.names]
|
||||
for name in names:
|
||||
require(name and (standard_library(name.split(".")[0]) or name in allowed),
|
||||
"import outside closure")
|
||||
if isinstance(node, ast.Call):
|
||||
name = node.func.id if isinstance(node.func, ast.Name) else getattr(node.func, "attr", "")
|
||||
require(name not in ("__import__", "eval", "import_module"), "dynamic code forbidden")
|
||||
require(name != "exec" or external == "tools/regression/owned_diagnostic_process.py",
|
||||
"unapproved exec")
|
||||
if name == "spec_from_file_location":
|
||||
for sub in ast.walk(node):
|
||||
if isinstance(sub, ast.Constant) and isinstance(sub.value, str):
|
||||
for path in re.findall(r"[\w./-]+\.py", sub.value):
|
||||
require(path in DEPENDENCIES or path in LOCAL or
|
||||
path in {Path(p).name for p in DEPENDENCIES}, "loader outside closure")
|
||||
if external and isinstance(node, ast.BinOp):
|
||||
for sub in ast.walk(node):
|
||||
if isinstance(sub, ast.Constant) and isinstance(sub.value, str) and sub.value.endswith(".py"):
|
||||
require(sub.value in DEPENDENCIES or sub.value in {Path(p).name for p in DEPENDENCIES},
|
||||
"external loader path outside closure")
|
||||
|
||||
|
||||
def standard_library(name):
|
||||
# Python 3.9 on the native host has no sys.stdlib_module_names.
|
||||
spec = importlib.util.find_spec(name)
|
||||
if spec is None:
|
||||
return False
|
||||
if spec.origin in ("built-in", "frozen"):
|
||||
return True
|
||||
if not spec.origin:
|
||||
return False
|
||||
path = Path(spec.origin).resolve()
|
||||
return path.is_relative_to(Path(sysconfig.get_path("stdlib")).resolve()) and not any(
|
||||
part in ("site-packages", "dist-packages") for part in path.parts)
|
||||
|
||||
|
||||
def check_modules(modules=None, root=ROOT):
|
||||
for module in (sys.modules if modules is None else modules).values():
|
||||
name = getattr(module, "__file__", None)
|
||||
if name:
|
||||
path = Path(name).resolve()
|
||||
if path.is_relative_to(root):
|
||||
require(path.relative_to(root).as_posix() in set(DEPENDENCIES) | LOCAL,
|
||||
"loaded project module outside closure")
|
||||
|
||||
|
||||
def check_probe(text):
|
||||
function = None
|
||||
blocks = []
|
||||
pauses = 0
|
||||
for raw in text.splitlines():
|
||||
line = raw.split("#", 1)[0]
|
||||
match = re.match(r"^func (\w+)\(", line)
|
||||
if match:
|
||||
function = match[1]
|
||||
if function == "deterministic_step":
|
||||
blocks.append([])
|
||||
if function == "deterministic_step":
|
||||
blocks[-1].append(line)
|
||||
require(not re.search(r"set_paused\s*\(\s*false|\bclear\s*\(|spawn_guest|SimulationRuntime\.new|\binitialize\s*\(|enable_production_|_toggle_pause", line),
|
||||
"forbidden probe API")
|
||||
if re.search(r"\badvance_", line):
|
||||
require(function == "deterministic_step" and line.strip() == "GameClock.advance_minutes(1)",
|
||||
"advance outside deterministic_step")
|
||||
if "set_paused(" in line:
|
||||
require(function == "on_tick" and line.strip() == "GameClock.set_paused(true)", "pause outside tick")
|
||||
pauses += 1
|
||||
require(not re.search(r"Time\.get_ticks_usec|\b\w+_us\b|Performance\.|get_performance_report", line),
|
||||
"performance instrumentation")
|
||||
require(len(blocks) == 1 and pauses == 1, "deterministic/tick functions required")
|
||||
block = "\n".join(blocks[0])
|
||||
require(block.count("GameClock.advance_minutes(1)") == 1 and
|
||||
"io_ready" in block and "GameClock.paused" in block, "step admission")
|
||||
|
||||
|
||||
def event_id(counter):
|
||||
require(type(counter) is int and counter >= 0, "counter type")
|
||||
return "domain_event.%012d" % (counter + 1)
|
||||
|
||||
|
||||
def transform(witness, event):
|
||||
"""Consistency oracle for recorded IO paths, not a Godot state decoder."""
|
||||
result = copy.deepcopy(witness)
|
||||
eid = event_id(witness["counter"])
|
||||
ids, recent = witness["published_ids"], witness["recent_events"]
|
||||
require(event["event_id"] == eid, "IO event ID")
|
||||
require(ids == sorted(set(ids)) and eid not in ids and len(recent) <= 256, "event history")
|
||||
require(len({x["event_id"] for x in recent}) == len(recent) and
|
||||
all(x["event_id"] in ids for x in recent), "recent IDs")
|
||||
result["counter"] += 1
|
||||
result["published_ids"] = sorted(ids + [eid])
|
||||
result["recent_events"] = (recent[1:] if len(recent) == 256 else recent) + [event]
|
||||
return result
|
||||
|
||||
|
||||
def exact(row):
|
||||
require(row["method"] == "var_to_bytes" and row["equal"] is True and row["diff_paths"] == [],
|
||||
"typed bytes differ")
|
||||
require(row["excluded_paths"] == [], "state path exclusion")
|
||||
require(re.fullmatch("[0-9a-f]{64}", row["expected_sha256"]) and
|
||||
row["actual_sha256"] == row["expected_sha256"], "typed comparison contradiction")
|
||||
|
||||
|
||||
def check_io(row, kind):
|
||||
require(row["kind"] == kind and row["signal_count"] == 1, "one IO signal required")
|
||||
signal, latest, ev = row["signal_payload"], row["latest"], row["event"]
|
||||
require(signal == latest == ev["payload"], "signal/latest/event mismatch")
|
||||
require(row["signal_projection"] == "_save_event_payload" and row["payload_bytes_equal"] is True,
|
||||
"typed payload comparison")
|
||||
success = kind != "tamper"
|
||||
require(type(signal["ok"]) is bool and signal["ok"] is success and
|
||||
((signal["code"] == "OK") is success), "IO result")
|
||||
require(signal["operation"] == ("save" if kind == "save" else "load"), "operation")
|
||||
require(signal["save_path"].startswith("user://p28_restart/") and ".." not in signal["save_path"],
|
||||
"private IO path")
|
||||
require(row["return_kind"] == ("bool" if kind == "save" else "void"), "return kind")
|
||||
require(row["return_value"] is signal["ok"] if kind == "save" else row["return_value"] is None,
|
||||
"return/signal mismatch")
|
||||
expected_type = {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED",
|
||||
"tamper": "SAVE_LOAD_FAILED"}[kind]
|
||||
require(ev["event_type"] == expected_type and ev["source_service"] == "save_service" and
|
||||
ev["entity_ids"] == [] and ev["causation_command_id"] == 0, "event shape")
|
||||
require(ev["absolute_minute"] == row["minute_after"] == row["minute_before"], "IO minute")
|
||||
require(row["after"] == transform(row["before"], ev), "not X(P,event)")
|
||||
require(row["dropped"] == (row["before"]["recent_events"][0]
|
||||
if len(row["before"]["recent_events"]) == 256 else None), "wrong dropped head")
|
||||
exact(row["comparison"])
|
||||
if success:
|
||||
require(signal["saved_absolute_minute"] == row["minute_after"] and bool(signal["save_id"]),
|
||||
"save identity")
|
||||
require(row["paused_after"] is True, "IO clock not paused")
|
||||
else:
|
||||
failure = row["failure"]
|
||||
require(failure["preload_pause_restored"] is True and
|
||||
failure["loaded_clock_paused"] is row["paused_before"] and
|
||||
row["paused_after"] is row["paused_before"], "tamper pause")
|
||||
if failure["recovery_report"] is not None:
|
||||
require(failure["recovery_report"]["restore_committed"] is False, "tamper restore")
|
||||
require(type(failure["migration_attempted"]) is bool, "migration observation")
|
||||
|
||||
|
||||
def check_report(report):
|
||||
check_no_metrics(report)
|
||||
require(report["schema"] == "p28-restart-v1" and report["stop_reason"] == "complete", "incomplete probe")
|
||||
require(report["role"] in PHASES[3:] and type(report["pid"]) is int and report["pid"] > 0, "role/PID")
|
||||
require(report["main_added"] is True and report["k_target"] == 30 and report["actual_k"] == 30, "main/K")
|
||||
require(report["comparison_scope"] == "complete_typed_var_to_bytes" and report["excluded_paths"] == [],
|
||||
"partial digest/path exclusion")
|
||||
require(report["transfer_before_guard_writes"] == 0 and report["probe_deadline_seconds"] == 1740,
|
||||
"profile/deadline")
|
||||
writer = report["role"].endswith("writer")
|
||||
kinds = ["save"] if writer else ["tamper", "load"]
|
||||
require(len(report["io"]) == len(kinds), "IO sequence")
|
||||
for row, kind in zip(report["io"], kinds):
|
||||
check_io(row, kind)
|
||||
if writer:
|
||||
exact(report["package_matches_before"])
|
||||
require(report["package_matches_before"]["actual_sha256"] == report["states"]["P"]["sha256"],
|
||||
"P not package snapshot")
|
||||
else:
|
||||
require(report["failure_event_absent"] is True and report["transfer_verified"] is True,
|
||||
"failed event survived/transfer not verified")
|
||||
io = report["io"][-1]
|
||||
require(report["event_id"] == io["event"]["event_id"] and report["saved_minute"] == io["minute_after"],
|
||||
"continuation IO identity")
|
||||
require(report["paused_idle"]["before"] == report["paused_idle"]["after"], "paused clock consumed backlog")
|
||||
require(report["paused_idle"]["before"]["paused"] is True, "paused observation")
|
||||
exact(report["paused_idle"]["comparison"])
|
||||
rows = report["minutes"]
|
||||
require(len(rows) == 30, "actual K not 30")
|
||||
previous, lost = report["saved_minute"], False
|
||||
for row in rows:
|
||||
require(row["before"] == previous and row["after"] == previous + 1, "K counts actual minute advances")
|
||||
require(row["paused"] is True and row["backlog_before"] == row["backlog_after"], "step consumed backlog")
|
||||
require(row["scope"] == "complete_typed_var_to_bytes" and row["excluded_paths"] == [], "partial state")
|
||||
require(re.fullmatch("[0-9a-f]{64}", row["sha256"]), "minute SHA")
|
||||
if row["replacement"] is not None:
|
||||
require(not lost and row["replacement"] == io["event"] and row["replacement_count"] == 1,
|
||||
"replacement must be unchanged in-window E")
|
||||
else:
|
||||
lost = True
|
||||
require(row["replacement_count"] == 0, "replacement after E left window")
|
||||
require(0 <= row["recent_count"] <= 256, "recent limit")
|
||||
require(len(row["recent_ids"]) == len(set(row["recent_ids"])) == row["recent_count"] and
|
||||
row["recent_ids"].count(report["event_id"]) == row["replacement_count"],
|
||||
"replacement does not match actual window")
|
||||
previous = row["after"]
|
||||
require(report["final_minute"] == previous, "final minute")
|
||||
for name, entry in report["states"].items():
|
||||
require(re.fullmatch("[0-9a-f]{64}", entry["sha256"]) and entry["bytes"] > 0 and
|
||||
entry["path"] == "p28_restart/evidence/" + name + ".bin", "state binding")
|
||||
required = {"P", "after_save", "final"} if writer else {"P", "B", "after_tamper", "after_load", "final"}
|
||||
require(set(report["states"]) == required, "missing complete state files")
|
||||
require(report["states"]["after_save" if writer else "after_load"]["sha256"] ==
|
||||
io["comparison"]["actual_sha256"], "IO state file differs")
|
||||
if not writer:
|
||||
require(report["states"]["after_tamper"]["sha256"] == report["io"][0]["comparison"]["actual_sha256"],
|
||||
"tamper state file differs")
|
||||
if report["role"].startswith("O-"):
|
||||
boundary = report["boundary"]
|
||||
require(boundary is not None and boundary["guest_auto_count"] > 0, "arrival not processed")
|
||||
require(len(boundary["guest_auto_ids"]) == len(set(boundary["guest_auto_ids"])) ==
|
||||
boundary["guest_auto_count"], "arrival IDs")
|
||||
require(all(x.startswith("guest-auto-") for x in boundary["guest_auto_ids"]), "arrival identity")
|
||||
require(boundary["clock"] == report["saved_minute"] and boundary["processed_arrival_minutes"],
|
||||
"arrival boundary not processed")
|
||||
if writer:
|
||||
require(0 < report["saved_minute"] - report["start_minute"] <= 120, "arrival cap")
|
||||
ticks = report["ui_ticks"]
|
||||
require(ticks and ticks[-1] == boundary and all(t["guest_auto_count"] == 0 for t in ticks[:-1]),
|
||||
"not first arrival tick")
|
||||
next_boundary = report["next_boundary"]
|
||||
added = [m for m in next_boundary["after"] if m not in next_boundary["before"]]
|
||||
require(next_boundary["before"] == boundary["processed_arrival_minutes"] and
|
||||
added == [next_boundary["minute"]] and next_boundary["processed_once"] is True and
|
||||
next_boundary["minute"] > boundary["clock"] and
|
||||
next_boundary["minute"] <= report["final_minute"], "next arrival missing/duplicate")
|
||||
minutes = next_boundary["after"]
|
||||
require(minutes == sorted(set(minutes)), "duplicate processed arrivals")
|
||||
require([r["minute"] for r in next_boundary["arrival_reports"]] == minutes,
|
||||
"arrival report duplicates/omissions")
|
||||
else:
|
||||
require(report["boundary"] is None and report["next_boundary"] is None, "R Operations boundary")
|
||||
return True
|
||||
|
||||
|
||||
def check_no_metrics(value):
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
# Mandatory verbatim domain/IO payloads contain product diagnostics.
|
||||
# They are evidence, never performance measurements or verdicts.
|
||||
if key in ("payload", "signal_payload", "latest"):
|
||||
continue
|
||||
require(not key.endswith(("_us", "_ms")) and key not in
|
||||
("performance", "statistics", "fps", "budget_verdict"), "performance field forbidden")
|
||||
check_no_metrics(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
check_no_metrics(child)
|
||||
|
||||
|
||||
def check_pair(writer, reader):
|
||||
check_report(writer)
|
||||
check_report(reader)
|
||||
require(writer["role"].replace("writer", "reader") == reader["role"] and writer["pid"] != reader["pid"],
|
||||
"pair roles/PID")
|
||||
require(writer["event_id"] == reader["event_id"] and writer["saved_minute"] == reader["saved_minute"],
|
||||
"pair IO identity")
|
||||
require(writer["states"]["P"] == reader["states"]["P"], "package P differs")
|
||||
require(writer["io"][0]["before"] == reader["io"][1]["before"], "package history differs")
|
||||
for left, right in zip(writer["minutes"], reader["minutes"]):
|
||||
require(all(left[k] == right[k] for k in
|
||||
("before", "after", "sha256", "scope", "replacement_count", "recent_count")),
|
||||
"cross-PID complete typed state mismatch")
|
||||
require(writer["boundary"] == reader["boundary"] and writer["next_boundary"] == reader["next_boundary"],
|
||||
"Operations boundary differs")
|
||||
|
||||
|
||||
def bound_raw(folder, row):
|
||||
folder = Path(folder)
|
||||
require(set(row["raw"]) == {"stdout.log", "stderr.log", "engine-native.log"}, "all raw required")
|
||||
for name, expected in {**row["raw"], **row["owned_files"]}.items():
|
||||
require(Path(name).name == name and pin(folder / name) == expected, "raw/owned SHA mismatch")
|
||||
require(set(row["owned_files"]) == {"process.json", "started.json", "ended.json",
|
||||
"adapter-owned-cleanup.json", "outer-owned-finally.json"}, "owned receipts missing")
|
||||
require(json.loads((folder / "process.json").read_text()) == row["process"], "process receipt mismatch")
|
||||
process, capture = row["process"], row["capture"]
|
||||
require(row["passed"] is True and process["exit_code"] == 0 and process["helper_exit_code"] == 0 and
|
||||
process["timed_out"] is False and process["error_lines"] == [] and process["lifecycle_error"] is None and
|
||||
process["external_process_or_group_signals"] == 0 and process["owned_popen_ended"] is True and
|
||||
process["old_helper_engine_ended"] is True and process["start_receipt_written"] is True and
|
||||
process["end_receipt_written"] is True, "owned phase failed")
|
||||
require(capture["passed"] is True and capture["all_captured_ended"] is True and
|
||||
capture["outer_cleanup_errors"] == [] and capture["adapter_exception"] is None and
|
||||
capture["foreign_process_or_group_signals"] == 0 and
|
||||
capture["all_cleanup_before_outer_receipt"] is True and capture["outer_receipt_written"] is True and
|
||||
capture["outer_receipt_error"] is None and len(capture["actual_captured_pids"]) == 1, "outer capture")
|
||||
actual = json.loads((folder / "outer-owned-finally.json").read_text())
|
||||
require(actual == {k: v for k, v in capture.items() if k not in
|
||||
("passed", "outer_receipt_written", "outer_receipt_error")}, "outer raw differs")
|
||||
pid = capture["actual_captured_pids"][0]
|
||||
require(process["pid"] == pid and process["argv"] == row["argv"], "process PID/argv")
|
||||
for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")):
|
||||
binding = json.loads((folder / filename).read_text())
|
||||
require(binding == process[key] and binding["pid"] == binding["actual_pid"] == pid and
|
||||
binding["argv"] == binding["command"] == row["argv"] and binding["owner_pid"] > 0,
|
||||
"START/END binding")
|
||||
require(process["actual_start_binding"]["owner_pid"] == process["actual_end_binding"]["owner_pid"],
|
||||
"START/END owners differ")
|
||||
require(json.loads((folder / "adapter-owned-cleanup.json").read_text()) == process["adapter_owned_cleanup"]
|
||||
and process["adapter_owned_cleanup"]["engine_ended"] is True and
|
||||
process["adapter_owned_cleanup"]["errors"] == [], "adapter cleanup binding")
|
||||
require(process["stdout_sha256"] == row["raw"]["stdout.log"]["sha256"] and
|
||||
process["stderr_sha256"] == row["raw"]["stderr.log"]["sha256"], "process raw binding")
|
||||
require(row["timeout_seconds"] == 1800 and row["diagnostics"] == [], "phase timeout/diagnostics")
|
||||
raw = (folder / "stdout.log").read_text()
|
||||
for name in ("stdout.log", "stderr.log", "engine-native.log"):
|
||||
require(not re.search(r"SCRIPT ERROR|Parse Error|ERROR:", (folder / name).read_text()),
|
||||
"raw engine error")
|
||||
if row["phase"] not in PHASES[3:]:
|
||||
require("P28_RESTART_REPORT=" not in raw, "parse/import report")
|
||||
if row["phase"].startswith("parse"):
|
||||
require("--check-only" in row["argv"], "parse not check-only")
|
||||
return None
|
||||
reports = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_REPORT=")]
|
||||
closes = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_AUDIO_CLOSE=")]
|
||||
require(len(reports) == len(closes) == 1, "report/audio close count")
|
||||
report, close = json.loads(reports[0]), json.loads(closes[0])
|
||||
require(report == row["report"] and report["role"] == row["phase"] and
|
||||
capture["actual_captured_pids"] == [report["pid"]], "raw report/PID binding")
|
||||
require(close.get("coordinator") is True and close.get("passed") is True and
|
||||
close.get("owned_engine_pid") == report["pid"] and close.get("all_owned_targets_released") is True and
|
||||
close.get("deadline_reached") is False and close.get("clock_process_held_stopped") is True and
|
||||
close.get("window_input_held_disabled") is True and close.get("restored_before_quit") is False and
|
||||
close.get("shared_bus_deleted") is False and close.get("settings_mutated") is False, "audio close")
|
||||
check_report(report)
|
||||
return report
|
||||
|
||||
|
||||
def check_receipt(receipt, out):
|
||||
from restart_pair import COLLECTOR_PINS, check_transfer
|
||||
out = Path(out)
|
||||
require(receipt["passed"] is True and receipt["error"] is None and receipt["cleanup_errors"] == [],
|
||||
"execution failed")
|
||||
require(receipt["source_before"] == receipt["source_after"] and len(receipt["source_before"]) == 1407,
|
||||
"source map mismatch")
|
||||
require(receipt["default_player_before"] == receipt["default_player_after"], "ordinary player changed")
|
||||
require(receipt["dependencies_before"] == receipt["dependencies_after"] == DEPENDENCIES, "dependency pins")
|
||||
require(receipt["collector_before"] == receipt["collector_after"] == COLLECTOR_PINS,
|
||||
"collector changed")
|
||||
require(receipt["worker_before"] == receipt["worker_after"] and set(receipt["worker_before"]) ==
|
||||
{"restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py"}, "worker changed")
|
||||
require(receipt["cross_cutting"] == CROSS, "cross-cutting record")
|
||||
require(receipt["original_credit"] == 0 and receipt["windows_verified"] is False and
|
||||
receipt["operations_200_guests_verified"] is False, "scope expanded")
|
||||
rows = receipt["phases"]
|
||||
require([r["phase"] for r in rows] == PHASES, "phase order/incomplete execution")
|
||||
pids = []
|
||||
for row in rows:
|
||||
report = bound_raw(out / row["phase"], row)
|
||||
pids.extend(row["capture"]["actual_captured_pids"])
|
||||
require(row["stage_before"] == row["stage_after"] if row["phase"] != "import"
|
||||
else row["stage_preserved"] is True, "stage changed")
|
||||
require(row["source_preserved"] is True and row["player_preserved"] is True, "phase preservation")
|
||||
startup = row["startup_project"]
|
||||
require(startup["fresh_before"] is True and sha(startup["path"]) == startup["sha256"], "startup profile")
|
||||
if report:
|
||||
guard = row["guard"]
|
||||
expected = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5,
|
||||
"actual_user_data_dir": row["runtime_profile"],
|
||||
"expected_user_data_dir": row["runtime_profile"],
|
||||
"globalized_probe": str(Path(row["runtime_profile"]) / "guard-probe"),
|
||||
"custom_user_dir_name": row["runtime_name"], "fresh_before_parent_initialize": True,
|
||||
"profile_writes_before_guard": 0, "project_settings_persisted": False}
|
||||
require(all(type(guard.get(k)) is type(v) and guard[k] == v for k, v in expected.items()),
|
||||
"guard mismatch")
|
||||
guard_path = out / (row["phase"] + "-guard.json")
|
||||
require(sha(guard_path) == row["guard_sha256"] and json.loads(guard_path.read_text()) == guard,
|
||||
"guard raw binding")
|
||||
for name, evidence in report["states"].items():
|
||||
require(pin(out / (row["phase"] + "-states") / (name + ".bin")) ==
|
||||
{k: evidence[k] for k in ("sha256", "bytes")}, "state bytes SHA")
|
||||
require(len(set(pids)) == len(pids), "reused PID")
|
||||
require({r["pid"] for r in receipt["cleanup"]} == set(pids) and all(
|
||||
r["engine_ended"] is True and r["outer_cleanup_errors"] == [] and
|
||||
r["foreign_process_or_group_signals"] == 0 for r in receipt["cleanup"]), "cleanup mismatch")
|
||||
for start in (3, 5):
|
||||
check_pair(rows[start]["report"], rows[start + 1]["report"])
|
||||
transfer = out / (rows[start]["phase"][0] + "-transfer")
|
||||
manifest_path = transfer / "manifest.json"
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
check_transfer(transfer, manifest)
|
||||
require(rows[start + 1]["report"]["transfer_manifest_sha256"] == sha(manifest_path),
|
||||
"reader manifest SHA")
|
||||
for phase_index, kinds in ((start, ("good",)), (start + 1, ("good", "tampered"))):
|
||||
profile = rows[phase_index]["runtime_profile"]
|
||||
for kind in kinds:
|
||||
for name, expected in manifest["files"][kind].items():
|
||||
relative = "p28_restart/" + kind + "/" + name
|
||||
require(receipt["profiles"][profile].get(relative) == expected,
|
||||
"private package differs from transfer")
|
||||
for path, expected in receipt["script_pins"].items():
|
||||
require(sha(path) == expected, "executed script changed")
|
||||
check_probe((out / "restart_probe.gd").read_text())
|
||||
require(sha(out / "restart_probe.gd") == receipt["worker_before"]["restart_probe.gd"],
|
||||
"executed probe differs from submitted source")
|
||||
require(receipt["owned_writes"] == [], "probe/driver wrote owned folder")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("execution", type=Path)
|
||||
args = parser.parse_args()
|
||||
check_receipt(json.loads(args.execution.read_text()), args.execution.parent)
|
||||
print("P28_RESTART_CHECK=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-only four-PID RESTART driver. --check performs no writes or launches."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
import check_restart as check
|
||||
|
||||
HERE, ROOT = check.HERE, check.ROOT
|
||||
COLLECTOR = check.BASE + "collector/collect_p28_01.py"
|
||||
COLLECTOR_PINS = {
|
||||
"collect_p28_01.py": check.DEPENDENCIES[COLLECTOR],
|
||||
"check_p28_01_report.py": check.DEPENDENCIES[check.BASE + "collector/check_p28_01_report.py"],
|
||||
"measure_p28_01.gd": "4acef88bce4ac77f3f5d2659d766afcc5fd139cee381118839012022772946bb",
|
||||
"test_collect_p28_01.py": "99d86ea723c976c95dbd283acbb85981eabf427b30994e652002ec0e76dc4ab0",
|
||||
}
|
||||
|
||||
|
||||
def load_dependencies():
|
||||
check.check_dependencies()
|
||||
for name in check.DEPENDENCIES:
|
||||
check.check_imports((ROOT / name).read_text(), external=name)
|
||||
for name in ("restart_pair.py", "check_restart.py", "test_restart.py"):
|
||||
check.check_imports((HERE / name).read_text())
|
||||
check.check_modules()
|
||||
sys.path.insert(0, str((ROOT / COLLECTOR).parent))
|
||||
spec = importlib.util.spec_from_file_location("restart_collector", ROOT / COLLECTOR)
|
||||
collector = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = collector
|
||||
spec.loader.exec_module(collector)
|
||||
standard = collector.load_tool(collector.EXTERNAL_IMPORTS[0])
|
||||
outer = collector.load_tool(collector.EXTERNAL_IMPORTS[1])
|
||||
check.check_modules()
|
||||
check.check_modules({"collector": collector, "standard": standard, "outer": outer, "native": outer.native})
|
||||
check.require(Path(outer.native.helper["__file__"]).resolve() ==
|
||||
ROOT / "tools/regression/p09_owned_engine_process.py", "exec helper closure")
|
||||
return collector, standard, outer
|
||||
|
||||
|
||||
def collector_pins():
|
||||
actual = {name: check.sha((ROOT / COLLECTOR).parent / name) for name in COLLECTOR_PINS}
|
||||
check.require(actual == COLLECTOR_PINS, "collector differs from Q")
|
||||
return actual
|
||||
|
||||
|
||||
def plan(collector, engine, out):
|
||||
stage, wrapper = out / "stage", out / "guarded.gd"
|
||||
return [collector.argv_for(engine, stage, wrapper, True),
|
||||
collector.parse_argv(engine, stage, out / "restart_probe.gd"),
|
||||
collector.parse_argv(engine, stage, wrapper)] + [
|
||||
collector.argv_for(engine, stage, wrapper) for _ in range(4)]
|
||||
|
||||
|
||||
def schedule(rows):
|
||||
for index, label in enumerate(check.PHASES):
|
||||
check.require(all(row["passed"] is True for row in rows), "previous phase failed")
|
||||
if label.endswith("reader"):
|
||||
check.require(rows and rows[-1]["phase"] == label.replace("reader", "writer") and
|
||||
rows[-1].get("report", {}).get("stop_reason") == "complete",
|
||||
"reader requires successful writer")
|
||||
yield index, label
|
||||
check.require(len(rows) == index + 1 and rows[-1]["phase"] == label and rows[-1]["passed"] is True,
|
||||
"phase did not complete")
|
||||
|
||||
|
||||
def safe_inventory(directory):
|
||||
directory = Path(directory)
|
||||
check.require(directory.is_dir() and not directory.is_symlink(), "package missing/symlink")
|
||||
result = {}
|
||||
for path in sorted(directory.rglob("*")):
|
||||
check.require(not path.is_symlink(), "package symlink")
|
||||
if path.is_file():
|
||||
result[path.relative_to(directory).as_posix()] = check.pin(path)
|
||||
check.require(result, "empty package")
|
||||
return result
|
||||
|
||||
|
||||
def create_transfer(source, destination, write):
|
||||
"""Copy only after writer exit; never pre-populate the reader profile."""
|
||||
before = safe_inventory(source)
|
||||
check.require("world.bin" in before and not destination.exists(), "transfer admission")
|
||||
destination.mkdir()
|
||||
shutil.copytree(source, destination / "good")
|
||||
check.require(safe_inventory(destination / "good") == before, "good copy differs")
|
||||
shutil.copytree(destination / "good", destination / "tampered")
|
||||
world = destination / "tampered/world.bin"
|
||||
data = bytearray(world.read_bytes())
|
||||
check.require(data, "empty world")
|
||||
offset = len(data) // 2
|
||||
data[offset] ^= 1
|
||||
world.write_bytes(data)
|
||||
manifest = {"files": {"good": before, "tampered": safe_inventory(destination / "tampered")},
|
||||
"tamper": {"file": "world.bin", "offset": offset, "xor": 1,
|
||||
"before_sha256": before["world.bin"]["sha256"], "after_sha256": check.sha(world)}}
|
||||
write(destination / "manifest.json", manifest)
|
||||
check_transfer(destination, manifest)
|
||||
check.require(safe_inventory(source) == before, "writer package changed")
|
||||
return manifest
|
||||
|
||||
|
||||
def check_transfer(directory, manifest):
|
||||
check.require(set(manifest["files"]) == {"good", "tampered"}, "transfer kinds")
|
||||
for kind in ("good", "tampered"):
|
||||
check.require(safe_inventory(directory / kind) == manifest["files"][kind], "transfer SHA mismatch")
|
||||
good = (directory / "good/world.bin").read_bytes()
|
||||
bad = (directory / "tampered/world.bin").read_bytes()
|
||||
tamper = manifest["tamper"]
|
||||
check.require(len(good) == len(bad) > 0 and tamper["offset"] == len(good) // 2 and tamper["xor"] == 1,
|
||||
"tamper location")
|
||||
expected = bytearray(good)
|
||||
expected[len(good) // 2] ^= 1
|
||||
check.require(bytes(expected) == bad and tamper["before_sha256"] == check.sha(directory / "good/world.bin")
|
||||
and tamper["after_sha256"] == check.sha(directory / "tampered/world.bin"), "tamper bytes")
|
||||
for name, value in manifest["files"]["good"].items():
|
||||
if name != "world.bin":
|
||||
check.require(manifest["files"]["tampered"].get(name) == value, "extra tamper difference")
|
||||
check.require(set(manifest["files"]["good"]) == set(manifest["files"]["tampered"]), "tamper inventory")
|
||||
|
||||
|
||||
def fresh_profile(path):
|
||||
check.require(not path.exists(), "profile must be fresh; pre-guard writes forbidden")
|
||||
|
||||
|
||||
def committed_source(source, commit):
|
||||
"""Read-only Git operations: the 1407 files must be the actual HEAD bytes."""
|
||||
tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-tree", "-r", commit], text=True)
|
||||
selected = {}
|
||||
for line in tracked.splitlines():
|
||||
metadata, name = line.split("\t", 1)
|
||||
if name.split("/", 1)[0] in ("autoload", "scripts", "scenes", "data", "tests", "assets", "tools") or \
|
||||
name in ("project.godot", "export_presets.cfg", "Makefile", "AGENTS.md"):
|
||||
if Path(name).name != ".DS_Store" and "__pycache__" not in Path(name).parts:
|
||||
selected[name] = metadata.split()[2]
|
||||
check.require(set(selected) == set(source), "source map differs from HEAD")
|
||||
for name, expected in selected.items():
|
||||
actual = subprocess.check_output(["git", "-C", str(ROOT), "hash-object", str(ROOT / name)], text=True).strip()
|
||||
check.require(actual == expected, "dirty product source: " + name)
|
||||
|
||||
|
||||
def bind_report(out, label, row):
|
||||
text = (out / label / "stdout.log").read_text()
|
||||
reports = [s.partition("=")[2] for s in text.splitlines() if s.startswith("P28_RESTART_REPORT=")]
|
||||
check.require(len(reports) == 1, "one raw report required")
|
||||
row["report"] = json.loads(reports[0])
|
||||
check.bound_raw(out / label, row)
|
||||
return row["report"]
|
||||
|
||||
|
||||
def copy_states(out, row):
|
||||
folder = out / (row["phase"] + "-states")
|
||||
folder.mkdir()
|
||||
profile = Path(row["runtime_profile"])
|
||||
for name, expected in row["report"]["states"].items():
|
||||
source = profile / expected["path"]
|
||||
check.require(source.resolve().is_relative_to(profile.resolve()) and
|
||||
check.pin(source) == {k: expected[k] for k in ("bytes", "sha256")}, "state binding")
|
||||
shutil.copyfile(source, folder / (name + ".bin"))
|
||||
check.require(check.pin(source) == check.pin(folder / (name + ".bin")), "state copy differs")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--run", action="store_true")
|
||||
for name in ("readonly", "fixture", "engine", "output"):
|
||||
parser.add_argument("--" + name, type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
collector, standard, outer = load_dependencies()
|
||||
pinned = collector_pins()
|
||||
readonly, fixture, engine, out = (p.resolve() for p in (args.readonly, args.fixture, args.engine, args.output))
|
||||
inputs = collector.inputs(readonly)
|
||||
check.require(check.sha(fixture) == collector.FIXTURE_SHA and check.sha(engine) == collector.ENGINE_SHA,
|
||||
"fixture/engine SHA")
|
||||
check.check_probe((HERE / "restart_probe.gd").read_text())
|
||||
source = collector.source_map(standard, ROOT)
|
||||
check.require(len(source) == 1407, "source count")
|
||||
commit = subprocess.check_output(["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True).strip()
|
||||
committed_source(source, commit)
|
||||
worker_pins = {name: check.sha(HERE / name) for name in
|
||||
("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")}
|
||||
commands = plan(collector, engine, out)
|
||||
admission = {"commit": commit, "source_map": source, "readonly": inputs, "planned_argv": commands,
|
||||
"engine_sha256": check.sha(engine), "fixture_sha256": check.sha(fixture),
|
||||
"dependencies": check.DEPENDENCIES, "collector_Q": pinned,
|
||||
"worker_pins": worker_pins,
|
||||
"phases": check.PHASES, "engines_started": 0}
|
||||
check.require(not out.exists(), "new output required")
|
||||
if args.check:
|
||||
print(json.dumps(admission, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
check.require(sys.platform == "darwin", "reviewed native platform is macOS")
|
||||
collector.no_live_engine()
|
||||
check.require(shutil.disk_usage(out.parent).free >= 1200000000, "disk space")
|
||||
player = standard.default_player(ROOT)
|
||||
out.mkdir(parents=True)
|
||||
collector.write(out / "admission.json", admission)
|
||||
rows, profiles = [], []
|
||||
error = None
|
||||
scripts = {}
|
||||
receipt = {}
|
||||
try:
|
||||
stage = out / "stage"
|
||||
stage.mkdir()
|
||||
for name in source:
|
||||
target = stage / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
standard.copy_source(ROOT / name, target)
|
||||
expected = standard.inventory(stage, stage=True)
|
||||
check.require(expected == source, "stage source differs")
|
||||
probe, guard, wrapper = (out / n for n in ("restart_probe.gd", "profile_guard.gd", "guarded.gd"))
|
||||
probe.write_bytes((HERE / probe.name).read_bytes())
|
||||
guard.write_bytes((readonly / "run-r02/profile_guard.gd").read_bytes())
|
||||
wrapper.write_text("extends " + json.dumps(str(probe)) + "\nconst Guard = preload(" +
|
||||
json.dumps(str(guard)) + ")\nfunc _initialize() -> void:\n"
|
||||
"\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n")
|
||||
scripts = {str(p): check.sha(p) for p in (probe, guard, wrapper)}
|
||||
public = (stage / "project.godot").read_bytes()
|
||||
for index, label in schedule(rows):
|
||||
check.check_dependencies()
|
||||
collector_pins()
|
||||
check.check_modules()
|
||||
check.require(collector.source_map(standard, ROOT) == source and standard.default_player(ROOT) == player,
|
||||
"source/player changed before phase")
|
||||
check.require(standard.inventory(stage, stage=True) == expected and
|
||||
all(check.sha(p) == value for p, value in scripts.items()) and
|
||||
all(check.sha(HERE / p) == value for p, value in worker_pins.items()),
|
||||
"stage/scripts changed")
|
||||
startup_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-startup"
|
||||
runtime_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-runtime"
|
||||
startup, runtime = (standard.profile_path(n) for n in (startup_name, runtime_name))
|
||||
fresh_profile(startup)
|
||||
fresh_profile(runtime)
|
||||
profiles.extend([startup, runtime])
|
||||
transfer = out / (label[0] + "-transfer")
|
||||
env = {"THE_TREE_PACK_PROFILE_NAME": runtime_name, "THE_TREE_PACK_PROFILE_EXPECTED": str(runtime),
|
||||
"THE_TREE_PACK_PROFILE_REPORT": str(out / (label + "-guard.json")),
|
||||
"THE_TREE_RESTART_ROLE": label, "THE_TREE_RESTART_K": "30",
|
||||
"THE_TREE_RESTART_TRANSFER": str(transfer),
|
||||
"THE_TREE_RESTART_TRANSFER_MANIFEST": str(transfer / "manifest.json"),
|
||||
"THE_TREE_P28_FIXTURE": str(fixture), "THE_TREE_P28_FIXTURE_SHA": collector.FIXTURE_SHA}
|
||||
if label.endswith("reader"):
|
||||
check_transfer(transfer, json.loads((transfer / "manifest.json").read_text()))
|
||||
before_stage = collector.digest(expected)
|
||||
row = collector.run_private_engine(standard, outer, commands[index], out / label, env,
|
||||
stage, public, startup_name)
|
||||
row["phase"] = label
|
||||
rows.append(row)
|
||||
check.require(row["passed"], "owned engine failure: " + label)
|
||||
after_stage = standard.inventory(stage, stage=True)
|
||||
collector.stage_gate(expected, after_stage, importing=label == "import")
|
||||
row.update(stage_before=before_stage, stage_after=collector.digest(after_stage), stage_preserved=True,
|
||||
source_preserved=collector.source_map(standard, ROOT) == source,
|
||||
player_preserved=standard.default_player(ROOT) == player)
|
||||
expected = after_stage
|
||||
if label in check.PHASES[3:]:
|
||||
guard_path = out / (label + "-guard.json")
|
||||
observation = json.loads(guard_path.read_text())
|
||||
collector.guard_gate(observation, runtime, runtime_name)
|
||||
row.update(runtime_profile=str(runtime), runtime_name=runtime_name,
|
||||
guard=observation, guard_sha256=check.sha(guard_path))
|
||||
report = bind_report(out, label, row)
|
||||
copy_states(out, row)
|
||||
collector.write(out / (label + "-report.json"), report)
|
||||
if label.endswith("writer"):
|
||||
create_transfer(runtime / "p28_restart/good", transfer, collector.write)
|
||||
else:
|
||||
check.require(report["transfer_manifest_sha256"] == check.sha(transfer / "manifest.json"),
|
||||
"reader transfer manifest binding")
|
||||
check.check_pair(rows[-2]["report"], report)
|
||||
else:
|
||||
check.bound_raw(out / label, row)
|
||||
check.require(row["source_preserved"] and row["player_preserved"] and
|
||||
check.sha(fixture) == collector.FIXTURE_SHA and
|
||||
all(check.sha(p) == value for p, value in scripts.items()), "post-phase preservation")
|
||||
except BaseException as caught:
|
||||
error = repr(caught)
|
||||
finally:
|
||||
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
|
||||
receipt = {"admission": admission, "phases": rows, "error": error, "passed": error is None,
|
||||
"source_before": source, "source_after": collector.source_map(standard, ROOT),
|
||||
"default_player_before": player, "default_player_after": standard.default_player(ROOT),
|
||||
"dependencies_before": check.DEPENDENCIES,
|
||||
"dependencies_after": {p: check.sha(ROOT / p) for p in check.DEPENDENCIES},
|
||||
"collector_before": pinned,
|
||||
"collector_after": {p: check.sha((ROOT / COLLECTOR).parent / p) for p in pinned},
|
||||
"script_pins": scripts, "cleanup": cleanup, "cleanup_errors": cleanup_errors,
|
||||
"worker_before": worker_pins,
|
||||
"worker_after": {p: check.sha(HERE / p) for p in worker_pins},
|
||||
"profiles": {str(p): collector.profile_files(p) for p in profiles}, "owned_writes": [],
|
||||
"cross_cutting": check.CROSS, "original_credit": 0,
|
||||
"windows_verified": False, "operations_200_guests_verified": False}
|
||||
if receipt["passed"]:
|
||||
try:
|
||||
check.check_receipt(receipt, out)
|
||||
except (ValueError, KeyError, TypeError, OSError) as caught:
|
||||
receipt.update(passed=False, error="checker: " + repr(caught))
|
||||
collector.write(out / "execution.json", receipt)
|
||||
return 0 if receipt["passed"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
extends SceneTree
|
||||
## Root-owned execution only. All files are written after Guard.activate().
|
||||
var GameClock: Variant
|
||||
var runtime: Variant
|
||||
var main: Variant
|
||||
var report: Dictionary = {}
|
||||
var signals: Array = []
|
||||
var key_forms: Dictionary = {}
|
||||
var role := ""
|
||||
var start_minute := 0
|
||||
var selected_tick := false
|
||||
var overloaded := false
|
||||
var io_ready := false
|
||||
var finished := false
|
||||
var E := ""
|
||||
var package: Dictionary = {}
|
||||
var failure_event: Dictionary = {}
|
||||
var boundary: Variant = null
|
||||
|
||||
func field(d: Variant, name: String) -> Variant:
|
||||
if d is Dictionary:
|
||||
if d.has(StringName(name)):
|
||||
key_forms[name] = "StringName"
|
||||
return d[StringName(name)]
|
||||
if d.has(String(name)):
|
||||
key_forms[name] = "String"
|
||||
return d[String(name)]
|
||||
key_forms[name] = "missing_product_field"
|
||||
return null
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("begin")
|
||||
|
||||
func bytes_hash(data: PackedByteArray) -> String:
|
||||
var context := HashingContext.new()
|
||||
context.start(HashingContext.HASH_SHA256)
|
||||
context.update(data)
|
||||
return context.finish().hex_encode()
|
||||
|
||||
func runtime_part(snapshot: Dictionary) -> Dictionary:
|
||||
return field(field(snapshot, "systems"), "runtime")
|
||||
|
||||
func witness(snapshot: Dictionary) -> Dictionary:
|
||||
var part := runtime_part(snapshot)
|
||||
var events: Dictionary = field(part, "domain_events")
|
||||
return {"counter": field(field(field(part, "id_generator"), "counters"), "domain_event"),
|
||||
"published_ids": field(events, "published_ids").duplicate(true),
|
||||
"recent_events": field(events, "recent_events").duplicate(true)}
|
||||
|
||||
func different_paths(a: Variant, b: Variant, path := "$", depth := 0) -> Array:
|
||||
var result: Array = []
|
||||
if var_to_bytes(a) == var_to_bytes(b):
|
||||
return result
|
||||
if depth >= 4 or typeof(a) != typeof(b):
|
||||
return [path]
|
||||
if a is Dictionary:
|
||||
for key: Variant in a:
|
||||
if not b.has(key):
|
||||
result.append(path + "." + str(key))
|
||||
else:
|
||||
result.append_array(different_paths(a[key], b[key], path + "." + str(key), depth + 1))
|
||||
for key: Variant in b:
|
||||
if not a.has(key):
|
||||
result.append(path + "." + str(key))
|
||||
elif a is Array and a.size() == b.size():
|
||||
for index in range(a.size()):
|
||||
result.append_array(different_paths(a[index], b[index], path + "[%d]" % index, depth + 1))
|
||||
else:
|
||||
result.append(path)
|
||||
# Typed container/key/order differences can exist without value differences.
|
||||
if result.is_empty():
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
func compare(expected: Dictionary, actual: Dictionary) -> Dictionary:
|
||||
var a := var_to_bytes(expected)
|
||||
var b := var_to_bytes(actual)
|
||||
return {"method": "var_to_bytes", "equal": a == b, "expected_sha256": bytes_hash(a),
|
||||
"actual_sha256": bytes_hash(b), "excluded_paths": [],
|
||||
"diff_paths": different_paths(expected, actual)}
|
||||
|
||||
func record_state(name: String, value: Dictionary) -> bool:
|
||||
var path := "user://p28_restart/evidence/" + name + ".bin"
|
||||
if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path).get_base_dir()) != OK:
|
||||
return false
|
||||
var data := var_to_bytes(value)
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_buffer(data)
|
||||
file.close()
|
||||
report.states[name] = {"path": path.trim_prefix("user://"), "bytes": data.size(),
|
||||
"sha256": bytes_hash(data)}
|
||||
return FileAccess.get_sha256(path) == report.states[name].sha256
|
||||
|
||||
func on_save(result: Dictionary) -> void:
|
||||
signals.append(result.duplicate(true))
|
||||
|
||||
func auto_ids() -> Array:
|
||||
var ids: Array = []
|
||||
for guest: Variant in runtime.guest_service.get_guests():
|
||||
if str(guest.guest_id).begins_with("guest-auto-"):
|
||||
ids.append(str(guest.guest_id))
|
||||
ids.sort()
|
||||
return ids
|
||||
|
||||
func operations_observation() -> Dictionary:
|
||||
var ids := auto_ids()
|
||||
var processed: Array = []
|
||||
var arrivals: Array = []
|
||||
if runtime.operations_coordinator != null:
|
||||
var state: Dictionary = runtime.operations_coordinator.create_snapshot()
|
||||
processed = field(state, "processed_arrival_minutes").duplicate()
|
||||
arrivals = runtime.operations_coordinator.get_arrival_reports().duplicate(true)
|
||||
return {"clock": GameClock.absolute_minute, "guest_auto_ids": ids, "guest_auto_count": ids.size(),
|
||||
"operations": runtime.get_operations_report(), "processed_arrival_minutes": processed,
|
||||
"arrival_reports": arrivals}
|
||||
|
||||
func on_tick(minute: int) -> void:
|
||||
if io_ready or finished or selected_tick:
|
||||
return
|
||||
var observed := operations_observation()
|
||||
if role == "O-writer":
|
||||
report.ui_ticks.append(observed)
|
||||
if minute == 390:
|
||||
report["at_390"] = observed.duplicate(true)
|
||||
report.at_390["domain_events"] = field(runtime_part(runtime.create_snapshot()), "domain_events")
|
||||
var stop: bool = (role == "R-writer" and minute - start_minute >= 10) or \
|
||||
(role == "O-writer" and (observed.guest_auto_count > 0 or minute - start_minute >= 120))
|
||||
if stop:
|
||||
GameClock.set_paused(true)
|
||||
selected_tick = true
|
||||
|
||||
func on_overload(backlog: int) -> void:
|
||||
overloaded = true
|
||||
report.overloads.append({"minute": GameClock.absolute_minute, "backlog": backlog})
|
||||
|
||||
func clock_observation() -> Dictionary:
|
||||
return {"minute": GameClock.absolute_minute, "paused": GameClock.paused,
|
||||
"backlog": GameClock.get_backlog_minutes(),
|
||||
"accumulator_hex": PackedFloat64Array([GameClock.get("_accumulator")]).to_byte_array().hex_encode()}
|
||||
|
||||
func begin() -> void:
|
||||
GameClock = root.get_node("GameClock")
|
||||
runtime = root.get_node("SimulationRuntime")
|
||||
role = OS.get_environment("THE_TREE_RESTART_ROLE")
|
||||
report = {"schema": "p28-restart-v1", "role": role, "pid": OS.get_process_id(),
|
||||
"main_added": false, "k_target": 30, "actual_k": 0, "io": [], "states": {},
|
||||
"minutes": [], "ui_ticks": [], "overloads": [], "boundary": null, "next_boundary": null,
|
||||
"comparison_scope": "complete_typed_var_to_bytes", "excluded_paths": [],
|
||||
"transfer_before_guard_writes": 0, "transfer_verified": false,
|
||||
"probe_deadline_seconds": 1740}
|
||||
create_timer(1740.0).timeout.connect(func() -> void: finish("probe_deadline"))
|
||||
if OS.get_environment("THE_TREE_RESTART_K") != "30" or role not in ["R-writer", "R-reader", "O-writer", "O-reader"]:
|
||||
finish("invalid_environment")
|
||||
return
|
||||
# No await before main: the default autoload clock cannot process here.
|
||||
if role == "R-writer":
|
||||
var fixture := OS.get_environment("THE_TREE_P28_FIXTURE")
|
||||
if FileAccess.get_sha256(fixture) != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"):
|
||||
finish("fixture_sha")
|
||||
return
|
||||
var file := FileAccess.open(fixture, FileAccess.READ)
|
||||
if file == null:
|
||||
finish("fixture_missing")
|
||||
return
|
||||
var value: Variant = file.get_var(false)
|
||||
file.close()
|
||||
if not value is Dictionary or not runtime.restore_snapshot(value):
|
||||
finish("fixture_restore")
|
||||
return
|
||||
main = load("res://scenes/main/main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
report.main_added = true
|
||||
report["startup"] = {"start_report": main.get("_operations_start_report").duplicate(true),
|
||||
"operations": runtime.get_operations_report(), "clock": clock_observation()}
|
||||
if role != "R-writer" and not GameClock.paused:
|
||||
finish("main_did_not_pause")
|
||||
return
|
||||
var start_result: Dictionary = report.startup.start_report
|
||||
var operations_enabled: Variant = field(report.startup.operations, "enabled")
|
||||
if role == "R-writer":
|
||||
if operations_enabled != false or field(start_result, "code") != "INVALID_START_TIME":
|
||||
finish("unexpected_R_start")
|
||||
return
|
||||
elif operations_enabled != true or field(start_result, "ok") != true or GameClock.absolute_minute != 360:
|
||||
finish("unexpected_fresh_start")
|
||||
return
|
||||
runtime.save_operation_completed.connect(on_save)
|
||||
GameClock.tick_completed.connect(on_tick)
|
||||
GameClock.simulation_overloaded.connect(on_overload)
|
||||
start_minute = GameClock.absolute_minute
|
||||
report["start_minute"] = start_minute
|
||||
if role.ends_with("writer"):
|
||||
main.call("_set_speed", 1)
|
||||
while not GameClock.paused and not finished:
|
||||
await process_frame
|
||||
if finished:
|
||||
return
|
||||
if not selected_tick and not overloaded:
|
||||
finish("unexpected_ui_stop")
|
||||
return
|
||||
if role == "O-writer" and auto_ids().is_empty():
|
||||
report["boundary"] = null
|
||||
report["boundary_reason"] = "no_arrival_within_cap" if GameClock.absolute_minute - start_minute >= 120 else "stopped_before_arrival"
|
||||
finish(report.boundary_reason)
|
||||
return
|
||||
if not writer_save():
|
||||
finish("save_comparison_failed")
|
||||
return
|
||||
else:
|
||||
if not import_transfer():
|
||||
finish("transfer_failed")
|
||||
return
|
||||
if not reader_load():
|
||||
finish("load_comparison_failed")
|
||||
return
|
||||
report["event_id"] = E
|
||||
report["saved_minute"] = GameClock.absolute_minute
|
||||
boundary = operations_observation() if role.begins_with("O-") else null
|
||||
report["boundary"] = boundary
|
||||
var idle_before: Dictionary = runtime.create_snapshot()
|
||||
var clock_before := clock_observation()
|
||||
await process_frame
|
||||
await process_frame
|
||||
if finished:
|
||||
return
|
||||
var idle_after: Dictionary = runtime.create_snapshot()
|
||||
report["paused_idle"] = {"before": clock_before, "after": clock_observation(),
|
||||
"comparison": compare(idle_before, idle_after)}
|
||||
if not report.paused_idle.comparison.equal or report.paused_idle.before != report.paused_idle.after:
|
||||
finish("paused_backlog_consumed")
|
||||
return
|
||||
io_ready = true
|
||||
for _index in range(30):
|
||||
if not deterministic_step():
|
||||
finish("no_tick_advance_or_invalid_state")
|
||||
return
|
||||
# Keep the event loop responsive to deadline/audio while remaining paused.
|
||||
await process_frame
|
||||
if finished:
|
||||
return
|
||||
if not record_state("final", runtime.create_snapshot()):
|
||||
finish("state_write_failed")
|
||||
return
|
||||
report["final_minute"] = GameClock.absolute_minute
|
||||
if role.begins_with("O-"):
|
||||
var last := operations_observation()
|
||||
var added: Array = []
|
||||
for minute: Variant in last.processed_arrival_minutes:
|
||||
if minute not in boundary.processed_arrival_minutes:
|
||||
added.append(minute)
|
||||
report["next_boundary"] = {"minute": added[0] if added.size() == 1 else null,
|
||||
"processed_once": added.size() == 1, "before": boundary.processed_arrival_minutes,
|
||||
"after": last.processed_arrival_minutes, "arrival_reports": last.arrival_reports,
|
||||
"guest_auto_ids": last.guest_auto_ids}
|
||||
if added.size() != 1:
|
||||
finish("next_arrival_boundary_missing_or_duplicate")
|
||||
return
|
||||
finish("complete")
|
||||
|
||||
func io_proof(base: Dictionary, after: Dictionary, kind: String, returned: Variant) -> bool:
|
||||
var row := {"kind": kind, "signal_count": signals.size(),
|
||||
"return_kind": "bool" if kind == "save" else "void", "return_value": returned}
|
||||
report.io.append(row)
|
||||
if signals.size() != 1:
|
||||
return false
|
||||
var signal_result: Dictionary = signals[0]
|
||||
var payload: Dictionary = runtime.call("_save_event_payload", signal_result)
|
||||
var latest: Dictionary = runtime.latest_save_operation_result.duplicate(true)
|
||||
row["signal_projection"] = "_save_event_payload"
|
||||
row["signal_payload"] = payload
|
||||
row["latest"] = latest
|
||||
row["payload_bytes_equal"] = var_to_bytes(payload) == var_to_bytes(latest)
|
||||
var before := witness(base)
|
||||
var after_witness := witness(after)
|
||||
var eid := "domain_event.%012d" % (int(before.counter) + 1)
|
||||
var found: Array = []
|
||||
for event: Dictionary in after_witness.recent_events:
|
||||
if field(event, "event_id") == eid:
|
||||
found.append(event)
|
||||
if found.size() != 1 or before.published_ids.has(eid):
|
||||
return false
|
||||
var event: Dictionary = found[0]
|
||||
row["event"] = event
|
||||
row["before"] = before
|
||||
row["after"] = after_witness
|
||||
row["minute_before"] = field(field(base, "clock"), "absolute_minute")
|
||||
row["minute_after"] = field(field(after, "clock"), "absolute_minute")
|
||||
row["paused_before"] = field(field(base, "clock"), "paused")
|
||||
row["paused_after"] = field(field(after, "clock"), "paused")
|
||||
row["dropped"] = before.recent_events[0] if before.recent_events.size() == 256 else null
|
||||
var expected := base.duplicate(true)
|
||||
var part := runtime_part(expected)
|
||||
var counters: Dictionary = field(field(part, "id_generator"), "counters")
|
||||
counters["domain_event"] = int(before.counter) + 1
|
||||
var events: Dictionary = field(part, "domain_events")
|
||||
var ids: Array = field(events, "published_ids")
|
||||
ids.append(eid)
|
||||
ids.sort()
|
||||
var recent: Array = field(events, "recent_events")
|
||||
if recent.size() > 256:
|
||||
return false
|
||||
if recent.size() == 256:
|
||||
recent.pop_front()
|
||||
recent.append(event.duplicate(true))
|
||||
row["comparison"] = compare(expected, after)
|
||||
var expected_type := {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", "tamper": "SAVE_LOAD_FAILED"}
|
||||
var ok: bool = row.payload_bytes_equal and row.comparison.equal and \
|
||||
var_to_bytes(field(event, "payload")) == var_to_bytes(payload) and \
|
||||
field(event, "event_type") == expected_type[kind] and field(event, "source_service") == "save_service" and \
|
||||
field(event, "entity_ids").is_empty() and field(event, "causation_command_id") == 0 and \
|
||||
row.minute_before == row.minute_after and field(event, "absolute_minute") == row.minute_after
|
||||
if kind == "tamper":
|
||||
var recovery: Variant = field(signal_result, "recovery_report")
|
||||
var migration: Variant = field(signal_result, "migration_attempted")
|
||||
row["failure"] = {"preload_pause_restored": field(signal_result, "preload_pause_restored"),
|
||||
"loaded_clock_paused": field(signal_result, "loaded_clock_paused"), "recovery_report": recovery,
|
||||
"migration_attempted": migration == true}
|
||||
failure_event = event.duplicate(true)
|
||||
return ok and field(payload, "ok") == false and field(payload, "code") != "OK" and \
|
||||
row.failure.preload_pause_restored == true and row.failure.loaded_clock_paused == row.paused_before and \
|
||||
row.paused_before == row.paused_after and (recovery == null or field(recovery, "restore_committed") == false)
|
||||
E = eid
|
||||
return ok and field(payload, "ok") == true and field(payload, "code") == "OK" and \
|
||||
(returned == true if kind == "save" else returned == null) and row.paused_after == true and \
|
||||
field(payload, "saved_absolute_minute") == row.minute_after
|
||||
|
||||
func writer_save() -> bool:
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
signals = []
|
||||
var returned: bool = main.call("_save_manual_at_path", "user://p28_restart/good", "P28 RESTART")
|
||||
var after: Dictionary = runtime.create_snapshot()
|
||||
var loaded := SaveService.load_snapshot("user://p28_restart/good")
|
||||
if field(loaded, "ok") != true:
|
||||
return false
|
||||
package = field(loaded, "snapshot")
|
||||
report["package_matches_before"] = compare(before, package)
|
||||
if not record_state("P", package) or not record_state("after_save", after):
|
||||
return false
|
||||
return report.package_matches_before.equal and io_proof(package, after, "save", returned)
|
||||
|
||||
func import_transfer() -> bool:
|
||||
var transfer := OS.get_environment("THE_TREE_RESTART_TRANSFER")
|
||||
var manifest_path := OS.get_environment("THE_TREE_RESTART_TRANSFER_MANIFEST")
|
||||
if not transfer.is_absolute_path() or not manifest_path.is_absolute_path():
|
||||
return false
|
||||
var manifest: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path))
|
||||
if not manifest is Dictionary:
|
||||
return false
|
||||
for kind: String in ["good", "tampered"]:
|
||||
var target := "user://p28_restart/" + kind
|
||||
if DirAccess.dir_exists_absolute(target):
|
||||
return false
|
||||
for relative: String in manifest.files[kind]:
|
||||
if relative.is_absolute_path() or ".." in relative.split("/") or "\\" in relative:
|
||||
return false
|
||||
var source := transfer.path_join(kind).path_join(relative)
|
||||
var expected: Dictionary = manifest.files[kind][relative]
|
||||
if FileAccess.get_sha256(source) != expected.sha256:
|
||||
return false
|
||||
var data := FileAccess.get_file_as_bytes(source)
|
||||
if data.size() != int(expected.bytes):
|
||||
return false
|
||||
var destination := target.path_join(relative)
|
||||
if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(destination).get_base_dir()) != OK:
|
||||
return false
|
||||
var file := FileAccess.open(destination, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_buffer(data)
|
||||
file.close()
|
||||
if FileAccess.get_sha256(destination) != expected.sha256:
|
||||
return false
|
||||
report["transfer_manifest_sha256"] = FileAccess.get_sha256(manifest_path)
|
||||
report["transfer_verified"] = true
|
||||
return true
|
||||
|
||||
func reader_load() -> bool:
|
||||
var loaded := SaveService.load_snapshot("user://p28_restart/good")
|
||||
if field(loaded, "ok") != true:
|
||||
return false
|
||||
package = field(loaded, "snapshot")
|
||||
var before: Dictionary = runtime.create_snapshot()
|
||||
if not record_state("P", package) or not record_state("B", before):
|
||||
return false
|
||||
signals = []
|
||||
main.call("_load_manual_at_path", "user://p28_restart/tampered", true)
|
||||
var failed: Dictionary = runtime.create_snapshot()
|
||||
if not record_state("after_tamper", failed) or not io_proof(before, failed, "tamper", null):
|
||||
return false
|
||||
signals = []
|
||||
main.call("_load_manual_at_path", "user://p28_restart/good", true)
|
||||
var after: Dictionary = runtime.create_snapshot()
|
||||
if not record_state("after_load", after) or not io_proof(package, after, "load", null):
|
||||
return false
|
||||
report["failure_event_absent"] = true
|
||||
for event: Dictionary in witness(after).recent_events:
|
||||
if var_to_bytes(event) == var_to_bytes(failure_event):
|
||||
report.failure_event_absent = false
|
||||
return report.failure_event_absent
|
||||
|
||||
func deterministic_step() -> bool:
|
||||
if not io_ready or not GameClock.paused:
|
||||
return false
|
||||
var before: int = GameClock.absolute_minute
|
||||
var backlog: int = GameClock.get_backlog_minutes()
|
||||
GameClock.advance_minutes(1)
|
||||
var after: int = GameClock.absolute_minute
|
||||
if after != before + 1:
|
||||
return false
|
||||
var state: Dictionary = runtime.create_snapshot()
|
||||
var normalized := state.duplicate(true)
|
||||
var recent: Array = field(field(runtime_part(normalized), "domain_events"), "recent_events")
|
||||
var replacement: Variant = null
|
||||
var count := 0
|
||||
var recent_ids: Array = []
|
||||
for index in range(recent.size()):
|
||||
recent_ids.append(field(recent[index], "event_id"))
|
||||
if field(recent[index], "event_id") == E:
|
||||
replacement = recent[index].duplicate(true)
|
||||
recent[index] = {"event_id": E, "replaced_for_cross_pid_compare": true}
|
||||
count += 1
|
||||
if count > 1:
|
||||
return false
|
||||
report.minutes.append({"before": before, "after": after, "paused": GameClock.paused,
|
||||
"backlog_before": backlog, "backlog_after": GameClock.get_backlog_minutes(),
|
||||
"sha256": bytes_hash(var_to_bytes(normalized)), "scope": "complete_typed_var_to_bytes",
|
||||
"excluded_paths": [], "replacement": replacement, "replacement_count": count,
|
||||
"recent_count": recent.size(), "recent_ids": recent_ids})
|
||||
report.actual_k += after - before
|
||||
return GameClock.paused and backlog == GameClock.get_backlog_minutes()
|
||||
|
||||
func finish(reason: String) -> void:
|
||||
if finished:
|
||||
return
|
||||
finished = true
|
||||
report["stop_reason"] = reason
|
||||
report["key_forms"] = key_forms
|
||||
print("P28_RESTART_REPORT=" + JSON.stringify(report))
|
||||
if is_instance_valid(main) and main.is_inside_tree():
|
||||
main.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT)
|
||||
get_root().close_requested.emit()
|
||||
else:
|
||||
quit(1)
|
||||
|
||||
func emit_audio_close(result: Dictionary) -> void:
|
||||
print("P28_RESTART_AUDIO_CLOSE=" + JSON.stringify(result))
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Engine-free positive and adversarial controls. No native acceptance claimed."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
import check_restart as check
|
||||
import restart_pair as driver
|
||||
|
||||
DATA = b"synthetic typed bytes fixture (not engine evidence)"
|
||||
HASH = hashlib.sha256(DATA).hexdigest()
|
||||
|
||||
|
||||
def comparison():
|
||||
return {"method": "var_to_bytes", "equal": True, "diff_paths": [], "excluded_paths": [],
|
||||
"expected_sha256": HASH, "actual_sha256": HASH}
|
||||
|
||||
|
||||
def history(count=256):
|
||||
ids = ["domain_event.%012d" % n for n in range(1, count + 1)]
|
||||
return {"counter": count, "published_ids": ids,
|
||||
"recent_events": [{"event_id": eid, "event_type": "OLD", "payload": {"n": n}}
|
||||
for n, eid in enumerate(ids)]}
|
||||
|
||||
|
||||
def io(kind, minute=612, count=256):
|
||||
before = history(count)
|
||||
payload = {"ok": kind != "tamper", "code": "OK" if kind != "tamper" else "CORRUPT_WORLD",
|
||||
"operation": "save" if kind == "save" else "load",
|
||||
"save_path": "user://p28_restart/" + ("tampered" if kind == "tamper" else "good"),
|
||||
"save_id": "synthetic", "saved_absolute_minute": minute}
|
||||
event = {"event_id": check.event_id(count),
|
||||
"event_type": {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED",
|
||||
"tamper": "SAVE_LOAD_FAILED"}[kind],
|
||||
"source_service": "save_service", "entity_ids": [], "causation_command_id": 0,
|
||||
"absolute_minute": minute, "payload": copy.deepcopy(payload)}
|
||||
return {"kind": kind, "signal_count": 1, "signal_payload": payload, "latest": copy.deepcopy(payload),
|
||||
"event": event, "signal_projection": "_save_event_payload", "payload_bytes_equal": True,
|
||||
"return_kind": "bool" if kind == "save" else "void",
|
||||
"return_value": True if kind == "save" else None,
|
||||
"before": before, "after": check.transform(before, event),
|
||||
"dropped": before["recent_events"][0] if count == 256 else None,
|
||||
"comparison": comparison(), "minute_before": minute, "minute_after": minute,
|
||||
"paused_before": True, "paused_after": True,
|
||||
"failure": {"preload_pause_restored": True, "loaded_clock_paused": True,
|
||||
"migration_attempted": False, "recovery_report": {"restore_committed": False}}}
|
||||
|
||||
|
||||
def report(role="R-writer", pid=100):
|
||||
writer, ops = role.endswith("writer"), role.startswith("O-")
|
||||
minute = 391 if ops else 612
|
||||
rows = [io("save", minute)] if writer else [io("tamper", 360, 3), io("load", minute)]
|
||||
names = ["P", "after_save", "final"] if writer else ["P", "B", "after_tamper", "after_load", "final"]
|
||||
event = rows[-1]["event"]
|
||||
boundary = {"clock": minute, "guest_auto_ids": ["guest-auto-00000001"], "guest_auto_count": 1,
|
||||
"operations": {"enabled": True}, "processed_arrival_minutes": [390],
|
||||
"arrival_reports": [{"minute": 390}]} if ops else None
|
||||
next_boundary = {"minute": 420, "processed_once": True, "before": [390], "after": [390, 420],
|
||||
"arrival_reports": [{"minute": 390}, {"minute": 420}],
|
||||
"guest_auto_ids": ["guest-auto-00000001", "guest-auto-00000002"]} if ops else None
|
||||
clock = {"minute": minute, "paused": True, "backlog": 121, "accumulator_hex": "00"}
|
||||
return {"schema": "p28-restart-v1", "role": role, "pid": pid, "stop_reason": "complete",
|
||||
"main_added": True, "k_target": 30, "actual_k": 30, "comparison_scope": "complete_typed_var_to_bytes",
|
||||
"excluded_paths": [], "transfer_before_guard_writes": 0, "probe_deadline_seconds": 1740,
|
||||
"io": rows, "package_matches_before": comparison(), "failure_event_absent": True,
|
||||
"transfer_verified": True, "event_id": event["event_id"], "saved_minute": minute,
|
||||
"start_minute": 360 if ops else 602, "final_minute": minute + 30,
|
||||
"states": {n: {"path": "p28_restart/evidence/" + n + ".bin", "bytes": len(DATA), "sha256": HASH}
|
||||
for n in names},
|
||||
"paused_idle": {"before": clock, "after": copy.deepcopy(clock), "comparison": comparison()},
|
||||
"boundary": boundary, "next_boundary": next_boundary, "ui_ticks": [copy.deepcopy(boundary)] if ops else [],
|
||||
"minutes": [{"before": minute + n, "after": minute + n + 1, "paused": True,
|
||||
"backlog_before": 121, "backlog_after": 121, "sha256": HASH,
|
||||
"scope": "complete_typed_var_to_bytes", "excluded_paths": [],
|
||||
"replacement": copy.deepcopy(event) if n < 5 else None,
|
||||
"replacement_count": 1 if n < 5 else 0, "recent_count": 256,
|
||||
"recent_ids": history(255)["published_ids"] +
|
||||
([event["event_id"]] if n < 5 else ["domain_event.000000000256"])}
|
||||
for n in range(30)]}
|
||||
|
||||
|
||||
def write(path, value):
|
||||
with Path(path).open("x") as stream:
|
||||
json.dump(value, stream)
|
||||
|
||||
|
||||
def audio(pid):
|
||||
return {"coordinator": True, "passed": True, "owned_engine_pid": pid,
|
||||
"all_owned_targets_released": True, "deadline_reached": False,
|
||||
"clock_process_held_stopped": True, "window_input_held_disabled": True,
|
||||
"restored_before_quit": False, "shared_bus_deleted": False, "settings_mutated": False}
|
||||
|
||||
|
||||
def synthetic_receipt(out):
|
||||
rows = []
|
||||
profiles = {}
|
||||
source_package = out / "synthetic-package"
|
||||
source_package.mkdir()
|
||||
(source_package / "world.bin").write_bytes(DATA)
|
||||
(source_package / "manifest.json").write_text("{}")
|
||||
transfers = {kind: driver.create_transfer(source_package, out / (kind + "-transfer"), write)
|
||||
for kind in ("R", "O")}
|
||||
for index, label in enumerate(check.PHASES):
|
||||
folder = out / label
|
||||
folder.mkdir()
|
||||
pid = 100 + index
|
||||
process = {"exit_code": 0, "helper_exit_code": 0, "timed_out": False, "error_lines": [],
|
||||
"lifecycle_error": None, "external_process_or_group_signals": 0,
|
||||
"owned_popen_ended": True, "old_helper_engine_ended": True,
|
||||
"start_receipt_written": True, "end_receipt_written": True}
|
||||
capture = {"passed": True, "all_captured_ended": True, "actual_captured_pids": [pid],
|
||||
"outer_cleanup_errors": [], "adapter_exception": None, "foreign_process_or_group_signals": 0,
|
||||
"all_cleanup_before_outer_receipt": True, "outer_receipt_written": True,
|
||||
"outer_receipt_error": None, "cleanup": []}
|
||||
write(folder / "outer-owned-finally.json", {k: v for k, v in capture.items() if k not in
|
||||
("passed", "outer_receipt_written", "outer_receipt_error")})
|
||||
for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")):
|
||||
binding = {"pid": pid, "actual_pid": pid, "owner_pid": 99,
|
||||
"argv": ["fake", "--check-only"], "command": ["fake", "--check-only"]}
|
||||
write(folder / filename, binding)
|
||||
process[key] = binding
|
||||
process["adapter_owned_cleanup"] = {"engine_ended": True, "errors": []}
|
||||
write(folder / "adapter-owned-cleanup.json", process["adapter_owned_cleanup"])
|
||||
process.update(pid=pid, argv=["fake", "--check-only"])
|
||||
raw = ""
|
||||
row = {"phase": label, "passed": True, "process": process, "capture": capture,
|
||||
"timeout_seconds": 1800, "diagnostics": [], "argv": ["fake", "--check-only"],
|
||||
"stage_before": HASH, "stage_after": HASH, "stage_preserved": True,
|
||||
"source_preserved": True, "player_preserved": True}
|
||||
startup = out / (label + "-startup")
|
||||
startup.write_bytes(DATA)
|
||||
row["startup_project"] = {"fresh_before": True, "path": str(startup), "sha256": HASH}
|
||||
if index >= 3:
|
||||
observed = report(label, pid)
|
||||
if label.endswith("reader"):
|
||||
observed["transfer_manifest_sha256"] = check.sha(out / (label[0] + "-transfer/manifest.json"))
|
||||
row["report"] = observed
|
||||
raw = "P28_RESTART_REPORT=" + json.dumps(observed) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(pid)) + "\n"
|
||||
profile = str(out / (label + "-private"))
|
||||
row.update(runtime_profile=profile, runtime_name=label)
|
||||
profiles[profile] = {}
|
||||
for kind in (("good",) if label.endswith("writer") else ("good", "tampered")):
|
||||
for name, value in transfers[label[0]]["files"][kind].items():
|
||||
profiles[profile]["p28_restart/" + kind + "/" + name] = value
|
||||
guard = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5,
|
||||
"actual_user_data_dir": profile, "expected_user_data_dir": profile,
|
||||
"globalized_probe": str(Path(profile) / "guard-probe"), "custom_user_dir_name": label,
|
||||
"fresh_before_parent_initialize": True, "profile_writes_before_guard": 0,
|
||||
"project_settings_persisted": False}
|
||||
guard_path = out / (label + "-guard.json")
|
||||
write(guard_path, guard)
|
||||
row.update(guard=guard, guard_sha256=check.sha(guard_path))
|
||||
states = out / (label + "-states")
|
||||
states.mkdir()
|
||||
for name in observed["states"]:
|
||||
(states / (name + ".bin")).write_bytes(DATA)
|
||||
for name, content in (("stdout.log", raw), ("stderr.log", ""), ("engine-native.log", "native\n")):
|
||||
(folder / name).write_text(content)
|
||||
row["raw"] = {n: check.pin(folder / n) for n in ("stdout.log", "stderr.log", "engine-native.log")}
|
||||
process.update(stdout_sha256=row["raw"]["stdout.log"]["sha256"],
|
||||
stderr_sha256=row["raw"]["stderr.log"]["sha256"])
|
||||
write(folder / "process.json", process)
|
||||
row["owned_files"] = {n: check.pin(folder / n) for n in
|
||||
("process.json", "started.json", "ended.json", "adapter-owned-cleanup.json", "outer-owned-finally.json")}
|
||||
rows.append(row)
|
||||
probe = out / "restart_probe.gd"
|
||||
probe.write_bytes((check.HERE / probe.name).read_bytes())
|
||||
source = {str(i): {"bytes": 1, "sha256": HASH} for i in range(1407)}
|
||||
worker = {n: check.sha(check.HERE / n) for n in
|
||||
("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")}
|
||||
return {"passed": True, "error": None, "cleanup_errors": [], "phases": rows,
|
||||
"source_before": source, "source_after": copy.deepcopy(source),
|
||||
"default_player_before": {}, "default_player_after": {},
|
||||
"dependencies_before": check.DEPENDENCIES.copy(), "dependencies_after": check.DEPENDENCIES.copy(),
|
||||
"collector_before": driver.COLLECTOR_PINS.copy(), "collector_after": driver.COLLECTOR_PINS.copy(),
|
||||
"worker_before": worker, "worker_after": worker.copy(), "profiles": profiles,
|
||||
"cross_cutting": copy.deepcopy(check.CROSS), "original_credit": 0,
|
||||
"windows_verified": False, "operations_200_guests_verified": False,
|
||||
"script_pins": {str(probe): check.sha(probe)}, "owned_writes": [],
|
||||
"cleanup": [{"pid": 100 + n, "engine_ended": True, "outer_cleanup_errors": [],
|
||||
"foreign_process_or_group_signals": 0} for n in range(7)]}
|
||||
|
||||
|
||||
class RestartTests(unittest.TestCase):
|
||||
def rejects(self, original, mutations, validator):
|
||||
for name, mutation in mutations.items():
|
||||
with self.subTest(rejection=name):
|
||||
value = copy.deepcopy(original)
|
||||
mutation(value)
|
||||
with self.assertRaises((ValueError, KeyError, TypeError)):
|
||||
validator(value)
|
||||
|
||||
def test_rs1_signal_bool_void(self):
|
||||
for kind in ("save", "load", "tamper"):
|
||||
check.check_io(io(kind), kind)
|
||||
self.rejects(io("save"), {
|
||||
"true only": lambda x: x.pop("signal_count"),
|
||||
"no signal": lambda x: x.update(signal_count=0),
|
||||
"duplicate signal": lambda x: x.update(signal_count=2),
|
||||
"bool differs": lambda x: x.update(return_value=False),
|
||||
"latest differs": lambda x: x["latest"].update(code="FAILED"),
|
||||
"payload typed mismatch": lambda x: x.update(payload_bytes_equal=False),
|
||||
"state mismatch": lambda x: x["comparison"].update(equal=False),
|
||||
}, lambda x: check.check_io(x, "save"))
|
||||
self.rejects(io("load"), {
|
||||
"void success": lambda x: x.update(return_value=True),
|
||||
"void coerced bool": lambda x: x.update(return_kind="bool"),
|
||||
"missing signal": lambda x: x.update(signal_count=0),
|
||||
}, lambda x: check.check_io(x, "load"))
|
||||
|
||||
def test_rs2_exact_event_transform(self):
|
||||
for kind in ("save", "load", "tamper"):
|
||||
for count in (0, 255, 256):
|
||||
check.check_io(io(kind, count=count), kind)
|
||||
self.rejects(io(kind), {
|
||||
"two events": lambda x: x["after"]["recent_events"].append(copy.deepcopy(x["event"])),
|
||||
"different E": lambda x: x["event"].update(event_id="domain_event.000009999999"),
|
||||
"counter": lambda x: x["after"].update(counter=x["before"]["counter"] + 2),
|
||||
"missing ID": lambda x: x["after"]["published_ids"].pop(0),
|
||||
"duplicate ID": lambda x: x["after"]["published_ids"].append(x["event"]["event_id"]),
|
||||
"unsorted IDs": lambda x: x["after"]["published_ids"].reverse(),
|
||||
"drop tail": lambda x: x["after"].update(recent_events=x["before"]["recent_events"][:-1] + [x["event"]]),
|
||||
"wrong dropped record": lambda x: x.update(dropped=x["before"]["recent_events"][-1]),
|
||||
"excluded path": lambda x: x["comparison"].update(excluded_paths=["systems.runtime.economy"]),
|
||||
"diff hidden": lambda x: x["comparison"].update(diff_paths=["$.clock"]),
|
||||
"digest only": lambda x: x["comparison"].update(method="SimulationDigest"),
|
||||
"bytes SHA disagreement": lambda x: x["comparison"].update(actual_sha256="f" * 64),
|
||||
}, lambda x: check.check_io(x, kind))
|
||||
|
||||
def test_clarification01_tamper(self):
|
||||
self.rejects(io("tamper"), {
|
||||
"failure event ignored": lambda x: x.update(after=x["before"]),
|
||||
"failure ok": lambda x: x["signal_payload"].update(ok=True),
|
||||
"failure OK code": lambda x: x["signal_payload"].update(code="OK"),
|
||||
"not restored": lambda x: x["failure"].update(preload_pause_restored=False),
|
||||
"paused differs": lambda x: x.update(paused_after=False),
|
||||
"loaded pause differs": lambda x: x["failure"].update(loaded_clock_paused=False),
|
||||
"restore committed": lambda x: x["failure"]["recovery_report"].update(restore_committed=True),
|
||||
"migration unrecorded": lambda x: x["failure"].pop("migration_attempted"),
|
||||
}, lambda x: check.check_io(x, "tamper"))
|
||||
self.rejects(report("R-reader"), {
|
||||
"failed event retained": lambda x: x.update(failure_event_absent=False),
|
||||
"backlog consumed": lambda x: x["paused_idle"]["after"].update(backlog=120),
|
||||
"reader not paused": lambda x: x["io"][1].update(paused_after=False),
|
||||
}, check.check_report)
|
||||
|
||||
def test_rs3_actual_k_full_typed_cross_pid(self):
|
||||
writer, reader = report(), report("R-reader", 101)
|
||||
check.check_pair(writer, reader)
|
||||
self.rejects(reader, {
|
||||
"K call count": lambda x: x["minutes"][0].update(after=x["saved_minute"]),
|
||||
"different K": lambda x: x.update(actual_k=29),
|
||||
"missing minute": lambda x: x["minutes"].pop(),
|
||||
"partial digest": lambda x: x["minutes"][0].update(scope="SimulationDigest"),
|
||||
"minute SHA": lambda x: x["minutes"][0].update(sha256="f" * 64),
|
||||
"wrong replacement": lambda x: x["minutes"][0]["replacement"].update(event_id="other"),
|
||||
"two replacements": lambda x: x["minutes"][0].update(replacement_count=2),
|
||||
"after window": lambda x: x["minutes"][6].update(replacement=x["io"][-1]["event"], replacement_count=1),
|
||||
"excluded path": lambda x: x["minutes"][0].update(excluded_paths=["clock"]),
|
||||
"same PID": lambda x: x.update(pid=100),
|
||||
}, lambda x: check.check_pair(writer, x))
|
||||
|
||||
def test_rs4_rs5_arrival_boundary(self):
|
||||
writer, reader = report("O-writer"), report("O-reader", 101)
|
||||
check.check_pair(writer, reader)
|
||||
self.rejects(writer, {
|
||||
"boundary only": lambda x: x["boundary"].update(guest_auto_count=0),
|
||||
"null arrival": lambda x: x.update(boundary=None),
|
||||
"unprocessed": lambda x: x["boundary"].update(processed_arrival_minutes=[]),
|
||||
"not first tick": lambda x: x["ui_ticks"].insert(0, copy.deepcopy(x["boundary"])),
|
||||
"duplicate guest": lambda x: x["boundary"]["guest_auto_ids"].append("guest-auto-00000001"),
|
||||
"next missing": lambda x: x["next_boundary"].update(processed_once=False),
|
||||
"next duplicate": lambda x: x["next_boundary"]["after"].append(420),
|
||||
"cap exceeded": lambda x: x.update(start_minute=200),
|
||||
}, check.check_report)
|
||||
|
||||
def test_d1_import_closure(self):
|
||||
check.check_dependencies()
|
||||
check.check_modules()
|
||||
for name in ("restart_pair.py", "check_restart.py", "test_restart.py"):
|
||||
check.check_imports((check.HERE / name).read_text())
|
||||
for text in ("import forbidden_project_module", "from tools.strict_test_markers import main",
|
||||
"importlib.util.spec_from_file_location('x', 'tools/evil.py')",
|
||||
"__import__('tools.evil')", "exec('import tools.evil')"):
|
||||
with self.subTest(text=text), self.assertRaises(ValueError):
|
||||
check.check_imports(text)
|
||||
with self.assertRaises(ValueError):
|
||||
check.check_modules({"bad": SimpleNamespace(__file__=str(check.ROOT / "tools/evil.py"))})
|
||||
for path in check.DEPENDENCIES:
|
||||
original = check.sha
|
||||
with self.subTest(pin=path), patch.object(check, "sha", side_effect=lambda p:
|
||||
"0" * 64 if Path(p) == check.ROOT / path else original(p)), self.assertRaises(ValueError):
|
||||
check.check_dependencies()
|
||||
|
||||
def test_d2_order_and_check_plan(self):
|
||||
collector, _, _ = driver.load_dependencies()
|
||||
commands = driver.plan(collector, Path("/engine"), Path("/out"))
|
||||
self.assertEqual(len(commands), 7)
|
||||
self.assertIn("--import", commands[0])
|
||||
self.assertIn("--check-only", commands[1])
|
||||
self.assertIn("--check-only", commands[2])
|
||||
rows = []
|
||||
for _, label in driver.schedule(rows):
|
||||
rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}})
|
||||
self.assertEqual([r["phase"] for r in rows], check.PHASES)
|
||||
rows = []
|
||||
iterator = driver.schedule(rows)
|
||||
for _ in range(4):
|
||||
_, label = next(iterator)
|
||||
rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}})
|
||||
rows[-1]["passed"] = False
|
||||
with self.assertRaises(ValueError):
|
||||
next(iterator)
|
||||
rows[-1]["passed"] = True
|
||||
rows[-1]["report"]["stop_reason"] = "failed"
|
||||
iterator = driver.schedule([])
|
||||
next(iterator)
|
||||
with self.assertRaises(ValueError):
|
||||
next(iterator)
|
||||
|
||||
def test_d3_transfer_and_fresh_profiles(self):
|
||||
with tempfile.TemporaryDirectory(dir=check.HERE) as temporary:
|
||||
base = Path(temporary)
|
||||
source = base / "source"
|
||||
source.mkdir()
|
||||
(source / "world.bin").write_bytes(b"0123456789")
|
||||
(source / "manifest.json").write_text("{}")
|
||||
target = base / "transfer"
|
||||
manifest = driver.create_transfer(source, target, write)
|
||||
driver.check_transfer(target, manifest)
|
||||
self.assertEqual((target / "tampered/world.bin").read_bytes(), b"0123446789")
|
||||
driver.fresh_profile(base / "new-profile")
|
||||
with self.assertRaises(ValueError):
|
||||
driver.fresh_profile(source)
|
||||
self.rejects(manifest, {
|
||||
"SHA": lambda x: x["files"]["good"]["world.bin"].update(sha256="f" * 64),
|
||||
"offset": lambda x: x["tamper"].update(offset=0),
|
||||
"xor": lambda x: x["tamper"].update(xor=2),
|
||||
}, lambda x: driver.check_transfer(target, x))
|
||||
(target / "tampered/manifest.json").write_text("changed")
|
||||
with self.assertRaises(ValueError):
|
||||
driver.check_transfer(target, manifest)
|
||||
|
||||
def test_d4_d5_d7_probe_static(self):
|
||||
text = (check.HERE / "restart_probe.gd").read_text()
|
||||
check.check_probe(text)
|
||||
snippets = ["GameClock.advance_minutes(1)", "GameClock.set_paused(false)",
|
||||
"x.clear()", "spawn_guest()", "SimulationRuntime.new()", "initialize()",
|
||||
"enable_production_checkpoint()", "main._toggle_pause()", "GameClock.set_paused(true)",
|
||||
"var elapsed_us = 1", "Time.get_ticks_usec()"]
|
||||
for snippet in snippets:
|
||||
with self.subTest(snippet=snippet), self.assertRaises(ValueError):
|
||||
check.check_probe(text + "\nfunc bad():\n\t" + snippet + "\n")
|
||||
for replacement in ("GameClock.advance_minutes(2)", "Time.get_ticks_usec()",
|
||||
"GameClock.advance_one_minute()"):
|
||||
with self.subTest(step=replacement), self.assertRaises(ValueError):
|
||||
check.check_probe(text.replace("GameClock.advance_minutes(1)", replacement))
|
||||
|
||||
def test_rs7_rs8_full_receipt_rejections(self):
|
||||
with tempfile.TemporaryDirectory(dir=check.HERE) as temporary:
|
||||
out = Path(temporary)
|
||||
receipt = synthetic_receipt(out)
|
||||
check.check_receipt(receipt, out)
|
||||
self.rejects(receipt, {
|
||||
"source map": lambda x: x["source_after"].pop("0"),
|
||||
"ordinary player": lambda x: x["default_player_after"].update(changed=True),
|
||||
"guard": lambda x: x["phases"][3]["guard"].update(profile_writes_before_guard=1),
|
||||
"raw SHA": lambda x: x["phases"][3]["raw"]["stdout.log"].update(sha256="f" * 64),
|
||||
"PID": lambda x: x["phases"][3]["capture"].update(actual_captured_pids=[999]),
|
||||
"owned write": lambda x: x["owned_writes"].append("R-writer/report.json"),
|
||||
"cleanup": lambda x: x["cleanup"][0].update(engine_ended=False),
|
||||
"stage": lambda x: x["phases"][1].update(stage_after="f" * 64),
|
||||
"collector": lambda x: x["collector_after"].pop("measure_p28_01.gd"),
|
||||
"dependency": lambda x: x["dependencies_after"].update({"tools/run_all_tests.py": "f" * 64}),
|
||||
"cross-cutting": lambda x: x["cross_cutting"].pop(),
|
||||
"source count": lambda x: (x["source_after"].pop("1"), x["source_before"].pop("1")),
|
||||
"phase order": lambda x: x["phases"].reverse(),
|
||||
"foreign signal": lambda x: x["phases"][0]["capture"].update(foreign_process_or_group_signals=1),
|
||||
}, lambda x: check.check_receipt(x, out))
|
||||
(out / "R-reader-states/B.bin").write_bytes(b"changed")
|
||||
with self.assertRaises(ValueError):
|
||||
check.check_receipt(receipt, out)
|
||||
|
||||
def test_d6_raw_report_and_audio(self):
|
||||
with tempfile.TemporaryDirectory(dir=check.HERE) as temporary:
|
||||
out = Path(temporary)
|
||||
receipt = synthetic_receipt(out)
|
||||
row = receipt["phases"][3]
|
||||
folder = out / "R-writer"
|
||||
original = (folder / "stdout.log").read_text()
|
||||
for text in ("", original + original, original.split("P28_RESTART_AUDIO_CLOSE=")[0],
|
||||
original.replace('"owned_engine_pid": 103', '"owned_engine_pid": 999')):
|
||||
(folder / "stdout.log").write_text(text)
|
||||
row["raw"]["stdout.log"] = check.pin(folder / "stdout.log")
|
||||
with self.assertRaises(ValueError):
|
||||
check.bound_raw(folder, row)
|
||||
|
||||
def test_d2_d6_real_owned_outer_fake_child(self):
|
||||
collector, _, outer = driver.load_dependencies()
|
||||
child = (
|
||||
"import os,sys,json,time,pathlib\n"
|
||||
"mode=sys.argv[1]\n"
|
||||
"identity={'pid':os.getpid(),'ppid':os.getppid(),'pgid':os.getpgrp(),"
|
||||
"'args':' '.join([sys.executable]+sys.argv)}\n"
|
||||
"path=pathlib.Path(sys.argv[0]).parent/(mode+'-identity')\n"
|
||||
"tmp=path.with_suffix('.tmp'); tmp.write_text(json.dumps(identity)); tmp.replace(path)\n"
|
||||
"pathlib.Path(sys.argv[sys.argv.index('--log-file')+1]).write_text('native raw\\n')\n"
|
||||
"print('Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org',flush=True)\n"
|
||||
"time.sleep(0.15)\n"
|
||||
"if mode=='timeout': time.sleep(5)\n"
|
||||
"sys.exit(3 if mode=='exit' else 0)\n")
|
||||
with tempfile.TemporaryDirectory(dir=check.HERE) as temporary:
|
||||
base = Path(temporary)
|
||||
script = base / "fake-child"
|
||||
script.write_text(child)
|
||||
def diagnostic(command, folder, timeout, env, **kwargs):
|
||||
def probe(pid):
|
||||
path = base / (command[2] + "-identity")
|
||||
deadline = time.monotonic() + 2
|
||||
while time.monotonic() < deadline:
|
||||
if path.exists():
|
||||
identity = json.loads(path.read_text())
|
||||
if identity["pid"] == pid:
|
||||
return identity
|
||||
time.sleep(.005)
|
||||
raise RuntimeError("fake child identity unavailable")
|
||||
return outer.native.run_owned_diagnostic(command, folder, timeout, env, probe_fn=probe, **kwargs)
|
||||
for mode in ("ok", "exit", "timeout"):
|
||||
folder = base / mode
|
||||
def run_phase(command, destination, limit, env, **kwargs):
|
||||
self.assertEqual(limit, 1800)
|
||||
return outer.run_phase(command, destination, .3 if mode == "timeout" else 5,
|
||||
env, diagnostic_fn=diagnostic, **kwargs)
|
||||
def guarded_write(path, value):
|
||||
self.assertFalse(Path(path).is_relative_to(folder), "driver writes owned folder")
|
||||
write(path, value)
|
||||
def real_phase(*args, **kwargs):
|
||||
kwargs["dump_fn"] = write
|
||||
return run_phase(*args, **kwargs)
|
||||
with patch.object(collector, "live_engines", return_value=[]), \
|
||||
patch.object(collector, "write", side_effect=guarded_write):
|
||||
row = collector.run_engine(SimpleNamespace(run_phase=real_phase),
|
||||
[sys.executable, str(script), mode], folder, {})
|
||||
row["phase"] = "import"
|
||||
self.assertEqual(row["passed"], mode == "ok", row)
|
||||
self.assertTrue(row["capture"]["all_captured_ended"])
|
||||
if mode == "ok":
|
||||
check.bound_raw(folder, row)
|
||||
else:
|
||||
with self.assertRaises(ValueError):
|
||||
check.bound_raw(folder, row)
|
||||
_, errors = outer.finish_captured(outer.OWNED)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user