Files
Python-Profiler-Visualizer/engine/tests/test_contract.py
2026-09-12 14:19:56 +08:00

125 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""跨语言契约测试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