- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】cangjie_runtime
仓颉编程语言运行时与标准库。
导读
std.unittest.diff是仓颉(Cangjie)标准库std.unittest单元测试框架中的辅助包,专门负责在断言失败时以"差异化对比(diff)"的形式打印期望值与实际值的差异信息。本文以 unittest_diff_package_overview.md 为主线,完整梳理其公开 API(AssertPrintable<T>接口)、内置扩展实现,并结合 stdlib/libs/std/unittest/diff 目录下的真实源码,剖析其底层 diff 算法与打印原理,帮助开发者理解断言失败信息是如何被生成、渲染的,以及如何为自己的类型接入这套差异化打印机制。
一、包定位:unittest.diff 在测试框架中的角色
从官方文档的定义看,unittest.diff提供的是"单元测试框架中打印 diff 比较信息所需的 API"(详见 包总览文档)。
在仓颉标准库中,std.unittest是一个包含多模块的测试框架,unittest.diff是其内部实现模块之一。通过查看 unittest 目录结构 可以发现,该框架主要由common(公共基础设施)、diff(差异打印)、mock(模拟对象)、prop_test(属性测试)、testmacro(测试宏)等模块组成。其中:
std.unittest.common提供底层的PrettyPrinter抽象类与配色枚举Color(见 pretty.cj);std.unittest.testmacro提供@Test、@TestCase、@Assert、@Expect等测试宏;std.unittest.diff则承接"断言失败后如何把期望值与实际值之间的差异直观展示出来"这一职责。
unittest.diff的构建清单在 diff/CMakeLists.txt 中可见,共包含assertion_pretty_printable.cj、collections.cj、common.cj、floats.cj、multiline_diff.cj、one_line_diff.cj、option.cj、strings.cj等源文件,其内部组织与本文后续章节一一对应。
二、核心 API:接口 AssertPrintable<T>
unittest.diff对外暴露的公开 API 只有一个接口AssertPrintable<T>,其定义位于 assertion_pretty_printable.cj:
public interface AssertPrintable<T> { prop hasNestedDiff: Bool func pprintForAssertion( pp: PrettyPrinter, that: T, thisPrefix: String, thatPrefix: String, level: Int64 ): PrettyPrinter }官方接口文档(unittest_diff_package_interfaces.md)给出的功能说明为:提供打印@Assert/@Expect检查结果的方法。
2.1 属性 hasNestedDiff
- 类型:
Bool(对应仓颉内置布尔类型,参见 core 包内建类型文档)。 - 功能:获取当前类型是否存在嵌套的 diff 层级。
该属性决定了打印输出时前缀的拼接策略:当值为true时,说明被打印的类型内部还有嵌套结构(如集合中的元素、Option包裹的值),此时pprintElementDiff会把索引/键追加到前缀上形成类似array[0]的定位信息;反之(如标量类型)则直接复用外层前缀(见 collections.cj 的indexedLeftPrefix/indexedRightPrefix逻辑)。
2.2 方法 pprintForAssertion
签名如下(完整定义见 接口文档):
func pprintForAssertion( pp: PrettyPrinter, that: T, thisPrefix: String, thatPrefix: String, level: Int64 ): PrettyPrinter参数说明:
| 参数 | 类型 | 含义 |
|---|---|---|
pp | PrettyPrinter | 打印器实例,所有 diff 输出都写入该对象。PrettyPrinter是std.unittest.common中的抽象类,支持缩进、对齐与颜色(见 PrettyPrinter 类文档) |
that | T | 待打印的"另一方"内容(调用方自身即"这一方") |
thisPrefix/thatPrefix | String | 期望内容与实际内容的前缀标签,通常是断言宏中传入的表达式文本,如"expected"、"actual" |
level | Int64 | 嵌套层级。level == 1表示最外层,嵌套打印时逐层递增,用于控制缩进深度与省略策略 |
返回值:PrettyPrinter,支持链式调用。
三、内置扩展实现
AssertPrintable<T>是一个开放接口,仓颉标准库为常用类型提供了开箱即用的扩展实现。官方接口文档逐一列举了Float16、Float32、Float64、Option<T>、String五类扩展;而在实际源码中,集合类型(Array、ArrayList、LinkedList、HashSet、HashMap、TreeMap)也实现了该接口。
3.1 浮点类型:Float16 / Float32 / Float64
三个浮点扩展的声明与实现集中在 floats.cj,例如:
extend Float64 <: AssertPrintable<Float64> { public prop hasNestedDiff: Bool { get() { false } } public func pprintForAssertion(pp: PrettyPrinter, right: Float64, leftPrefix: String, rightPrefix: String, level: Int64): PrettyPrinter { return AssertPrintableFloat<Float64>.pprint(this, right, this - right, pp, leftPrefix, rightPrefix, level) } }浮点数 diff 打印的逻辑集中在内部接口AssertPrintableFloat<F>(约束为F <: AnyFloat & Formattable & ToString & Equatable<F>)的静态方法pprint中,值得注意的细节有:
- 最短形式打印:浮点数默认以
%.6g格式输出,代码注释说明使用r'g'说明符以获得最短形式并去除尾部零;当小数部分为空时,会显式追加.0以突出"这是一个浮点数"(addTrailingPointZeroIfNeeded,见 floats.cj)。 - 自动提升有效位数:当左右两侧在
6位有效数字下恰好相等(例如1.0与1.0000001),代码会按sigDigits = ceil(sigDigits * 3 / 2)的规则逐级放大有效位数(上限 50 位),直到打印出足以区分两者的差异(见 floats.cj)。 - 特殊值处理:
NaN、Inf等特殊值直接用toString()输出,避免格式化带来的歧义(见 floats.cj)。 - delta 差异高亮:输出三行内容——左侧值、右侧值以及两者的差
delta,其中左右值的差异片段用红色(RED)标注、delta用黄色(YELLOW)标注(见pprintFloatDiff,floats.cj)。
另外需要说明的是,源码中AnyFloat这个中间接口的引入是"用于规避编译器在浮点类型直接扩展AssertPrintableFloat时产生的符号错误(linker failed)"的临时方案(见 floats.cj 注释),这属于仓颉编译器尚在完善过程中的兼容性处理。
3.2 字符串类型:String
String的扩展实现在 string.cj:
extend String <: AssertPrintable<String> { public prop hasNestedDiff: Bool { get() { false } } public func pprintForAssertion(pp: PrettyPrinter, right: String, leftPrefix: String, rightPrefix: String, level: Int64): PrettyPrinter { return pp.pprintStringDiff(this, right, leftPrefix, rightPrefix, level) } }pprintStringDiff会根据字符串是否包含换行符自动分派到两种模式(见 string.cj):
- 单行模式:调用
printOneLineDiff,输出左右两个字符串及一行用=、-、+、!标注的"字符级对齐差异行"; - 多行模式:若任一字符串含
\n,则输出(multiline difference below)提示,并进入 Myers diff 行级对比(详见下文算法章节)。
3.3 Option<T>
Option<T>的扩展实现在 option.cj,约束条件为T <: Equatable<T>:
extend<T> Option<T> <: AssertPrintable<Option<T>> where T <: Equatable<T> { public prop hasNestedDiff: Bool { get() { false } } public func pprintForAssertion(pp: PrettyPrinter, right: Option<T>, leftPrefix: String, rightPrefix: String, level: Int64): PrettyPrinter { return pp.pprintOptionDiff(this, right, leftPrefix, rightPrefix, level) } }pprintOptionDiff对左右两个Option做四象限匹配(见 option.cj):
Some(l)vsSome(r):若元素本身实现了AssertPrintable则递归使用差异化打印,否则回退到朴素打印(pprintForAssertionOrSimple);Some(l)vsNone或NonevsSome(r):直接输出left: .../right: ...两行对比;NonevsNone:什么都不输出(断言本应通过,不会走到这里)。
3.4 集合类型:Array / ArrayList / LinkedList / HashSet / HashMap / TreeMap
虽然官方接口文档只列了五类扩展,但源码中集合类型的实现更加丰富(见 collections.cj),它们通过两个中间接口组织:
AssertPrintableCollection<C>:抽象"集合类"的 diff 行为,其hasNestedDiff固定返回true(集合内部有嵌套元素),并处理"两侧 size 不同"的情况——先输出Different sizes: 3(expected) != 5(actual)再输出元素差异(见 collections.cj);AssertPrintableOrderedCollection<C, E>:为有序集合(Array<T>、ArrayList<T>、LinkedList<T>,约束T <: Equatable<T>)逐下标比较元素,输出格式形如Different elements (left - array, right - array):后跟[0]: ...逐项差异。
无序集合与映射的处理则面向"差集":
HashSet<T>分别找出"仅在左侧存在"与"仅在右侧存在"的元素,输出Missed elements in ...(见 collections.cj);HashMap<K, V>/TreeMap<K, V>通过内部接口AssertPrintableMap<C, K, V>做键集合差集 + 同键值差异的双重对比,输出Missed keys in ...与Different values (left - ..., right - ...):两类信息(见 collections.cj)。
为控制输出规模,集合打印内置了两类截断策略:单个值超过 21 字符时截断为前 20 字符加...(pprintTrimmedValue);差异元素超过 20 个时省略,并给出...further N different elements are omitted的统计信息(见 collections.cj 与 collections.cj)。
四、底层原理:两个 diff 算法与 PrettyPrinter
unittest.diff差异化打印的核心质量取决于其底层算法实现,这正是它区别于"朴素地把两个值打印出来"的关键。
4.1 多行文本:Myers diff 算法
多行字符串的差异对比由 multiline_diff.cj 中的MultilineDiffBuilder实现,源码注释明确标注"它实现了 Myers diff 算法"。其流程要点:
- 先把文本按
\n切分为行序列,超过MAX_EDIT_ITEMS = 50行时只取前 50 行并记录省略行数(splitAndTrim); - 用编辑图(edit graph)+ frontier 方式求最短编辑脚本,输出
Edit<String>序列(Insert/Remove/Keep/Modify),见computeEditScript(multiline_diff.cj); - 块级差异(连续删除/插入的行块)再通过带"弱比较"(
weakCmp,基于 Wagner-Fischer 距离限制上限)的二次 Myers diff 精细到行内(printBlocksDiff,multiline_diff.cj); - 输出时:删除行以红色
-前缀、插入行以绿色+前缀、修改行调用单行 diff、相同行以灰色打印(连续相同行超过 1 行时折叠为(...N equal lines skipped...)),见printEditScript(multiline_diff.cj)。
另外,多行比较前会先统一换行符(unifyLineBreaks把\r\n归一为\n),如果归一后两侧完全相等,则输出"只有换行风格不同"的提示(见 multiline_diff.cj)。
4.2 单行文本:Needleman-Wunsch 算法
单行字符串(或行内差异)的字符级对比由 one_line_diff.cj 中的OneLineDiffBuilder实现,源码注释标注"它实现了 Needleman-Wunsch 算法",这是一种经典的序列对齐动态规划算法。关键参数与行为:
- 打分规则:匹配奖励
-1.0、错配惩罚missPenalty = 1.0、空位惩罚gapPenalty = 1.0、末端空位使用较小的skewPenalty = 0.5以减少"前缀/后缀错位"的干扰(见 one_line_diff.cj); - 相似度门槛:仅当整体相似度(
Keep占比)不低于minSimilarity = 0.5,或前缀/后缀片段足够相似(prefixSuffixFactor = 0.25、minPrefixSuffixSimilarity = 0.75)时才展示详细字符级 diff,否则直接整段黄色标注(build方法,one_line_diff.cj); - 长串采样:任一侧超过 150 字符时,优先从公共前缀附近截取约 150 字符的窗口,并在开头标注
(N characters omitted...)、结尾标注...(sampleStrings与SampledAssertValue,见 common.cj); - 输出对齐:左右两行按字符宽度右对齐逐列对齐,差异行以
=(相同)、-(删除)、+(插入)、!(修改)标注(printOneLineDiff,one_line_diff.cj)。
4.3 打印载体:PrettyPrinter 与 Color
所有 diff 输出都通过std.unittest.common的PrettyPrinter抽象类渲染。该抽象类(见 pretty.cj)支持:
- 缩进控制:
indent(body)/indent(n, body)以空格串维护当前缩进; - 对齐排版:
appendRightAligned实现右对齐; - 颜色渲染:通过
Color枚举(RED、GREEN、YELLOW、BLUE、CYAN、MAGENTA、GRAY、DEFAULT_COLOR,见 pretty.cj)与底层setColor/put钩子输出 ANSI 彩色文本。
unittest.diff对PrettyPrinter做了大量扩展(pprintNotEqual、pprintFloatDiff、pprintStringDiff、pprintOptionDiff、pprintLeftRight等),全部集中在 diff 模块各源文件中,形成了"diff 模块负责策略、common 模块负责渲染"的清晰分层。
五、调用链路:断言如何触发差异化打印
要理解AssertPrintable<T>的用途,必须看它在哪里被调用。在 assert.cj 中,assertEqual/expectEqual的核心逻辑如下(见buildCheckResult,assert.cj):
match (expected) { case te: AssertPrintable<T> => let message = PrettyText() te.pprintForAssertion(message, actual, leftStr, rightStr, 1) DiffCheckResult(checkResultKind, message) case _ => // 非 AssertPrintable 类型走朴素比较结果 PlainCompareCheckResult / PlainDeltaCompareCheckResult }由此可以得到完整的调用链路:
- 测试代码中使用
@Assert/@Expect宏或直接调用assertEqual/expectEqual(leftStr、rightStr为期望值/实际值的表达式文本,由宏自动注入); buildCheckResult检查expected是否实现了AssertPrintable<T>:- 是 → 调用
pprintForAssertion,以level = 1从最外层开始渲染差异,产出DiffCheckResult(内部是PrettyText缓冲的格式化文本); - 否 → 回退到朴素打印(
toStringOrPlaceholder输出两侧字符串),或isDelta时的近似比较结果;
- 是 → 调用
assertEqual失败时抛出AssertException,expectEqual失败时仅记录错误(不中断执行),最终差异文本由测试框架的ConsoleReporter/XmlReporter等报告器输出到控制台或报告文件。
关键点在于:AssertPrintable的分派发生在运行时类型匹配。因此用户自定义类型只要实现了该接口,就能自动接入这套差异化输出,无需修改框架本身——这正是unittest.diff设计上"开放扩展"的体现。若类型未实现该接口,打印会降级为朴素的left: ... != right: ...形式(pprintLeftRight,见 common.cj)。
六、为自定义类型接入差异化打印
基于前文分析,开发者可以让自己的类型获得"开箱即用"的差异输出。实现要点如下:
import std.unittest.common.PrettyPrinter import std.unittest.diff.AssertPrintable class Point <: Equatable<Point> { let x: Int64 let y: Int64 init(x: Int64, y: Int64) { this.x = x; this.y = y } operator func ==(that: Point): Bool { x == that.x && y == that.y } func toString(): String { "Point(${x}, ${y})" } } extend Point <: AssertPrintable<Point> { public prop hasNestedDiff: Bool { get() { false } } public func pprintForAssertion( pp: PrettyPrinter, that: Point, thisPrefix: String, thatPrefix: String, level: Int64 ): PrettyPrinter { // 自定义输出:例如高亮差异字段 pp.pprintNotEqual(thisPrefix, thatPrefix).newLine() pp.indent { pp.appendRightAligned("x", 2).append(": ").colored(RED, x.toString()).newLine() pp.appendRightAligned("y", 2).append(": ").colored(RED, y.toString()) } pp } }设计建议:
hasNestedDiff:若你的类型内部包含实现了AssertPrintable的子元素(如集合、Option、嵌套结构体),应返回true,这样在作为集合元素或Option内容打印时,索引/键会被追加到前缀上,形成[0]、["key"]式的定位(参照 collections.cj 的indexedLeftPrefix逻辑);level与缩进:level == 1时通常先输出前缀A != 前缀B的概要行,更深层级则只输出细节;可参照浮点与字符串扩展的写法;- 颜色约定:差异值用
RED、delta/概要用YELLOW、插入内容用GREEN、次要信息用GRAY,与内置实现保持一致; - 规模控制:内容较大时借鉴
pprintTrimmedValue(超 21 字符截断)与"省略统计"策略,避免巨型输出。
七、总结
std.unittest.diff虽小,却是仓颉单元测试框架"失败信息可读性"的关键拼图。它以单个公开接口AssertPrintable<T>定义了一套差异化打印协议,通过hasNestedDiff表达嵌套结构、通过pprintForAssertion以PrettyPrinter为载体输出带层级、对齐与配色的差异文本;标准库已为浮点、字符串、Option及全部常用集合类型提供了实现,底层分别由 Myers diff(多行文本)与 Needleman-Wunsch(单行字符对齐)两种算法驱动,并内建了超长文本采样、超大集合省略等规模控制机制。理解这套机制后,开发者既能更高效地读懂框架生成的断言报告,也能轻松为自定义类型接入同样专业级的差异输出。
如需进一步实践,可参阅仓颉单元测试框架的入门指南与基础概念文档,并在运行测试后观察ConsoleReporter输出的彩色 diff 报告。
- 编程语言
- 语言运行时
- 标准库
- 垃圾回收
- 并发编程
【免费下载链接】cangjie_runtime
仓颉编程语言运行时与标准库。
相关推荐
cangjie_runtime 标准库 std.unittest.diff 接口解析:AssertPrintable\<T\> 与断言差异打印机制
cangjie_runtime 标准库 std.unittest.diff 接口解析:AssertPrintable\<T\ 与断言差异打印机制 导读 std.
编程语言语言运行时标准库垃圾回收并发编程仓颉单元测试断言差异打印接口 AssertPrintable 深度解析(std.unittest.diff)
仓颉单元测试断言差异打印接口 AssertPrintable 深度解析(std.unittest.diff) 本文以仓颉标准库 std.unittest.dif
编程语言语言运行时标准库垃圾回收并发编程仓颉 std.unittest.diff 单元测试差异打印详解:AssertPrintable 接口与断言 diff 输出机制
仓颉 std.unittest.diff 单元测试差异打印详解:AssertPrintable 接口与断言 diff 输出机制 std.unittest.dif
编程语言语言运行时标准库垃圾回收并发编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考