Давно хотел поразглагольствовать о таких вещах, как скрипты configure. Они косвенно вносят большой вклад в мою головную боль в рабочее время в эти дни. Учитывая, что я возобновляю ведение личного блога здесь, давайте посмотрим, смогу ли я превратить эту тираду в пост.
Что такое configure
Предположим, вам нужно скомпилировать некоторое программное обеспечение из исходного tarball. Старая unix-традиция выглядит так: ./configure && make && make install
. В части make
нет ничего причудливого, но зачем вообще нужна configure
?
Ну, configure
— это просто сценарий оболочки, который проверяет среду сборки и генерирует некоторый заголовочный файл C для включения в исходный код. Каждый проект имеет свой уникальный сценарий configure
, который проверяет различные вещи, заголовки, функции, любую функцию, которая нужна исходному коду, чтобы проверить ее наличие во время сборки и обеспечить путь отступления кода, если она отсутствует.
Скажем, исходному коду нужно вызвать функцию foo
, которая существует только на некоторых поддерживаемых платформах проекта. Если configure
обнаружит foo
, он запишет строку #define HAVE_FOO 1
в сгенерированный заголовок. Затем исходный код может включать автоматически сгенерированный заголовок функции, использовать объявления CPP, такие как #if defined(HAVE_FOO)
, чтобы решить, существует ли функция foo
в среде сборки.
configure
обычно автоматически генерируется из шаблона с помощью autoconf. В некоторых проектах это может быть написанный вручную скрипт на python. Существуют также системы сборки, такие как cmake, которые полностью берут на себя роль configure
, самостоятельно исследуют среду сборки и генерируют заголовок функции.
В любом случае, мои разглагольствования здесь относятся только к идее обнаружения особенностей во время сборки, и не имеют отношения к тому, как configure
на самом деле реализована (хотя это тоже достаточно раздражает для отдельного поста в блоге).
Что такое вектор признаков
Сколько макросов HAVE_
у вас в проекте?
~/ubuntu/ghc$ grep -rIF HAVE_ | wc -l
842
Подождите секунду. Большинство из них должны быть простыми дубликатами, например, HAVE_FOO
с большой вероятностью встречается в нескольких местах исходного текста. Следует действительно проверить, сколько возможностей (заголовков, функций и т.д.) проверяется командой configure
.
~/ubuntu/ghc$ grep -rIF AC_CHECK_ | wc -l
171
Приведенное выше число является заниженной оценкой, поскольку в autoconf одна строка AC_CHECK_HEADERS
или AC_CHECK_FUNCS
может проверять несколько функций.
Теперь мы можем ввести понятие «вектор признаков»: N
-мерный булев вектор, где N
соответствует количеству вещей, которые вы проверяете во время сборки. Каждое значение вектора признаков — это точка в пространстве признаков, определяющая конфигурацию времени сборки.
Насколько велико пространство признаков?
- Определенно не так велико, как
2^N
. Большинство измерений не ортогональны, можно представить скопления вещей, которые либо существуют как единое целое, либо не существуют вообще. - Тем не менее, это намного больше, чем пространство, в котором люди действительно тестируют CI, чтобы избежать гниения битов.
Моя тирада — это второй пункт выше.
К чему эта тирада
- В GHC можно передавать различные аргументы
configure
для включения/выключения таких функций, как unreg codegen, большое адресное пространство, родной менеджер ввода-вывода и т.д. Конфигурация по умолчанию проходит набор тестов, но как только вы начнете изменять конфигурациюconfigure
, ожидайте неудачных тестовых случаев. По крайней мере, эти случаи должны быть явно помечены как хрупкие/сломанные в этих конфигурациях! - В GHC,
unix
, возможно, в других местах, которые я взломал и забыл: API развивается, но люди забывают обновить код в#else
— охраняемых частях, потому что он не тестируется на CI, возможно, эта конкретная CPP-проверяемая вещь, как считается, существует на всех платформах. Ну, WASI — это довольно ограниченная платформа, так что все эти гнилые части возвращаются, чтобы укусить вас, когда я нацеливаюсь на WASI.
Нет ничего плохого в том, что нужно писать переносимый код и делать проверку возможностей во время сборки. Мы все знаем, что непроверенный код — это плохо, но гораздо меньше людей знают: непроверенные векторы возможностей — это тоже плохо!
Кроме того, это не просто вопрос «покрытия кода». Вполне возможно достичь высокого уровня покрытия, тестируя всего несколько векторов возможностей, оставляя при этом потенциально сломанные конфигурации времени сборки в тени.
Как решить эту проблему
Большинство программ, написанных с помощью тонны #ifdef
, не обладают мышлением вектора возможностей, и у них нет логики тестирования для этого:
- Выполните случайное тестирование в стиле
QuickCheck
в пространстве признаков. Генерируйте вектор признаков, запускайте тесты по нему, а «уменьшение» — это просто перемещение точки ближе к известной базовой точке, конфигурации по умолчанию, которую вы получаете при настройке на типичной платформе без каких-либо пользовательских аргументов. Это позволяет обнаружить сбои, возникающие из-за сложных & непреднамеренных взаимодействий между различными измерениями в векторе признаков. - Скрыть некоторые существующие автоопределяемые признаки. Это позволяет проводить тестирование на ограниченных/экзотических платформах, но при этом выполнять тесты на обычной платформе. Реализовать это нетривиально, особенно для вещей в стандартных библиотеках, но это должно быть возможно с помощью прагмы
poison
, создания обертокcc
или даже изолированных системных корней.