Модульное тестирование чтения двоичного формата в C - PullRequest
4 голосов
/ 10 марта 2012

Я пишу библиотеку C, которая читает двоичный формат файла.Я не контролирую двоичный формат;это произведено частной программой сбора данных и является относительно сложным.Поскольку это один из моих первых набегов на программирование на C и парсинг двоичных файлов, у меня возникли некоторые проблемы с выяснением того, как структурировать код для тестирования и переносимости.действия заключались в создании библиотеки для чтения произвольного потока байтов.Но в итоге я реализовал тип данных stream , который инкапсулирует тип потока (memstream, filestream и т. Д.).Интерфейс имеет такие функции, как stream_read_uint8, так что клиентскому коду не нужно ничего знать о том, откуда поступают байты.Мои тесты против memstream, а filestream - это просто обертка вокруг FILE*, fread и т. Д.

С точки зрения ООП, я думаю это разумный дизайн.Однако у меня возникает ощущение, что я вставляю неверную парадигму в язык и в результате получаю в результате чрезмерно абстрагированный, слишком сложный код.

Поэтому мой вопрос: есть ли более простой идиоматичный способ сделатьчтение двоичного формата в простом C при сохранении автоматических тестов?

Примечание: я понимаю, что FILE* по сути является интерфейсом абстрактного потока.Но реализация потоков памяти (fmemopen) является нестандартной, и я хочу стандарт C для переносимости.

1 Ответ

2 голосов
/ 10 марта 2012

То, что вы описали, - это низкоуровневая функциональность ввода / вывода. Поскольку fmemopen() не является на 100% переносимым (от Linux, я думаю, что он скрипит), вам нужно предоставить себе что-то переносимое, что вы пишете достаточно близко, чтобы вы могли использовать свои суррогатные функции (только) при необходимости и использовать нативные функции, когда это возможно. Конечно, вы должны иметь возможность принудительно использовать свои функции даже в своей естественной среде обитания, чтобы вы могли протестировать свой код.

Этот код можно протестировать с известными данными, чтобы убедиться, что вы выбрали все символы во входных потоках и можете их верно вернуть. Если исходные данные находятся в определенном порядке байтов, вы можете убедиться, что ваши «большие» типы & mdash; гипотетически, такие функции, как stream_read-uint2(), stream_read_uint4(), stream_read_string() и т. д. & mdash; все ведут себя соответственно. На этом этапе вам не нужны фактические данные; Вы можете изготовить данные, подходящие для себя и своих испытаний.

Как только вы это сделаете, вам также понадобится написать код для чтения данных с большими типами и обеспечения того, чтобы эти функции более высокого уровня действительно могли точно интерпретировать двоичные данные и вызывать соответствующие действия. Для этого вам, наконец, нужны примеры того, что предоставил формат; вплоть до этого этапа вы, вероятно, могли бы получить данные, которые вы изготовили. Но как только вы читаете реальные файлы, вам нужны примеры тех, с которыми можно работать. Или вам придётся изготовить их из вашего понимания и проверить как можно лучше. Насколько это легко, зависит от того, насколько четко задокументирован двоичный формат.


Одним из ключевых инструментов тестирования и отладки будут канонические функции «дампа», которые могут предоставить вам данные. Схема, которую я использую:

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, и он дал:

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 является строго неправильным, так как он использует представление base-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)

Каждая программа идентифицирует себя и свой статус (PASS или FAIL) и сводную статистику.Я занимался поиском и исправлением ошибок, отличных от тех, которые я обнаружил по совпадению, поэтому есть некоторые «ожидаемые ошибки».Это должно быть временное положение дел;это позволяет мне обоснованно утверждать, что все тесты пройдены.Если бы я хотел получить более подробную информацию, я мог бы выполнить любой из тестов на любом из этапов (поднаборы тестов, которые в некоторой степени связаны, хотя «что-то» на самом деле произвольно), и увидеть результаты полностью и т. Д.Как показано, выполнение этого набора тестов занимает менее секунды.

Я считаю это полезным, когда есть повторяющиеся вычисления - но мне пришлось рассчитывать или проверять правильный ответ для каждого из них.тесты в какой-то момент.

...