Files
endorphin-aster-vm/scripts/test_generate_ast.py
T

76 lines
4.6 KiB
Python

"""Generator regression checks for the designated remote validation environment."""
from pathlib import Path
import json
import re
import unittest
from generate_ast import generate, parse_schema, recursive_edges
# 从固定仓库路径读取真实 ASDL,全体测试以同一协议输入为基线,不改磁盘 schema。
SCHEMA = (Path(__file__).resolve().parents[1] / "crates/frontend/Endor.asdl").read_text(encoding="utf-8")
# unittest 直接调用下列回归场景;generate 仅返回内存文本,不触发 main 的 rustfmt 或文件写入。
class AstGenerationTests(unittest.TestCase):
# unittest 检查生成 TS 中每个 walk 调用都有声明,防止内置标量被错误生成为不存在的 walker。
def test_editor_walkers_only_call_declared_functions(self):
# 获取内存生成的编辑器源码,不导入或执行 TypeScript。
editor = generate(SCHEMA)["crates/frontend/editor/ast.ts"]
# 提取导出的 walk 函数名,用集合表示可调用的完整声明空间。
declared = set(re.findall(r"export function (walk\w+)\(", editor))
# 提取所有 walk 调用名(含声明中的名字),每项必须属于 declared。
calls = set(re.findall(r"\b(walk\w+)\(", editor))
self.assertLessEqual(calls, declared)
self.assertIn("walkSpan(node.span, visitor)", editor)
# unittest 在内存 schema 增加构造器,确认三种语言契约及协议身份跟着更新且生成结果确定。
def test_schema_changes_reach_all_languages_and_schema_identity(self):
# 原 schema 的全目标输出,作为修改后的身份和源码比较基线。
before = generate(SCHEMA)
# 只给 UnaryOp 增加 LogicalNot,不写原 ASDL 或生成文件;输出应包含新分支。
after = generate(SCHEMA.replace("UnaryOp = Negate", "UnaryOp = Negate | LogicalNot"))
self.assertIn("LogicalNot", after["crates/frontend/src/ast/generated.rs"])
self.assertIn('"LogicalNot"', after["crates/frontend/editor/ast.ts"])
# JSON Schema 在内存解析,检查 UnaryOp 的 enum 同样获得新项。
editor_schema = json.loads(after["crates/frontend/editor/ast.schema.json"])
self.assertIn("LogicalNot", editor_schema["$defs"]["UnaryOp"]["enum"])
self.assertNotEqual(before["crates/frontend/src/ast/generated.rs"], after["crates/frontend/src/ast/generated.rs"])
self.assertEqual(after, generate(SCHEMA.replace("UnaryOp = Negate", "UnaryOp = Negate | LogicalNot")))
# unittest 增加可选自引用 Link,验证 Rust 的 Option<Box>、访问递归及 TS 可空/可缺省形状。
def test_recursive_optional_nodes_are_boxed_and_walked(self):
# 在模块右括号前追加测试定义,原始 SCHEMA 常量保持不变。
extended = SCHEMA.rsplit("}", 1)[0] + "Link = (Link? next)\n}"
# 一次生成得到所有目标源码,分别核对同一递归边在不同目标上的表示。
outputs = generate(extended)
self.assertIn("pub next: Option<Box<Link>>", outputs["crates/frontend/src/ast/generated.rs"])
self.assertIn("visitor.visit_link(value)?", outputs["crates/frontend/src/ast/visitor.rs"])
self.assertIn("next?: Link | null", outputs["crates/frontend/editor/ast.ts"])
# 直接解析真实 schema 检查 DFS 选择的回边,不要求所有递归方向都装箱。
definitions = parse_schema(SCHEMA)
self.assertIn(("ExprKind", "Expr"), recursive_edges(definitions))
self.assertNotIn(("Expr", "ExprKind"), recursive_edges(definitions))
# unittest 覆盖未定义类型、重名、不完整、尾随输入及未知字符,均须失败而非输出部分类型。
def test_invalid_schemas_fail_instead_of_emitting_partial_types(self):
# 每份输入只针对一个非法结构,subTest 让失败报告指明当前 schema。
for schema in [
"module Endor { Root = (Missing value) }",
"module Endor { Root = (string x, string x) }",
"module Endor { Root = A | A }",
"module Endor { Root = A Root = B }",
"module Endor { Root = (string value",
"module Endor { Root = A } trailing",
"module Endor { Root = @ }",
]:
# 当前子用例要求 parse_schema 明确抛 ValueError,不接受静默成功或无关异常。
with self.subTest(schema=schema), self.assertRaises(ValueError):
parse_schema(schema)
# 脚本直接执行时启动 unittest;被其他检查导入时不自动运行用例。
if __name__ == "__main__":
unittest.main()