2012-03-09 73 views
4

我正在编写一个读取二进制文件格式的C库。我不控制二进制格式;它是由专有的数据采集程序生成的,并且相对复杂。由于这是我第一次尝试C编程和二进制文件解析,所以我在确定如何构造用于测试和可移植性的代码时遇到了一些问题。单元测试C中的二进制格式阅读器

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

从一个面向对象的角度来看,我觉得是一个合理的设计。但是,我感觉到我正在将错误的范例塞进语言中,并最终导致过度抽象,过于复杂的代码。

所以我的问题:是否有一个更简单,更习惯的方式来做二进制格式阅读纯C同时保留自动化测试?

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

+1

在我的专家看来,我认为只要你的封装使你的用户可以使用和/或解决一个问题,并且代码是合理可维护和直接的,你可能是没问题的。 – prelic 2012-03-09 23:34:22

+0

@prelic,谢谢,当然这是有道理的。我只是将这个项目作为一个学习练习,所以我对C最佳实践和规范感兴趣。 – yamad 2012-03-09 23:44:01

+1

我参与过读取二进制格式的项目,这是我们使用过的方法,并且被预先存在的程序使用 - 一次读取X位,读取后在缓冲区中标记占位符,并将数据返回为uint8_t *,让调用者转换成任何需要的类型。不是你的问题的答案,只是我的经验。 – greg 2012-03-09 23:45:37

回答

2

您所描述的是低级I/O功能。由于fmemopen()不是100%可移植的(关闭Linux,它会吱吱嘎嘎,我怀疑),那么您需要为自己提供一些可移植的东西,这些东西足够接近,以便在必要时使用代理函数(仅),并使用native尽可能发挥功能。当然,即使在您的本地栖息地,您也应该能够强制使用您的功能,以便测试您的代码。

这段代码可以用已知数据进行测试,以确保您获取输入流中的所有字符,并可以忠实地返回它们。如果原始数据具有特定的字节顺序,则可以确保假设您的“较大”类型的函数(如stream_read-uint2(),stream_read_uint4(),stream_read_string(),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)到second(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) 

每个程序识别本身和它的状态(通过或失败)和总结统计。我一直在寻找bug,并修复了一些我偶然发现的bug,所以有一些“预期的失败”。这应该是一种暂时的事态;它允许我合法地声称测试全部通过。如果我想要更多的细节,我可以运行任何测试,任何阶段(有些相关测试的子集,虽然'有点'实际上是任意的),并且看到全部结果等。如图所示,运行该组测试需要不到一秒的时间。

我发现这有助于有重复计算的地方 - 但我必须计算或验证每个测试在某个点上的正确答案。