Недавно я начал учиться в необычной новой школе кодирования под названием 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);
}
}
Вот и все! Надеюсь, вы найдете что-то полезное из этого. Изучение того, как перенаправить стандартный вывод в файл, было интересной задачей. Даже если вы не используете это для тестирования, возможно, это будет полезно для чего-то еще!