Руководства по разработке

Автоматический поиск регрессий Xcode через git bisect

Автоматический поиск регрессий Xcode через git bisect

Один из UI-тестов стабильно падает с прошлой недели, но за последнее время в проект были влиты десятки коммитов. Проверять каждый из них вручную, открывать Xcode и запускать тесты — медленно, к тому же легко не заметить различия в окружении. Гораздо эффективнее оформить проверку «внёс ли этот коммит исследуемую ошибку» в виде воспроизводимой команды и передать двоичный поиск git bisect. Для 64 коммитов-кандидатов теоретически потребуется всего около 6 проверок. Основная сложность заключается не в алгоритме двоичного поиска, а в достоверности результата каждого запуска.

Сначала задайте регрессию в машиночитаемом виде

Перед началом нужны как минимум две границы: коммит, на котором всё заведомо работает, и коммит, на котором ошибка заведомо воспроизводится. Недостаточно указать, что «со страницей что-то не так». Сведите регрессию к одному наблюдаемому признаку: например, падению конкретной проверки XCTest, невозможности архивировать заданную Scheme или неверному результату для фиксированных входных данных.

В условии проверки необходимо зафиксировать следующие переменные:

  • Scheme, Configuration и тестовую цель
  • Путь к выбранной версии Xcode и lock-файлы зависимостей
  • Модель симулятора, версию системы, язык и часовой пояс
  • Тестовые данные, сетевые зависимости и учётную запись запуска
  • Единственный критерий, однозначно определяющий нормальный и ошибочный результат

git bisect точно воспроизводит и усиливает ошибки проверяющего скрипта. Если случайный сбой будет принят за плохой коммит, итоговый вывод может оказаться полностью неверным, даже если дальнейший поиск завершится без проблем.

Сначала вручную запустите одну и ту же команду по два раза на хорошем и плохом коммитах. Если результат не воспроизводится стабильно, сначала изолируйте тест и только потом приступайте к двоичному поиску.

Изолируйте окружение для каждого коммита-кандидата

При запуске на облачном Mac G-Mini рекомендуется использовать отдельную рабочую копию, а не каталог, в котором разработчик редактирует код. Во время двоичного поиска Git часто переключает коммиты, поэтому неотслеживаемые файлы, автоматически созданные настройки и общие кэши могут исказить результаты.

Что изолировать Рекомендуемый подход Причина
Рабочее дерево Git Использовать отдельный clone или worktree Не перезаписывает текущие изменения
DerivedData Создавать отдельный каталог для каждого хеша коммита Не позволяет повторно использовать старые артефакты между коммитами
Симулятор Зафиксировать UDID Исключает смену устройства из-за автоматического выбора destination
Пакет результатов Сохранять отдельно для каждого запуска Упрощает повторную проверку первого плохого коммита
Зависимости Сохранять lock-файлы и отключить неявные обновления Предотвращает изменение результатов разрешения версий

Перед началом выполните git status --porcelain: вывод должен быть пустым. Симулятор следует заранее создать и запустить, а его UDID записать в переменную окружения. Не выбирайте в проверяющем скрипте временное «любое доступное устройство» по имени: устройства с одинаковыми именами и обновления системы могут изменить фактическую цель запуска.

Напишите трёхстатусный проверяющий скрипт для Xcode

git bisect run различает не только успех и ошибку. Код завершения 0 означает хороший коммит, коды от 1 до 127 — плохой, а 125 указывает, что текущий коммит невозможно оценить и его следует пропустить. Третье состояние особенно важно для проектов с долгой историей: старый коммит может не собираться с текущими зависимостями, но это ещё не означает, что он содержит исследуемую регрессию.

Следующий скрипт сначала проверяет возможность сборки проекта, а затем запускает только целевой тест. Ошибка базовой сборки помечается как неопределённый результат; плохим коммит считается только при явном падении теста.

#!/bin/zsh
set -u

: "${DESTINATION_ID:?Set DESTINATION_ID first}"

sha="$(git rev-parse --short HEAD)"
root="${TMPDIR:-/tmp}/xcode-bisect"
derived="$root/derived-$sha"
result="$root/result-$sha.xcresult"
log="$root/test-$sha.log"

mkdir -p "$root"
rm -rf "$result"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  build >"$root/build-$sha.log" 2>&1

if [[ $? -ne 0 ]]; then
  exit 125
fi

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  -only-testing:AppTests/CheckoutReducerTests/testExpiredCart \
  test >"$log" 2>&1

status=$?

if [[ $status -eq 0 ]]; then
  exit 0
fi

if grep -q "TEST FAILED" "$log"; then
  exit 1
fi

exit 125

Настройте классификацию под тип ошибки

Если исследуется регрессия компиляции, ошибка сборки должна возвращать 1, а не 125. Если исследуется регрессия поведения, следует пропускать проблемы базового окружения: сбои загрузки зависимостей, ошибки службы симулятора, невозможность записи на диск и другие подобные ситуации. Не интерпретируйте любой ненулевой код завершения xcodebuild как исследуемую регрессию.

Запустите двоичный поиск и проверьте первый плохой коммит

Подготовив скрипт, зафиксируйте текущую ветку и состояние рабочего дерева, затем выполните:

chmod +x ./scripts/bisect-xcode.sh
export DESTINATION_ID="固定的模拟器UDID"

git bisect start
git bisect bad BAD_COMMIT
git bisect good GOOD_COMMIT
git bisect run ./scripts/bisect-xcode.sh

После завершения поиска Git укажет первый плохой коммит. Не закрывайте задачу сразу: вручную запустите скрипт отдельно на этом коммите и на его родительском коммите, а затем проверьте сохранённые журналы сборки и .xcresult. Также изучите изменения в коммите и убедитесь, что они действительно объясняют наблюдаемое поведение, а не просто вызывают другую ошибку.

Завершив проверку, выполните git bisect reset, чтобы вернуться к исходной ветке. Если за время поиска накопилось много DerivedData, удалите их после повторной проверки. Не удаляйте пакеты результатов преждевременно во время расследования, иначе будут утрачены доказательства, на которых основывались решения.

Обрабатывайте нестабильные тесты и разрывы в истории проекта

Используйте решение по большинству для нестабильных тестов

Если один запуск не даёт стабильного результата, проверяющий скрипт может выполнить тест три раза подряд и вернуть 1 только тогда, когда одна и та же целевая ошибка возникает как минимум дважды. Если результаты трёх запусков противоречат друг другу, следует вернуть 125. Это увеличит время выполнения, но обойдётся дешевле, чем дальнейшее расследование неверно определённого коммита. Перед каждым повторным запуском по-прежнему необходимо сбрасывать тестовые данные, чтобы состояние предыдущей попытки не влияло на следующую.

Сужайте границы, если пропущено слишком много коммитов

При большом количестве результатов 125 Git не сможет однозначно указать один коммит. Обычно это происходит, если в истории менялись формат проекта, способ управления зависимостями или имя теста. В таком случае ограничьте диапазон поиска коммитами после миграции либо добавьте в скрипт ветвление для поддержки старой структуры каталогов. При этом скрипт не должен автоматически изменять проверяемый исходный код.

По итогам следует сохранить хороший коммит, плохой коммит, первый плохой коммит, версию проверяющего скрипта, UDID симулятора, версию Xcode и путь к пакету результатов. Тогда другой инженер сможет повторить поиск, а после подготовки исправления тот же процесс можно будет без изменений использовать для проверки регрессии.

Часто задаваемые вопросы

Какие коды завершения должен возвращать сценарий git bisect?

Код 0 означает исправный коммит, 1 подтверждает искомую регрессию, а 125 пропускает коммит, который нельзя надёжно классифицировать. Ошибки инфраструктуры следует пропускать.

Можно ли искать через git bisect причину нестабильного теста?

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

Выделенный физический узел

Облачный Mac для следующей очереди сборки

Сравните две конфигурации M4 и пять узлов и выберите аренду на день, неделю, месяц или квартал в зависимости от рабочего цикла.

Выбрать облачный Mac