Рефакторинг тестовых источников

У меня есть пять тестовых исходных файлов со следующим содержимым:

#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "acutest.h"

#include "../src/FileBuf.h"
#include "../src/readlines_fread.c"

void test_readlines_fread_success(void)
{
    /* The lines and line lengths are at most 1024 in sample1.txt. */
    char buf[1024];
    char *lines[1024];
    size_t line_count = 0;
    FILE *const fp = fopen("test/sample.txt", "r");

    TEST_ASSERT(fp);

    while (fgets(buf, sizeof buf, fp)) {
        buf[strcspn(buf, "\n")] = '\0';
        TEST_ASSERT((lines[line_count] = strdup(buf)));
        ++line_count;
    }
    
    rewind(fp);

    FileBuf fbuf = {};

    TEST_CHECK(readlines_fread(fp, &fbuf));
    TEST_CHECK(fbuf.count == 555);
    TEST_CHECK(fbuf.size == 120635);

    for (size_t i = 0; i < fbuf.count; ++i) {
        TEST_CHECK((strcmp(lines[i], fbuf.lines[i].line) == 0));
    }

    fclose(fp);
}

TEST_LIST = {
    { "test_readlines_fread_success", test_readlines_fread_success },
    { nullptr, nullptr },
};

Единственная разница между ними заключается в том, что последняя директива include другая, имя функции другое, функция, вызываемая в первой TEST_CHECK(), другая и первая запись в массиве TEST_LIST другая.

Ну, это всего лишь одно имя в 5 разных местах, fread здесь.

Остальные 4 тестовых файла такие же, за исключением того, что fread заменен на getline, mmap_getline, mmap_memchr и read соответственно.

Я хочу свести эти 5 источников тестирования к одному. Я знаю, что могу напрямую передать заголовок с флагом -include в GCC и Clang, но как мне аналогичным образом заменить имена функций (fread, getline, mmap_getline, mmap_memchr и read) с помощью макросов?

Скажем, при первой сборке все экземпляры fread должны оставаться прежними. Затем при второй сборке все экземпляры fread должны быть заменены на getline, а затем на mmap_getline() и так далее.

Не было бы разумнее заменить все это макросами в самом включаемом файле, а затем изменить только это?

tripleee 01.06.2024 15:24

@tripleee Директива include включает непосредственно тестируемый .c. Я не могу изменить тестируемый код. Если вы имели в виду что-то другое, пожалуйста, уточните.

Harith 01.06.2024 15:28

Тогда, возможно, поместите эти определения в отдельный include файл.

tripleee 01.06.2024 15:30

Пожалуйста, не включайте исходные файлы. Встройте их в объектные файлы, а затем свяжите все исходные файлы в исполняемый файл программы.

Some programmer dude 01.06.2024 15:35

Используйте #ifdef и -D... определение компиляции, как в gcc -DWITH_FREAD -std=c2x -pedantic -W -Wall ...

pmg 01.06.2024 15:37

Я понимаю, что вы делаете (что хотите) с макросами и #include, поскольку я делал подобное несколько раз. Но, я думаю, вам не нужно использовать макросы. Вместо этого используйте указатель на функцию. Одна копия тестовой функции: void test_readlines_fread_success(int (*fnc)(FILE *,FileBuf *)) Затем замените TEST_CHECK(readlines_fread(fp, &fbuf)); на TEST_CHECK(fnc(fp, &fbuf));.

Craig Estey 01.06.2024 19:54

@CraigEstey Хорошо, это штраф и третий вариант. Но TEST_LIST уже является таким массивом, по которому тестовый код в acutest.h будет выполняться в цикле. Я бы использовал это решение, когда не использую внешнюю библиотеку. Спасибо.

Harith 01.06.2024 21:57
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
8
96
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Скажем, при первой сборке все экземпляры fread должны оставаться прежними. Затем при второй сборке все экземпляры fread должны быть заменены на getline, а затем на mmap_getline() и так далее.

Не уверен, какое обычное расширение файла для этого взлома.

testbit.chchc

/* don't know if this works: might need another layer of macro */
#include "../src/" #TESTME ".c"

void test_ ## TESTME ## _success(void) {
    FILE *const fp = fopen("test/sample.txt", "r");
    FileBuf fbuf = {};

    TEST_CHECK(TESTME(fp, &fbuf));

    fclose(fp);
}

TEST_LIST = {
    { "test_" #TESTME  "_success", test_ ## TESTME ## _success },
    { nullptr, nullptr },
};

testall.c

#define TESTME readlines_fread
#include "testbit.chchc"
#define TESTME readlines_getline
#include "testbit.chchc"
/// ...

Этот код в том виде, в каком он написан, довольно бессмысленен (определение TEST_LIST столько раз определенно является какой-то ошибкой), но общий подход верен.

Да, нам нужен STRINGIFY() для #.

Harith 01.06.2024 16:41

Хорошо, ## TESTME ## не будет компилироваться. ## можно использовать только в определениях макросов. Макрос CONCAT(a, b) CONCAT_INDIRECT(a, b) с одним уровнем косвенности (через секунду CONCAT_INDIRECT(a, b) a ## b) подойдет. См.: stackoverflow.com/a/12630576/20017547

Harith 01.06.2024 17:02

Обозначение #include не будет работать. Конкатенация строк происходит на этапе 6 (см. C11 §5.1.1.2 Фазы перевода ), а предварительная обработка происходит на этапе 4. Вы можете использовать имя макроса в #include MACRO_NAME, но MACRO_NAME должно расширяться до "file.name" или <file.name> практически напрямую. См. также §6.10.2 Включение исходного файла ¶4 и особенно сноску 170.

Jonathan Leffler 01.06.2024 22:22

@JonathanLeffler Итак, на этапе 4 #include "../src/" #TESTME ".c" станет #include "../src/" "TESTME" ".c", что будет недопустимо, поскольку конкатенация строк происходит на этапе 6, а также не будет работать, потому что оператор ###, если уж на то пошло) не расширяет свой операнд, это правильно?

Harith 03.06.2024 18:13

@Harith — Операторы строковой обработки (#) и конкатенации (##) применимы только внутри RHS (правой части) расширения макроса. В противном случае это просто символы # и ##. Страйк 1 — #include "../src/" #TESTME ".c" не является правой частью макроса. Удар 2 — даже если бы это было исправлено, результатом было бы #include "../src/" "readlines_fread" ".c", что не является допустимой директивой #include — см. сноску 170 в стандарте C11.

Jonathan Leffler 03.06.2024 21:13
Ответ принят как подходящий

Если у вас есть макрос, который расширяется и выглядит как имя заголовка, вы можете использовать:

#include INCLUDE_FILE_NAME

Итак, вы можете скомпилировать с помощью -DINCLUDE_FILE_NAME='"../src/readlines_fread.c"'.

Линия:

{ "test_readlines_fread_success", test_readlines_fread_success },

можно преобразовать для использования макроса:

{ FUNCTION_NAME_AND_POINTER(test_readlines_fread_success) },

с:

#define FUNCTION_NAME_AND_POINTER(name) #name, name

Непонятно, как используется объявление массива TEST_LIST — похоже, оно объединяет тип с каким-то именем массива. Честно говоря, это скорее сбивает с толку, чем приносит пользу. Лучше было бы разделить на:

TEST_ARRAY_TYPE Test_Array_Name[] =
{
    …
};

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

В вопросе не очень ясно, какие имена являются «именем функции». Кажется, есть много мест, в которые встроены различные вещи, которые могут быть «именем функции». Если FUT (тестируемая функция) равна readlines_fread, то среда сборки может выполнить за вас значительную часть работы.

Вот версия вашего файла с некоторыми макросами, которые предположительно будут помещены в заголовок, вероятно acutest.h для использования в производстве. Мой источник находится в test97.c.

/* SO 7856-3912 */

//#include <assert.h>
//#include <stdio.h>
//#include <string.h>
//#include "acutest.h"
//#include "FileBuf.h"

/* Check configuration */
#ifndef INCLUDE_FILE_NAME
#error You must define INCLUDE_FILE_NAME as the name of the source file to test
#endif /* INCLUDE_FILE_NAME */
#ifndef FUNCTION_UNDER_TEST
#error You must define FUNCTION_UNDER_TEST as the name of the function under test
#endif /* FUNCTION_UNDER_TEST */

#include INCLUDE_FILE_NAME

#define TEST_FUNCTION_NAME(name)    test_ ## name ## _success
#define TEST_FUNCTION(name)         TEST_FUNCTION_NAME(name)

#define TEST_ARRAY_ENTRY(name)      #name, name
#define TEST_ARRAY_EXPANSION(name)  TEST_ARRAY_ENTRY(name)
#define TEST_ARRAY_MAPPING(name)    TEST_ARRAY_EXPANSION(TEST_FUNCTION(name))


void TEST_FUNCTION(FUNCTION_UNDER_TEST)(void)
{
    /* The lines and line lengths are at most 1024 in sample1.txt. */
    char buf[1024];
    char *lines[1024];
    size_t line_count = 0;
    FILE *const fp = fopen("test/sample.txt", "r");

    TEST_ASSERT(fp);

    while (fgets(buf, sizeof buf, fp)) {
        buf[strcspn(buf, "\n")] = '\0';
        TEST_ASSERT((lines[line_count] = strdup(buf)));
        ++line_count;
    }
    
    rewind(fp);

    FileBuf fbuf = {};

    TEST_CHECK(FUNCTION_UNDER_TEST(fp, &fbuf));
    TEST_CHECK(fbuf.count == 555);
    TEST_CHECK(fbuf.size == 120635);

    for (size_t i = 0; i < fbuf.count; ++i) {
        TEST_CHECK((strcmp(lines[i], fbuf.lines[i].line) == 0));
    }

    fclose(fp);
}

TEST_ARRAY_TYPE Test_Array_Name[] = {
    { TEST_ARRAY_MAPPING(FUNCTION_UNDER_TEST) },
    { nullptr, nullptr },
};

Я закомментировал заголовки и протестировал файл readlines_fread.c в текущем каталоге, который содержит только:

Contents of readlines_fread.c

makefile выглядит так:

FUNCTION_UNDER_TEST = readlines_fread
INCLUDE_FILE_NAME = "$(FUNCTION_UNDER_TEST).c"

CFLAGS = -E -DINCLUDE_FILE_NAME='$(INCLUDE_FILE_NAME)' -DFUNCTION_UNDER_TEST=$(FUNCTION_UNDER_TEST)

PROG = test97

all: $(PROG).o

Предварительно обработанный результат выглядит следующим образом:

# 1 "test97.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 418 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test97.c" 2
# 17 "test97.c"
# 1 "./readlines_fread.c" 1
Content of file readlines_fread.c
# 18 "test97.c" 2
# 27 "test97.c"
void test_readlines_fread_success(void)
{

    char buf[1024];
    char *lines[1024];
    size_t line_count = 0;
    FILE *const fp = fopen("test/sample.txt", "r");

    TEST_ASSERT(fp);

    while (fgets(buf, sizeof buf, fp)) {
        buf[strcspn(buf, "\n")] = '\0';
        TEST_ASSERT((lines[line_count] = strdup(buf)));
        ++line_count;
    }

    rewind(fp);

    FileBuf fbuf = {};

    TEST_CHECK(readlines_fread(fp, &fbuf));
    TEST_CHECK(fbuf.count == 555);
    TEST_CHECK(fbuf.size == 120635);

    for (size_t i = 0; i < fbuf.count; ++i) {
        TEST_CHECK((strcmp(lines[i], fbuf.lines[i].line) == 0));
    }

    fclose(fp);
}

TEST_ARRAY_TYPE Test_Array_Name[] = {
    { "test_readlines_fread_success", test_readlines_fread_success },
    { nullptr, nullptr },
};

Я считаю, что это очень близко к тому, чего вы хотели достичь. Запустив make FUNCTION_UNDER_TEST=readlines_getline, вы можете переключиться на другой исходный файл и имя функции.

Очевидно, что вы можете определить, откуда следует включать файлы, с помощью такой опции, как -I../src, предоставленной в качестве одного из CFLAGS.

GNU Make копирует вывод в test97.o, потому что он добавляет -c и -o test97.o в командную строку компилятора:

cc -E -DINCLUDE_FILE_NAME='"readlines_fread.c"' -DFUNCTION_UNDER_TEST=readlines_fread   -c -o test97.o test97.c

Другие варианты make могут иметь другие правила.

gcc -E -DINCLUDE_FILE_NAME='"readlines_fread.c"' -DFUNCTION_UNDER_TEST=readlines_fread -c test97.c

См. также Каковы преимущества относительного пути, например «../include/header.h», для заголовка?


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

Обозначение #include, показанное в ответе wizzwizz4 (версия 1), не будет работать. Конкатенация строк происходит на этапе 6 (см. C11 §5.1.1.2 Фазы перевода ), а предварительная обработка происходит на этапе 4. Вы можете использовать имя макроса в #include MACRO_NAME, но MACRO_NAME должно расширяться до "file.name" или <file.name> практически напрямую. См. также §6.10.2 Включение исходного файла ¶4 и особенно сноску 170.

Тем не менее, вы можете использовать обозначение угловых скобок для достижения хорошего эффекта.

Например:

#define HEADER_NAME(base) <base.h>

#include HEADER_NAME(somename)

При правильных параметрах -I (-I. для случая, когда somename.h находится в текущем каталоге), это будет включать somename.h для вас. Конечно, вы можете адаптировать это, чтобы включить исходный файл в другой каталог, но вам все равно придется включить соответствующие параметры -I в командную строку:

#define SOURCE_FILE(base) <../src/base.c>

#include SOURCE_FILE(testfile)

Или (с более правдоподобным набором RELATIVE_PATH в командной строке):

#define RELATIVE_PATH ../src
#define HEADER_NAME(base) <RELATIVE_PATH/base.h>

#include HEADER_NAME(somename)

Во всяком случае, это работает с GCC (он же Clang) на macOS.


То, что вам нужно использовать, также зависит от того, где вы выполняете компиляцию. Обратите внимание, что спецификация POSIX для c99 компилятор включает спецификацию того, как #include <name.h> и #include "name.h" взаимодействуют с -Isome/path/name:

-I directory

Измените алгоритм поиска заголовков, имена которых не являются абсолютными путями, чтобы искать в каталоге, указанном в пути к каталогу, прежде чем искать в обычных местах. Таким образом, заголовки, имена которых заключены в двойные кавычки (""), будут искаться сначала в каталоге файла со строкой #include, затем в каталогах, указанных в опциях -I, и в последнюю очередь в обычных местах. Для заголовков, имена которых заключены в угловые скобки ("<>"), поиск заголовка осуществляется только в каталогах, указанных в опциях -I, и то в обычных местах. Поиск в каталогах, указанных в опциях -I, осуществляется в указанном порядке. Если опция -I используется для указания каталога, который является одним из обычных мест поиска по умолчанию, результаты не указаны. Реализации должны поддерживать не менее десяти экземпляров этой опции при одном вызове команды c99.

Обратите особое внимание на фрагмент правила:

… следует искать сначала в каталоге файла со строкой #include

В комментарии вы спрашиваете:

А если файлы отмечены "cread/src/FileBuf.h", что мне нужно?

Тогда в каталоге cread/test вам понадобится -I../... Или, если проект находится в /home/username/project/cread (с подкаталогами test и src), вы можете использовать -I$HOME/project (в нотации оболочки; -I$(HOME)/project или -I${HOME}/project в нотации make — и нотация -I${HOME}/project будет работать и в make-файле).

По сути, препроцессор будет брать имя из директивы #include (например, cread/src/fileBuf.h и добавлять элементы из списка опций -I для создания имени файла.

Итак, используя -I../.., вы получите ../../cread/src/fileBuf.h, который должен быть путем к файлу относительно подкаталога test. Или с -I${HOME}/project у вас будет /home/username/project/cread/src/fileBuf.h.

«Re: Непонятно, как используется объявление массива TEST_LIST — похоже, оно объединяет тип с каким-то именем массива». ==> Это для acutest.h (библиотека тестирования только заголовков), TEST_LIST — это то, что она ожидает и выполняет цикл. 2) Мне нравится ваше альтернативное решение моей проблемы.

Harith 01.06.2024 21:47

Поскольку вы не включили в вопрос содержание acutest.h (и для этого нет реальной причины), остается неясным, что такое TEST_LIST. Однако, как я также отметил, это не имеет большого значения — в контексте у вас это работает адекватно.

Jonathan Leffler 01.06.2024 21:58

Я прочитал связанный вопрос и уже в восторге (я презирал ".."), но не понимаю, как использовать предложенные включения. Название проекта cread. cread состоит из src и test. test имеет эти относительные включает. Я пробовал ./cread/src/FileBuf.h, и cread/src/FileBuf.h, и ./src/FileBuf.h, и src/FileBuf.h, но ни один из них не был найден, когда я запускал make. Есть ли вопрос SO, который объясняет это? Раньше я особо не играл с включениями. Я считаю, что это потому, что когда вы включаете с помощью "", относительный путь начинается с текущего местоположения файла.

Harith 01.06.2024 22:18

Я добавил несколько дополнительных примечаний к своему ответу. Возможно, вам придется добавить -I../src в командную строку компилятора, чтобы найти файлы в каталоге src из кода в каталоге test. Если существуют директивы #include с именами исходных файлов, например src/header.h или test/header.h, то вам понадобится -I.. в командной строке (вместо или вместе с -I../src). Если файлы включены с помощью "../src/header.h", вам необходимо указать каталог с -I, чтобы файлы можно было найти — чтобы -I../test или -I../src выполнили эту работу. Это похоже на игру «Сколько способов?»

Jonathan Leffler 01.06.2024 22:45

А если файлы отмечены "cread/src/FileBuf.h", что мне нужно? (Я все еще экспериментирую. :)) Хорошо -I.. работает.

Harith 01.06.2024 22:49

Другие вопросы по теме