- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
导读
serial-test-05 是 Unison 编译器仓库中一组「序列化回归测试」的第五个用例。它的核心任务不是测试某个业务功能,而是验证一个把函数当作普通值塞进自定义 Map 的复杂 Unison 值,能否被序列化为自包含文件、并在后续的序列化/哈希版本(v4、v5)下被可靠地读回、还原和校验。读完本文,你将理解 Unison 的 transcript 测试体系如何运作、saveTestCase系列工具如何生成和保存测试样本、v4/v5 版本号的真实含义,以及随机回放脚本如何用「输出比对 + Sha3_512 哈希比对」双重手段守住序列化兼容性底线。
一、transcript 输出文件在 Unison 测试体系中的角色
本文关联文档 serial-test-05.output.md 属于unison-src/transcripts-using-base/目录。该目录下的每个主题都维护一对文件:手写的 transcript 脚本(如 serial-test-05.md)与运行后自动生成的输出实录(.output.md)。
两者关系可以这样理解:
.md文件里是unison代码块与ucm命令块,描述「要加载什么代码、执行什么命令」;.output.md则是 Unison Codebase Manager(ucm)实际执行后自动回填的 stdout 快照,其中:added-by-ucm前缀的行(见输出第 19–28 行)就是 ucm 在运行过程中补写的类型检查结果,例如:
Loading changes detected in scratch.u. + f : (Nat, Nat, Nat) -> Nat + g : Map Nat ((Nat, Nat, Nat) -> Nat) ->{g} Text + m : Map Nat ((Nat, Nat, Nat) -> Nat) + mkTestCase : '{IO, Exception} () Run `update` to apply these changes to your codebase.这段输出原样展示了 ucm 对四个声明的类型推断结果(其中g的类型打印含 ability 请求标记{g},是 ucm 当时的原始渲染,本文照实引用)。随后执行> add与> run mkTestCase,最终返回(),表示用例生成成功。
需要注意目录内 README 明确写了一句约束:「Transcripts in this directory are only run using the new runtime.」也就是说,这组序列化用例是绑定到新运行时之上的,这也是为何它们要单独放在transcripts-using-base而不是普通 transcripts 目录的原因。此外,运行前需要先加载 _base.md 描述的 prelude:执行builtins.mergeio、load base.u、add,把测试辅助函数灌入代码库。
二、被测数据:把函数当作值存进 Map
serial-test-05 的输入代码非常短,但结构上刻意选择了一个「刁钻」的数据形态——函数被当作普通值存放在自定义的平衡树/Map 里:
f : (Nat, Nat, Nat) -> Nat f = cases (x, y, z) -> x + y + z g : Map Nat ((Nat, Nat, Nat) -> Nat) -> Text g m = match Map.get 0 m with Some f -> Nat.toText (f (1, 2, 3)) None -> "problem" m : Map Nat ((Nat, Nat, Nat) -> Nat) m = Bin 1 0 f Tip Tip mkTestCase = do saveTestCase None "case-05" "v4" g m saveTestCase (Some 5) "case-05" "v5" g m逐项拆解:
f:一个三元组求和函数,(1,2,3) -> 6。Map不是内建类型。它来自测试前导文件 base.u 中自定义的structural type:unique[s9drbo3urtmpecjn6ivkj5mn0vr11gfn] type Map k v = Tip | Bin Nat k v (Map k v) (Map k v)即一棵带 size 字段的二叉查找树:
Tip为空,Bin Nat k v l r存放键值对与左右子树。m:Bin 1 0 f Tip Tip构造一棵只有一个键值对的树——键是0,值是函数f本身。这是整个用例的测试重点:序列化器必须能处理「数据结构的叶子节点是闭包/函数」这种场景。g:通过Map.get 0 m取出该函数,若能取到就f (1,2,3)得到6,再经Nat.toText转为文本;取不到则返回"problem"。Map.get的实现同样在 base.u:按compare k kx的结果(-1/+1/+0)递归下降查找,命中键即返回Some x。
由于g m的结果固定为"6",序列化产物对应的期望输出文件case-05.out内容正是单个字符6(已在仓库产物中验证)。这样回放脚本就有一个明确的、与代码行为强绑定的判据。
三、mkTestCase 与 saveTestCase:序列化样本的生成机制
mkTestCase两次调用辅助函数saveTestCase,其签名与实现在 base.u:
saveTestCase : Optional Nat -> Text -> Text -> (a ->{} Text) -> a ->{IO,Exception} () saveTestCase mv name ver f i = dir = "unison-src/transcripts-using-base/serialized-cases/" sfile = dir ++ name ++ "." ++ ver ++ ".ser" ofile = dir ++ name ++ ".out" hfile = dir ++ name ++ "." ++ ver ++ ".hash" output = f i saveSelfContained mv (f, i) sfile writeFile ofile (toUtf8 output) writeFile hfile (Bytes.toBase32 (crypto.hash Sha3_512 (f, i)))参数含义与调用对应关系如下:
| 参数 | 含义 | case-05 的两次调用 |
|---|---|---|
mv | 版本标记(Optional Nat,None或Some 5) | None(v4)、Some 5(v5) |
name | 用例名,用于拼文件名 | "case-05" |
ver | 序列化/哈希版本字符串 | "v4"/"v5" |
f | 把输入转成期望输出的函数(此处是g) | g |
i | 被测值本体(此处是含函数的 Map) | m |
一次调用会向serialized-cases/目录写出三类产物:
.ser文件:saveSelfContained mv (f, i) sfile把(f, i)二元组序列化为自包含(self-contained)值写入。自包含意味着依赖闭包里的函数定义必须一并嵌入序列化流,读回时无需依赖外部代码库。.out文件:output = f i即g m的文本结果,此处为"6",作为回放时的期望输出。.hash文件:对(f, i)本体计算Sha3_512哈希再 base32 编码,作为内容指纹,回放时用来校验「读回的值在结构上是否与原值完全一致」。
对应地,读取侧在 base.u 的loadSelfContained里体现了完整链路:fromB32 (readFile path)把 base32 文本还原为二进制 →loadValueBytes反序列化出依赖列表与值 →cache deps用 validateLinks / cache_ 验证依赖并重建缓存(失败会报"cache: missing binding group"、"cache: rehash failed"、"code not self-contained"等错误)→Value.load v加载出可运行的值。这些错误信息本身就是对「自包含」约束的精确描述:序列化产物必须在无外部依赖的情况下被还原成可计算的值。
仓库中已存在的 case-05.v4.ser 与 case-05.v5.ser 是两份实际的运行产物,大小分别为4648 字节与 1392 字节——同样一个值,两种序列化格式的体积差异明显,说明 v4/v5 不是简单的版本号重命名,而是两套真实的格式实现。而两个版本的.hash文件内容完全一致(同为 base32 编码的同一Sha3_512摘要),恰好验证了哈希针对的是值本身而非序列化字节流:值不变,指纹就不变;格式变了,字节流可以变,指纹必须稳定。
四、v4 / v5 版本号的真实含义
为什么一个测试样本要保存 v4、v5 两个版本?这对应 Unison 序列化与哈希方案的版本演进。哈希版本机制的权威说明位于 unison-hashing-v2/src/Unison/Hashing/V2/Tokenizable.hs:
-- | The version of the current hashing function. -- This should be incremented every time the hashing function is changed. -- -- The reasoning is that, if a change to the hashing function changes the hashes for _some_ -- values, it should change it for _all_ values so that we don't have collisions between -- different hashing function versions. ... hashingVersion :: Token hashingVersion = Tag 2源码注释点明了设计动机:哈希函数每次修改都必须整体升级版本号。如果只影响部分值,那么「旧版本哈希」与「新版本哈希」可能同时存在于哈希表中,某些结构简单、哈希恰好没变的值就会与新版本的同名哈希发生碰撞。Tag 2这样的全局版本标记,以及Tokenizable类型类(Tokenizable.hs)的 token 化哈希管线,就是为了让哈希变化是「全或无」的。
因此transcripts-using-base里的 v4/v5 双版本产物,其战略意义是:把旧格式的样本保留在仓库中,随时可以用新代码重新加载它们,确认反序列化、哈希与计算结果没有回归。这也是随机回放脚本会固定覆盖 3、4、5 三个版本号的原因。
五、回放验证:random-deserial 的随机化双指标校验
序列化样本存好之后,谁来「考试」?答案是同目录的 random-deserial.md,它实现了回放与校验的完整逻辑:
collectFailures name version target(第 29–43 行):对某个版本的.ser文件执行loadSelfContained,把读回的值(f, i)重新应用:若f i == target不成立则报"output mismatch";若.hash文件与对读回值重新计算的Sha3_512base32 不一致则报"hash mismatch"。同一份样本同时经受「行为一致性」与「结构指纹一致性」双重校验。runTestCase name(第 45–62 行):从.out读期望值,对版本集合[3, 4, 5]逐一跑collectFailures,全部通过记Ok (name ++ " v" ++ toText ver),有失败则记Fail。serialTests(第 64–68 行):枚举serialized-cases/下所有含.ser的用例(即 case-05.v4/v5 等会被自动纳入),用shuffle打乱执行顺序,再经bSort排序输出,最后汇总为测试结果。随机化顺序是为了暴露潜在的用例间状态依赖。
注意这里校验的是「读回值重算哈希 == 仓库保存的哈希」,而仓库保存的哈希由saveTestCase在生成时对原值计算。只要序列化/反序列化有任何信息丢失或重排,output mismatch或hash mismatch就会立刻出现——这正是 serial-test-05 这类用例存在的全部意义。
六、从 serial-test-00 到 05:被序列化对象的覆盖面设计
case-05 并非孤例,而是覆盖矩阵中的一环。对比同目录各serial-test-*.md的saveTestCase调用即可看出被测数据形态的多样性:
| 用例 | 被测数据结构 | 对应脚本 |
|---|---|---|
| case-00 | 结构类型Tree a(Leaf/Node)与foldMap | serial-test-00.md |
| case-01 | 多参数组合combines (l1, l2, l3) | serial-test-01.md |
| case-02 | 积类型/产品值products (l1, l2, l3) | serial-test-02.md |
| case-03 | 记录式 trip 值 | serial-test-03.md |
| case-04 | 相互递归定义mutual1 5 | serial-test-04.md |
| case-05 | Map 中存储函数并取出调用 | serial-test-05.md |
每一档都对应一类序列化器的难点:树形结构、多参函数、嵌套积、记录、递归闭包、以及「数据结构承载一等函数」。case-05 补上的正是最后一块拼图——函数的序列化依赖闭包环境的完整捕获,这比序列化普通数据更考验自包含机制的完备性。
七、如何在本仓库中复现与继续探索
复现这组测试的前置动作已记录在 _base.md 的 Usage 一节:在 ucm 中执行builtins.mergeio、load unison-src/transcripts-using-base/base.u、add,把saveTestCase、loadSelfContained、Map等辅助定义装入代码库,然后即可运行 transcript。整体测试的批量入口可以参考仓库的 scripts/transcripts.sh 与 scripts/unisonloop.sh,而 random-deserial.output.md 中也能看到case-05 v3/v4/v5被逐一列出并验证的实录(其中 v3 是仓库预置的更早样本)。
想深入阅读源码的读者,建议按三条线索展开:
- 序列化工具层:base.u 中的
saveSelfContained/loadSelfContained/cache与loadValueBytes,理解「自包含 + 依赖校验」的完整往返; - 哈希版本层:unison-hashing-v2/src/Unison/Hashing/V2/Tokenizable.hs 中
Tokenizable与hashingVersion = Tag 2的机制注释; - 回放校验层:random-deserial.md 中
collectFailures的「输出 + 哈希」双指标逻辑。
小结
serial-test-05 表面上只是一小段 Unison 代码和一段 ucm 输出,但它完整地串联了 Unison 序列化测试的四个关键环节:用真实运行时生成自包含序列化样本(.ser)→ 用Sha3_512固化值指纹(.hash)→ 用期望输出固化行为(.out)→ 用随机化脚本跨 v3/v4/v5 版本回放比对。当「函数作为值藏在 Map 里」这种最考验序列化器的场景都能在任意旧版本样本上稳定读回、正确重算、哈希不变时,Unison 的代码库迁移与序列化格式演进就有了可信的回归底线。
- 编程语言
- 编译器
- 语言运行时
- 开发工具
【免费下载链接】unison
A friendly programming language from the future
相关推荐
深入解析 Unison 序列化回归测试:以 serial-test-05 为例的函数值持久化与版本兼容验证
深入解析 Unison 序列化回归测试:以 serial test 05 为例的函数值持久化与版本兼容验证 Unison 是一门"内容寻址"的函数式编程语言,其
编程语言编译器语言运行时开发工具Unison 序列化回归测试实战:从 serial-test-00 Transcript 理解跨版本 Value 序列化
Unison 序列化回归测试实战:从 serial test 00 Transcript 理解跨版本 Value 序列化 导读 本篇文章围绕 Unison 语言
编程语言编译器语言运行时开发工具Unison 序列化测试深度解析:互递归函数的跨版本持久化验证(serial-test-04)
Unison 序列化测试深度解析:互递归函数的跨版本持久化验证(serial test 04) 导读 本文以仓库中 serial test 04.output.
编程语言编译器语言运行时开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考