subunit测试流协议的Python实现
项目描述
子单元:测试结果流式协议 版权所有 (C) 2005-2013 罗伯特·柯林斯 <robertc@robertcollins.net>
用户可选择 Apache 许可证 2.0 版或 BSD 3 条款许可证。项目源代码中提供了这两种许可证的副本。您只能在这些许可证之一的规定下使用此文件。
除非适用法律要求或书面同意,否则在这些许可证下分发的软件按“原样”基础分发,不提供任何明示或暗示的保证或条件。请参阅您选择的许可证,了解该许可证下权限和限制的具体语言。
有关 Subunit 许可证的详细信息,请参阅 COPYING 文件。
Subunit
Subunit 是一种测试结果流式协议。
该协议有两个主要版本。版本 1 虽然易于阅读,但在高度并行测试方面存在重大缺陷——它无法并行执行发现和执行,在多路复用时需要大量缓冲,且非常脆弱——一个损坏的字节可能导致整个流被错误解析。版本 1.1 添加了对二进制流的封装,这缓解了一些问题,但核心问题依然存在。
版本 2 与版本 1 具有许多良好特性——它可以嵌入到常规文本流中(例如来自构建系统),并且仍然模拟 xUnit 风格的测试执行。它还解决了版本 1 中的许多问题——版本 2 可以在不过度缓冲的情况下进行多路复用(在时间或空间上),它具有处理损坏流的良好定义的恢复机制(例如,在两个进程同时写入同一流时,或当流生成器出现错误时)。
关于这两个协议版本的更多详细信息,请参阅本文件的“协议”部分。
Subunit 随附命令行过滤器以处理 subunit 流,并为 Python、C、C++ 和 shell 提供语言绑定。为其他语言编写绑定也很容易。
- 使用 subunit 可以轻松完成许多有用的事情
测试聚合:可以单独运行的测试可以组合在一起,然后一起报告/显示。例如,不同语言的测试可以作为一个无缝的整体显示,在多台机器上运行的测试可以通过多路复用器聚合到一个流中。
测试归档:可以记录一次测试运行并在以后重新播放。
测试隔离:可能会崩溃或与其他测试严重交互的测试可以单独运行,然后聚合在一起,而不是相互干扰或需要临时的测试->运行器报告协议。
网格测试:subunit 可以作为必要的序列化和反序列化,使分布式机器上的测试运行能够实时报告。
- Subunit 提供以下过滤器
tap2subunit - 将 Perl 的 TestAnythingProtocol 转换为 subunit。
subunit2csv - 将 subunit 流转换为 csv。
subunit2disk - 将 subunit 流导出到磁盘上的文件。
subunit2pyunit - 将 subunit 流转换为 pyunit 测试结果。
subunit2gtk - 在 GTK 中显示 subunit 流。
subunit2junitxml - 将 subunit 流转换为 JUnit 的 XML 格式。
subunit-diff - 比较两个 subunit 流。
subunit-filter - 从 subunit 流中过滤测试。
subunit-ls - 列出 subunit 流中存在的测试信息。
subunit-stats - 生成 subunit 流的摘要。
subunit-tags - 向流中添加或删除标签。
与其他工具的集成
Subunit 的语言绑定作为与各种测试运行器(如“check”,“cppunit”,Python 的“unittest”)的集成。除此之外,少量胶水(通常是几行代码)就可以让 Subunit 以更复杂的方式使用。
Python
Subunit 对 Python 支持出色:大多数过滤器和工具都是用 Python 编写的,并且有使用 Subunit 在测试套件内部无缝增加测试隔离的设施。
最常见的方法是运行现有的Python测试套件,并通过 subunit.run 模块输出subunit。
$ python -m subunit.run mypackage.tests.test_suite
有关Subunit提供的Python支持的更多信息,请参阅 pydoc subunit 或 python/subunit/ 中的源代码。
C
Subunit提供了C绑定以发出协议。C单元测试项目‘check’已经包含了几年的subunit支持。有关更多详细信息,请参阅‘c/README’。
C++
C库可以直接从C++中包含和使用。Subunit发行版中包含了一个用于CPPUnit的TestListener。有关详细信息,请参阅‘c++/README’。
shell
有两个shell工具集。有过滤器,它们接受stdin上的subunit流,并在stdout上输出处理后的数据(或转换后的流)。
然后是类似于C的unittest功能:由简单函数组成的shell绑定,用于输出协议元素,以及将subunit输出添加到‘ShUnit’shell测试运行器的补丁。有关详细信息,请参阅‘shell/README’。
过滤器配方
忽略一些已知根本原因的失败测试
subunit-filter --without 'AttributeError.*flavor'
xUnit测试模型
Subunit实现了一个略有修改的xUnit测试模型。标准模型是存在测试,它们有一个id(),可以运行,当运行开始时,会发出结果(如成功或失败),然后完成。
Subunit通过测试枚举(在不运行它们的情况下找出运行器拥有的测试)、标签(允许用户以测试框架不应用任何语义值的方式描述测试)、文件附件(允许任意数据以便轻松分析失败)和时间戳来扩展这一点。
协议
版本2,或v2是新的,仍在开发中,但预计在不久的将来将取代版本1。Subunit捆绑的工具仅接受版本2,并且仅发出版本2,但新的过滤器subunit-1to2和subunit-2to1可用于与较旧的第三方库进行互操作。
版本2
版本2是一个二进制协议,由独立的数据包组成,可以嵌入make等工具的输出中,只要每个数据包没有其他字节与之混合(这是‘make -j N>1’倾向于做的)。版本2目前处于草案形式,早期采用者应愿意丢弃存储的结果(如果协议发生变化),或者将它们批量转换回v1,然后转换到更新的版本2。
协议在流开始时、数据包之后或任何0x0A字节之后同步。也就是说,subunit v2数据包在换行符之后或直接在先前数据包的末尾之后开始。
Subunit旨在通过TCP等可靠的流式传输协议进行传输。因此,它不关心数据包的顺序交付。然而,由于发送者中的错误或由于在嵌入时对同一fd的并发写入导致的混合数据,Subunit努力从损坏的数据中合理地恢复。
Subunit版本2的一个关键设计目标是允许无缓冲处理和复用,而不强制进行缓冲以获得语义正确性,因为缓冲往往会隐藏挂起或表现不佳的测试。话虽如此,为了网络效率进行有限的基于时间的缓冲是一个好主意 - 这最终是实施者的选择。也不鼓励对subunit流进行行缓冲,因为即使行缓冲不会造成问题,也可能需要交互式流量以将调试器或其他工具插入其中。
在版本二中有两个概念性事件 - 一个测试状态事件和一个文件附件事件。事件可以有时间戳,记录事件通过的多路复用器路径,以便将操作发送回源(例如运行新测试或stdin以驱动调试器和其它交互式输入)。测试状态事件用于列出测试,报告正在运行的测试和测试助手。测试可以有标签,允许通过subunit传输额外的含义,而无需解析任意文件附件。不是独立测试的东西可以通过将“可运行”标志设置为false来标记。例如,TAP中的单个断言不是可运行的测试,只有顶层TAP测试脚本才是可运行的。
文件附件用于提供关于失败性质的丰富细节。文件附件也可以在测试期间和测试之外封装stdout和stderr。
大多数数字以网络字节序存储 - 最不重要的字节首先编码,使用http://www.dlugosz.com/ZIP2/VLI.html的变体。第一个字节的最高两位编码了数字中八位的总数。这种编码可以编码从0到2**30-1的值,足够编码纳秒。非可变长度编码的数字仍然以MSB顺序存储。
前缀 |
八位字节 |
最大 |
最大 |
---|---|---|---|
00 |
1 |
2**6-1 |
63 |
01 |
2 |
2**14-1 |
16383 |
10 |
3 |
2**22-1 |
4194303 |
11 |
4 |
2**30-1 |
1073741823 |
数据包的所有可变长度元素都以长度前缀数字存储,以便它们可以被不需要解释它们的消费者跳过。
UTF-8字符串没有终止的NUL,不应包含任何嵌入的NUL(实现应验证它们处理的任何此类字符串,并采取一些补救措施(例如,丢弃损坏的数据包))。
简而言之,数据包的结构是
- PACKET := 签名 标志 数据包长度 时间戳? 测试ID? 标签? MIME?
文件内容? 路由代码? CRC32
更详细...
数据包通过单个字节的签名识别 - 0xB3,它在UTF-8流中作为字符的第一个字节是非法的。0xB3以第一个位设置为1,第二位未设置开始,这是UTF-8中延续字节的签名。0xB3被选择为0x73(ASCII中的's'),将最高两位替换为延续字节中的1和0。
如果subunit数据包被嵌入到非UTF-8文本流中,其中0x73是一个合法字符,请考虑将文本重新编码为UTF-8,或使用subunit的“文件”数据包在subunit中嵌入文本流,而不是相反。
在签名字节之后是一个16位的标志字段,它包括一个4位的版本字段 - 如果版本不是0x2,则无法读取数据包。建议在此处发出错误信号(例如,通过发出一个合成的错误数据包并返回到顶级循环以查找新的数据包,或者带错误退出)。如果需要恢复,将数据包签名视为一个不可见的字节,并扫描新的同步点。注意:Subunit V1和V2数据包可以合法地包含内部0xB3,因为它们是8位安全容器格式,因此从这种情况下恢复可能涉及任意数量的误报,直到遇到实际的数据包:即使这样,它也可能仍然错误,由于巧合在通过版本检查后失败。
标志也以网络字节序存储。
高字节 |
低字节 |
|
15 14 13 12 11 10 9 8 |
7 6 5 4 3 2 1 0 |
|
版本 |
功能位 |
有效的版本值是:0x2 - 版本2
功能位
位11 |
掩码 0x0800 |
测试ID存在。 |
位10 |
掩码 0x0400 |
路由代码存在。 |
位9 |
掩码 0x0200 |
时间戳存在。 |
位8 |
掩码 0x0100 |
测试是“可运行的”。 |
位7 |
掩码 0x0080 |
存在标签。 |
位6 |
掩码 0x0040 |
存在文件内容。 |
位5 |
掩码 0x0020 |
存在文件MIME类型。 |
位4 |
掩码 0x0010 |
EOF 标记。 |
位 3 |
掩码 0x0008 |
版本 2 中必须为零。 |
测试状态获取三个位:位 2 | 位 1 | 位 0 - 掩码 0x0007 - 测试状态枚举查找
000 - 未定义 / 无测试
001 - 枚举 / 存在
002 - 进行中
003 - 成功
004 - 非预期成功
005 - 跳过
006 - 失败
007 - 预期失败
标志字段之后是一个数字字段,表示整个数据包(包括签名和校验和)的字节数。这个长度必须小于 4MiB - 4194303 字节。编码显然可以记录更大的数字,但一个目标是避免需要大缓冲区,或者导致数据包转发/处理管道中的大延迟。较大的文件附件可以通过多个数据包进行通信,此类 4MiB 数据包的开销约为 0.2%。
数据包的其余部分是一系列可选功能,这些功能由标志字段中指定的功能位指定。当不存在时,它们完全不存在。
可以在不解释数据包其余部分的情况下转发和复用数据包,直到路由代码和校验和(这两个都在数据包的末尾)。此外,路由器通常可以避免复制或移动数据包的大部分内容,只要路由代码大小增加不会迫使长度编码占用新字节(这只会发生在长度小于或等于 16KiB 的数据包中) - 大数据包非常高效。
如果有时间戳,它是一个 32 位无符号整数,表示秒,以及一个可变长度的数字,表示纳秒,代表自 Unix Epoch 以来 UTC 时间(秒和纳秒)。
如果有测试 ID,它是一个 UTF-8 字符串。测试 ID 应该唯一标识可运行的测试,以便可以单独选择它们。对于不能单独运行(例如测试夹具/层/子测试)的测试和其他操作,唯一性不是必需的(尽管人类可识别性强烈推荐)。
如果有标签,它是一个长度前缀的 UTF-8 字符串向量,每个标签一个。对标签内容没有限制(除了对子单元中 UTF-8 字符串的一般限制)。标签没有顺序。
如果有 MIME 类型,它定义了所有数据包中文件的 MIME 类型(同一文件(路由代码 + 测试 ID + 名称唯一标识一个文件,在 EOF 标记时重置)的 MIME 类型)。如果一个文件从未设置 MIME 类型,则应将其视为 application/octet-stream。
如果有文件内容,它是一个 UTF-8 字符串,表示名称,然后是内容的字节数,然后是内容字节。
如果有路由代码,它是一个 UTF-8 字符串。路由代码用于确定测试运行在哪个测试后端时进行数据分析,以及在需要交互时将 stdin 转发到测试进程。
复用器应该添加路由代码(如果不存在),并在已存在路由代码的情况下,用路由代码(用斜杠 '/' 分隔)作为前缀。例如,复用器可能将每个复用流标记为一个简单的序号('0'、'1' 等),并且从流 '0' 接收路由代码 '3' 的传入数据包时,在转发数据包时会调整路由代码,使其变为 '0/3'。
在数据包末尾是数据包内容的 CRC-32 校验和。
示例数据包
简单的“foo”枚举数据包,包含测试 ID、可运行集合、状态=枚举。以下空格用于在视觉上分隔签名/标志/长度/测试 ID/CRC-32
b3 2901 0c 03666f6f 08555f1b
版本 1(和 1.1)
版本 1(和 1.1)主要是人类可读的协议。
样本 subunit 线内容
以下
test: test foo works success: test foo works test: tar a file. failure: tar a file. [ .. ].. space is eaten. foo.c:34 WARNING foo is not defined. ] a writeln to stdout
当通过 subunit2pyunit 运行时
.F a writeln to stdout ======================== FAILURE: tar a file. ------------------- .. ].. space is eaten. foo.c:34 WARNING foo is not defined.
Subunit v1 协议描述
此描述正在转换为EBNF风格。目前它只是部分采用这种风格,但仍然相当清晰。如有疑问,请参考源代码(并最好帮助修复描述!)通常,该协议是按行定向的,由指令及其参数组成,或者在DETAILS区域之外,由解析器不解释的意外行 - 应该转发未更改。
test|testing|test:|testing: test LABEL success|success:|successful|successful: test LABEL success|success:|successful|successful: test LABEL DETAILS failure: test LABEL failure: test LABEL DETAILS error: test LABEL error: test LABEL DETAILS skip[:] test LABEL skip[:] test LABEL DETAILS xfail[:] test LABEL xfail[:] test LABEL DETAILS uxsuccess[:] test LABEL uxsuccess[:] test LABEL DETAILS progress: [+|-]X progress: push progress: pop tags: [-]TAG ... time: YYYY-MM-DD HH:MM:SSZ LABEL: UTF8* NAME: UTF8* DETAILS ::= BRACKETED | MULTIPART BRACKETED ::= '[' CR UTF8-lines ']' CR MULTIPART ::= '[ multipart' CR PART* ']' CR PART ::= PART_TYPE CR NAME CR PART_BYTES CR PART_TYPE ::= Content-Type: type/sub-type(;parameter=value,parameter=value) PART_BYTES ::= (DIGITS CR LF BYTE{DIGITS})* '0' CR LF
意外输出到stdout -> stdout。以0退出或最后测试完成 -> 错误
在测试之外给出的标签应用于所有后续测试。在测试之后给出的标签:在同一测试的结果行之前仅适用于该测试,并继承当前全局标签。在标签之前使用“-”来删除标签 - 例如,防止全局标签应用于单个测试,或取消全局标签。
进度指令用于提供有关流的状态信息,以便流消费者可以提供完成估计、进度条等。知道流中将要包含多少测试的流生成器应输出“progress: COUNT”。添加测试的流过滤器应输出“progress: +COUNT”,而删除测试的过滤器应输出“progress: -COUNT”。绝对计数应重置正在使用的进度指示器 - 它表明来自不同生成器的两个单独的流已被简单连接在一起,并且没有关于即将到来的更多完整流的信息。智能连接可以扫描每个流以查找它们的计数并将它们相加,或者将绝对计数转换为行内相对计数。建议输出器除非必要,否则避免使用绝对计数。推送和弹出指令用于提供本地区域以进行进度报告。这符合分层操作的测试环境 - 例如,将测试组织为套件的测试环境 - 最顶层的运行器可以报告套件的数目,每个套件都用一个(推送,弹出)对包围其输出。解释器应将弹出解释为将恢复级别的进度推进一步。在测试对的开始和结束之间遇到进度指令表明之前的测试被中断并且没有干净地终止:它应该隐式地用一个错误关闭(与流结束时没有为最近启动的测试关闭测试指令的情况相同)。
时间指令充当时钟事件 - 它设置所有未来事件的时间。值应该是有效的ISO8601时间。
skip、xfail和uxsuccess结果不是所有测试环境都支持。在Python中,如果使用不支持它们的较旧Python版本,则使用testtools库(https://launchpad.net/testtools)自动翻译这些结果。有关翻译策略,请参阅testtools文档。
skip用于表示测试被发现但未执行。xfail用于表示以某种预期方式出错的测试(在某些框架中也称为“TODO”测试)。uxsuccess用于表示意外成功,即被认为会失败的测试实际上通过了。它与xfail相辅相成。
在subunit上做黑客工作
发布
更新configure.ac和python/subunit/__init__.py中的版本号。
更新NEWS。
执行make distcheck,这将更新Makefile等。
执行PyPI发布:PYTHONPATH=python python setup.py sdist bdist_wheel; twine upload -s dist/*
将常规版本上传到LP。
推送一个标记提交。git push -t origin master:master
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。