test(lifetime): 新增生命期负向运行器;NewTest 补 memgr 用例

run_lifetime_negative_tests.py:只用 TransPyV 编译并断言编译失败且输出含 LifetimeError(TPC 解析不了 new、也产不出 LifetimeError,故与 NegativeTest 的 TPC 运行器分开);默认跑 EscapeTest / EscapeMethodTest / EscapeXmodTest,支持 --only/--rebuild-includes。

NewTest:按 MemhubTest 的写法补上 new memgr(m) hlife Point(5,6) 段(memhub.MemBuddy(arena, 65536))。README 同步负向运行器用法。
This commit is contained in:
TermiNexus committed 2026-10-08 17:50:53 +08:00
1 parent 42ca901b28
commit ed204852c4
3 files changed
+130 -5

No files matched your search

+8 -5
View File
@@ -3,6 +3,8 @@ import w32.win32console
import t, c
from t import CInt, CExport
import stdio
import stdlib
import memhub
# =============================================================================
@@ -32,11 +34,12 @@ def main() -> CInt | CExport:
p2: Point* = new heap Point(3, 4)
stdio.printf("heap : %d %d\n", p2.x, p2.y)
# (3) memgr(x):从 memhub 管理器取存储
# 待确认 memhub 管理器的构造 API(MemBuddy 建立方式)后打开本段:
# m: memhub.MemBuddy* = <构造管理器>
# p3: Point* = new memgr(m) hlife Point(5, 6)
# stdio.printf("memgr: %d %d\n", p3.x, p3.y)
# (3) memgr(x):从 memhub 管理器取存储(管理器构造方式见 Test/MemhubTest)
arena: bytes = stdlib.malloc(65536)
m: memhub.MemBuddy = memhub.MemBuddy(arena, 65536)
p3: Point* = new memgr(m) hlife Point(5, 6)
stdio.printf("memgr: %d %d\n", p3.x, p3.y)
stdlib.free(arena)
# 非法组合(应报 LifetimeError)——要验证时打开其中一条:
# 1) 语义矛盾:bad: Point* = new stack hlife Point(7, 8)
+9
View File
@@ -142,6 +142,15 @@ python Test/SequentialRegressionTest.py --escape --exe <TransPyV.exe> --rebuild-
> `--escape` / `--only` 会在 `Test/<名>` 与**仓库根** `<名>` 两处查找 `project.json` / `project.vpj`,所以根级 escape 项目也能直接跑;`--escape` 会忽略 README 的难度顺序。
**负向断言**("应报 `LifetimeError`")需要 TransPyV 编译——`NegativeTest/run_negative_tests.py` 用的是 TPC,而 `new` 描述符与 `LifetimeError` 只在 TPV 侧实现,故单独提供:
```bash
python Test/run_lifetime_negative_tests.py --exe .\TransPyV\output\TransPyV.exe
python Test/run_lifetime_negative_tests.py --exe .\TransPyV\output\TransPyV.exe --only EscapeMethodTest
```
> 默认跑 `EscapeTest` / `EscapeMethodTest` / `EscapeXmodTest`:断言**编译失败且输出含 `LifetimeError`**;编译成功 = 漏检(FAIL),报错但方向不符 = SKIP。
---
## 二、建议实现顺序(难度升序,共 68 步)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""生命期/逃逸负向测试运行器(TransPyV 专用)
与 NegativeTest/run_negative_tests.py 的区别:
· 那个脚本用 **TPC**(Python 编译器,`TransPyC.py -f`)编译并断言通用错误;
· `new` 描述符与 `LifetimeError` 只在 **TransPyV** 侧实现——TPC 的解析器不认识 `new`,
也产不出 LifetimeError,所以本脚本统一用 TransPyV.exe 编译,
断言"**编译失败且输出含 LifetimeError**"。
用法:
python Test/run_lifetime_negative_tests.py --exe .\\TransPyV\\output\\TransPyV.exe
python Test/run_lifetime_negative_tests.py --exe ... --only EscapeMethodTest
python Test/run_lifetime_negative_tests.py --exe ... --rebuild-includes
"""
import os
import subprocess
import sys
import argparse
# 强制 stdout/stderr 使用 UTF-8,避免 Windows GBK 编码崩溃
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(TEST_DIR)
# 期望"编译失败 + 报 LifetimeError"的项目(名字;目录优先 Test/<名>,其次仓库根 <名>)。
# 注意:EscapeMethodTest / EscapeXmodTest 内部有多条互斥用例(编译在第一个错误处终止),
# 每个项目只保证"至少触发一条 LifetimeError"。
NEGATIVE_PROJECTS = ["EscapeTest", "EscapeMethodTest", "EscapeXmodTest"]
EXPECT_KEYWORD = "LifetimeError"
COMPILE_TIMEOUT = 300
def find_project_file(proj_dir):
"""项目目录下找 project.json(Test/ 约定)或 project.vpj(仓库根约定)"""
for fn in ("project.json", "project.vpj"):
p = os.path.join(proj_dir, fn)
if os.path.isfile(p):
return p
return None
def resolve(name):
"""名字 → (项目目录, 项目文件路径);找不到目录返回 (dir, None)"""
d = os.path.join(TEST_DIR, name)
if not os.path.isdir(d):
d = os.path.join(PROJECT_ROOT, name)
return d, find_project_file(d)
def main() -> int:
ap = argparse.ArgumentParser(
description="生命期/逃逸负向测试(只用 TransPyV 编译,断言 LifetimeError)")
ap.add_argument("--exe", required=True,
help="TransPyV 可执行文件(如 .\\TransPyV\\output\\TransPyV.exe)")
ap.add_argument("--only", default=None,
help="只跑指定项目(逗号分隔,名字即目录名)")
ap.add_argument("--rebuild-includes", action="store_true",
help="每次编译追加 --rebuild-includes(全量重编 includes)")
args = ap.parse_args()
names = [n.strip() for n in args.only.split(",") if n.strip()] if args.only else NEGATIVE_PROJECTS
suffix = " --rebuild-includes" if args.rebuild_includes else ""
passed, failed, skipped = [], [], []
print("=" * 70)
print("生命期/逃逸负向测试(期望编译失败 + LifetimeError)")
print("=" * 70)
for name in names:
proj_dir, proj_file = resolve(name)
if proj_file is None:
skipped.append((name, "找不到 project.json / project.vpj"))
print(f"[SKIP] {name}: 找不到 project.json / project.vpj")
continue
cmd = f'"{args.exe}" --project "{proj_file}" --clean{suffix}'
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True,
encoding='utf-8', errors='replace',
cwd=proj_dir, timeout=COMPILE_TIMEOUT)
except subprocess.TimeoutExpired:
failed.append((name, "TIMEOUT", ""))
print(f"[FAIL] {name}: 编译超时")
continue
out = (r.stdout or "") + (r.stderr or "")
if r.returncode == 0:
failed.append((name, "COMPILED_OK(漏检)", out.strip()[-400:]))
print(f"[FAIL] {name}: 编译成功(漏检,应报 LifetimeError)")
elif EXPECT_KEYWORD.lower() in out.lower():
passed.append((name, out.strip()[-200:]))
print(f"[PASS] {name}: 已按预期拒绝(含 {EXPECT_KEYWORD})")
else:
skipped.append((name, f"报错但未见 {EXPECT_KEYWORD}", out.strip()[-300:]))
print(f"[SKIP] {name}: 报错但未见 {EXPECT_KEYWORD}")
print()
print("=" * 70)
print(f"PASS {len(passed)} FAIL {len(failed)} SKIP {len(skipped)}")
for name, tail in skipped:
print(f" [SKIP] {name}: {tail if isinstance(tail, str) else ''}")
if tail: print(f" {tail}")
for name, tag, tail in failed:
print(f" [FAIL] {name}: {tag}")
if tail: print(f" {tail}")
print("=" * 70)
return 1 if failed else 0
if __name__ == '__main__':
sys.exit(main())