P28-02-DIAG: 意味を保つ軽量化:実測上位の特定(受動的な診断)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# 過去の blocked 記録(repair2 で解消済み)
|
||||
|
||||
OPUS_STARTUP_LIFECYCLE_CLARIFICATION により、以下の pause の質問も解消しました。
|
||||
この文書は履歴です。現在の実装・検証状況は IMPLEMENTATION.md と UNIT_OUTPUT.txt を参照してください。
|
||||
以下の「未提出」「未実行」は、当時の状態を記録したものです。
|
||||
|
||||
## P28-02-DIAG: repair1 起動時の pause 呼出しの適用範囲
|
||||
|
||||
## repair1 の判断
|
||||
|
||||
OPUS_D2_SCOPE_CLARIFICATION により、以前の S/K/C の比較境界の質問は
|
||||
解消しました。以下の以前の記録は履歴であり、現在の阻害理由ではありません。
|
||||
|
||||
残る確認は、起動準備で probe が `GameClock.set_paused(true)` を呼んでよいかです。
|
||||
補足の「collector の SPEC §4.3 と同じ順序」に参照される
|
||||
`execution/p28-01/SPEC.md` の §4.3 手順2は、autoload 準備後、R の復元前に
|
||||
この呼出しを要求しています。一方、P28-02 の SPEC §3.1 と TASK_INSTRUCTIONS は、
|
||||
live を変更できる API を fixture の restore と `advance_minutes(1)` のみに
|
||||
限定しています。補足は main の追加を明示していますが、probe によるこの
|
||||
pause 呼出しの例外は明示していません。
|
||||
|
||||
`scripts/core/simulation_kernel.gd:175-178` の `set_paused(true)` は
|
||||
`paused` と `_pause_request_serial` を変更します。後者は今回明示された K 層の
|
||||
対象です。したがって、読取 API とみなすことはできません。
|
||||
main の `_ready` による副作用は明示された main 追加に伴うものとして扱えますが、
|
||||
それとは別の probe の呼出しについて、許可を独自に広げません。
|
||||
|
||||
必要な確定は、collector §4.3 手順2の pause を起動準備の例外として許すのか、
|
||||
DIAG ではその手順を省略して restore → main 追加とするのか、の一点です。
|
||||
collector の windowed 実行や速度設定を DIAG に持ち込む要求ではありません。
|
||||
|
||||
確認した SPEC の strip SHA は
|
||||
`0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6`、
|
||||
raw SHA は `f58378b6cb317478f9d26776d0a236804f4ac80789d93f1555133298e4328d24`。
|
||||
TASK_INSTRUCTIONS の raw SHA は
|
||||
`b9b635b2598aee32efac662aa21d21fc88f55fbfdc6097cf07483e7fa5d32a7d` と一致しました。
|
||||
|
||||
実装 S は未提出、unit と native は未実行、PASS の主張はありません。
|
||||
git 書込み、他エージェント起動、Runner 操作、製品変更は行っていません。
|
||||
|
||||
## 以前の記録(S/K/C の質問は解消済み)
|
||||
|
||||
対象 base: `8c06c3b44235a52f35898b1f6263e8ee064af256`。
|
||||
実装提出ではなく、仕様判断への差戻しです。受入の達成は主張しません。
|
||||
|
||||
## 必要な判断
|
||||
|
||||
SPEC §3.1・§3.3 と D2 の「live の全状態」は、
|
||||
`SimulationRuntime.create_snapshot()` が返す保存対象の全状態を指すのか、
|
||||
snapshot に含まれない内部状態も指すのかを確定してください。
|
||||
|
||||
前者なら、返された Dictionary 全体の `var_to_bytes` 完全一致を比較範囲として
|
||||
明記する必要があります。後者なら、内部状態の対象と観測方法
|
||||
(Object 参照、循環参照、signal 接続、性能計測状態などの扱いを含む)が必要です。
|
||||
worker が独自に対象を除外して「全状態一致」と報告することはしません。
|
||||
|
||||
## base のソースで確認した根拠
|
||||
|
||||
- `scripts/core/simulation_kernel.gd:39-46` に性能計測状態、`_accumulator`、
|
||||
`_skip_next_process`、handler 登録、pause serial などが存在します。
|
||||
同ファイル `clock_snapshot()`(209-215行)は `absolute_minute`、`speed`、
|
||||
`paused`、`snapshot_version` の4項目だけを返します。
|
||||
- `scripts/core/simulation_snapshot.gd:6-14` は、この `clock_snapshot()` と
|
||||
GameState の snapshot を取り込みます。
|
||||
- `autoload/simulation_runtime.gd:521-585` の `create_snapshot()` は
|
||||
上記 envelope に各 system の保存用 snapshot を追加します。
|
||||
同ファイル134-136行の `_mvp_manifest_cache_catalog`、
|
||||
`_mvp_manifest_cache_generation`、`_mvp_manifest_cache` は含まれません。
|
||||
- 同ファイル560行から呼ぶ `_create_mvp_content_manifest()` は、
|
||||
11189-11195行で cache hit を判定し、miss 時にはこの内部キャッシュを書き換えます。
|
||||
したがって `create_snapshot()` の結果を前後で比較するだけでは、
|
||||
その呼出しによる内部キャッシュの変更を検出できません。
|
||||
|
||||
これは実際の測定区間で cache miss が発生したという報告ではありません。
|
||||
warm 30分によって cache が構築される可能性はあります。しかし、cache hit の場合も、
|
||||
snapshot の bytes 一致が証明する範囲と「全状態」の要求範囲を確定する必要があります。
|
||||
読み取り API としての `get_property_list()` / `get()` による独自走査を採る場合も、
|
||||
参照や実行時メタデータをどこまで含めるかは、この受入境界の判断になります。
|
||||
|
||||
## 確認済み事項と作業状態
|
||||
|
||||
- AGENTS.md、SPEC.md、TASK_INSTRUCTIONS.md と RESTART CLARIFICATION_02 を読取。
|
||||
- SPEC の `sha256(text.strip())` は契約値
|
||||
`0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6` と一致。
|
||||
raw-bytes SHA との差を阻害理由にはしていません。
|
||||
- 許可された外部 Python 依存6ファイルの SHA は CLARIFICATION_02 D1 とすべて一致。
|
||||
- tick ID による結合、inclusive 区間、observer 別記録の仕様に不足は見つかっていません。
|
||||
- scratch を構成できない項目は理由つき未測定にする規定があるため、
|
||||
その可否自体を今回の阻害理由にはしていません。
|
||||
- 変更はこの `diag/BLOCKED.md` のみ。driver・probe・checker・verify・unit 試験は未実装。
|
||||
- unit 試験、engine、正式 validation は未実行。PASS の主張はありません。
|
||||
指定 tmp 配下の一時ディレクトリ作成・削除の確認のみ実施しました。
|
||||
- git 書込み、他エージェント起動、Runner 操作、製品変更は行っていません。
|
||||
@@ -0,0 +1,83 @@
|
||||
# P28-02-DIAG repair2 実装提出
|
||||
|
||||
対象: P28-02-D1/D2/D3/D4。変更範囲はこの diag ディレクトリのみ。
|
||||
製品、SPEC、TASK_INSTRUCTIONS、validation、予算、分母は変更していません。
|
||||
git 書込み、engine 起動、他エージェント、Runner 操作は行っていません。
|
||||
局所試験は engine 不使用の `python3 test_diag.py`。生出力は UNIT_OUTPUT.txt。
|
||||
native import/parse/3 PID、正式 validation、独立レビューは未実行で、Runner/Root の担当です。
|
||||
|
||||
適用した補足(ユーザー DATA の全文):
|
||||
|
||||
- OPUS_D2_SCOPE_CLARIFICATION: `5775e7b512d9e90dbcfbcb181f5b64dc0fa8207e0947c0a4a6a5c9195565f9bd`
|
||||
- OPUS_STARTUP_LIFECYCLE_CLARIFICATION: `967aebf27ff412f7a16ce771ef7d68abf8cfde0bb76ed19cf6dbf2817ca7acc6`
|
||||
- SPEC strip SHA: `0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6`
|
||||
|
||||
## 実装上の選択
|
||||
|
||||
- startup は pause → R の SHA と get_var(false) → restore → pause → main → 2 frame。
|
||||
main の前後、各 frame 後、B0/B1、復元直後を記録します。speed 操作や resume はありません。
|
||||
- warm と measurement は各30回の advance_minutes(1)、各回後に1 frame await。
|
||||
tick ID、13本の信号境界、query を全件残します。I0〜I11 は checker が差分で算出します。
|
||||
tick 完了後から次の tick 開始までの wall は独立欄です。warm は順位・統計から除きます。
|
||||
- wall は observer を含む inclusive 値です。callback 自身の時間を別記し、引きません。
|
||||
自身の最終時刻取得後の timing record の追加・return は自己費用の計測外です。
|
||||
- 各区間について平均・nearest-rank P95・最大・N、query 件数・合計・統計を再計算します。
|
||||
平均 wall の降順(同値時は区間番号)による3回の順位と一致結果を保持します。
|
||||
query が0件なら件数・合計は実測0、query の平均等は null と理由にします。
|
||||
- Runtime、GameState、Runtime が create_snapshot を呼ぶ32 service を各 N=5 で独立計測。
|
||||
service 一覧と現在の製品 source の一致も unit で検査します。合算はしません。
|
||||
- 各 sample 前後の S/K/C の bytes を probe 内で完全比較し、全層の SHA、K の値、C の状態、
|
||||
差分 path を report に残します。S 不一致時のみ型・key・配列順を保持する正規形で差を説明し、
|
||||
dict 列挙順だけの差も失敗扱いです。診断では OPT の同等性緩和を適用しません。
|
||||
- C を S の観測前に読んで、観測自体の cache 書換えを隠さない構成です。
|
||||
cache miss は値を保持したまま明記し、公開 catalog 読取から manifest の一致も検査します。
|
||||
前後 C 完全一致の条件を満たさなければ失敗です。返却 manifest の top-level と配列を
|
||||
caller 側で変更して live が変わらないことも検査します。
|
||||
- scratch は snapshot の deep copy と tree に追加しない Kernel/GameState です。
|
||||
deep copy、SimulationSnapshot.validate、SimulationSnapshot.restore(envelope のみ)、
|
||||
SaveService.save_snapshot/load_snapshot を各 N=5。load 用保存は計測外です。
|
||||
各操作後に scratch のコンテナを変更して S/K/C が保たれることを検査し、
|
||||
Kernel/GameState/budget の instance ID が解放後に無効であることを検査します。
|
||||
保存先は guard 後の private user://p28_diag/scratch/ 配下です。
|
||||
- Runtime 全体の restore は initialize が必要で、列挙された scratch API 外のため理由つき未測定。
|
||||
Runtime 内部 validator と connector 再構築も、公開の独立観測境界がないため理由つき未測定。
|
||||
envelope の値を Runtime 全体の値、query の値を connector 全体の値とは表記しません。
|
||||
- SaveService は静的 API ですが、渡すデータと path は scratch だけです。静的な SaveService 自身の
|
||||
診断 budget は更新されます。これは補足の S/K/C 境界には含まれません。live Kernel の budget は
|
||||
全量比較の対象です。Runtime オブジェクトを scratch として起動したという主張はしません。
|
||||
- observer は main と2 frame の準備後に接続します。指定4 signal に加え、autosave の発生を
|
||||
記録する save_operation_completed にも受動接続し、終了前に全て元の接続一覧へ戻します。
|
||||
- PID 内の C 同一性には instance ID を含めます。PID 間では ID の数値を比べず、同一 catalog を
|
||||
指す関係・generation・manifest bytes を比較します。K の PID 間比較は補足に列挙された6項目
|
||||
(minute、paused、pause serial、skip、snapshot version、handler 集合)です。K の全 SHA と
|
||||
budget の実測時間は PID 間の同一条件にしませんが、raw と PID 内の検査には全量残します。
|
||||
- 初期条件不一致は値を raw に保持して失敗し、管制判断へ戻します。実測を補正しません。
|
||||
|
||||
## Root の実行インターフェース(worker は実行しない)
|
||||
|
||||
凍結した S を checkout した環境で、以下の CLI を使います。`--check` は読取と計画の表示だけです。
|
||||
`--run` は import → parse-base → parse-wrapper → diag-1 → diag-2 → diag-3 を順番に実行します。
|
||||
|
||||
```
|
||||
python3 diag_p28_02.py --check|--run --readonly <p28-01-readonly> --fixture <reference.variant> --engine <Godot> --output <new-directory>
|
||||
```
|
||||
|
||||
driver は6ファイルの依存 SHA を確認して collector を読取専用 import します。
|
||||
RESTART の依存検査・owned raw 結合のコードを局所に適合して再利用していますが、
|
||||
RESTART の Python 自体は import しません。追加 package、ネットワークは使いません。
|
||||
全1407 source と提出した5コードファイルが frozen commit S と同じことを確認します。
|
||||
private profile、guard、1800秒 owned timeout、audio close drain、cleanup は collector と同じ経路です。
|
||||
|
||||
成功時、driver が execution.json と、stage 以外の全ファイルを SHA/size で列挙した bundle.json を
|
||||
作ります。これは Root による実行結果です。失敗時は execution.json を保存し、成功 bundle を作りません。
|
||||
stage は Root に残し、各 phase の全 stage inventory を execution.json に記録します。
|
||||
全 raw、owned receipts、guard、startup project、probe/wrapper、入力 manifest は bundle に含みます。
|
||||
通常の profile ファイルも collector と同様に inventory を保存し、owned process cleanup と混同しません。
|
||||
|
||||
Root は出力一式(stage は省略可)を `$BUILD_OUTPUT_DIR/p28-02-diag-evidence` に置きます。
|
||||
`python3 verify_diag.py --project-root .` は bundle 欠落を拒否し、全 raw/SHA/PID/guard/cleanup と
|
||||
現候補の1407 source・5コードの一致を結合します。S の実測として記録し、Q の実測とは呼びません。
|
||||
bundle 作成後にファイルを追加・編集すると inventory 不一致で拒否します。
|
||||
|
||||
原本加算は0、正式完了23/88、4/30、予算 false 6件を保持します。P27 完了は主張しません。
|
||||
repair3: 最終の局所 unit を再実行して PASS(19件、0.780秒)。
|
||||
@@ -0,0 +1,25 @@
|
||||
test_bundle_missing_and_tampered (__main__.DiagnosticTests) ... ok
|
||||
test_connections_and_preparation (__main__.DiagnosticTests) ... ok
|
||||
test_cross_pid_only_named_kernel_fields (__main__.DiagnosticTests) ... ok
|
||||
test_fixture_pause_and_int64_identity (__main__.DiagnosticTests) ... ok
|
||||
test_headless_plan_and_stop_on_failure (__main__.DiagnosticTests) ... ok
|
||||
test_import_closure_and_pins (__main__.DiagnosticTests) ... ok
|
||||
test_missing_not_zero (__main__.DiagnosticTests) ... ok
|
||||
test_nonadditive_scope (__main__.DiagnosticTests) ... ok
|
||||
test_pid_local_catalog_identity (__main__.DiagnosticTests) ... ok
|
||||
test_queries_and_observer_rejection (__main__.DiagnosticTests) ... ok
|
||||
test_real_owned_python_child_and_raw_binding (__main__.DiagnosticTests) ... RUNTIME_SCALE_PROCESS_START {"name": "import", "pid": 62682}
|
||||
ok
|
||||
test_scratch_ownership (__main__.DiagnosticTests) ... ok
|
||||
test_service_snapshot_inventory_matches_current_product (__main__.DiagnosticTests) ... ok
|
||||
test_source_map_count_and_mutations (__main__.DiagnosticTests) ... ok
|
||||
test_state_and_cache_mismatch (__main__.DiagnosticTests) ... ok
|
||||
test_static_probe_and_product_mutations (__main__.DiagnosticTests) ... ok
|
||||
test_three_pid_comparison (__main__.DiagnosticTests) ... ok
|
||||
test_tick_id_and_minute_rejection (__main__.DiagnosticTests) ... ok
|
||||
test_valid_raw_intervals (__main__.DiagnosticTests) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 19 tests in 0.780s
|
||||
|
||||
OK
|
||||
@@ -0,0 +1,595 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DIAG evidence validation; no engine launch."""
|
||||
import ast
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[5]
|
||||
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_NAMES = {"diag_p28_02.py", "check_diag.py", "verify_diag.py", "test_diag.py", "diag_probe.gd"}
|
||||
LOCAL = {"docs/plans/original-completion-2026-10-02/execution/p28-02/diag/" + n for n in LOCAL_NAMES}
|
||||
PHASES = ["import", "parse-base", "parse-wrapper", "diag-1", "diag-2", "diag-3"]
|
||||
FIXTURE_SHA = "eb4f770071eb07cd7e136fe54c3f78768cc8de7a4264e62a9af1f06a8f90f9bd"
|
||||
ENGINE_SHA = "c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf"
|
||||
SEMANTICS = {"intervals": "inclusive_wall_including_observer", "observer_subtracted": False,
|
||||
"breakdown": "independent_inclusive_not_additive", "path": "query_only_not_connector"}
|
||||
SERVICES = ['weather', 'command_queue', 'id_generator', 'domain_events', 'world_service',
|
||||
'path_service', 'mana_network_service', 'mana_reserve', 'economy_service', 'facility_upkeep',
|
||||
'upkeep_settlement', 'inventory_service', 'kitchen_production', 'auto_purchase_plans',
|
||||
'elevators', 'legacy_stairs', 'logistics_dispatcher', 'facility_service', 'facility_cleanliness',
|
||||
'guest_service', 'staff_service', 'progression_service', 'relationship_service',
|
||||
'event_service', 'authority_service', 'defense_service', 'construction_service',
|
||||
'excavation_service', 'spatial_subslot_service', 'route_subslot_path_adapter',
|
||||
'facility_infrastructure_projector', 'peeping_room_service']
|
||||
SCRATCH = {'deep_copy', 'envelope_validate', 'envelope_restore', 'save', 'load'}
|
||||
UNMEASURED = {'runtime_restore', 'runtime_validation_internal', 'connector_rebuild'}
|
||||
K_FIELDS = {'absolute_minute', 'speed', 'paused', 'snapshot_version', '_skip_next_process',
|
||||
'_pause_request_serial', '_abort_current_tick_requested', '_last_tick_advanced',
|
||||
'_accumulator_f64_hex', 'handlers', '_sample_counts', '_totals', '_samples'}
|
||||
|
||||
|
||||
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):
|
||||
"""Conservative finite call policy, plus mutation-site checks; native parse is separate."""
|
||||
code = '\n'.join(line.split('#', 1)[0] for line in text.splitlines())
|
||||
tokens = re.sub(r'"(?:\\.|[^"\\])*"', '""', code)
|
||||
forbidden = (r'\b(?:set_script|set_process|set_physics_process|set_deferred|callv?|call_thread_safe|'
|
||||
r'call_deferred_thread_group|emit_signal|subscribe|unsubscribe|initialize|reset|'
|
||||
r'clear_performance_reports|register_tick_handler|unregister_tick_handler|'
|
||||
r'process_real_delta|advance_one_minute|_set_speed|_toggle_pause)\s*\(')
|
||||
require(not re.search(forbidden, tokens), 'forbidden product/hook call')
|
||||
require(not re.search(r'\b(?:clock|runtime|main|target)\s*(?:\.[\w]+|\[[^\n]+\])+\s*(?:[+*/-]?=)(?!=)', code),
|
||||
'live property assignment')
|
||||
require(not re.search(r'\b(?:set|set_indexed)\s*\(', code), 'dynamic mutation')
|
||||
functions = dict(re.findall(r'^func (\w+)\([^\n]*\).*?:\n(.*?)(?=^func |\Z)', code, re.M | re.S))
|
||||
require(functions and len(functions) == len(re.findall(r'^func ', code, re.M)), 'probe functions')
|
||||
for name, body in functions.items():
|
||||
if 'clock.set_paused(' in body:
|
||||
require(name == 'begin' and body.count('clock.set_paused(true)') == 2 and
|
||||
body.count('clock.set_paused(') == 2, 'pause only twice in preparation')
|
||||
require(body.rfind('clock.set_paused(') < body.index('main = load('), 'pause after main')
|
||||
if re.search(r'\badvance_\w+\(', body):
|
||||
require(name == 'step_window' and body.count('clock.advance_minutes(1)') == 1 and
|
||||
len(re.findall(r'\badvance_\w+\(', body)) == 1, 'manual stepping site')
|
||||
if 'runtime.restore_snapshot(' in body:
|
||||
require(name == 'begin' and body.count('runtime.restore_snapshot(snapshot)') == 1 and
|
||||
body.index('runtime.restore_snapshot(') < body.index('main = load('), 'fixture restore only')
|
||||
if 'SaveService.' in body or 'SimulationSnapshot.' in body:
|
||||
require(name == 'scratch_samples', 'scratch APIs outside scratch')
|
||||
if '.free(' in body:
|
||||
require(name == 'scratch_samples' and body.count('.free(') == 2 and
|
||||
'scratch_clock.free()' in body and 'scratch_state.free()' in body, 'scratch free only')
|
||||
require('set_paused(false)' not in code, 'resume forbidden')
|
||||
for receiver, allowed in {
|
||||
'clock': {'get', 'set_paused', 'advance_minutes'},
|
||||
'runtime': {'get', 'create_snapshot', 'restore_snapshot', 'get_save_policy'},
|
||||
'main': {'get'}, 'target': {'create_snapshot'},
|
||||
'SaveService': {'save_snapshot', 'load_snapshot'},
|
||||
'SimulationSnapshot': {'validate', 'restore'},
|
||||
}.items():
|
||||
calls = re.findall(r'\b' + receiver + r'\.(\w+)\s*\(', code)
|
||||
require(set(calls) <= allowed, 'unapproved call on ' + receiver)
|
||||
# Private calls are forbidden; named get() reads above are the finite K/C boundary.
|
||||
require(not re.search(r'\.\s*_\w+\s*\(', code), 'private product call')
|
||||
require(code.count('root.add_child(main)') == 1 and code.count('root.close_requested.emit()') == 1,
|
||||
'main lifecycle')
|
||||
require('get_var(false)' in code and 'await process_frame' in code, 'fixture/frame lifecycle')
|
||||
require('scratch: Dictionary = before.snapshot.duplicate(true)' in code and
|
||||
'is_instance_id_valid(id)' in code and 'signal_inventory()' in code, 'ownership checks absent')
|
||||
# Mutation receivers may not be aliased to bypass the site checks above.
|
||||
for method, receiver, count in (('set_paused', 'clock', 2), ('advance_minutes', 'clock', 1),
|
||||
('restore_snapshot', 'runtime', 1), ('add_child', 'root', 1)):
|
||||
require(len(re.findall(r'\.\s*' + method + r'\s*\(', tokens)) == count and
|
||||
len(re.findall(r'\b' + receiver + r'\.' + method + r'\s*\(', tokens)) == count,
|
||||
'mutation receiver/site: ' + method)
|
||||
require(len(re.findall(r'\bcall_deferred\s*\(', tokens)) == 1 and 'call_deferred("begin")' in code,
|
||||
'deferred product call')
|
||||
methods = set(re.findall(r'\.\s*(\w+)\s*\(', tokens))
|
||||
require(methods <= {
|
||||
'new', 'start', 'update', 'finish', 'hex_encode', 'append', 'get', 'resize', 'encode_double',
|
||||
'sort', 'duplicate', 'get_instance_id', 'get_manifest_generation', 'create_snapshot',
|
||||
'keys', 'sort_custom', 'size', 'has', 'append_array', 'build_all_guest_definitions',
|
||||
'get_guest_class_ids', 'get_species_ids', 'get_staff_candidate_ids', 'get_unique_character_ids',
|
||||
'get_connections', 'get_object_id', 'get_method', 'get_name', 'stringify', 'is_empty',
|
||||
'get_ticks_usec', 'get_ticks_msec', 'advance_minutes', 'save_snapshot', 'validate', 'restore',
|
||||
'load_snapshot', 'free', 'merge', 'get_node', 'get_environment', 'get_process_id', 'parse_string',
|
||||
'set_paused', 'get_sha256', 'open', 'get_var', 'close', 'restore_snapshot', 'connect',
|
||||
'instantiate', 'add_child', 'get_save_policy', 'snapshot', 'disconnect', 'emit'
|
||||
}, 'unapproved member call')
|
||||
for path in re.findall(r'"(user://[^"\n]*)"', code):
|
||||
require(path.startswith('user://p28_diag/scratch/'), 'nonprivate scratch path')
|
||||
|
||||
|
||||
def integer(value, label):
|
||||
require(type(value) is int and value >= 0, 'invalid nonnegative integer: ' + label)
|
||||
return value
|
||||
|
||||
|
||||
def hash_value(value):
|
||||
require(isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value), 'invalid SHA256')
|
||||
|
||||
|
||||
def stats(values):
|
||||
require(values, 'missing samples cannot become zero')
|
||||
for value in values:
|
||||
integer(value, 'sample')
|
||||
ordered = sorted(values)
|
||||
return {'count': len(values), 'mean_us': sum(values) / len(values),
|
||||
'p95_us': ordered[math.ceil(len(values) * .95) - 1], 'max_us': ordered[-1]}
|
||||
|
||||
|
||||
def check_layers(value, require_paused=True):
|
||||
for layer in ('S', 'K', 'C'):
|
||||
hash_value(value[layer])
|
||||
kernel, cache = value['kernel'], value['cache']
|
||||
require(set(kernel) == K_FIELDS, 'finite K boundary')
|
||||
require(type(kernel['paused']) is bool and (not require_paused or kernel['paused'] is True) and
|
||||
kernel['speed'] in (1, 2, 3), 'clock not paused')
|
||||
require(re.fullmatch('[0-9a-f]{16}', kernel['_accumulator_f64_hex']), 'f64 accumulator')
|
||||
require(set(kernel['handlers']) == {str(i) for i in range(11)}, 'handler phases')
|
||||
for names in kernel['handlers'].values():
|
||||
require(names == sorted(set(names)), 'handler IDs')
|
||||
for metric in ('_sample_counts', '_totals', '_samples'):
|
||||
require(isinstance(kernel[metric], dict), 'budget state missing')
|
||||
require(cache['filled'] is True and cache['generation'] == cache['current_generation'] and
|
||||
cache['catalog_id'] == cache['current_catalog_id'], 'cache not filled')
|
||||
require(isinstance(cache['catalog_id'], str) and re.fullmatch(r'-?[1-9][0-9]*', cache['catalog_id']),
|
||||
'catalog identity must preserve int64 as decimal text')
|
||||
hash_value(cache['bytes_sha256'])
|
||||
|
||||
|
||||
def check_comparison(row):
|
||||
before, after = row['before'], row['after']
|
||||
check_layers(before)
|
||||
check_layers(after)
|
||||
require(row['equal'] == {'S': True, 'K': True, 'C': True}, 'S/K/C bytes differ')
|
||||
require(before == after and row['differences'] == [], 'state evidence contradicts equality')
|
||||
require(row['cache_miss_in_sample'] is False, 'cache changed in sample; return to governor')
|
||||
require(row['cache_rebuilt_matches_catalog'] is None and row['cache_rebuild_check_reason'] == 'no_cache_miss',
|
||||
'cache miss evidence contradiction')
|
||||
|
||||
|
||||
def check_connections(value):
|
||||
require(set(value) == {'tick_started', 'tick_phase_started', 'tick_completed',
|
||||
'path_query_measured', 'save_operation_completed'}, 'signal boundary')
|
||||
for row in value.values():
|
||||
require(row['count'] == len(row['targets']), 'connection count')
|
||||
for target in row['targets']:
|
||||
require(set(target) == {'object', 'method', 'flags'} and str(target['object']).isdigit()
|
||||
and target['method'], 'connection target')
|
||||
|
||||
|
||||
def summarize_ticks(ticks):
|
||||
values = [[] for _ in range(12)]
|
||||
paths = [[] for _ in range(12)]
|
||||
frames = []
|
||||
for tick in ticks:
|
||||
for index in range(12):
|
||||
values[index].append(tick['edges'][index + 1]['at_us'] - tick['edges'][index]['at_us'])
|
||||
for query in tick['queries']:
|
||||
paths[query['interval']].append(query['duration_us'])
|
||||
if tick['frame_wall_before_us'] is not None:
|
||||
frames.append(tick['frame_wall_before_us'])
|
||||
return {'intervals': {'I' + str(i): stats(values[i]) for i in range(12)},
|
||||
'query_only': {'I' + str(i): {'count': len(paths[i]), 'total_us': sum(paths[i]),
|
||||
'stats': stats(paths[i]) if paths[i] else None,
|
||||
'reason': None if paths[i] else 'no_queries_observed'} for i in range(12)},
|
||||
'inter_tick_frame_wall': stats(frames)}
|
||||
|
||||
|
||||
def check_report(report):
|
||||
require(report['schema'] == 'p28-02-diag-v1' and report['role'] in PHASES[3:], 'report schema/role')
|
||||
require(report['stop_reason'] == 'complete' and report['errors'] == [], 'probe failed')
|
||||
require(report['semantics'] == SEMANTICS, 'exclusive/additive/connector interpretation forbidden')
|
||||
require(report['input_sha256'] == FIXTURE_SHA, 'fixture differs')
|
||||
require(report['metadata']['engine_sha256'] == ENGINE_SHA and
|
||||
report['metadata']['machine'] == 'Mac16,10' and report['metadata']['headless'] is True,
|
||||
'engine/machine/headless condition')
|
||||
require(report['metadata']['measured_source'] == 'S' and report['metadata']['candidate_Q_measured'] is False,
|
||||
'S/Q attribution')
|
||||
require(re.fullmatch(r'[0-9a-f]{40}', report['metadata']['commit']), 'S commit')
|
||||
require(isinstance(report['seed'], str) and report['seed'].isdigit() and report['rng_state_typed_hex'],
|
||||
'seed/RNG state missing')
|
||||
for value in report['rng_state_typed_hex'].values():
|
||||
require(isinstance(value, str) and re.fullmatch(r'(?:[0-9a-f]{2})+', value), 'typed RNG bytes')
|
||||
for name in ('restored', 'B0', 'B1', 'outside_before'):
|
||||
check_layers(report['baselines'][name], require_paused=name != 'restored')
|
||||
for name, minute in (('B0', 602), ('B1', 632), ('outside_before', 662)):
|
||||
require(report['baselines'][name]['kernel']['absolute_minute'] == minute, 'baseline minute')
|
||||
require(report['startup']['ops'].get('code') == 'INVALID_START_TIME', 'main Ops result')
|
||||
for name in ('before_main', 'after_main', 'frame1', 'frame2'):
|
||||
check_layers(report['startup'][name])
|
||||
prep = report['preparation']
|
||||
require([row['operation'] for row in prep] == ['initial_pause', 'fixture_restore', 'restored_pause'],
|
||||
'preparation operations')
|
||||
require(prep[1]['ok'] is True and prep[1]['state'] == report['baselines']['restored'], 'fixture restore result')
|
||||
for row in (prep[0], prep[2]):
|
||||
require(row['K_after']['paused'] is True and row['K_after']['_pause_request_serial'] ==
|
||||
row['K_before']['_pause_request_serial'] + 1, 'preparation pause serial')
|
||||
ticks = report['ticks']
|
||||
require(len(ticks) == 60, 'warm/measurement counts')
|
||||
previous_end = None
|
||||
for index, tick in enumerate(ticks):
|
||||
minute = 602 + index
|
||||
require(tick['id'] == index + 1 and tick['start_minute'] == minute and tick['end_minute'] == minute + 1,
|
||||
'tick ID/minute continuity')
|
||||
require(tick['region'] == ('warm' if index < 30 else 'measurement'), 'warm included in measurement')
|
||||
edges = tick['edges']
|
||||
require(len(edges) == 13 and edges[0]['name'] == 'tick_started' and
|
||||
edges[-1]['name'] == 'tick_completed', 'signal edges')
|
||||
require(edges[0]['minute'] == minute and edges[-1]['minute'] == minute + 1, 'edge minute')
|
||||
for phase, edge in enumerate(edges[1:-1]):
|
||||
require(edge['name'] == 'phase' and edge['phase'] == phase and
|
||||
edge['minute'] == minute + (phase != 0), 'phase wrongly joined by minute')
|
||||
times = [integer(edge['at_us'], 'edge') for edge in edges]
|
||||
require(times == sorted(times), 'nonmonotonic edges')
|
||||
require(tick['frame_wall_before_us'] == (times[0] - previous_end if previous_end is not None else None),
|
||||
'inter-tick wall attribution')
|
||||
if previous_end is not None:
|
||||
integer(tick['frame_wall_before_us'], 'frame wall')
|
||||
previous_end = times[-1]
|
||||
for query in tick['queries']:
|
||||
slot = integer(query['interval'], 'query interval')
|
||||
require(slot < 12 and query['tick_id'] == tick['id'] and query['region'] == tick['region'],
|
||||
'query tick/interval binding')
|
||||
integer(query['duration_us'], 'query duration')
|
||||
require(times[slot] <= query['at_us'] <= times[slot + 1], 'query timestamp outside interval')
|
||||
for query in report['queries_outside']:
|
||||
require(query['tick_id'] is None and query['interval'] is None, 'outside query attribution')
|
||||
integer(query['duration_us'], 'outside query')
|
||||
observations = report['observer']
|
||||
for row in observations:
|
||||
integer(row['duration_us'], 'observer cost')
|
||||
require(row['callback'] in {'tick_started', 'tick_phase_started', 'tick_completed',
|
||||
'path_query_measured', 'save_operation_completed'}, 'observer callback')
|
||||
for tick in ticks:
|
||||
selected = [o for o in observations if o['tick_id'] == tick['id']]
|
||||
counts = {name: sum(o['callback'] == name for o in selected) for name in
|
||||
('tick_started', 'tick_phase_started', 'tick_completed', 'path_query_measured')}
|
||||
require(counts == {'tick_started': 1, 'tick_phase_started': 11, 'tick_completed': 1,
|
||||
'path_query_measured': len(tick['queries'])}, 'missing observer costs')
|
||||
measurements = report['measurements']
|
||||
snapshots = {'runtime_snapshot', 'game_state_snapshot'} | {s + '_snapshot' for s in SERVICES}
|
||||
require(set(measurements) == snapshots | SCRATCH | UNMEASURED, 'measurement inventory')
|
||||
for name, measurement in measurements.items():
|
||||
if name in UNMEASURED:
|
||||
require(measurement['samples'] is None and isinstance(measurement['reason'], str) and
|
||||
measurement['reason'].strip() and measurement['scope'] == 'unmeasured', 'unmeasured is not zero')
|
||||
continue
|
||||
require(measurement['scope'] == 'independent_inclusive' and measurement['reason'] is None and
|
||||
len(measurement['samples']) == 5, 'N=5 independent samples')
|
||||
for sample in measurement['samples']:
|
||||
integer(sample['duration_us'], 'outside duration')
|
||||
check_comparison(sample)
|
||||
require(sample['before']['kernel']['absolute_minute'] == 662, 'outside-tick time advanced')
|
||||
if name in snapshots:
|
||||
hash_value(sample['result_sha256'])
|
||||
else:
|
||||
require(sample['input'] == 'live_snapshot_deep_copy' and sample['ok'] is True and
|
||||
sample['detached_mutation_checked'] is True, 'scratch ownership')
|
||||
require(sample['scratch_path'].startswith('user://p28_diag/scratch/') and
|
||||
'..' not in sample['scratch_path'].split('/'), 'scratch private path')
|
||||
require(len(sample['scratch_ids']) == len(sample['released']) == 3 and
|
||||
all(value is True for value in sample['released']), 'scratch not released')
|
||||
require(len(set(sample['scratch_ids'])) == 3, 'scratch IDs')
|
||||
require(all(isinstance(v, str) and re.fullmatch(r'-?[1-9][0-9]*', v)
|
||||
for v in sample['scratch_ids']), 'scratch int64 IDs')
|
||||
check_comparison(report['outside_total'])
|
||||
check_comparison(report['manifest_detached'])
|
||||
for name in ('observer_before', 'observer_connected', 'observer_disconnected'):
|
||||
check_connections(report[name])
|
||||
require(report['observer_before'] == report['observer_disconnected'] and
|
||||
report['all_connections_restored'] is True, 'observer leak')
|
||||
for name in report['observer_before']:
|
||||
expected = report['observer_before'][name]['count'] + 1
|
||||
require(report['observer_connected'][name]['count'] == expected, 'observer connection not made')
|
||||
require([v['label'] for v in report['cache_preparations']] == ['restored', 'B1', 'outside_tick'],
|
||||
'cache preparation records')
|
||||
for row in report['cache_preparations']:
|
||||
require(row['after']['filled'] is True and row['filled_outside_sample'] is (not row['before']['filled']),
|
||||
'cache preparation mismatch')
|
||||
return summarize_ticks(ticks[30:])
|
||||
|
||||
|
||||
def preparation_key(report):
|
||||
"""Object IDs are PID-local identities; compare catalog relationship and contents across PIDs."""
|
||||
def stable(value):
|
||||
result = copy.deepcopy(value)
|
||||
if isinstance(result, dict):
|
||||
if set(result) == K_FIELDS:
|
||||
# Lifecycle clarification names this finite cross-PID K subset.
|
||||
# The full K bytes (including nondeterministic path-query wall
|
||||
# budgets) remain mandatory for within-PID outside-tick checks.
|
||||
return {k: result[k] for k in ('absolute_minute', 'paused', '_pause_request_serial',
|
||||
'_skip_next_process', 'snapshot_version', 'handlers')}
|
||||
# C's SHA contains the required local object identity. Compare its underlying
|
||||
# stable generation/content fields across PID, preserving raw C SHA in report.
|
||||
result.pop('C', None)
|
||||
result.pop('K', None)
|
||||
for key in ('catalog_id', 'current_catalog_id'):
|
||||
result.pop(key, None)
|
||||
return {k: stable(v) for k, v in result.items()}
|
||||
if isinstance(result, list):
|
||||
return [stable(v) for v in result]
|
||||
return result
|
||||
return stable({'input': report['input_sha256'], 'seed': report['seed'],
|
||||
'rng': report['rng_state_typed_hex'], 'preparation': report['preparation'],
|
||||
'baselines': {k: report['baselines'][k] for k in ('restored', 'B0', 'B1')},
|
||||
'startup': report['startup'], 'cache_preparations': report['cache_preparations']})
|
||||
|
||||
|
||||
def compare_runs(reports):
|
||||
require(len(reports) == 3 and len({r['pid'] for r in reports}) == 3, 'three separate PIDs required')
|
||||
summaries = [check_report(r) for r in reports]
|
||||
keys = [preparation_key(r) for r in reports]
|
||||
require(keys[0] == keys[1] == keys[2], 'preparation mismatch; raw values retained, return to governor')
|
||||
ranks = [sorted(s['intervals'], key=lambda k: (-s['intervals'][k]['mean_us'], int(k[1:]))) for s in summaries]
|
||||
return {'runs': summaries, 'mean_wall_rankings': ranks, 'rankings_agree': ranks[0] == ranks[1] == ranks[2],
|
||||
'rank_method': 'descending mean inclusive wall; ties by interval index',
|
||||
'outside': [{name: stats([v['duration_us'] for v in m['samples']]) if m['samples'] is not None
|
||||
else {'value': None, 'reason': m['reason']} for name, m in r['measurements'].items()}
|
||||
for r in reports],
|
||||
'observer': [stats([o['duration_us'] for o in r['observer'] if o['region'] == 'measurement'])
|
||||
for r in reports], 'semantics': SEMANTICS}
|
||||
|
||||
|
||||
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_DIAG_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_DIAG_REPORT=")]
|
||||
closes = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_DIAG_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 digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
|
||||
def check_source_maps(before, after):
|
||||
require(before == after and len(before) == 1407, 'source map mismatch')
|
||||
for name, ref in before.items():
|
||||
require(not Path(name).is_absolute() and '..' not in Path(name).parts and
|
||||
set(ref) == {'bytes', 'sha256'}, 'source map path/ref')
|
||||
hash_value(ref['sha256'])
|
||||
integer(ref['bytes'], 'source bytes')
|
||||
|
||||
|
||||
def check_receipt(receipt, out):
|
||||
from diag_p28_02 import COLLECTOR_PINS
|
||||
out = Path(out)
|
||||
require(receipt['passed'] is True and receipt['error'] is None and receipt['cleanup_errors'] == [],
|
||||
'execution failed')
|
||||
source = receipt['source_before']
|
||||
check_source_maps(source, receipt['source_after'])
|
||||
require(receipt['default_player_before'] == receipt['default_player_after'], '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']) == LOCAL_NAMES,
|
||||
'worker changed')
|
||||
require(receipt['owned_writes'] == [] and receipt['original_credit'] == 0 and
|
||||
receipt['formal_completed'] == 23 and receipt['denominator'] == 88 and
|
||||
receipt['packages_completed'] == 4 and receipt['package_denominator'] == 30 and
|
||||
receipt['budget_false_count_unchanged'] == 6, 'scope or credit changed')
|
||||
admission = receipt['admission']
|
||||
require(json.loads((out / 'admission.json').read_text()) == admission, 'admission raw')
|
||||
require(admission['source_map'] == source and admission['metadata']['source_map_sha256'] == digest(source)
|
||||
and admission['metadata']['source_count'] == 1407 and
|
||||
admission['metadata']['worker_pins'] == receipt['worker_before'], 'admission source binding')
|
||||
require(admission['fixture_sha256'] == FIXTURE_SHA and admission['dependencies'] == DEPENDENCIES and
|
||||
admission['collector_pins'] == COLLECTOR_PINS and admission['phases'] == PHASES,
|
||||
'admission inputs')
|
||||
require(set(receipt['script_pins']) == {'diag_probe.gd', 'profile_guard.gd', 'guarded.gd'}, 'script inventory')
|
||||
for name, ref in receipt['script_pins'].items():
|
||||
require(pin(out / name) == ref, 'executed script differs')
|
||||
require(sha(out / 'diag_probe.gd') == receipt['worker_before']['diag_probe.gd'], 'probe differs from S')
|
||||
require(sha(out / 'profile_guard.gd') == admission['readonly']['files']['run-r02/profile_guard.gd'],
|
||||
'guard differs from pinned input')
|
||||
manifest_path = out / 'root-input-manifest.json'
|
||||
require(sha(manifest_path) == admission['readonly']['manifest_sha256'] ==
|
||||
'9dac0a51d52087e691c5723b4f78f171f7998c78b582b2b6c4c904798e77644d', 'Root input manifest pin')
|
||||
guard_refs = [r for r in json.loads(manifest_path.read_text())['files']
|
||||
if r['destination'].endswith('/run-r02/profile_guard.gd')]
|
||||
require(len(guard_refs) == 1 and guard_refs[0]['sha256'] == sha(out / 'profile_guard.gd'),
|
||||
'guard not bound to original Root manifest')
|
||||
script_path = Path(admission['planned_argv'][2][-1])
|
||||
wrapper = 'extends ' + json.dumps(str(script_path.parent / 'diag_probe.gd')) + '\nconst Guard = preload(' + \
|
||||
json.dumps(str(script_path.parent / 'profile_guard.gd')) + ')\nfunc _initialize() -> void:\n' + \
|
||||
'\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n'
|
||||
require((out / 'guarded.gd').read_text() == wrapper, 'guard wrapper differs')
|
||||
check_probe((out / 'diag_probe.gd').read_text())
|
||||
rows = receipt['phases']
|
||||
require([r['phase'] for r in rows] == PHASES, 'phase order/incomplete execution')
|
||||
require(len(admission['planned_argv']) == len(PHASES), 'phase plan')
|
||||
pids, profiles = [], []
|
||||
previous = source
|
||||
reports = []
|
||||
for index, row in enumerate(rows):
|
||||
require('--headless' in row['argv'], 'native run not headless')
|
||||
require(row['argv'][:-3] == admission['planned_argv'][index] and
|
||||
row['argv'][-3] == '--log-file' and row['argv'][-1] == '--verbose', 'argv differs from plan')
|
||||
report = bound_raw(out / row['phase'], row)
|
||||
pids.extend(row['capture']['actual_captured_pids'])
|
||||
current = row['stage_inventory']
|
||||
require(row['stage_before'] == digest(previous) and row['stage_after'] == digest(current),
|
||||
'stage inventory digest')
|
||||
require(all(current.get(k) == v for k, v in previous.items()), 'stage source changed')
|
||||
added = set(current) - set(previous)
|
||||
require(not added or row['phase'] == 'import' and all(
|
||||
(n.endswith('.gd.uid') and n[:-4] in previous) or
|
||||
(n.endswith('.import') and n[:-7] in previous) for n in added), 'unexpected stage files')
|
||||
previous = current
|
||||
require(row['stage_preserved'] is True and row['source_preserved'] is True and
|
||||
row['player_preserved'] is True, 'phase preservation')
|
||||
startup = row['startup_project']
|
||||
startup_path = out / (row['phase'] + '-startup-project.godot')
|
||||
require(startup['fresh_before'] is True and sha(startup_path) == startup['sha256'] and
|
||||
startup['restored_sha256'] == source['project.godot']['sha256'], 'startup profile')
|
||||
profiles.append(startup['profile'])
|
||||
if report:
|
||||
require(report['metadata'] == admission['metadata'], 'report build/machine metadata differs')
|
||||
require(json.loads((out / (row['phase'] + '-report.json')).read_text()) == report, 'report copy differs')
|
||||
reports.append(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')
|
||||
profiles.append(row['runtime_profile'])
|
||||
require(len(set(pids)) == len(pids) and len(set(profiles)) == len(profiles), 'reused PID/profile')
|
||||
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')
|
||||
summary = compare_runs(reports)
|
||||
require(receipt['summary'] == summary, 'summary/ranking differs from raw')
|
||||
return summary
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-only three-PID headless DIAG 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_diag 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 ("diag_p28_02.py", "check_diag.py", "verify_diag.py", "test_diag.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("diag_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 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 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 / 'diag_probe.gd'),
|
||||
collector.parse_argv(engine, stage, wrapper)] + [
|
||||
[str(engine), '--headless', '--path', str(stage), '--script', str(wrapper)] for _ in range(3)]
|
||||
|
||||
|
||||
def schedule(rows):
|
||||
for index, label in enumerate(check.PHASES):
|
||||
check.require(all(row['passed'] is True for row in rows), 'previous phase failed')
|
||||
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 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) == check.FIXTURE_SHA and check.sha(engine) == check.ENGINE_SHA,
|
||||
'fixture/engine SHA')
|
||||
check.check_probe((HERE / 'diag_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)
|
||||
# Frozen S must contain the submitted diagnostic too, not just product files.
|
||||
for name in check.LOCAL_NAMES:
|
||||
relative = (HERE / name).relative_to(ROOT).as_posix()
|
||||
committed = subprocess.check_output(['git', '-C', str(ROOT), 'show', commit + ':' + relative])
|
||||
check.require(committed == (HERE / name).read_bytes(), 'diagnostic differs from frozen S: ' + name)
|
||||
worker_pins = {name: check.sha(HERE / name) for name in sorted(check.LOCAL_NAMES)}
|
||||
commands = plan(collector, engine, out)
|
||||
check.require(sys.platform == 'darwin', 'reviewed native platform is macOS')
|
||||
machine = subprocess.check_output(['sysctl', '-n', 'hw.model'], text=True).strip()
|
||||
check.require(machine == 'Mac16,10', 'machine differs from R')
|
||||
metadata = {'commit': commit, 'measured_source': 'S', 'candidate_Q_measured': False,
|
||||
'engine_sha256': check.sha(engine), 'engine_version': collector.VERSION,
|
||||
'machine': machine, 'headless': True, 'source_count': len(source),
|
||||
'source_map_sha256': collector.digest(source), 'worker_pins': worker_pins}
|
||||
admission = {'metadata': metadata, 'source_map': source, 'readonly': inputs, 'planned_argv': commands,
|
||||
'fixture_sha256': check.sha(fixture), 'dependencies': check.DEPENDENCIES,
|
||||
'collector_pins': pinned, '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
|
||||
collector.no_live_engine()
|
||||
player = standard.default_player(ROOT)
|
||||
out.mkdir(parents=True)
|
||||
(out / 'root-input-manifest.json').write_bytes((readonly / 'ROOT_INPUT_MANIFEST.json').read_bytes())
|
||||
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 ('diag_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 = {p.name: check.pin(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.pin(out / 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 + '-diag-startup'
|
||||
runtime_name = 'TheTree-PCK-tests-' + uuid.uuid4().hex + '-diag-runtime'
|
||||
startup, runtime = (standard.profile_path(n) for n in (startup_name, runtime_name))
|
||||
check.require(not startup.exists() and not runtime.exists(), 'profile must be fresh')
|
||||
profiles.extend([startup, runtime])
|
||||
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_DIAG_ROLE': label, 'THE_TREE_DIAG_META': json.dumps(metadata),
|
||||
'THE_TREE_P28_FIXTURE': str(fixture), 'THE_TREE_P28_FIXTURE_SHA': check.FIXTURE_SHA}
|
||||
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,
|
||||
stage_inventory=after_stage,
|
||||
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))
|
||||
raw = (out / label / 'stdout.log').read_text()
|
||||
reports = [s.partition('=')[2] for s in raw.splitlines() if s.startswith('P28_DIAG_REPORT=')]
|
||||
check.require(len(reports) == 1, 'one report required')
|
||||
row['report'] = json.loads(reports[0])
|
||||
collector.write(out / (label + '-report.json'), row['report'])
|
||||
check.bound_raw(out / label, row)
|
||||
check.require(row['source_preserved'] and row['player_preserved'] and
|
||||
check.sha(fixture) == check.FIXTURE_SHA and check.sha(engine) == check.ENGINE_SHA and
|
||||
all(check.pin(out / 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': [],
|
||||
'original_credit': 0, 'formal_completed': 23, 'denominator': 88,
|
||||
'packages_completed': 4, 'package_denominator': 30, 'budget_false_count_unchanged': 6}
|
||||
if receipt['passed']:
|
||||
try:
|
||||
receipt['summary'] = check.compare_runs([row['report'] for row in rows[3:]])
|
||||
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)
|
||||
if receipt['passed']:
|
||||
from verify_diag import seal
|
||||
seal(out)
|
||||
return 0 if receipt['passed'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
extends SceneTree
|
||||
## External passive diagnostic. Root alone executes this through the profile guard.
|
||||
const GameStateScript = preload("res://autoload/game_state.gd")
|
||||
const SERVICES := ["weather", "command_queue", "id_generator", "domain_events", "world_service",
|
||||
"path_service", "mana_network_service", "mana_reserve", "economy_service", "facility_upkeep",
|
||||
"upkeep_settlement", "inventory_service", "kitchen_production", "auto_purchase_plans", "elevators",
|
||||
"legacy_stairs", "logistics_dispatcher", "facility_service", "facility_cleanliness", "guest_service",
|
||||
"staff_service", "progression_service", "relationship_service", "event_service", "authority_service",
|
||||
"defense_service", "construction_service", "excavation_service", "spatial_subslot_service",
|
||||
"route_subslot_path_adapter", "facility_infrastructure_projector", "peeping_room_service"]
|
||||
const K_FIELDS := ["absolute_minute", "speed", "paused", "snapshot_version", "_skip_next_process",
|
||||
"_pause_request_serial", "_abort_current_tick_requested", "_last_tick_advanced"]
|
||||
var clock: Variant
|
||||
var runtime: Variant
|
||||
var main: Variant
|
||||
var report: Dictionary = {}
|
||||
var ticks: Array = []
|
||||
var queries_outside: Array = []
|
||||
var observer: Array = []
|
||||
var saves: Array = []
|
||||
var active: Dictionary = {}
|
||||
var interval := -1
|
||||
var region := "preparation"
|
||||
var next_tick_id := 0
|
||||
var last_completed := -1
|
||||
var connections_before: Dictionary = {}
|
||||
var failed: Array = []
|
||||
var finished := false
|
||||
var cache_preparations: Array = []
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("begin")
|
||||
|
||||
func hashed(data: PackedByteArray) -> String:
|
||||
var context := HashingContext.new()
|
||||
context.start(HashingContext.HASH_SHA256)
|
||||
context.update(data)
|
||||
return context.finish().hex_encode()
|
||||
|
||||
func demand(ok: bool, reason: String) -> void:
|
||||
if not ok:
|
||||
failed.append(reason)
|
||||
|
||||
func kernel_state() -> Dictionary:
|
||||
var result := {}
|
||||
for key: String in K_FIELDS:
|
||||
result[key] = clock.get(key)
|
||||
var f64 := PackedByteArray()
|
||||
f64.resize(8)
|
||||
f64.encode_double(0, float(clock.get("_accumulator")))
|
||||
result["_accumulator_f64_hex"] = f64.hex_encode()
|
||||
var handlers: Dictionary = clock.get("_phase_handlers")
|
||||
var handler_ids := {}
|
||||
for phase: int in range(11):
|
||||
var names: Array = []
|
||||
for key: Variant in handlers.get(phase, {}):
|
||||
names.append(str(key))
|
||||
names.sort()
|
||||
handler_ids[str(phase)] = names
|
||||
result["handlers"] = handler_ids
|
||||
var budget: Variant = clock.get("performance_budget")
|
||||
for key: String in ["_sample_counts", "_totals", "_samples"]:
|
||||
result[key] = budget.get(key).duplicate(true)
|
||||
return result
|
||||
|
||||
func cache_state() -> Dictionary:
|
||||
var catalog: Variant = runtime.get("_mvp_manifest_cache_catalog")
|
||||
var current: Variant = runtime.get("mvp_content_catalog")
|
||||
var value: Dictionary = runtime.get("_mvp_manifest_cache")
|
||||
return {"generation": runtime.get("_mvp_manifest_cache_generation"),
|
||||
"catalog_id": str(catalog.get_instance_id()) if catalog != null else null,
|
||||
"current_catalog_id": str(current.get_instance_id()), "current_generation": current.get_manifest_generation(),
|
||||
"bytes_sha256": hashed(var_to_bytes(value)),
|
||||
"filled": catalog == current and runtime.get("_mvp_manifest_cache_generation") == current.get_manifest_generation()}
|
||||
|
||||
func ensure_cache(label: String) -> void:
|
||||
var before := cache_state()
|
||||
if not before.filled:
|
||||
runtime.create_snapshot()
|
||||
cache_preparations.append({"label": label, "before": before, "after": cache_state(),
|
||||
"filled_outside_sample": not before.filled})
|
||||
demand(cache_state().filled, "cache preparation failed")
|
||||
|
||||
func layers() -> Dictionary:
|
||||
# Read C/K before the S observation so a cache mutation cannot be hidden.
|
||||
var k := kernel_state()
|
||||
var c := cache_state()
|
||||
var snapshot: Dictionary = runtime.create_snapshot()
|
||||
var s := var_to_bytes(snapshot)
|
||||
demand(var_to_bytes(k) == var_to_bytes(kernel_state()), "state witness changed K")
|
||||
demand(var_to_bytes(c) == var_to_bytes(cache_state()), "state witness changed C")
|
||||
return {"S": hashed(s), "K": hashed(var_to_bytes(k)), "C": hashed(var_to_bytes(c)),
|
||||
"kernel": k, "cache": c, "snapshot": snapshot, "s_bytes": s,
|
||||
"k_bytes": var_to_bytes(k), "c_bytes": var_to_bytes(c)}
|
||||
|
||||
func brief(value: Dictionary) -> Dictionary:
|
||||
return {"S": value.S, "K": value.K, "C": value.C, "kernel": value.kernel, "cache": value.cache}
|
||||
|
||||
func typed(value: Variant) -> Variant:
|
||||
if value is Dictionary:
|
||||
var keys: Array = value.keys()
|
||||
keys.sort_custom(func(a: Variant, b: Variant) -> bool: return str(a) < str(b))
|
||||
var pairs: Array = []
|
||||
for index: int in range(keys.size()):
|
||||
if index > 0:
|
||||
demand(str(keys[index - 1]) != str(keys[index]), "typed key collision")
|
||||
pairs.append([typed(keys[index]), typed(value[keys[index]])])
|
||||
return [typeof(value), pairs]
|
||||
if value is Array:
|
||||
var children: Array = []
|
||||
for child: Variant in value:
|
||||
children.append(typed(child))
|
||||
return [typeof(value), children]
|
||||
return [typeof(value), var_to_bytes(value).hex_encode()]
|
||||
|
||||
func diff_paths(a: Variant, b: Variant, path: String = "$") -> Array:
|
||||
if var_to_bytes(a) == var_to_bytes(b):
|
||||
return []
|
||||
var result: Array = []
|
||||
if a is Dictionary and b is Dictionary:
|
||||
if var_to_bytes(typed(a)) == var_to_bytes(typed(b)):
|
||||
result.append({"path": path, "kind": "dictionary_enumeration_order_only"})
|
||||
var keys: Array = a.keys()
|
||||
for key: Variant in b:
|
||||
if not keys.has(key):
|
||||
keys.append(key)
|
||||
for key: Variant in keys:
|
||||
var child_path := path + "/" + str(typeof(key)) + ":" + str(key)
|
||||
if not a.has(key) or not b.has(key):
|
||||
result.append({"path": child_path, "kind": "missing_key"})
|
||||
else:
|
||||
result.append_array(diff_paths(a[key], b[key], child_path))
|
||||
elif a is Array and b is Array and a.size() == b.size():
|
||||
for index: int in range(a.size()):
|
||||
result.append_array(diff_paths(a[index], b[index], path + "/" + str(index)))
|
||||
else:
|
||||
result.append({"path": path, "kind": "type_or_value"})
|
||||
return result
|
||||
|
||||
func comparison(before: Dictionary, after: Dictionary) -> Dictionary:
|
||||
var equal := {"S": before.s_bytes == after.s_bytes, "K": before.k_bytes == after.k_bytes,
|
||||
"C": before.c_bytes == after.c_bytes}
|
||||
var differences: Array = []
|
||||
if not equal.S:
|
||||
differences = diff_paths(before.snapshot, after.snapshot)
|
||||
var rebuilt_matches: Variant = null
|
||||
if not equal.C:
|
||||
# Public catalog reads reconstruct the exact finite manifest fields from
|
||||
# simulation_runtime.gd:11201-11214; never call its private builder.
|
||||
var catalog: Variant = runtime.get("mvp_content_catalog")
|
||||
var guest_ids: Array[String] = []
|
||||
for definition: GuestDefinition in catalog.build_all_guest_definitions():
|
||||
guest_ids.append(String(definition.guest_type_id))
|
||||
var expected := {&"manifest_version": SimulationRuntimeService.MVP_CONTENT_MANIFEST_VERSION,
|
||||
&"schema_version": MvpContentCatalog.SCHEMA_VERSION,
|
||||
&"guest_class_ids": string_names(catalog.get_guest_class_ids()),
|
||||
&"species_ids": string_names(catalog.get_species_ids()),
|
||||
&"staff_candidate_ids": string_names(catalog.get_staff_candidate_ids()),
|
||||
&"unique_character_ids": string_names(catalog.get_unique_character_ids()),
|
||||
&"guest_definition_ids": guest_ids}
|
||||
rebuilt_matches = var_to_bytes(expected) == var_to_bytes(runtime.get("_mvp_manifest_cache"))
|
||||
demand(rebuilt_matches, "cache differs from current catalog manifest")
|
||||
demand(equal.S and equal.K and equal.C, "S/K/C changed outside tick")
|
||||
return {"before": brief(before), "after": brief(after), "equal": equal,
|
||||
"differences": differences, "cache_miss_in_sample": not equal.C,
|
||||
"cache_rebuilt_matches_catalog": rebuilt_matches,
|
||||
"cache_rebuild_check_reason": "no_cache_miss" if equal.C else null}
|
||||
|
||||
func string_names(values: Array[StringName]) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
for value: StringName in values:
|
||||
result.append(String(value))
|
||||
return result
|
||||
|
||||
func signal_inventory() -> Dictionary:
|
||||
var result := {}
|
||||
var signals: Array = [clock.tick_started, clock.tick_phase_started, clock.tick_completed,
|
||||
runtime.path_service.path_query_measured, runtime.save_operation_completed]
|
||||
for sig: Signal in signals:
|
||||
var items: Array = []
|
||||
for connection: Dictionary in sig.get_connections():
|
||||
var callback: Callable = connection.callable
|
||||
items.append({"object": str(callback.get_object_id()), "method": str(callback.get_method()),
|
||||
"flags": connection.flags})
|
||||
items.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return JSON.stringify(a) < JSON.stringify(b))
|
||||
result[str(sig.get_name())] = {"count": items.size(), "targets": items}
|
||||
return result
|
||||
|
||||
func observe_start(minute: int) -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
next_tick_id += 1
|
||||
demand(active.is_empty(), "overlapping ticks")
|
||||
active = {"id": next_tick_id, "region": region, "start_minute": minute, "edges": [], "queries": [],
|
||||
"frame_wall_before_us": started - last_completed if last_completed >= 0 else null}
|
||||
active.edges.append({"name": "tick_started", "minute": minute, "at_us": started})
|
||||
interval = 0
|
||||
observer.append({"callback": "tick_started", "tick_id": next_tick_id, "region": region,
|
||||
"duration_us": Time.get_ticks_usec() - started})
|
||||
|
||||
func observe_phase(phase: int, minute: int) -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
demand(not active.is_empty(), "phase without tick")
|
||||
active.edges.append({"name": "phase", "phase": phase, "minute": minute, "at_us": started})
|
||||
interval = phase + 1
|
||||
observer.append({"callback": "tick_phase_started", "tick_id": next_tick_id, "region": region,
|
||||
"duration_us": Time.get_ticks_usec() - started})
|
||||
|
||||
func observe_completed(minute: int) -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
active.edges.append({"name": "tick_completed", "minute": minute, "at_us": started})
|
||||
active["end_minute"] = minute
|
||||
ticks.append(active)
|
||||
active = {}
|
||||
interval = -1
|
||||
last_completed = started
|
||||
observer.append({"callback": "tick_completed", "tick_id": next_tick_id, "region": region,
|
||||
"duration_us": Time.get_ticks_usec() - started})
|
||||
|
||||
func observe_query(duration_us: int) -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
var item := {"duration_us": duration_us, "at_us": started, "tick_id": next_tick_id if interval >= 0 else null,
|
||||
"interval": interval if interval >= 0 else null, "region": region}
|
||||
if interval >= 0:
|
||||
active.queries.append(item)
|
||||
else:
|
||||
queries_outside.append(item)
|
||||
observer.append({"callback": "path_query_measured", "tick_id": item.tick_id, "region": region,
|
||||
"duration_us": Time.get_ticks_usec() - started})
|
||||
|
||||
func observe_save(result: Dictionary) -> void:
|
||||
var started := Time.get_ticks_usec()
|
||||
saves.append({"at_us": started, "tick_id": next_tick_id if interval >= 0 else null,
|
||||
"interval": interval if interval >= 0 else null, "region": region, "result": result.duplicate(true)})
|
||||
observer.append({"callback": "save_operation_completed", "tick_id": next_tick_id if interval >= 0 else null,
|
||||
"region": region, "duration_us": Time.get_ticks_usec() - started})
|
||||
|
||||
func step_window(label: String) -> void:
|
||||
region = label
|
||||
for index: int in range(30):
|
||||
var before: int = clock.absolute_minute
|
||||
demand(clock.paused, "clock resumed")
|
||||
clock.advance_minutes(1)
|
||||
demand(clock.absolute_minute == before + 1 and clock.paused, "manual step did not advance one minute paused")
|
||||
await process_frame
|
||||
demand(clock.absolute_minute == before + 1 and clock.paused, "inter-tick frame advanced clock")
|
||||
if Time.get_ticks_msec() > 1740000 or not failed.is_empty():
|
||||
failed.append("window stopped")
|
||||
return
|
||||
|
||||
func snapshot_samples(name: String, target: Variant) -> void:
|
||||
var rows: Array = []
|
||||
for index: int in range(5):
|
||||
var before := layers()
|
||||
var started := Time.get_ticks_usec()
|
||||
var value: Dictionary = target.create_snapshot()
|
||||
var duration := Time.get_ticks_usec() - started
|
||||
var after := layers()
|
||||
var row := comparison(before, after)
|
||||
row["duration_us"] = duration
|
||||
row["result_sha256"] = hashed(var_to_bytes(value))
|
||||
rows.append(row)
|
||||
report.measurements[name] = {"samples": rows, "reason": null, "scope": "independent_inclusive"}
|
||||
|
||||
func scratch_samples() -> void:
|
||||
for operation: String in ["deep_copy", "envelope_validate", "envelope_restore", "save", "load"]:
|
||||
var rows: Array = []
|
||||
for index: int in range(5):
|
||||
var before := layers()
|
||||
var scratch: Dictionary = before.snapshot.duplicate(true)
|
||||
var scratch_clock := SimulationKernel.new()
|
||||
var scratch_state := GameStateScript.new()
|
||||
var ids := [scratch_clock.get_instance_id(), scratch_state.get_instance_id(), scratch_clock.performance_budget.get_instance_id()]
|
||||
var path := "user://p28_diag/scratch/" + operation + "-" + str(index)
|
||||
if operation == "load":
|
||||
var prepared: Dictionary = SaveService.save_snapshot(path, scratch)
|
||||
demand(prepared.get(&"ok", false), "scratch load preparation failed")
|
||||
var started := Time.get_ticks_usec()
|
||||
var result: Variant
|
||||
match operation:
|
||||
"deep_copy": result = scratch.duplicate(true)
|
||||
"envelope_validate": result = SimulationSnapshot.validate(scratch)
|
||||
"envelope_restore": result = SimulationSnapshot.restore(scratch, scratch_clock, scratch_state)
|
||||
"save": result = SaveService.save_snapshot(path, scratch)
|
||||
"load": result = SaveService.load_snapshot(path)
|
||||
var duration := Time.get_ticks_usec() - started
|
||||
var ok: bool = result if result is bool else true if operation == "deep_copy" else result.get(&"ok", false)
|
||||
demand(ok, "scratch operation failed: " + operation)
|
||||
# Mutate only detached caller-owned containers, then destroy scratch objects.
|
||||
scratch[&"game_state"][&"resources"][&"gold"] = -987654
|
||||
if result is Dictionary:
|
||||
result[&"diag_detached_sentinel"] = true
|
||||
result = null
|
||||
scratch = {}
|
||||
scratch_clock.free()
|
||||
scratch_state.free()
|
||||
scratch_clock = null
|
||||
scratch_state = null
|
||||
var released: Array = []
|
||||
var id_text: Array = []
|
||||
for id: int in ids:
|
||||
id_text.append(str(id))
|
||||
released.append(not is_instance_id_valid(id))
|
||||
demand(not is_instance_id_valid(id), "scratch object retained")
|
||||
var after := layers()
|
||||
var row := comparison(before, after)
|
||||
row.merge({"duration_us": duration, "ok": ok, "scratch_path": path, "scratch_ids": id_text,
|
||||
"released": released, "input": "live_snapshot_deep_copy", "detached_mutation_checked": true})
|
||||
rows.append(row)
|
||||
report.measurements[operation] = {"samples": rows, "reason": null, "scope": "independent_inclusive"}
|
||||
report.measurements["runtime_restore"] = {"samples": null, "reason": "Runtime restore requires initialize(clock,state,seed) before restore; initialize is outside the listed scratch restore/save/load/validator API set. Envelope restore above does not restore Runtime systems.", "scope": "unmeasured"}
|
||||
report.measurements["runtime_validation_internal"] = {"samples": null, "reason": "Full Runtime validation is internal to restore; no public standalone validator. Envelope validation is measured separately and is not Runtime validation.", "scope": "unmeasured"}
|
||||
report.measurements["connector_rebuild"] = {"samples": null, "reason": "path_query_measured observes queries only; no passive connector rebuild timing signal.", "scope": "unmeasured"}
|
||||
|
||||
func begin() -> void:
|
||||
clock = root.get_node("GameClock")
|
||||
runtime = root.get_node("SimulationRuntime")
|
||||
report = {"schema": "p28-02-diag-v1", "role": OS.get_environment("THE_TREE_DIAG_ROLE"),
|
||||
"pid": OS.get_process_id(), "metadata": JSON.parse_string(OS.get_environment("THE_TREE_DIAG_META")),
|
||||
"semantics": {"intervals": "inclusive_wall_including_observer", "observer_subtracted": false,
|
||||
"breakdown": "independent_inclusive_not_additive", "path": "query_only_not_connector"},
|
||||
"observer_note": "Callback wall through final timestamp; excludes appending its own timing record and return. Included in intervals, never subtracted.",
|
||||
"measurements": {}, "preparation": [], "baselines": {}, "seed": str(runtime.world_seed)}
|
||||
var k0 := kernel_state()
|
||||
clock.set_paused(true)
|
||||
report.preparation.append({"operation": "initial_pause", "K_before": k0, "K_after": kernel_state()})
|
||||
var path := OS.get_environment("THE_TREE_P28_FIXTURE")
|
||||
report["input_sha256"] = FileAccess.get_sha256(path)
|
||||
if report.input_sha256 != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"):
|
||||
failed.append("fixture SHA")
|
||||
finish()
|
||||
return
|
||||
var input := FileAccess.open(path, FileAccess.READ)
|
||||
if input == null:
|
||||
failed.append("fixture open")
|
||||
finish()
|
||||
return
|
||||
var snapshot: Variant = input.get_var(false)
|
||||
input.close()
|
||||
var restored: bool = runtime.restore_snapshot(snapshot)
|
||||
demand(restored, "fixture restore")
|
||||
var restored_cache := cache_state()
|
||||
ensure_cache("restored")
|
||||
report.baselines["restored"] = brief(layers())
|
||||
report.preparation.append({"operation": "fixture_restore", "ok": restored, "cache_before_snapshot": restored_cache,
|
||||
"state": report.baselines.restored})
|
||||
k0 = kernel_state()
|
||||
clock.set_paused(true)
|
||||
report.preparation.append({"operation": "restored_pause", "K_before": k0, "K_after": kernel_state()})
|
||||
var before_main := brief(layers())
|
||||
main = load("res://scenes/main/main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
var after_main := brief(layers())
|
||||
await process_frame
|
||||
var frame1 := brief(layers())
|
||||
await process_frame
|
||||
var frame2 := brief(layers())
|
||||
report["startup"] = {"before_main": before_main, "after_main": after_main, "frame1": frame1, "frame2": frame2,
|
||||
"save_policy": runtime.get_save_policy(), "ops": main.get("_operations_start_report").duplicate(true)}
|
||||
demand(report.startup.ops.get(&"code", "") == "INVALID_START_TIME", "unexpected startup Ops result")
|
||||
demand(clock.paused and clock.absolute_minute == 602, "B0 clock")
|
||||
report.baselines["B0"] = brief(layers())
|
||||
connections_before = signal_inventory()
|
||||
report["observer_before"] = signal_inventory()
|
||||
clock.tick_started.connect(observe_start)
|
||||
clock.tick_phase_started.connect(observe_phase)
|
||||
clock.tick_completed.connect(observe_completed)
|
||||
runtime.path_service.path_query_measured.connect(observe_query)
|
||||
runtime.save_operation_completed.connect(observe_save)
|
||||
report["observer_connected"] = signal_inventory()
|
||||
await step_window("warm")
|
||||
ensure_cache("B1")
|
||||
report.baselines["B1"] = brief(layers())
|
||||
demand(clock.absolute_minute == 632, "B1 minute")
|
||||
if failed.is_empty():
|
||||
await step_window("measurement")
|
||||
region = "outside_tick"
|
||||
ensure_cache("outside_tick")
|
||||
var before := layers()
|
||||
report.baselines["outside_before"] = brief(before)
|
||||
report["seed"] = str(runtime.world_seed)
|
||||
var rng := {}
|
||||
for key: Variant in runtime.get("_rng_streams"):
|
||||
# Preserve int64 state without JSON floating point truncation.
|
||||
rng[str(key)] = var_to_bytes(runtime.get("_rng_streams")[key].snapshot()).hex_encode()
|
||||
report["rng_state_typed_hex"] = rng
|
||||
if failed.is_empty():
|
||||
snapshot_samples("runtime_snapshot", runtime)
|
||||
snapshot_samples("game_state_snapshot", root.get_node("GameState"))
|
||||
for service: String in SERVICES:
|
||||
snapshot_samples(service + "_snapshot", runtime.get(service))
|
||||
scratch_samples()
|
||||
# Test ownership of the returned manifest without mutating live state.
|
||||
var detached: Dictionary = runtime.create_snapshot()
|
||||
detached[&"systems"][&"runtime"][&"catalog_manifest"][&"diag_detached_sentinel"] = true
|
||||
detached[&"systems"][&"runtime"][&"catalog_manifest"][&"guest_class_ids"].append("diag_detached_sentinel")
|
||||
report["manifest_detached"] = comparison(before, layers())
|
||||
report["outside_total"] = comparison(before, layers())
|
||||
clock.tick_started.disconnect(observe_start)
|
||||
clock.tick_phase_started.disconnect(observe_phase)
|
||||
clock.tick_completed.disconnect(observe_completed)
|
||||
runtime.path_service.path_query_measured.disconnect(observe_query)
|
||||
runtime.save_operation_completed.disconnect(observe_save)
|
||||
report["observer_disconnected"] = signal_inventory()
|
||||
demand(report.observer_before == report.observer_disconnected, "observer connections not restored")
|
||||
report["all_connections_restored"] = connections_before == signal_inventory()
|
||||
demand(report.all_connections_restored, "save observer not restored")
|
||||
demand(clock.paused and clock.absolute_minute == 662, "final clock")
|
||||
finish()
|
||||
|
||||
func finish() -> void:
|
||||
if finished:
|
||||
return
|
||||
finished = true
|
||||
report.merge({"ticks": ticks, "queries_outside": queries_outside, "observer": observer, "save_events": saves,
|
||||
"cache_preparations": cache_preparations, "errors": failed, "stop_reason": "complete" if failed.is_empty() else "failed"})
|
||||
print("P28_DIAG_REPORT=" + JSON.stringify(report))
|
||||
if is_instance_valid(main):
|
||||
main.ui_audio_window_close_drained.connect(audio_close, CONNECT_ONE_SHOT)
|
||||
root.close_requested.emit()
|
||||
else:
|
||||
quit(1)
|
||||
|
||||
func audio_close(result: Dictionary) -> void:
|
||||
print("P28_DIAG_AUDIO_CLOSE=" + JSON.stringify(result))
|
||||
@@ -0,0 +1,339 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Engine-free tests. All report values below are synthetic, never native evidence."""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
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_diag as check
|
||||
import diag_p28_02 as driver
|
||||
import verify_diag as verify
|
||||
|
||||
H = 'a' * 64
|
||||
|
||||
|
||||
def layers(minute=662):
|
||||
kernel = {name: False for name in check.K_FIELDS}
|
||||
kernel.update(absolute_minute=minute, speed=1, paused=True, snapshot_version=minute,
|
||||
_pause_request_serial=2, _accumulator_f64_hex='0000000000000000',
|
||||
handlers={str(i): ['handler'] for i in range(11)}, _sample_counts={}, _totals={}, _samples={})
|
||||
return {'S': H, 'K': H, 'C': H, 'kernel': kernel,
|
||||
'cache': {'generation': 1, 'current_generation': 1, 'catalog_id': '10', 'current_catalog_id': '10',
|
||||
'bytes_sha256': H, 'filled': True}}
|
||||
|
||||
|
||||
def comparison():
|
||||
return {'before': layers(), 'after': layers(), 'equal': {'S': True, 'K': True, 'C': True},
|
||||
'differences': [], 'cache_miss_in_sample': False, 'cache_rebuilt_matches_catalog': None,
|
||||
'cache_rebuild_check_reason': 'no_cache_miss'}
|
||||
|
||||
|
||||
def synthetic_report(pid=1, role='diag-1'):
|
||||
connections = {name: {'count': 0, 'targets': []} for name in
|
||||
('tick_started', 'tick_phase_started', 'tick_completed', 'path_query_measured',
|
||||
'save_operation_completed')}
|
||||
connected = copy.deepcopy(connections)
|
||||
for name in connected:
|
||||
connected[name] = {'count': 1, 'targets': [{'object': '1', 'method': 'observer', 'flags': 0}]}
|
||||
result = {'schema': 'p28-02-diag-v1', 'role': role, 'pid': pid, 'errors': [], 'stop_reason': 'complete',
|
||||
'semantics': copy.deepcopy(check.SEMANTICS), 'input_sha256': check.FIXTURE_SHA,
|
||||
'metadata': {'engine_sha256': check.ENGINE_SHA, 'engine_version': '4.7.2.stable.official.ed1daf0bf',
|
||||
'machine': 'Mac16,10', 'headless': True, 'measured_source': 'S',
|
||||
'candidate_Q_measured': False, 'commit': 'b' * 40},
|
||||
'seed': '123', 'rng_state_typed_hex': {'world': '0001'},
|
||||
'baselines': {name: layers(minute) for name, minute in
|
||||
[('restored', 602), ('B0', 602), ('B1', 632), ('outside_before', 662)]},
|
||||
'startup': {name: layers(602) for name in ('before_main', 'after_main', 'frame1', 'frame2')},
|
||||
'preparation': [], 'ticks': [], 'queries_outside': [], 'observer': [], 'save_events': [],
|
||||
'measurements': {}, 'outside_total': comparison(), 'manifest_detached': comparison(),
|
||||
'observer_before': connections, 'observer_connected': connected,
|
||||
'observer_disconnected': copy.deepcopy(connections), 'all_connections_restored': True,
|
||||
'cache_preparations': [{'label': name, 'before': layers()['cache'], 'after': layers()['cache'],
|
||||
'filled_outside_sample': False} for name in ('restored', 'B1', 'outside_tick')]}
|
||||
result['startup']['ops'] = {'code': 'INVALID_START_TIME'}
|
||||
result['startup']['save_policy'] = {'enabled': True}
|
||||
for operation in ('initial_pause', 'fixture_restore', 'restored_pause'):
|
||||
if operation == 'fixture_restore':
|
||||
row = {'operation': operation, 'ok': True, 'state': layers(602), 'cache_before_snapshot': layers()['cache']}
|
||||
else:
|
||||
row = {'operation': operation, 'K_before': layers()['kernel'], 'K_after': layers()['kernel']}
|
||||
row['K_before']['_pause_request_serial'] = 1
|
||||
result['preparation'].append(row)
|
||||
for index in range(60):
|
||||
minute, at, region = 602 + index, index * 200, 'warm' if index < 30 else 'measurement'
|
||||
edges = [{'name': 'tick_started', 'minute': minute, 'at_us': at}]
|
||||
edges += [{'name': 'phase', 'phase': phase, 'minute': minute + (phase != 0),
|
||||
'at_us': at + 10 * (phase + 1)} for phase in range(11)]
|
||||
edges.append({'name': 'tick_completed', 'minute': minute + 1, 'at_us': at + 120})
|
||||
query = {'duration_us': 0, 'at_us': at + 115, 'interval': 11, 'tick_id': index + 1, 'region': region}
|
||||
result['ticks'].append({'id': index + 1, 'region': region, 'start_minute': minute, 'end_minute': minute + 1,
|
||||
'edges': edges, 'queries': [query], 'frame_wall_before_us': 80 if index else None})
|
||||
for callback in ['tick_started'] + ['tick_phase_started'] * 11 + ['tick_completed', 'path_query_measured']:
|
||||
result['observer'].append({'callback': callback, 'tick_id': index + 1, 'region': region, 'duration_us': 0})
|
||||
snapshots = {'runtime_snapshot', 'game_state_snapshot'} | {s + '_snapshot' for s in check.SERVICES}
|
||||
for name in snapshots | check.SCRATCH:
|
||||
samples = []
|
||||
for index in range(5):
|
||||
row = comparison()
|
||||
row['duration_us'] = index
|
||||
if name in snapshots:
|
||||
row['result_sha256'] = H
|
||||
else:
|
||||
row.update(ok=True, input='live_snapshot_deep_copy', detached_mutation_checked=True,
|
||||
scratch_path='user://p28_diag/scratch/' + name + '-' + str(index),
|
||||
scratch_ids=['100', '101', '102'], released=[True] * 3)
|
||||
samples.append(row)
|
||||
result['measurements'][name] = {'samples': samples, 'reason': None, 'scope': 'independent_inclusive'}
|
||||
for name in check.UNMEASURED:
|
||||
result['measurements'][name] = {'samples': None, 'reason': 'synthetic unmeasurable public boundary',
|
||||
'scope': 'unmeasured'}
|
||||
return result
|
||||
|
||||
|
||||
class DiagnosticTests(unittest.TestCase):
|
||||
def reject(self, mutate):
|
||||
report = synthetic_report()
|
||||
mutate(report)
|
||||
with self.assertRaises((ValueError, KeyError, TypeError)):
|
||||
check.check_report(report)
|
||||
|
||||
def test_valid_raw_intervals(self):
|
||||
summary = check.check_report(synthetic_report())
|
||||
self.assertEqual(summary['intervals']['I0'], {'count': 30, 'mean_us': 10, 'p95_us': 10, 'max_us': 10})
|
||||
self.assertEqual(summary['query_only']['I11']['count'], 30)
|
||||
self.assertEqual(summary['query_only']['I11']['total_us'], 0)
|
||||
self.assertIsNone(summary['query_only']['I0']['stats'])
|
||||
self.assertEqual(summary['inter_tick_frame_wall']['mean_us'], 80)
|
||||
|
||||
def test_tick_id_and_minute_rejection(self):
|
||||
for mutate in (lambda r: r['ticks'][0]['edges'][2].update(minute=602),
|
||||
lambda r: r['ticks'][1].update(id=1),
|
||||
lambda r: r['ticks'][0]['edges'].pop(2),
|
||||
lambda r: r['ticks'][30].update(region='warm'),
|
||||
lambda r: r['ticks'][30].update(frame_wall_before_us=0),
|
||||
lambda r: r['ticks'].pop()):
|
||||
with self.subTest(mutate=mutate):
|
||||
self.reject(mutate)
|
||||
|
||||
def test_queries_and_observer_rejection(self):
|
||||
for mutate in (lambda r: r['ticks'][0]['queries'][0].update(interval=10),
|
||||
lambda r: r['ticks'][0]['queries'][0].update(tick_id=2),
|
||||
lambda r: r['ticks'][0]['queries'][0].update(duration_us=None),
|
||||
lambda r: r['observer'].pop(0),
|
||||
lambda r: r['observer'][0].update(duration_us=-1),
|
||||
lambda r: r['queries_outside'].append({'tick_id': 1, 'interval': None, 'duration_us': 0})):
|
||||
with self.subTest(mutate=mutate):
|
||||
self.reject(mutate)
|
||||
|
||||
def test_nonadditive_scope(self):
|
||||
for key, value in [('intervals', 'exclusive'), ('observer_subtracted', True),
|
||||
('breakdown', 'sum_to_100_percent'), ('path', 'connector')]:
|
||||
with self.subTest(key=key):
|
||||
self.reject(lambda r: r['semantics'].update({key: value}))
|
||||
|
||||
def test_state_and_cache_mismatch(self):
|
||||
for layer in ('S', 'K', 'C'):
|
||||
self.reject(lambda r: r['outside_total']['equal'].update({layer: False}))
|
||||
self.reject(lambda r: r['outside_total']['after'].update({layer: 'b' * 64}))
|
||||
self.reject(lambda r: r['outside_total'].update(cache_miss_in_sample=True))
|
||||
self.reject(lambda r: r['outside_total']['before']['cache'].update(filled=False))
|
||||
self.reject(lambda r: r['manifest_detached']['after']['kernel'].update(_samples={'frame': [1]}))
|
||||
self.reject(lambda r: r['outside_total']['before']['kernel'].pop('_accumulator_f64_hex'))
|
||||
|
||||
def test_missing_not_zero(self):
|
||||
self.assertEqual(check.stats([0] * 5)['mean_us'], 0)
|
||||
for value in ([], [None], [True], [-1], [float('nan')]):
|
||||
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||
check.stats(value)
|
||||
self.reject(lambda r: r['measurements']['runtime_restore'].update(samples=[0] * 5))
|
||||
self.reject(lambda r: r['measurements']['runtime_restore'].update(reason=''))
|
||||
self.reject(lambda r: r['measurements']['save']['samples'].pop())
|
||||
|
||||
def test_scratch_ownership(self):
|
||||
for mutate in (lambda s: s.update(input='live'), lambda s: s.update(released=[True, False, True]),
|
||||
lambda s: s.update(scratch_path='user://saves/player'),
|
||||
lambda s: s.update(detached_mutation_checked=False), lambda s: s.update(ok=False)):
|
||||
self.reject(lambda r: mutate(r['measurements']['save']['samples'][0]))
|
||||
|
||||
def test_connections_and_preparation(self):
|
||||
self.reject(lambda r: r.update(all_connections_restored=False))
|
||||
self.reject(lambda r: r['observer_disconnected']['tick_started'].update(count=1))
|
||||
self.reject(lambda r: r['baselines']['B1']['kernel'].update(absolute_minute=631))
|
||||
self.reject(lambda r: r['preparation'][2]['K_after'].update(_pause_request_serial=1))
|
||||
self.reject(lambda r: r['startup']['ops'].update(code='OK'))
|
||||
|
||||
def test_three_pid_comparison(self):
|
||||
reports = [synthetic_report(i + 1, 'diag-' + str(i + 1)) for i in range(3)]
|
||||
self.assertTrue(check.compare_runs(reports)['rankings_agree'])
|
||||
reports[1]['pid'] = 1
|
||||
with self.assertRaises(ValueError):
|
||||
check.compare_runs(reports)
|
||||
reports[1]['pid'] = 2
|
||||
reports[1]['baselines']['B0']['S'] = 'b' * 64
|
||||
with self.assertRaisesRegex(ValueError, 'preparation mismatch'):
|
||||
check.compare_runs(reports)
|
||||
|
||||
def test_pid_local_catalog_identity(self):
|
||||
a, b = synthetic_report(), synthetic_report(2, 'diag-2')
|
||||
b['baselines']['B0']['cache'].update(catalog_id='20', current_catalog_id='20')
|
||||
b['baselines']['B0']['C'] = 'b' * 64
|
||||
self.assertEqual(check.preparation_key(a), check.preparation_key(b))
|
||||
b['baselines']['B0']['cache']['bytes_sha256'] = 'c' * 64
|
||||
self.assertNotEqual(check.preparation_key(a), check.preparation_key(b))
|
||||
|
||||
def test_cross_pid_only_named_kernel_fields(self):
|
||||
a, b = synthetic_report(), synthetic_report(2, 'diag-2')
|
||||
b['baselines']['B1']['kernel']['_totals'] = {'path_query': 999}
|
||||
b['baselines']['B1']['kernel']['_samples'] = {'path_query': [999]}
|
||||
b['baselines']['B1']['K'] = 'b' * 64
|
||||
self.assertEqual(check.preparation_key(a), check.preparation_key(b))
|
||||
b['baselines']['B1']['kernel']['snapshot_version'] += 1
|
||||
self.assertNotEqual(check.preparation_key(a), check.preparation_key(b))
|
||||
|
||||
def test_fixture_pause_and_int64_identity(self):
|
||||
report = synthetic_report()
|
||||
report['baselines']['restored']['kernel']['paused'] = False
|
||||
report['preparation'][1]['state']['kernel']['paused'] = False
|
||||
report['baselines']['B0']['cache'].update(catalog_id='-9223372000000000001',
|
||||
current_catalog_id='-9223372000000000001')
|
||||
check.check_report(report)
|
||||
self.reject(lambda r: r['baselines']['B0']['kernel'].update(paused=False))
|
||||
|
||||
def test_static_probe_and_product_mutations(self):
|
||||
source = (check.HERE / 'diag_probe.gd').read_text()
|
||||
check.check_probe(source)
|
||||
mutations = [source.replace('clock.set_paused(true)', 'clock.set_paused(false)', 1),
|
||||
source.replace('clock.advance_minutes(1)', 'clock.advance_minutes(2)'),
|
||||
source + '\nfunc illegal():\n\tclock.advance_minutes(1)\n',
|
||||
source + '\nfunc illegal():\n\truntime.initialize(clock, null)\n',
|
||||
source + '\nfunc illegal():\n\truntime.spawn_guest()\n',
|
||||
source + '\nfunc illegal():\n\truntime.set_script(null)\n',
|
||||
source + '\nfunc illegal():\n\tclock.paused = false\n',
|
||||
source + '\nfunc illegal():\n\truntime.economy_service.spend({})\n',
|
||||
source + '\nfunc illegal():\n\tvar alias = runtime\n\talias.restore_snapshot({})\n',
|
||||
source.replace('user://p28_diag/scratch/', 'user://saves/'),
|
||||
source + '\nfunc illegal():\n\truntime.restore_snapshot({})\n',
|
||||
source + '\nfunc illegal():\n\tSaveService.load_snapshot("user://x")\n']
|
||||
for mutation in mutations:
|
||||
with self.subTest(mutation=mutation[-100:]), self.assertRaises(ValueError):
|
||||
check.check_probe(mutation)
|
||||
|
||||
def test_service_snapshot_inventory_matches_current_product(self):
|
||||
source = (check.ROOT / 'autoload/simulation_runtime.gd').read_text()
|
||||
body = source.split('func create_snapshot() -> Dictionary:', 1)[1].split('\nfunc restore_snapshot', 1)[0]
|
||||
names = set(re.findall(r'\b(\w+)\.create_snapshot\(\)', body))
|
||||
self.assertEqual(names, set(check.SERVICES))
|
||||
|
||||
def test_source_map_count_and_mutations(self):
|
||||
source = {'scripts/f%d.gd' % i: {'bytes': 1, 'sha256': H} for i in range(1407)}
|
||||
check.check_source_maps(source, copy.deepcopy(source))
|
||||
for mutation in (lambda m: m.pop('scripts/f0.gd'),
|
||||
lambda m: m['scripts/f0.gd'].update(sha256='b' * 64)):
|
||||
after = copy.deepcopy(source)
|
||||
mutation(after)
|
||||
with self.assertRaises(ValueError):
|
||||
check.check_source_maps(source, after)
|
||||
|
||||
def test_import_closure_and_pins(self):
|
||||
check.check_dependencies()
|
||||
for name in check.LOCAL_NAMES - {'diag_probe.gd'}:
|
||||
check.check_imports((check.HERE / name).read_text())
|
||||
for text in ('import unauthorized_project_module', 'from restart_pair import main',
|
||||
'eval("1")', '__import__("os")',
|
||||
'importlib.util.spec_from_file_location("x", "other.py")'):
|
||||
with self.subTest(text=text), self.assertRaises(ValueError):
|
||||
check.check_imports(text)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
for name in check.DEPENDENCIES:
|
||||
target = root / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes((check.ROOT / name).read_bytes())
|
||||
check.check_dependencies(root)
|
||||
(root / next(iter(check.DEPENDENCIES))).write_text('changed')
|
||||
with self.assertRaises(ValueError):
|
||||
check.check_dependencies(root)
|
||||
with self.assertRaises(ValueError):
|
||||
check.check_modules({'bad': SimpleNamespace(__file__=str(check.ROOT / 'tools/unknown.py'))})
|
||||
|
||||
def test_headless_plan_and_stop_on_failure(self):
|
||||
collector, _, _ = driver.load_dependencies()
|
||||
commands = driver.plan(collector, Path('/engine'), Path('/output'))
|
||||
self.assertEqual(len(commands), 6)
|
||||
self.assertTrue(all('--headless' in c for c in commands))
|
||||
self.assertTrue(all('--check-only' in c for c in commands[1:3]))
|
||||
rows = []
|
||||
iterator = driver.schedule(rows)
|
||||
self.assertEqual(next(iterator), (0, 'import'))
|
||||
rows.append({'phase': 'import', 'passed': False})
|
||||
with self.assertRaises(ValueError):
|
||||
next(iterator)
|
||||
|
||||
def test_bundle_missing_and_tampered(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
with self.assertRaisesRegex(ValueError, 'bundle missing'):
|
||||
verify.verify(check.ROOT, path)
|
||||
(path / 'raw.log').write_text('raw')
|
||||
verify.seal(path)
|
||||
(path / 'raw.log').write_text('tampered')
|
||||
with self.assertRaisesRegex(ValueError, 'inventory/SHA'):
|
||||
verify.verify(check.ROOT, path)
|
||||
|
||||
def test_real_owned_python_child_and_raw_binding(self):
|
||||
collector, _, outer = driver.load_dependencies()
|
||||
child_text = (
|
||||
'import os,sys,json,time,pathlib\n'
|
||||
'p=pathlib.Path(sys.argv[0]).parent\n'
|
||||
'identity={"pid":os.getpid(),"ppid":os.getppid(),"pgid":os.getpgrp(),'
|
||||
'"args":" ".join([sys.executable]+sys.argv)}\n'
|
||||
'q=p/"identity.tmp"; q.write_text(json.dumps(identity)); q.replace(p/"identity")\n'
|
||||
'pathlib.Path(sys.argv[sys.argv.index("--log-file")+1]).write_text("native raw\\n")\n'
|
||||
'print("synthetic Python child; no Godot",flush=True)\n'
|
||||
'print("Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org",flush=True)\n'
|
||||
'time.sleep(.15)\n')
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = Path(tmp)
|
||||
script = base / 'child.py'
|
||||
script.write_text(child_text)
|
||||
def diagnostic(command, folder, timeout, env, **kwargs):
|
||||
def probe(pid):
|
||||
deadline = time.monotonic() + 2
|
||||
while time.monotonic() < deadline:
|
||||
if (base / 'identity').exists():
|
||||
identity = json.loads((base / 'identity').read_text())
|
||||
if identity['pid'] == pid:
|
||||
return identity
|
||||
time.sleep(.005)
|
||||
raise RuntimeError('identity unavailable')
|
||||
return outer.native.run_owned_diagnostic(command, folder, timeout, env, probe_fn=probe, **kwargs)
|
||||
def run_phase(command, folder, timeout, env, **kwargs):
|
||||
return outer.run_phase(command, folder, 5, env, diagnostic_fn=diagnostic, **kwargs)
|
||||
with patch.object(collector, 'live_engines', return_value=[]):
|
||||
row = collector.run_engine(SimpleNamespace(run_phase=run_phase), [sys.executable, str(script)],
|
||||
base / 'import', {})
|
||||
row['phase'] = 'import'
|
||||
self.assertTrue(row['passed'], row)
|
||||
check.bound_raw(base / 'import', row)
|
||||
bad = copy.deepcopy(row)
|
||||
bad['capture']['actual_captured_pids'] = [row['process']['pid'] + 1]
|
||||
with self.assertRaises(ValueError):
|
||||
check.bound_raw(base / 'import', bad)
|
||||
(base / 'import/stdout.log').write_text('changed')
|
||||
with self.assertRaises(ValueError):
|
||||
check.bound_raw(base / 'import', row)
|
||||
cleanup, errors = outer.finish_captured(outer.OWNED)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(all(r['engine_ended'] for r in cleanup))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Join Root's actual native bundle; absent or synthetic evidence never passes."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
import check_diag as check
|
||||
|
||||
|
||||
def inventory(directory):
|
||||
result = {}
|
||||
for path in sorted(Path(directory).rglob('*')):
|
||||
check.require(not path.is_symlink(), 'bundle symlink')
|
||||
relative = path.relative_to(directory).as_posix()
|
||||
# The stage is retained by Root; its full inventories are in execution.json.
|
||||
# Bundle files include every raw/owned receipt, every script and admission.
|
||||
if relative == 'bundle.json' or relative.startswith('stage/'):
|
||||
continue
|
||||
if path.is_file():
|
||||
result[relative] = check.pin(path)
|
||||
return result
|
||||
|
||||
|
||||
def seal(directory):
|
||||
"""Called by the Root driver only after owned cleanup and checker completion."""
|
||||
manifest = {'schema': 'p28-02-diag-bundle-v1', 'files': inventory(directory)}
|
||||
with (Path(directory) / 'bundle.json').open('x') as stream:
|
||||
json.dump(manifest, stream, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def verify(project_root, directory):
|
||||
project_root, directory = Path(project_root).resolve(), Path(directory).resolve()
|
||||
check.require(directory.is_dir() and (directory / 'bundle.json').is_file(), 'actual native bundle missing')
|
||||
manifest = json.loads((directory / 'bundle.json').read_text())
|
||||
check.require(manifest['schema'] == 'p28-02-diag-bundle-v1' and manifest['files'] == inventory(directory),
|
||||
'bundle inventory/SHA mismatch')
|
||||
execution = json.loads((directory / 'execution.json').read_text())
|
||||
check.check_dependencies(project_root)
|
||||
from diag_p28_02 import load_dependencies
|
||||
collector, standard, _ = load_dependencies()
|
||||
check.require(execution['source_before'] == execution['source_after'] and len(execution['source_before']) == 1407,
|
||||
'source map')
|
||||
check.require(collector.source_map(standard, project_root) == execution['source_before'],
|
||||
'candidate source inventory differs from measured S')
|
||||
for name, ref in execution['source_before'].items():
|
||||
path = project_root / name
|
||||
check.require(path.resolve().is_relative_to(project_root) and check.pin(path) == ref,
|
||||
'candidate product source differs from measured S: ' + name)
|
||||
local = project_root / check.HERE.relative_to(check.ROOT)
|
||||
for name, ref in execution['worker_before'].items():
|
||||
check.require(name in check.LOCAL_NAMES and check.sha(local / name) == ref,
|
||||
'candidate diagnostic differs from measured S')
|
||||
for name in check.LOCAL_NAMES - {'diag_probe.gd'}:
|
||||
check.check_imports((local / name).read_text())
|
||||
check.check_modules()
|
||||
return check.check_receipt(execution, directory)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--project-root', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
check.require(os.environ.get('BUILD_OUTPUT_DIR'), 'BUILD_OUTPUT_DIR required')
|
||||
directory = Path(os.environ['BUILD_OUTPUT_DIR']) / 'p28-02-diag-evidence'
|
||||
summary = verify(args.project_root, directory)
|
||||
print(json.dumps({'status': 'verified', 'measured_source': 'S', 'candidate_Q_measured': False,
|
||||
'original_credit': 0, 'summary': summary}, ensure_ascii=False))
|
||||
return 0
|
||||
except (ValueError, KeyError, TypeError, OSError) as error:
|
||||
print('FAIL: ' + str(error), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user