From ed204852c4bffb2b97a58d778ca50dbbc02d72dc Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 8 Oct 2026 17:50:53 +0800 Subject: [PATCH] =?UTF-8?q?test(lifetime):=20=E6=96=B0=E5=A2=9E=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E6=9C=9F=E8=B4=9F=E5=90=91=E8=BF=90=E8=A1=8C=E5=99=A8?= =?UTF-8?q?=EF=BC=9BNewTest=20=E8=A1=A5=20memgr=20=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 同步负向运行器用法。 --- NewTest/App/main.vp | 13 ++-- README.md | 9 +++ run_lifetime_negative_tests.py | 113 +++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 run_lifetime_negative_tests.py diff --git a/NewTest/App/main.vp b/NewTest/App/main.vp index 5a20168..d7ee35c 100644 --- a/NewTest/App/main.vp +++ b/NewTest/App/main.vp @@ -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) diff --git a/README.md b/README.md index 146badd..84122d0 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,15 @@ python Test/SequentialRegressionTest.py --escape --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 步) diff --git a/run_lifetime_negative_tests.py b/run_lifetime_negative_tests.py new file mode 100644 index 0000000..f390361 --- /dev/null +++ b/run_lifetime_negative_tests.py @@ -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())