2026-04-25 xcodebuild test: покрытие кода, xcresult в JUnit и ворота PR на арендованном cloud Mac (HK / JP / KR / SG / US)
Команды 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 артефактам, которые вы думаете, что поставили.
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 |
Связанные runbook и тот же хост
Если на машине крутится шлюз 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 ГБ. Редкий GUI — VNC; SSH + артефакты — основа 2026.
Воспроизводимый iOS test CI в регионе
M4 · по умолчанию SSH · 1–2 ТБ