- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
导读
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.
这段描述蕴含两个关键设计决策:
- 检测目标:pretty-printer(代码打印器)对几个主要公开库的渲染输出。Unison 的类型、Term、ability 声明、文档块(
{{...}})在打印回文本时,任何细微变化都可能影响用户阅读体验或下游工具解析,因此需要持续监控。 - 克隆 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 .其执行语义是:
- 在当前的默认项目/分支中,从 Unison Share 克隆
@unison/base的releases/3.19.0标签; - 进入克隆得到的项目分支上下文(提示符变为
@unison/base/releases/3.19.0>); - 执行
edit.namespace .,把当前命名空间(.表示根路径)里的全部定义以 pretty-printer 渲染的形式追加到 scratch 文件scratch.u顶部; - 对
@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' <$> paths0edit.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 missesgetNamesForEdit的关键细节(模块注释明确说明):它刻意不获取自动生成的记录访问器(如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 快照 进行人工对照。
维护要点如下:
- 版本固定:转录中 pin 的 release 标签(
3.19.0、3.3.2)决定输入快照。升级 base/http 版本时,应主动更新命令与.output.md,并确认 4278 等定义数量变化符合预期; - 输出即黄金标准:不要手工"修"
.output.md来掩盖 pretty-printer 改动——该转录的目的就是暴露这些改动;确属有意的打印改进时,应连同 review 说明一起更新快照; - 与其他转录配合:
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
相关推荐
Unison 代码库 Round-Trip 回归测试全解析:如何保证 pretty-printer 输出可被重新解析且哈希不变
Unison 代码库 Round Trip 回归测试全解析:如何保证 pretty printer 输出可被重新解析且哈希不变 这篇技术指南深入剖析 Uniso
编程语言编译器语言运行时开发工具Unison 回归修复解析:`update` 时 pretty-printer 误插入 `use bar baz` 导致的变量捕获问题(fix-5464)
Unison 回归修复解析: update 时 pretty printer 误插入 use bar baz 导致的变量捕获问题(fix 5464) 本文以 U
编程语言编译器语言运行时开发工具nlohmann/json 的 GDB 调试利器:Pretty Printer 安装、使用与源码实现解析
nlohmann/json 的 GDB 调试利器:Pretty Printer 安装、使用与源码实现解析 本篇技术指南围绕仓库 tools/gdb_pretty
序列化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考