【问题标题】:Unit testing a binary format reader in C用 C 对二进制格式阅读器进行单元测试
【发布时间】:2012-03-27 09:13:43
【问题描述】:

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

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

从 OOP 的角度来看,我认为这是一个合理的设计。但是,我觉得我在语言中填充了错误的范式,结果导致代码过于抽象、过于复杂。

所以我的问题是:有没有一种更简单、更惯用的方法来在纯 C 中进行二进制格式读取,同时保留自动化测试?

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

【问题讨论】:

  • 在我不那么专家的意见中,我认为只要您的封装使事情更容易为您的用户使用和/或解决问题,并且代码是合理的可维护且直截了当,您可能没问题。
  • @prelic,谢谢,这当然是有道理的。只是我也将该项目用作学习练习,因此我对 C 最佳实践和规范感兴趣。
  • 我参与了读取二进制格式的项目,这是我们使用的方法,并且已被预先存在的程序使用——一次读取 X 位,读取后在缓冲区中标记一个占位符,并将数据作为 uint8_t* 返回,让调用者转换为任何需要的类型。不是您问题的答案,只是经验中的我也是
  • 我更关心的不是如何从文件中读取数据(有时这也是一个非常重要的部分),而是对阅读器的实际测试。我想喂它全是垃圾。我想给它故意改变甚至损坏的输入(以不同的概率,在不同的位置,不同的值等)。我想看看我的测试是否至少给了我读者代码的完整语句覆盖率。所有这些加上阅读器永远不会挂起或崩溃。

标签: c stream binary-data


【解决方案1】:

您描述的是低级 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 = ...) 的内部人员。是的,这个名字 decimal严格来说是用词不当,因为它使用以 100 为底或百分数表示。)

默认情况下,该测试不会产生该级别的详细信息,但我只需使用足够高级别的调试集(通过命令行选项)运行它。我认为这是另一个有价值的功能。

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

test.bigintcvasc.......PASS (phases: 4 of 4 run, 4 pass, 0 fail)(tests: 92 run, 89 pass, 3 fail, 3 expected failures)
test.deccvasc..........PASS (phases: 4 of 4 run, 4 pass, 0 fail)(tests: 60 run, 60 pass, 0 fail)
test.decround..........PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 89 run, 89 pass, 0 fail)
test.dtcvasc...........PASS (phases: 25 of 25 run, 25 pass, 0 fail)(tests: 97 run, 97 pass, 0 fail)
test.interval..........PASS (phases: 15 of 15 run, 15 pass, 0 fail)(tests: 178 run, 178 pass, 0 fail)
test.intofmtasc........PASS (phases: 2 of 2 run, 2 pass, 0 fail)(tests: 12 run, 8 pass, 4 fail, 4 expected failures)
test.rdtaddinv.........PASS (phases: 3 of 3 run, 3 pass, 0 fail)(tests: 69 run, 69 pass, 0 fail)
test.rdtimestr.........PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 16 run, 16 pass, 0 fail)
test.rdtsub............PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 19 run, 15 pass, 4 fail, 4 expected failures)

每个程序都标识自己及其状态(通过或失败)和汇总统计信息。我一直在寻找错误并修复我偶然发现的错误以外的错误,因此存在一些“预期失败”。那应该是暂时的状态;它允许我合法地声称测试都通过了。如果我想要更多细节,我可以运行任何阶段的任何测试(测试的子集有些相关,尽管“有点”实际上是任意的),并查看完整的结果等。如图所示,运行这组测试只需不到一秒钟的时间。

我发现这在有重复计算的情况下很有用 - 但我不得不在某个时候计算或验证每一项测试的正确答案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-23
    • 1970-01-01
    • 2023-03-26
    • 2015-03-14
    • 2017-04-26
    • 2017-06-02
    相关资源
    最近更新 更多