DevOps / CI·CD 25 апреля 2026

2026-04-25 xcodebuild test: покрытие кода, xcresult в JUnit и ворота PR на арендованном cloud Mac (HK / JP / KR / SG / US)

MacXCode Engineering Team 25 апреля 2026 ~18 мин чтения

Команды iOS и macOS, которые уже гоняют headless xcodebuild test на арендованном Mac mini M4 с Apple Silicon, часто мержат pull request только по коду выхода и зелёной ячейке в CI. Руководство 2026-04-25 сужает зазор между «тесты прошли» и «мы объясняем, что изменилось в качестве на этой неделе»: -resultBundlePath в отдельный каталог на задачу внутри изолированного build root, -enableCodeCoverage YES для LLVM profdata, экспорт в JUnit (через xcresulttool или доверенную обёртку) и политика по строкам с xccov или парсером дашборда. Документ дополняет — не заменяет — Test Plan + parallel xcresult. См. Swift 6 strict concurrency для ворот компилятора; эта статья — про тест-артефакты и merge-сигналы.

Что ворота PR должны видеть кроме кода выхода 0

Нормальный набор: (1) JUnit (или xUnit) XML для аннотаций и флаков; (2) смерженный или единичный .xcresult в архиве с той ретенцией, что и xcresult для изоляции; (3) сводку покрытия (строка или ветвь) для diff к merge-base, чтобы рефактор с удалением мёртвого кода не считался регрессией. В HK / JP / KR / SG / US ворота задаются на агрегированных метриках при шарде по destination—не опирайтесь на кусок profdata одного шарда, если policy явно не сужает набор целей. Подписи по времени согласуйте с меткой пула в метаданных, чтобы нельзя было сравнивать холодный Simulator в Токио с тёплым в US East.

Ключевые флаги xcodebuild: result bundle, coverage, destination

Зафиксируйте DEVELOPER_DIR=/Applications/Xcode.app и строку Simulator -destination, а не плавающий «latest iOS» без имени. Минимальный пример:

DEVELOPER_DIR=/Applications/Xcode.app xcodebuild test -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' -enableCodeCoverage YES -resultBundlePath "$CI_ROOT/Test-$(date +%s).xcresult" -derivedDataPath "$CI_DERIVED/job-$CI_JOB_ID" -parallel-testing-enabled YES CODE_SIGNING_ALLOWED=YES

-only-testing / -skip-testing в шардах; координатор для merge. -retry-tests-on-failure — только с записанным бюджетом флаков; бесконечные ретраи скрывают keychain, о чём предупреждает статья про подпись. Отдельный этап Swift 6 strict — не дублируйте одни и те же unit-тесты без реального split схемы; лишнее покрытие сжигает минуты на общей NVMe.

От .xcresult к JUnit: путь, который переживёт headless

На CI с macOS есть xcresulttool: тесты и диагностика в JSON или JUnit, по возможностям вашего закреплённого Xcode. Типовой поток: (1) путь -resultBundlePath уникален и не ведёт через symlink на общий NFS; (2) xcresulttool get --format json после прогона; (3) конвертация в JUnit; важны стабильные testsuite и classname для дедупа ретраев. При merge нескольких пакетов — поддержка merge в Xcode или задокументированный порядок (время по возрастанию) и отклонение при конфликте testStatus. Сырой xcresult в холодное хранилище, как для триажа симулятора, не один XML.

LLVM profdata, xccov и пороги, которые можно защищать

С -enableCodeCoverage YES агрегируйте данные xcrun llvm-profdata merge и смотрите xcrun xccov в режимах, подходящих политике. Прагматично: нижняя граница для приложения и основного фреймворка, сгенерированный и сторонний код в проверенных exclusion-листахнезанесённый чужой код не должен поглощать бюджет. После переноса self-hosted runner сверяйте цифры: один git SHA на двух хостах даёт один хэш исходников, но инкрементальное покрытие может различаться, если цель пропустили—строгий lane иногда отключает инкремент. При загрузке dSYM в ту же ночь убедитесь, что dSYM соответствует bitcode/LLVM артефактам, которые вы думаете, что поставили.

Ограждение CI: падайте, если xccov не открывает profdata—тихо обнулить покрытие и остаться зелёным в 2026 значит поставлять дифы без тестов и винить дашборд потом.

Шардированные test: мержьте до ворот

Резка по -only-testing:Target/Class или destination даёт на шард отдельные profdata и xcresult. Координатор merge-gate: (1) все шарды завершились и артефакты залиты; (2) смержить profdata до одного вызова xccov; (3) агрегировать JUnit, помечая намеренно пропущенные сьюты. Потерянный шард из-за прерывания инфраструктуры — merge failed, если нет редкого письменного исключения. Сочетается с M4 fan-out: параллелизм работает, если плоскость данных (артефакты + покрытие) сериализуется в один отчёт.

Симптом / слой / что сделать

Симптом Слой Исправить / проверить
0% покрытия, тесты бежали Build settings / членство target Code Coverage в scheme, цели с флагами coverage
JUnit пуст, UI — «успех» Экспорт инструментов Проверить xcresulttool на версии хоста; не доверять заглушке
Два хоста — огромный разброс line rate Ветка / инкремент / шард Отключить инкремент на gate; смержить profdata; закрепить destination

Если на машине крутится шлюз OpenClaw, не шарьте один /tmp между агентами—TMPDIR и так per job. Релизы: удалённый archive и экспорт IPA через App Store Connect API — потребители той же модели доверия: тесты+покрытие в CI на том же идентитете ветки, что и теги релиза, с учётом задержек promotion.

FAQ: ворота качества в пулах Mac

Вопрос Ответ
Блокировать по ветвям покрытия? Только если стек это позволяет; line rate проще для PM, branch — для критичной безопасности.
Сколько хранить zip xcresult? Под ваш SLA на инциденты—часто 14+ дней; аренда 1–2 ТБ позволяет дольше.
Сначала US East или Азия в матрице? Регион ближе к большинству коммитеров для интерактивного repro; полная матрица на релизе.

Почему Mac mini M4 в аренду под эту нагрузку

Симулятор и coverage чаще упираются в CPU+NVMe+inode, чем в GPU. Bare-metal Mac mini M4 в регионах MacXCode даёт предсказуемый I/O и RAM для нескольких корней CoreSimulator в тёплом состоянии без шумного соседа с OpenClaw-шлюзом. Вторую дорожку для p95 — через страницу тарифов, а не тремя командами на одном 512 ГБ. Редкий GUIVNC; SSH + артефакты — основа 2026.

Воспроизводимый iOS test CI в регионе

M4 · по умолчанию SSH · 1–2 ТБ