125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
"""跨语言契约测试: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 |