☰
Unison pretty-printer 回归检测转录:pretty-print-libraries 的实现机制与源码解析
2026/10/10 8:17:57 网站建设 项目流程
  • 编程语言
  • 编译器
  • 语言运行时
  • 开发工具

【免费下载链接】unison

A friendly programming language from the future

项目地址:https://gitcode.com/gh_mirrors/un/unison
点击查看免费下载

导读

pretty-print-libraries是 Unison 语言仓库(当前仓库gh_mirrors/un/unison)中一个专门用于检测 pretty-printer(代码美化打印器)输出变化的 transcript(转录)测试文档。它通过克隆@unison/base与@unison/http两个公开库的固定 release 版本,并用edit.namespace命令把整个命名空间的数千条定义打印到 scratch 文件,从而以"快照对比"的方式捕获 pretty-printer 任何非预期的输出漂移。读完本文,你将理解 transcript 测试的完整运行链路(解析 → 执行 → 输出对比)、edit.namespace的底层实现原理,以及如何自行编写和运行这类回归检测转录。

一、什么是 Unison Transcript:可执行的 Markdown 测试

在当前仓库中,transcript 是 Unison 项目的核心测试基础设施之一。在 Transcript 数据模型 的模块注释里,它被明确定义为:

A transcript is executable Markdown, akin to a Jupyter Notebook.

也就是说,一个.md文件本身就是一段可以被解析、执行并生成对比输出的"程序"。转录文件里可以混合普通 Markdown 文本和三类特殊的围栏代码块(fenced code block),解析器 Transcript.Parser 会根据代码块的 info string 把它们分派给不同的处理器:

  • ucm块:包含一组 UCM(Unison Codebase Manager)命令行,逐行执行;
  • unison块:包含一段 Unison 源码,会被加入 scratch 文件并参与类型检查/求值;
  • api块:包含对代码库 HTTP API 的GET/POST请求与期望的RESPONSE,用于测试语言服务器/共享 API。

每个代码块还可以携带若干标签(info tags)来控制行为,例如:hide(隐藏输出)、:show(显示输出)、:hide-all(完全隐藏)、:error(期望失败)、:bug(已知 bug)、:added-by-ucm(标记由 UCM 命令自动生成的内容)。pretty-print-libraries中出现的:added-by-ucm标签,正是edit.namespace把定义写入scratch.u后由 UCM 自动标注的。

二、pretty-print-libraries 转录:要检测什么

关联文档 pretty-print-libraries.md 的开头两句话就阐明了它的全部目的:

This transcript is to detect changes in the pretty-printer for a few major public libraries.

We clone releases and not dev branches to avoid external changes, and also to reduce the time needed to clone the libraries.

这段描述蕴含两个关键设计决策:

  1. 检测目标:pretty-printer(代码打印器)对几个主要公开库的渲染输出。Unison 的类型、Term、ability 声明、文档块({{...}})在打印回文本时,任何细微变化都可能影响用户阅读体验或下游工具解析,因此需要持续监控。
  2. 克隆 release 而非 dev 分支:dev 分支会被上游持续改动,会让转录输出随外部变化而不稳定;固定到 release 标签则保证输入快照确定,且 release 代码量更小、克隆更快,从而降低测试成本、提高可复现性。

转录的完整操作步骤

原文档给出的操作序列非常精炼,只有两个克隆加两个编辑命令,全部内容如下:

> clone @unison/base/releases/3.19.0 @unison/base/releases/3.19.0> edit.namespace .
> clone @unison/http/releases/3.3.2 @unison/http/releases/3.3.2> edit.namespace .

其执行语义是:

  1. 在当前的默认项目/分支中,从 Unison Share 克隆@unison/base的releases/3.19.0标签;
  2. 进入克隆得到的项目分支上下文(提示符变为@unison/base/releases/3.19.0>);
  3. 执行edit.namespace .,把当前命名空间(.表示根路径)里的全部定义以 pretty-printer 渲染的形式追加到 scratch 文件scratch.u顶部;
  4. 对@unison/http/releases/3.3.2重复同样流程。

> clone行没有项目分支前缀,表示在当前默认上下文中执行;而带@unison/base/releases/3.19.0>前缀的行,则是 Parser.hs 中ucmCommand通过fullyQualifiedProjectAndBranchNamesParser解析出的"带项目分支上下文的 UCM 命令"(对应数据模型 UcmContext 中的UcmContextProject),它表示这条命令运行在该克隆出来的库分支上。

三、运行结果快照:pretty-printer 输出了什么

转录的期望输出保存在同名.output.md文件 pretty-print-libraries.output.md 中。对比运行结果可以直观看到 pretty-printer 的真实渲染能力:

> clone @unison/base/releases/3.19.0 Cloned @unison/base/releases/3.19.0. @unison/base/releases/3.19.0> edit.namespace . ☝️ I added 4278 definitions to the top of scratch.u You can edit them there, then run `update` to replace the definitions currently in this namespace.

关键信息:edit.namespace .一次性向scratch.u顶部添加了4278 条定义。随后:added-by-ucm标注的unison块展示了 pretty-printer 对这些定义的实际打印结果,覆盖了 Unison 语法的多种核心结构:

  • Ability 声明:如structural ability abilities.Abort where abort : {Abort} a、ability abilities.Clock where elapsed : {Clock} Duration / now : {Clock} Instant、structural ability abilities.Store a where get : {Store a} a / put : a ->{Store a} ();
  • 类型声明:如structural type abilities.Random.RNG = RNG (∀ g a. '{g, Random} a ->{g} a)、type abilities.Exception.Generic =;
  • 文档块:每个定义上方以{{ ... }}包裹的多行 Doc 文本,其中保留了{type Abort}、@signature{...}、@source{...}、@typecheck```等文档内嵌语法;
  • 内建类型说明:如-- abilities.Request is built-in.、-- Any is built-in.。

这个快照本身就是 pretty-printer 的"黄金标准":任何对上述语法元素打印方式的改动,都会导致.output.md对比失败,从而在 CI 中暴露回归。

四、edit.namespace 的源码级实现

edit.namespace的完整实现位于 EditNamespace.hs,核心函数是handleEditNamespace :: OutputLocation -> [Path.Path'] -> Cli ()。其内部流程值得拆解:

1. 构建 PrettyPrintEnv

let currentNames = Branch.toNames currentBranch let ppe = PPED.makePPED (PPE.hqNamer 10 currentNames) (PPE.suffixifyByHashName currentNames)

edit.namespace先取当前分支的全部名称,构造一个PrettyPrintEnvDecl:使用hqNamer 10(打印时名字超过 10 个字符即附加 hash 限定)与suffixifyByHashName(对 hash 冲突的名字做后缀化处理)。这正是 pretty-printer 打印 4278 条定义时使用的名字解析环境——pretty-print-libraries转录测的不仅是"渲染格式",还包括"命名消歧策略"。

2. 路径参数处理

let paths = if null paths0 then [mempty] else Path.fromPath' <$> paths0

edit.namespace .传入的是根路径;若完全不传参数,则按空路径处理。特别的,空路径(即根)会排除lib目录:

let allNamesToEdit = List.nubOrd paths & foldMap \path -> let branch = (if path == mempty then Branch.withoutLib else id) (Branch.getAt0 path currentBranch) names = Branch.toNames branch in case Path.toName path of Nothing -> names Just pathPrefix -> Names.prefix0 pathPrefix names

这段逻辑把多个路径的命名空间取并集(去重),并且对每个路径加上对应前缀,最终得到"待编辑名称集合"。

3. 获取待打印的定义并排除自动生成访问器

(types, terms) <- Cli.runTransaction (getNamesForEdit codebase ppe allNamesToEdit) ... showDefinitions outputLoc (const True) ppe terms types misses

getNamesForEdit的关键细节(模块注释明确说明):它刻意不获取自动生成的记录访问器(如Foo.bar.set),因为记录类型本身被解析进 scratch 文件后会自动生成这些访问器;若同时打印两者会造成重复定义。这也是edit.namespace输出能直接通过update回写命名空间的前提。最终所有定义经由showDefinitions(复用 ShowDefinition 模块)以 pretty-printer 渲染格式输出。

五、转录测试的启动与对比机制

转录的入口在 Transcripts.hs。test函数依次对四个目录运行转录并收集失败:

buildTests config (testBuilder False False recordFailure) ("unison-src" </> "transcripts") Nothing buildTests config (testBuilder False True recordFailure) ("unison-src" </> "transcripts" </> "idempotent") Nothing buildTests config (testBuilder False False recordFailure) ("unison-src" </> "transcripts-using-base") Nothing buildTests config (testBuilder True False recordFailure) ("unison-src" </> "transcripts" </> "errors") Nothing
  • 普通目录unison-src/transcripts的输出写入同名.output.md(outputFileForTranscript = replaceExtension filePath ".output.md",见 Transcripts.hs);pretty-print-libraries.md就属于这一类;
  • idempotent目录开启replaceOriginal,直接覆盖原文件,用于校验"运行前后不改变"的幂等性质;
  • errors目录用expectFailure=True,预期转录失败;
  • 以_开头的文件被当作 prelude(前导文件),不视为独立测试。

每个转录运行在一个全新的临时 SQLite codebase 中(withNewUcmCodebaseOrExit SC.init ...),由 Runner.hs 的withRunner启动代码库服务器、解析转录、把 UCM 命令队列化并逐步执行。运行成功后,输出与仓库中已有的.output.md进行比对(EasyTest 框架),任何差异都会在末尾以🚨 <文件路径>: ...汇总打印。这保证了pretty-print-libraries这类"快照式"转录能可靠地在 CI 中拦住 pretty-printer 的意外改动。

六、如何运行与维护这类转录

在当前仓库中运行转录测试的方式与 UCM 常规测试一致:经由scripts/test.sh或直接通过栈/项目构建运行unison-cli/transcripts/Transcripts.hs对应的测试可执行文件,并可用前缀参数筛选(handleArgs支持matchPrefix,例如只跑pretty开头的转录)。运行时会克隆@unison/base/releases/3.19.0与@unison/http/releases/3.3.2,因此需要网络可达 Unison Share;若只想做本地语法/解析层面的检查,可直接阅读 pretty-print-libraries.md 及其 output 快照 进行人工对照。

维护要点如下:

  1. 版本固定:转录中 pin 的 release 标签(3.19.0、3.3.2)决定输入快照。升级 base/http 版本时,应主动更新命令与.output.md,并确认 4278 等定义数量变化符合预期;
  2. 输出即黄金标准:不要手工"修".output.md来掩盖 pretty-printer 改动——该转录的目的就是暴露这些改动;确属有意的打印改进时,应连同 review 说明一起更新快照;
  3. 与其他转录配合:transcripts目录下还有 hello.md、alias-many.md 等通用 UCM 功能转录,而pretty-print-libraries是其中唯一以"真实大型库 + pretty-printer 回归检测"为目标的用例,覆盖面更大、更贴近真实用户代码。

七、从源码看:为什么快照对比能有效防回归

从 Parser.hs 可见,转录输出在写入.output.md前会经过format :: Transcript -> Text重新序列化(formatUcmLine、formatStanzas、CMark.nodeToCommonmark等),这意味着对比的是规范化后的输出,与终端宽度、颜色等展示层细节解耦;同时 Runner.hs 的testConfig会把fzfPath置为"NONE"并禁用交互选择,确保转录输出在 CI 环境中完全确定。pretty-print-libraries正是在这套"确定性快照"机制之上,为 pretty-printer 提供了一份来自真实世界库(base 与 http,含能力、类型、文档、内建声明等全部语法形态)的长期回归基线。

结语

pretty-print-libraries虽只有两条克隆命令和两条编辑命令,却是 Unison 项目中一条设计精密的回归防线:它用固定 release 的快照输入 +edit.namespace的全量打印 +.output.md的精确对比,持续守护 pretty-printer 对真实大型库的渲染稳定性。理解了 Transcript.hs、Parser.hs、Runner.hs 与 EditNamespace.hs 这条链路,你就能举一反三地编写自己的"库级 pretty-printer / 渲染回归"转录,或深入定制edit.namespace的打印行为。

  • 编程语言
  • 编译器
  • 语言运行时
  • 开发工具

【免费下载链接】unison

A friendly programming language from the future

项目地址:https://gitcode.com/gh_mirrors/un/unison
点击查看免费下载

相关推荐

上一篇:Hermes WebUI 三容器部署:Agent+Dashboard+WebUI 一键搭建,端口 8787 完整指南
下一篇:GHelper 完整指南:3 分钟装好替代奥创的华硕笔记本控制中心

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询