У меня есть пять тестовых исходных файлов со следующим содержимым:
#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 Директива include включает непосредственно тестируемый .c. Я не могу изменить тестируемый код. Если вы имели в виду что-то другое, пожалуйста, уточните.
Тогда, возможно, поместите эти определения в отдельный include файл.
Пожалуйста, не включайте исходные файлы. Встройте их в объектные файлы, а затем свяжите все исходные файлы в исполняемый файл программы.
Используйте #ifdef и -D... определение компиляции, как в gcc -DWITH_FREAD -std=c2x -pedantic -W -Wall ...
Я понимаю, что вы делаете (что хотите) с макросами и #include, поскольку я делал подобное несколько раз. Но, я думаю, вам не нужно использовать макросы. Вместо этого используйте указатель на функцию. Одна копия тестовой функции: void test_readlines_fread_success(int (*fnc)(FILE *,FileBuf *)) Затем замените TEST_CHECK(readlines_fread(fp, &fbuf)); на TEST_CHECK(fnc(fp, &fbuf));.
Некоторые примеры см. в моих ответах: как сделать сдвиг вправо, округляя середину к четному Как создать рекурсию для сложения векторов? Каков эффект float (*vals)[n] = malloc(sizeof(float[n]))?
@CraigEstey Хорошо, это штраф и третий вариант. Но TEST_LIST уже является таким массивом, по которому тестовый код в acutest.h будет выполняться в цикле. Я бы использовал это решение, когда не использую внешнюю библиотеку. Спасибо.





Скажем, при первой сборке все экземпляры
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() для #.
Хорошо, ## TESTME ## не будет компилироваться. ## можно использовать только в определениях макросов. Макрос CONCAT(a, b) CONCAT_INDIRECT(a, b) с одним уровнем косвенности (через секунду CONCAT_INDIRECT(a, b) a ## b) подойдет. См.: stackoverflow.com/a/12630576/20017547
Обозначение #include не будет работать. Конкатенация строк происходит на этапе 6 (см. C11 §5.1.1.2 Фазы перевода ), а предварительная обработка происходит на этапе 4. Вы можете использовать имя макроса в #include MACRO_NAME, но MACRO_NAME должно расширяться до "file.name" или <file.name> практически напрямую. См. также §6.10.2 Включение исходного файла ¶4 и особенно сноску 170.
@JonathanLeffler Итак, на этапе 4 #include "../src/" #TESTME ".c" станет #include "../src/" "TESTME" ".c", что будет недопустимо, поскольку конкатенация строк происходит на этапе 6, а также не будет работать, потому что оператор # (и ##, если уж на то пошло) не расширяет свой операнд, это правильно?
@Harith — Операторы строковой обработки (#) и конкатенации (##) применимы только внутри RHS (правой части) расширения макроса. В противном случае это просто символы # и ##. Страйк 1 — #include "../src/" #TESTME ".c" не является правой частью макроса. Удар 2 — даже если бы это было исправлено, результатом было бы #include "../src/" "readlines_fread" ".c", что не является допустимой директивой #include — см. сноску 170 в стандарте C11.
Если у вас есть макрос, который расширяется и выглядит как имя заголовка, вы можете использовать:
#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) Мне нравится ваше альтернативное решение моей проблемы.
Поскольку вы не включили в вопрос содержание acutest.h (и для этого нет реальной причины), остается неясным, что такое TEST_LIST. Однако, как я также отметил, это не имеет большого значения — в контексте у вас это работает адекватно.
Я прочитал связанный вопрос и уже в восторге (я презирал ".."), но не понимаю, как использовать предложенные включения. Название проекта cread. cread состоит из src и test. test имеет эти относительные включает. Я пробовал ./cread/src/FileBuf.h, и cread/src/FileBuf.h, и ./src/FileBuf.h, и src/FileBuf.h, но ни один из них не был найден, когда я запускал make. Есть ли вопрос SO, который объясняет это? Раньше я особо не играл с включениями. Я считаю, что это потому, что когда вы включаете с помощью "", относительный путь начинается с текущего местоположения файла.
Я добавил несколько дополнительных примечаний к своему ответу. Возможно, вам придется добавить -I../src в командную строку компилятора, чтобы найти файлы в каталоге src из кода в каталоге test. Если существуют директивы #include с именами исходных файлов, например src/header.h или test/header.h, то вам понадобится -I.. в командной строке (вместо или вместе с -I../src). Если файлы включены с помощью "../src/header.h", вам необходимо указать каталог с -I, чтобы файлы можно было найти — чтобы -I../test или -I../src выполнили эту работу. Это похоже на игру «Сколько способов?»
А если файлы отмечены "cread/src/FileBuf.h", что мне нужно? (Я все еще экспериментирую. :)) Хорошо -I.. работает.
Не было бы разумнее заменить все это макросами в самом включаемом файле, а затем изменить только это?