This commit is contained in:
2026-09-12 14:19:56 +08:00
commit 87ab0b794b
130 changed files with 30042 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
"""跨语言契约测试engine/schema.py 的输出必须与 src/shared/analysis.ts 的类型对齐。
之前这个文件把 key 集合硬编码在 Python 里,从不读 analysis.ts —— 也就是说
TS 侧加一个字段、或者两边字段名写歧了,这个"契约测试"一律绿灯,
而 README 声称的"用契约测试保证一致"并不成立。
现在两边都是解析出来的Python 侧从 dataclass 的注解拿TS 侧从 interface 声明拿。
"""
import dataclasses
import io
import json
import os
import re
import subprocess
import sys
from engine import schema
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
FX = os.path.join(os.path.dirname(__file__), "fixtures")
ANALYSIS_TS = os.path.join(ROOT, "src", "shared", "analysis.ts")
def _ts_interface_fields(name):
"""从 analysis.ts 里抽出某个 interface 的字段名集合。
只处理"一行一个字段"的写法(本仓库的风格),够用且不需要引 TS parser。
可选字段的 `?` 去掉后再比较。
"""
src = io.open(ANALYSIS_TS, encoding="utf-8").read()
m = re.search(r"export interface %s\s*\{(.*?)\n\}" % re.escape(name), src, re.S)
assert m, "analysis.ts 里找不到 interface %s" % name
fields = set()
for line in m.group(1).splitlines():
line = line.strip()
if not line or line.startswith("//") or line.startswith("*"):
continue
fm = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\??\s*:", line)
if fm:
fields.add(fm.group(1))
assert fields, "interface %s 解析出 0 个字段,解析器该修了" % name
return fields
def _py_dataclass_fields(cls):
return {f.name for f in dataclasses.fields(cls)}
def _run_engine(extra=(), env=None):
# 默认关掉 hide_internal契约测试要断言 keys / shapes,
# filtered 之后的 functions 列表可能为空IndexError 挂掉。
p = subprocess.run(
[sys.executable, "-m", "engine.runner",
"--script", os.path.join(FX, "nested_calls.py"),
"--no-hide-internal", *extra],
capture_output=True, cwd=ROOT, env=env,
)
return p
def _engine_result():
p = _run_engine()
return json.loads(p.stdout.decode("utf-8").strip().splitlines()[-1])
def test_top_level_keys_match_typescript():
data = _engine_result()
assert set(data.keys()) == _ts_interface_fields("AnalysisResult")
def test_top_level_keys_match_python_dataclass():
data = _engine_result()
assert set(data.keys()) == _py_dataclass_fields(schema.AnalysisResult)
def test_wall_time_keys_match_both_sides():
data = _engine_result()
keys = set(data["wallTime"].keys())
assert keys == _ts_interface_fields("WallTime")
assert keys == _py_dataclass_fields(schema.WallTime)
def test_function_keys_match_both_sides():
data = _engine_result()
keys = set(data["functions"][0].keys())
assert keys == _ts_interface_fields("FunctionNode")
assert keys == _py_dataclass_fields(schema.FunctionNode)
def test_output_is_pure_ascii():
"""输出必须是纯 ASCII。
反模式说明现在是历史的;保留 ensure_ascii=True 后输出应当逐字节 ASCII
跨 locale 一致。这个回归保护防止有人"为可读性"改回 ensure_ascii=False。
"""
env = {k: v for k, v in os.environ.items()
if k not in ("PYTHONUTF8", "PYTHONIOENCODING")}
p = _run_engine(env=env)
raw = p.stdout
bad = [b for b in raw if b > 127]
assert not bad, "引擎输出含 %d 个非 ASCII 字节" % len(bad)
raw.decode("ascii") # 不该抛
def test_out_flag_writes_result_to_file(tmp_path):
"""--out结果走独立文件stdout 留给用户代码。
锁的是那个回归 —— 结果曾和用户输出共用 stdout一句不带换行的
sys.stdout.write 就能让合法 Python 报"结果解析失败"
"""
out = os.path.join(str(tmp_path), "result.json")
script = os.path.join(str(tmp_path), "chatty.py")
io.open(script, "w", encoding="utf-8", newline="\n").write(
'import sys\ndef f():\n sys.stdout.write("done")\n return 1\nf()\n'
)
p = subprocess.run(
[sys.executable, "-m", "engine.runner", "--script", script, "--out", out],
capture_output=True, cwd=ROOT,
)
assert os.path.exists(out), "引擎没写出结果文件stderr=%s" % p.stderr[-400:]
data = json.loads(io.open(out, encoding="utf-8").read())
assert data["status"] == "ok"
# 用户的输出确实进了 stdout而且没污染结果
assert b"done" in p.stdout
assert b"schemaVersion" not in p.stdout