在C中单元测试二进制格式阅读器

时间:2012-03-09 23:09:38

标签: c stream binary-data

我正在编写一个读取二进制文件格式的C库。我不控制二进制格式;它是由专有数据采集程序生成的,相对复杂。由于这是我第一次尝试进行C编程和二进制文件解析,我在确定如何构建测试和可移植性代码时遇到了一些麻烦。

出于测试目的,我认为最简单的操作方法是构建库以读取任意字节流。但我最终实现了一个 stream 数据类型,它封装了流的类型(memstreamfilestream等)。该接口具有stream_read_uint8之类的功能,因此客户端代码不必知道字节来自何处。我的测试针对memstreamfilestream内容基本上只是FILE*fread等的包装。

从OOP的角度来看,我认为这是一个合理的设计。但是,我觉得我把错误的范例填充到语言中,最终导致过度抽象,过于复杂的代码。

所以我的问题是:在保留自动化测试的同时,是否有更简单,更惯用的方式在普通C中进行二进制格式读取?

注意:我意识到FILE*本质上是一个抽象的流接口。但是内存流(fmemopen)的实现是非标准的,我希望标准C具有可移植性。

1 个答案:

答案 0 :(得分:2)

您所描述的是低级I / O功能。由于fmemopen()不是100%便携式(在Linux之外,它吱吱作响,我怀疑),那么你需要为自己提供一些便携式的东西,你写的足够接近你可以在必要时使用你的代理功能(仅限)并尽可能使用本机功能。当然,即使在您的原生栖息地,您也应该能够强制使用您的功能,以便您可以测试您的代码。

可以使用已知数据测试此代码,以确保您获取输入流中的所有字符并可以忠实地返回它们。如果原始数据属于特定的字节序,您可以确保您的“较大”类型(假设为stream_read-uint2()stream_read_uint4()stream_read_string()等函数 - 都表现得恰当。对于这个阶段,您实际上并不需要实际数据;您可以制作适合自己和测试的数据。

一旦你有了这个,你还需要编写代码来读取更大类型的数据,并确保这些更高级别的功能实际上可以准确地解释二进制数据并调用适当的操作。为此,您最终需要提供格式的示例;直到这个阶段,你可能会得到你制造的数据。但是一旦你正在阅读实际的文件,你需要有一些工作的例子。或者你必须尽可能地从你的理解和测试中制造它们。这有多容易取决于二进制格式的清晰记录。


其中一个关键的测试和调试工具是规范的“转储”功能,可以为您提供数据。我使用的方案是:

extern void dump_XyzType(FILE *fp, const char *tag, const XyzType *data);

溪流是不言而喻的;通常它是stderr,但通过使其成为参数,您可以将数据提供给任何打开的文件。 tag包含在打印的信息中;识别呼叫的位置应该是唯一的。最后一个参数是指向数据类型的指针。你可以分析和打印它。您应该借此机会断言您能想到的所有有效性检查,以避免出现问题。

您可以使用, const char *file, int line, const char *func扩展界面,并安排在呼叫中添加__FILE____LINE____func__。我从来都不需要它,但如果我这样做,我会使用:

#define DUMP_XyzType(fp, tag, data) \
        dump_XyzType(fp, tag, data, __FILE__, __LINE__, __func__)

作为一个例子,我处理类型DATETIME,所以我有一个函数

extern void dump_datetime(FILE *fp, const char *tag, const ifx_dtime_t *dp);

我本周使用的其中一个测试可以被说服转储日期时间值,它给出了:

DATETIME: Input value -- address 0x7FFF2F27CAF0
Qualifier: 3594 -- type DATETIME YEAR TO SECOND
DECIMAL: +20120913212219 -- address 0x7FFF2F27CAF2
E:   +7, S = 1 (+), N =  7, M = 20 12 09 13 21 22 19

您可能会或可能无法在其中看到值2012-09-13 21:22:19。有趣的是,这个函数本身调用了族中的另一个函数dump_decimal()来打印出十进制值。一年,我将升级限定符打印以包含十六进制版本,这更容易阅读(3594是0x0E0A,这是知道的14位数(E)容易理解,从YEAR开始(第二个) 0)到秒​​(A),从十进制版本肯定不那么明显。当然,信息是类型字符串:DATETIME YEAR TO SECOND。(十进制格式对于局外人来说有点难以理解,但很清楚知道有指数(E),符号(S),数字(厘米)(N = 7)和实际数字(M = ...)的内部人员。是的,名称{{1严格来说是一个误称,因为它使用了基数为100或厘米的表示。)

默认情况下,测试不会产生这样的细节级别,但我只需要使用足够高的调试级别(通过命令行选项)运行它。我认为这是另一个有价值的特征。

运行测试的最安静方式产生:

decimal

每个程序都标识自己及其状态(通过或失败)和摘要统计。我一直在寻找bug并修复一个我偶然发现的bug之外的bug,所以有一些'预期的失败'。那应该是暂时的事态;它允许我合法地声称测试全部通过。如果我想要更多细节,我可以运行任何测试,任何阶段(测试的子集有些相关,虽然'有些'实际上是任意的),并完整地看到结果,等等。如图所示,运行该组测试只需不到一秒钟。

我觉得这在有重复计算的地方很有用 - 但我必须在某些时候计算或验证每一项测试的正确答案。