基于Markdown的测试套件

Hacker News Top 工具

摘要

作者解释了为EndBASIC的编译器和虚拟机切换到基于Markdown的测试套件的原因,目的是让这些测试作为LLM学习该语言独特特性的权威文档。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/21 06:27

# 基于 Markdown 的测试套件 来源:https://blogsystem5.substack.com/p/markdown-based-test-suite 这篇文章与 AI 无关,也并非用 AI 撰写,但我接下来要介绍的工作确实受到了 AI 的启发。因为我喜欢讲故事,所以得先交代一下背景。您随意就好……但要是因为第一段出现了“AI”这个词就离开,那可就太可惜了!我觉得后面的技术讲解至少相当有趣,而且独立于 AI 本身也值得一看。 [](https://substackcdn.com/image/fetch/$s_!aFRI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d4e26f9-0dde-4faf-a5a3-30941f7885d5_1129x844.png) 去年十二月,我开始摆弄代码智能体。我尝试了一件事,本来没指望能成功:将一个 AI 智能体指向 EndBASIC 的公开文档,让它从零编写《太空侵略者》或《超级马里奥》这样的游戏。结果虽然不完美,第一次也没运行成功,但稍作调整后就跑起来了。结合一些手写的 `AGENTS.md` 规则,这个智能体轻松地生成了 EndBASIC 的演示程序。 这个实验令人印象深刻,因为我压根没指望智能体能写出 EndBASIC 代码……而它居然成功了,这激发了我重新拾起 EndBASIC 自身开发的兴趣。我产生了三个想法: - 增强 EndBASIC 的“自我文档化”特性,让 AI 智能体能在无监督的情况下了解它的特性。 - 加速 EndBASIC,使其能运行更复杂的游戏。 - 用长久以来渴望的图素和声音等原语扩展 EndBASIC,最终实现项目的愿景。 这些想法共同催生了我从一月起一直在进行的 EndBASIC 核心重写工作,预计会在即将发布的版本中亮相。但在那之前,我想跟您聊聊新核心背后一个很酷的环节:它的测试方法。我已经不再为编译器和虚拟机编写 Rust 单元测试,转而使用 Markdown 来写测试。我认为这种做法相当不错。 为了让 AI 智能体写出正确的 EndBASIC 代码,我不得不手工制作了一堆 `AGENTS.md` 规则,告诉它 EndBASIC 与其他更传统的 BASIC 方言有何不同。这样做还行,但手工编写规则容易出错,也很难做到详尽。于是我打算让 LLM 直接从 EndBASIC 本身提取这些信息。 思路很简单:如果我用 Markdown(AI 的通用语言)编写新核心的集成测试,那么这些测试就能作为规范且正确的文档,展示语言的行为。LLM 很擅长总结信息,所以如果让它们阅读大量这样的“实战例子”,它们*大概*能搞清楚,对吧? 实际上确实如此!我给 GPT 5.4 提供了以下提示: > 基于你对 BASIC 方言的既有知识,请阅读所有 `core/tests/*.md` 文件,分析 EndBASIC 方言与你所知的不同之处,然后为自己制定一套规则,以便之后能编写 EndBASIC 代码。可以忽略 *Disassembly* 部分。注意,这些集成测试中的所有函数和命令仅供测试使用:实际可用的函数和命令记录在 `cli/tests/repl/help.out` 中,所以也要阅读那些内容,了解哪些功能可用。将你的发现写入 `rules.md` 文件。 结果生成了一个非常全面、规则精准的文件:看这里(https://jmmv.dev/notes/2026-05-17-endbasic-rules.md)。 不过,我们暂且抛开这个,来看看这个新的基于 Markdown 的测试套件的内部结构。 这是一组 Markdown 文件: ``` endbasic$ ls -l core/tests/*.md | head -n 10 -rw-r--r-- 1 jmmv users 22771 May 11 14:18 core/tests/test_args.md -rw-r--r-- 1 jmmv users 3785 May 11 13:22 core/tests/test_arithmetic_add.md -rw-r--r-- 1 jmmv users 2653 May 11 13:22 core/tests/test_arithmetic_div.md -rw-r--r-- 1 jmmv users 2668 May 11 13:22 core/tests/test_arithmetic_mod.md -rw-r--r-- 1 jmmv users 2131 May 11 13:22 core/tests/test_arithmetic_mul.md -rw-r--r-- 1 jmmv users 1466 May 11 13:22 core/tests/test_arithmetic_neg.md -rw-r--r-- 1 jmmv users 2517 May 11 13:22 core/tests/test_arithmetic_pow.md -rw-r--r-- 1 jmmv users 2208 May 11 13:22 core/tests/test_arithmetic_sub.md -rw-r--r-- 1 jmmv users 20388 May 11 13:22 core/tests/test_arrays.md -rw-r--r-- 1 jmmv users 5885 May 11 13:22 core/tests/test_assignments.md endbasic$ █ ``` 每个文件包含一个或多个*测试用例*: ``` endbasic$ grep '^# ' core/tests/test_end.md | head -n 5 # Test: Call to END and nothing else # Test: Exit code is an integer immediate # Test: Exit code is a double immediate and needs demotion # Test: Exit code is in a global variable # Test: Exit code is in a local variable endbasic$ █ ``` 每个测试用例有一个*标题*描述测试内容,以及若干子部分定义测试场景: - 一个*Source*代码块,作为编译器的输入。 - 如果编译失败,则有一个*Compilation errors*部分,包含错误信息,之后不再有其他内容。 - 如果编译成功: - 一个*Disassembly*部分,包含编译后的字节码。 - 一个可选的*Exit code*部分,显示程序的退出码(如果不为零)。 - 一个*Output*部分,包含执行程序打印到控制台的任何消息。 - 一个*Runtime errors*部分,包含执行程序产生的任何错误。 这是验证 `END` 命令的一个简单示例: ``` # Test: Exit code is an integer immediate ## Source ```basic END 42 ``` ## Disassembly ```asm 0000: LOADI R64, 42 ; 1:5 0001: END R64 ; 1:1 0002: EOF ; 0:0 ``` ## Exit code ```plain 42 ``` ``` 目前没有验证词法分析器或解析器内部结构的部分,但我正在考虑进一步扩展格式,也转储 AST,以简化这些组件的测试。 该测试套件的驱动程序(https://github.com/endbasic/endbasic/blob/24eb10e990668c01f2a71e4abb90632c4e4be8a2/core/tests/integration_test.rs)会枚举 tests 目录中的所有 Markdown 文件,并逐一处理。 对于每个文件,驱动程序提取所有测试用例的*标题*及其*Source*子部分,以计算出要执行的所有测试用例。获得这些 Markdown 文件中的子集信息后,驱动程序将每个单独的测试用例送入编译器,如果编译成功,再送入虚拟机。所有副作用都会被捕获,驱动程序会从零生成一个包含测试结果的*新* Markdown 文件。 驱动程序生成测试文件的新版本后,会比较生成的文件(实际结果)与预先记录、已检入的版本(黄金文件)。如果不同,测试失败,驱动程序使用 `diff` 工具打印差异。 就是这样。简单吧?这使驱动程序保持非常简洁,因为它只需要解析 Markdown 的最小子集,而生成的差异对人类来说也极易理解。 目前这个测试套件中有 448 个测试用例和 13k 行 Markdown,因此“手工”维护是不可行的。您不会希望在优化编译器后,不得不重写黄金文件中数百个反汇编块来反映变化,对吧? 关键在于,由于上述设计,核心更改后重新生成黄金文件很容易:驱动程序*已经在*执行测试时正是这么做的!技巧很简单:通过设置 `REGEN=true` 环境变量,让驱动程序重写黄金文件,而不是生成实际文件。瞧!所有黄金文件都会就地重新生成。然后我可以使用 Git 验证更改,并将它们与实际代码更改一起提交。 我们来谈谈这个基于 Markdown 的测试套件框架的优点: - 比之前的方法容易处理得多。以前我害怕修改前一个 EndBASIC 核心实现的编译器和虚拟机,因为调整几十个测试很痛苦。更改需要我摆弄位置和深度嵌套的类型,而现在测试修改起来轻而易举,还能与之前的状态进行差异对比。 - 几乎任何像样的文本编辑器都支持 Markdown,包括格式化围栏代码块。这使浏览测试套件和修改文件变得容易,实际上这也是我选择 Markdown 而不是自定义文本格式的主要原因。 - LLM 可以轻松“学习”。好吧,这只是一个猜测:我没有用文章开头那个提示去测试旧核心及其基于 Rust 的测试,也许 LLM 也能很好地逆向出规则。但因为 Markdown 测试对人类来说更容易阅读,我推测对 LLM 也是如此。 当然,也有一些缺点: - 重新生成一个或所有测试的输出*太容易了*。使用旧的基于 Rust 的测试时,我被迫手动输入行号、嵌套的 AST 树等内容。这个过程迫使我*仔细思考*每一个变化。而采用新方法……重新生成黄金文件太简单了,所以很容易忽略源代码位置或反汇编代码中的小错误。 - 反汇编中的差异通常很嘈杂且难以审查,因为每一行都有地址,因此任何新增或删除的指令都会使所有其他地址偏移。我当然可以选择*不*在转储中包含指令地址,但它们在手动验证跳转目标时很方便,所以感觉还是保留为好。 - Rust 无法动态生成顶级测试用例,这意味着 Markdown 文件中的各个测试用例对驱动程序是“不可见”的:我可以全部运行或全部不运行,但通过 `cargo test` 进行的常规测试过滤不适用。我可以将不同的 Markdown 文件“暴露”为不同的 Rust 原生测试用例,但这需要一个硬编码的测试文件列表——必须与磁盘上的文件保持同步,因此我通过添加一个交叉引用两者的测试来降低不同步的风险。 - 这个想法不能很好地泛化。这里展示的基于 Markdown 的测试套件适用于端到端测试有利且*成本低廉*的组件,但我不建议在其他场景中使用。保持测试快速对于快速迭代至关重要。 我想就这些了。如果以上内容感觉太抽象,我鼓励您看看驱动程序(https://github.com/endbasic/endbasic/blob/24eb10e990668c01f2a71e4abb90632c4e4be8a2/core/tests/integration_test.rs)、它的辅助代码(https://github.com/endbasic/endbasic/blob/24eb10e990668c01f2a71e4abb90632c4e4be8a2/core/tests/testutils/mod.rs)以及包含测试套件的目录(https://github.com/endbasic/endbasic/tree/24eb10e990668c01f2a71e4abb90632c4e4be8a2/core/tests)。 现在您掌握了这个新技巧,您觉得怎么样? #### 关于本文的讨论 ### 准备好了解更多了吗?

相似文章

Markdown在/src中

Lobsters Hottest

文章认为,在带有LLMs的智能编码工作流中,Markdown正在成为源代码而不是文档,并且应该与生成的代码一起提交到/src目录中。

构建自维护的Markdown文件(开源项目)

Reddit r/AI_Agents

作者介绍了一个名为Mex的开源项目,该项目通过让编码代理自动维护上下文,使Markdown文件实现自维护,从而防止文档过时并提升AI工作流的可靠性。