Проект iOS, в котором сочетаются зависимости на Swift, Objective-C и C/C++, может раз за разом проходить обычные тесты, но при выполнении редко используемой ветви обращаться за границы массива, повторно освобождать память или использовать её после освобождения. После переноса задачи на облачный Mac недостаточно просто повторно запускать имеющиеся тесты. Эффективнее добавить конвейер с Address Sanitizer (ASan), который будет немедленно останавливать сборку при ошибках доступа к памяти и сохранять пакет результатов для воспроизведения проблемы.
Определите, какие ошибки должен выявлять барьер
ASan прежде всего выявляет выход за границы стека и кучи, использование памяти после освобождения, повторное освобождение и некоторые случаи обращения по недопустимым указателям. Он особенно полезен при проверке мостов Objective-C, библиотек обработки изображений, обёрток баз данных и написанных вручную интерфейсов C. Проверка полезна и для кода исключительно на Swift, однако один успешный запуск не означает, что проблем с памятью нет: ветви, не выполненные тестами, останутся непроверенными, а для поиска утечек и гонок данных потребуются другие инструменты.
Сначала выберите для барьера стабильный набор тестов, не требующих ручного взаимодействия. Не стоит сразу включать в него все UI-тесты. Наивысший приоритет следует отдать основным парсерам, слою кэширования и межъязыковым интерфейсам.
| Уровень тестирования | Частота запуска | Действие при сбое |
|---|---|---|
| Основные модульные тесты | При каждом запросе на слияние | Блокировать слияние |
| Интеграционные тесты | В основной ветви или при запросе на слияние | Блокировать выпуск |
| Полный набор UI-тестов | По расписанию или на этапе кандидата в релиз | Зарегистрировать дефект и воспроизвести его |
Ценность Sanitizer не только в том, что задача получает статус ошибки. Важно, чтобы тот же ввод позволял повторно вызвать ту же ошибку, а стек вызовов, имя теста и среда сборки сохранялись для анализа.
Создайте отдельные Scheme и каталоги
В Xcode скопируйте существующий тестовый Scheme, назовите его App-ASan и сделайте Shared. Не изменяйте Scheme, которым разработчики пользуются ежедневно, иначе параметры локальной отладки, измерения производительности и CI начнут влиять друг на друга. Scheme следует добавить в репозиторий по пути:
App.xcodeproj/xcshareddata/xcschemes/App-ASan.xcscheme
Используйте отдельный каталог DerivedData для каждой задачи. Это не позволит ASan случайно применить объектные файлы из обычной сборки и предотвратит конкуренцию параллельных задач за индексы и промежуточные артефакты. Пакеты результатов также сохраняйте в отдельных каталогах по номеру задачи, а не перезаписывайте один и тот же файл.
Зафиксируйте целевое устройство для тестов
Сначала просмотрите список доступных симуляторов:
xcrun simctl list devices available
В CI лучше зафиксировать тип устройства и основную версию системы, разрешив версии исправлений следовать за установленной средой выполнения. Если команде необходимо проверять несколько версий системы, создайте матрицу отдельных задач, а не полагайтесь на автоматический выбор в одной команде.
Запустите проверку через xcodebuild
В следующем скрипте используются отдельный каталог сборки и пакет результатов. Рабочее пространство, Scheme и имя симулятора необходимо заменить фактическими значениями проекта.
#!/bin/zsh
set -u
ROOT="${PWD}"
OUT="${ROOT}/artifacts/asan"
DERIVED="${ROOT}/.ci/DerivedData-ASan"
rm -rf "${OUT}" "${DERIVED}"
mkdir -p "${OUT}"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-ASan \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "${DERIVED}" \
-resultBundlePath "${OUT}/App-ASan.xcresult" \
-enableAddressSanitizer YES \
CODE_SIGNING_ALLOWED=NO \
>"${OUT}/xcodebuild.log" 2>&1
status=$?
exit ${status}
Не добавляйте к команде тестирования безусловно успешное завершение || true. Если платформа CI требует последующей обработки журналов, сначала сохраните код выхода, а после архивирования верните его без изменений. CODE_SIGNING_ALLOWED=NO подходит для тестов на симуляторе. Для задач с физическими устройствами или созданием архивов нужна отдельная конфигурация подписи — копировать этот параметр в них нельзя.
ASan увеличивает потребление памяти и время выполнения, поэтому число параллельных процессов на одной машине должно быть ниже, чем при обычных модульных тестах. Если система принудительно завершает процесс, сначала сократите параллелизм и лишь затем выясняйте, связан ли сбой с дефектом тестируемого приложения.
Сохраняйте данные, необходимые для передачи ошибки
При сбое задачи необходимо сохранить как минимум xcodebuild.log и пакет .xcresult. В первом удобно искать отчёты Sanitizer, а второй содержит иерархию тестов, вложения с данными об ошибках и журналы операций. Перед архивированием можно проверить их целостность:
test -s artifacts/asan/xcodebuild.log
test -d artifacts/asan/App-ASan.xcresult
xcrun xcresulttool get test-results summary \
--path artifacts/asan/App-ASan.xcresult \
> artifacts/asan/summary.json
Подкоманды xcresulttool могут различаться между версиями Xcode, поэтому вызовы инструмента следует сопровождать и обновлять вместе с версией Xcode. Несовместимость команды не должна становиться причиной удаления исходного пакета результатов.
При анализе отчёта сначала найдите ERROR: AddressSanitizer, затем зафиксируйте тип ошибки, первый кадр стека из кода приложения, тест, вызвавший ошибку, и входные данные. Верхние кадры из системных библиотек обычно показывают лишь место сбоя, тогда как повредившая память запись могла произойти раньше. Если ошибка воспроизводится нестабильно, сначала запускайте соответствующий тест отдельно в цикле и отключите случайный порядок тестов, чтобы не менять несколько факторов одновременно.
Запускайте барьер по уровням и избегайте типичных ошибок
В повседневном барьере можно выполнять только набор тестов повышенного риска, а в основной ветви запускать полный комплект ASan. Теги тестов или Test Plan проще поддерживать, чем команды, составленные из имён файлов. При добавлении интерфейса C, арифметики указателей или небезопасной обработки буферов соответствующие тесты следует одновременно включать в быстрый набор.
Есть три распространённые ошибки. Первая — повторное использование кэша обычной сборки, из-за чего инструментация оказывается неполной. Вторая — загрузка только текстового журнала без сохранения пакета результатов. Третья — восприятие сбоя ASan как нестабильного теста с последующим простым перезапуском. Повторные запуски помогают проверить стабильность, но первый отчёт всё равно необходимо сохранить. Даже если следующие попытки прошли успешно, это не должно автоматически разрешать уже зафиксированный недопустимый доступ.
Наконец, явно определите критерии приёмки: тестовый процесс завершился с нулевым кодом, пакет результатов существует, в журнале нет ошибок ASan, а задача не была остановлена из-за превышения времени или нехватки ресурсов. Тогда барьер будет оценивать полный результат выполнения, а не одну строку сводки, которая лишь выглядит успешной.
Часто задаваемые вопросы
Нужно ли запускать Address Sanitizer для каждого коммита?
Для каждого изменения стоит запускать короткие тесты критичных модулей. Полный набор UI-тестов разумнее выполнять для запросов на слияние, основной ветки или по расписанию.
Означает ли успешная проверка отсутствие ошибок памяти?
Нет. Проверяются только выполненные пути и определённые виды некорректного доступа. Дополнительно нужны тесты утечек, гонок данных и проверки на реальных устройствах.
Выберите срок аренды облачного Mac в соответствии с длительностью задачи
Две конфигурации M4 доступны для аренды посуточно, понедельно, помесячно или поквартально; актуальные сведения об узлах и доступности отображаются в консоли в реальном времени.