Недавно я начал учиться в необычной новой школе кодирования под названием Hive Helsinki, школе с геймифицированным учебным планом совместных и проблемных проектов. Это школа-сестра знаменитой Школы 42, основанной в Париже, Франция.
Один из первых проектов, с которого мы начинаем, «libft», включает в себя переписывание многих стандартных функций библиотеки C и других пользовательских функций. Это отражает один из центральных принципов школы, который фокусируется на том, чтобы учащиеся учились учиться. Заставляя нас заново создавать стандартную библиотеку и использовать ее в наших будущих проектах, но запрещая нам использовать внешние функции, мы узнаем о программном обеспечении на гораздо более глубоком уровне. Мы также развиваем умение приобретать опыт независимо от учителя и сотрудничать с другими, чтобы мы могли процветать в реальной профессиональной среде.
Многие из функций, которые мы создаем в этом проекте, просто выводят некоторые данные на терминал, например:
void ft_putchar(char c);
чья работа заключается в выводе символа на стандартный вывод, или
void ft_putstr(char const *s);
который выводит на выход строку.
Есть много способов протестировать эти функции с помощью сценариев оболочки, и это прекрасно. Однако, если вы похожи на меня и решили создать среду C для тестирования этих функций, вам будет немного сложно придумать подходящий систематический тест.
Обычная вещь — просто заставить функции выводить что-то на терминал и просто смотреть на результаты. Это работает, если вы делаете простой тест. Однако, если вы хотите сделать вещи более автоматизированными и протестировать функции с несколькими входами, с чего начать?
Что, если вы хотите протестировать ft_putchar
и ft_putstr
со всеми возможными символами, всеми типами строк с пограничными случаями под солнцем? Было бы невозможно просмотреть каждый вывод на глаз, поэтому нам нужен другой способ.
Обратите внимание, что решение, которое я предлагаю здесь, находится в контексте тестирования функций. Тем не менее, он предоставляет что-то потенциально полезное в других обстоятельствах. Основной урок, который я здесь получил, заключался в том, как перенаправлять потоки данных на стандартный вывод в файл. Как оказалось, это было неожиданно сложно. Как перенаправить потоки данных, изначально предназначенные для печати на терминал, в файл?
Основная проблема заключается в том, как перенаправить stdout
для сохранения данных в памяти, которые затем можно использовать для сравнения с исходным вводом.
Функции ft_putchar
и ft_putstr
вызывают функцию write(int fd, const void *buf, size_t count)
и указывают файловый дескриптор fd
равным 1, чтобы он выполнял запись в стандартный вывод. Поскольку среда тестирования не может просто изменить значение fd
внутри самих функций на другое значение (таким образом, записывая в файл), как еще мы можем проверить результат этих функций?
Ответ, по крайней мере для меня, включает в себя тест, который;
- Создает временный файл,
- перенаправляет
stdout
во вновь созданный файл, - тестовая функция, которая печатает в
stdout
(теперь перенаправляется во временный файл) - сбросьте
stdout
обратно в исходное положение - наконец, прочитайте содержимое временного файла и сравните его с введенным символом.
Итак, как мы это сделаем? Читайте ниже, где я демонстрирую, как я пишу такой тест для функции ft_putchar
.
Если вам не интересно читать мое длинное объяснение и вы просто хотите перейти к действию и проанализировать его самостоятельно, вот ссылка на репозиторий git для этой тестовой функции:
Я покажу, как сделать такую тестовую функцию для одного символа ft_putchar
. Остальная часть процесса, итерация этой тестовой функции для нескольких символов, должна быть довольно простой, поэтому я ее опускаю.
Было бы проще обсуждать код по ходу дела, чтобы все компоненты и шаги имели смысл в соответствующем контексте.
Во-первых, мы создаем родительскую функцию с именем test_char
, которая управляет каждым из шагов, упомянутых выше.
int test_char(const unsigned char c) { int file_desc; #Holds an identifier for an open file int copy_out; #Will hold a duplicate copy of the stdout identifier int outcome; #Will hold the value of the test's outcome # (-1) on fail, (0) on success. init_redirect(&file_desc, ©_out); ft_putchar(c); #executes function to be tested reset_output(©_out); outcome = compare_results(c, file_desc); return (outcome); }
Возможно, вы заметили, что эта родительская функция по существу просто запускает 4 подпроцесса. То, что делает каждая из этих функций, просто соответствует шагам, описанным выше. Я объясню каждый из этих этапов здесь.
В обязанности init_redirect(&file_desc, ©_out)
входит:
- открыть новый файл с помощью функции
open()
, вызвать файлtemp
, а затем присвоить его идентификатор целому числу, на которое указывает&file_desc
. - продублируйте дескриптор объекта для
stdout
и присвойте его значение местоположению©_out
. При этом используется функцияint dup(int fildes)
, которая создает копию дескриптора, на который ссылаетсяfildes
, и возвращает дескриптор копии. - Наконец, мы заменяем объект, на который ссылается
stdout
, новым открытым файлом, на который ссылается&file_desc
. Этот шаг используетint dup2(int fildes, int fildes2)
для эффективной настройкиfildes2
, чтобы он ссылался на тот же открытый файл, что иfildes
.
Таким образом, мы просто создаем новый файл с именем temp
, сохраняем копию дескриптора stdout
, чтобы не потерять ее, а затем заменяем старый дескриптор stdout
, чтобы он теперь указывал на новый файл temp
. Вот как это выглядит
static void init_redirect(int *file_desc, int *copy_out) { *file_desc = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0666); *copy_out = dup(fileno(stdout)); dup2(*file_desc, fileno(stdout)); }
После этого все, что будет записано в объект stdout
, будет перенаправлено в файл temp
. Таким образом, вывод ft_putchar(c)
будет записан в файл вместо обычного вывода терминала.
Затем, после записи вывода в файл, мы хотим сбросить stdout
обратно в исходное местоположение. По сути, это делается для того, чтобы отменить изменения, внесенные в файловые дескрипторы, чтобы мы больше ничего не перенаправляли из файла stdout
в файл temp
.
Этот этап довольно прост, помните переменную copy_out
, которую мы создали ранее? В нем хранится фактическое местоположение stdout
, которое теперь мы можем переназначить обратно в соответствующий файловый дескриптор. Этого можно добиться, снова используя dup2()
, дублируя copy_out
обратно в дескриптор stdout
, а затем закрывая copy_out
с помощью close().
static void reset_output(int *copy_out) { dup2(*copy_out, fileno(stdout)); close(*copy_out); }
Следующий шаг, outcome = compare_results(c, file_desc)
, немного сложнее. Чтобы прочитать все содержимое файла temp
, нам нужна функция, которая может прочитать весь поток ввода.
Это означает использование int fscanf()
для чтения из temp
в виде потока. Однако для этого fscanf()
ожидает указатель на тип FILE
, так как функция будет считывать из «потокового указателя». Для этого мы используем функцию fdopen(int fd, const char *mode)
, чтобы получить указатель на поток. Он возвращает значение указателя потока на файл temp
, используя файловый дескриптор fd
.
Вот как работает этап compare_results
:
static int compare_results(const unsigned char c, const int fd) { FILE *file; unsigned char res; file = fdopen(fd, "r"); rewind(file); fscanf(file, "%c", &res); if (res != c) { printf("FAILED: Error in ft_putchar\n"); printf("Used char %c (%d), but got %c (%d)\n", c, c, res, res); return (-1); } clean_up(file); return (0); }
Обратите внимание на использование функции rewind()
. rewind()
устанавливает индикатор позиции файла для потока, на который указывает file
, на начало файла. Поскольку предыдущая функция ft_putchar
добавила бы данные в файл, fdopen()
вернет указатель на конец файла temp
, то есть мы не сможем ничего прочитать. Таким образом, rewind()
позволяет нам вернуться к началу файла и прочитать оттуда данные.
После этого мы просто сравниваем прочитанные данные в переменную res
и сравниваем их с исходными входными данными c
.
Наконец, нам также нужно сделать небольшую очистку после теста. В функции clean_up()
мы просто закрываем открытый файл temp
, а затем удаляем его.
static void clean_up(FILE * file) { if (fclose(file)) { fprintf(stderr, "Cannot close temp file\n"); exit (1); } if (remove ("temp")) { fprintf(stderr, "Cannot remove temp file\n"); exit (1); } }
Вот и все! Надеюсь, вы найдете что-то полезное из этого. Изучение того, как перенаправить стандартный вывод в файл, было интересной задачей. Даже если вы не используете это для тестирования, возможно, это будет полезно для чего-то еще!