"""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、访问递归及 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>", 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()