Недавно я начал учиться в необычной новой школе кодирования под названием 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 внутри самих функций на другое значение (таким образом, записывая в файл), как еще мы можем проверить результат этих функций?

Ответ, по крайней мере для меня, включает в себя тест, который;

  1. Создает временный файл,
  2. перенаправляет stdout во вновь созданный файл,
  3. тестовая функция, которая печатает в stdout (теперь перенаправляется во временный файл)
  4. сбросьте stdout обратно в исходное положение
  5. наконец, прочитайте содержимое временного файла и сравните его с введенным символом.

Итак, как мы это сделаем? Читайте ниже, где я демонстрирую, как я пишу такой тест для функции 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, &copy_out);
  ft_putchar(c);                     #executes function to be tested
  reset_output(&copy_out);
  outcome = compare_results(c, file_desc);
  return (outcome);
}

Возможно, вы заметили, что эта родительская функция по существу просто запускает 4 подпроцесса. То, что делает каждая из этих функций, просто соответствует шагам, описанным выше. Я объясню каждый из этих этапов здесь.

В обязанности init_redirect(&file_desc, &copy_out) входит:

  1. открыть новый файл с помощью функции open(), вызвать файлtemp, а затем присвоить его идентификатор целому числу, на которое указывает &file_desc.
  2. продублируйте дескриптор объекта для stdout и присвойте его значение местоположению &copy_out. При этом используется функция int dup(int fildes), которая создает копию дескриптора, на который ссылается fildes, и возвращает дескриптор копии.
  3. Наконец, мы заменяем объект, на который ссылается 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);
 }
}

Вот и все! Надеюсь, вы найдете что-то полезное из этого. Изучение того, как перенаправить стандартный вывод в файл, было интересной задачей. Даже если вы не используете это для тестирования, возможно, это будет полезно для чего-то еще!