☰
Unison 序列化兼容性测试深入解读:serial-test-05 与「Map 中存储函数」的跨版本回归保障
2026/10/9 1:29:09 网站建设 项目流程
  • 编程语言
  • 编译器
  • 语言运行时
  • 开发工具

【免费下载链接】unison

A friendly programming language from the future

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

导读

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

逐项拆解:

  1. f:一个三元组求和函数,(1,2,3) -> 6。

  2. 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存放键值对与左右子树。

  3. m:Bin 1 0 f Tip Tip构造一棵只有一个键值对的树——键是0,值是函数f本身。这是整个用例的测试重点:序列化器必须能处理「数据结构的叶子节点是闭包/函数」这种场景。

  4. 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/目录写出三类产物:

  1. .ser文件:saveSelfContained mv (f, i) sfile把(f, i)二元组序列化为自包含(self-contained)值写入。自包含意味着依赖闭包里的函数定义必须一并嵌入序列化流,读回时无需依赖外部代码库。
  2. .out文件:output = f i即g m的文本结果,此处为"6",作为回放时的期望输出。
  3. .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)与foldMapserial-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 5serial-test-04.md
case-05Map 中存储函数并取出调用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 是仓库预置的更早样本)。

想深入阅读源码的读者,建议按三条线索展开:

  1. 序列化工具层:base.u 中的saveSelfContained/loadSelfContained/cache与loadValueBytes,理解「自包含 + 依赖校验」的完整往返;
  2. 哈希版本层:unison-hashing-v2/src/Unison/Hashing/V2/Tokenizable.hs 中Tokenizable与hashingVersion = Tag 2的机制注释;
  3. 回放校验层: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

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

相关推荐

上一篇:CNTK 语音识别入门实战:基于 AN4 数据集训练 FF 与 LSTM 声学模型
下一篇:终极指南:解决ML-Agents Protobuf版本冲突的完整方案

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

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

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

立即咨询