После слияния реализации макроса все локальные тесты прошли успешно, однако следующая сборка на облачном Mac сгенерировала другой порядок членов, а позиция диагностики сместилась на одну строку. Итоговый код по-прежнему компилировался, но на проверке кода команда увидела уже не тот же самый API. Макросы Swift выполняются на этапе компиляции, поэтому результат одновременно зависит от реализации макроса, зависимости SwiftSyntax и текущего набора инструментов. По этой причине их нельзя тестировать как обычные функции.
Сначала определите результат, который нужно зафиксировать
Регрессионные тесты должны охватывать три уровня. На первом сравнивается полный исходный код после раскрытия макроса, чтобы обнаруживать изменения членов, уровней доступа, атрибутов и форматирования. На втором проверяются ошибки, предупреждения и предлагаемые исправления, особенно token, на который указывает диагностика. И только на третьем выполняются тесты компиляции и поведения, подтверждающие, что сгенерированный код действительно можно вызвать.
| Уровень | Что проверяется | Типичный сбой |
|---|---|---|
| Уровень раскрытия | Сгенерированный код и порядок объявлений | Пропущенный атрибут, изменённый уровень доступа |
| Уровень диагностики | Сообщение, позиция и предлагаемое исправление | Ошибка указывает не на тот узел |
| Уровень поведения | Результат компиляции и выполнения | Сгенерированная реализация компилируется, но работает неверно |
Успешная компиляция теста не означает, что вывод макроса не изменился. Публичным контрактом макроса часто служит именно форма генерируемого им исходного кода.
Сначала выберите минимальные входные данные и создайте эталон. Затем добавьте пустые объявления, обобщённые и вложенные типы, уже существующие одноимённые члены и недопустимые аргументы. Каждый тестовый сценарий должен проверять только одно правило. Тогда при сбое можно быстро определить, связана ли проблема с регрессией реализации или с изменением набора инструментов.
Зафиксируйте сгенерированный код тестами раскрытия
В тестовую цель пакета макроса можно подключить SwiftSyntaxMacrosTestSupport и с помощью assertMacroExpansion указать как входные данные, так и ожидаемый исходный код. В следующем примере предполагается, что TraceIDMacro добавляет в структуру строковое свойство:
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import TraceIDMacros
private let macros: [String: Macro.Type] = [
"TraceID": TraceIDMacro.self
]
final class TraceIDMacroTests: XCTestCase {
func testAddsTraceID() {
assertMacroExpansion(
"""
@TraceID
struct Job {}
""",
expandedSource:
"""
struct Job {
let traceID: String
}
""",
macros: macros
)
}
}
Ожидаемый текст следует сравнивать целиком, а не просто проверять наличие traceID. Проверки отдельных подстрок не обнаружат дублирующиеся члены, неправильные отступы и непреднамеренно добавленный интерфейс. Если макрос принимает аргументы, следует создать отдельные сценарии для значений по умолчанию, явно заданных значений и недопустимых выражений.
Проверяйте ошибочные сценарии отдельно
Не объединяйте недопустимые входные данные и успешное раскрытие в одном тесте. Отдельно проверяйте текст диагностики, позицию строки и столбца, а также предлагаемые исправления. В имени теста ясно указывайте условие, вызывающее ошибку. Тогда изменение одного сообщения об ошибке не заставит команду заново принимать весь снимок успешного раскрытия.
Записывайте отпечаток набора инструментов
Облачные Mac от BookaMac могут длительное время выполнять один и тот же конвейер, но стабильный узел не гарантирует неизменность набора инструментов. При каждом запуске тестов следует вместе архивировать путь к выбранной версии Xcode, версию Swift, архитектуру хоста и файл блокировки зависимостей.
set -euo pipefail
export LANG=C
export LC_ALL=C
mkdir -p artifacts
xcode-select -p | tee artifacts/xcode-path.txt
xcrun swift --version | tee artifacts/swift-version.txt
uname -m | tee artifacts/host-arch.txt
cp Package.resolved artifacts/Package.resolved
xcrun swift test 2>&1 | tee artifacts/swift-test.log
После фиксации LANG и LC_ALL текст диагностических сообщений не будет меняться из-за языковых настроек учётной записи, запускающей тесты. Команда xcrun гарантирует, что тесты используют набор инструментов выбранной версии Xcode, а не другой swift, случайно оказавшийся в пути shell. Если в репозитории нет Package.resolved, скрипт должен явно пропустить копирование, а не создавать пустой файл, выдавая его за зафиксированный результат.
Изолируйте параллельное выполнение и общее состояние
Если реализация макроса читает переменные окружения, текущий каталог, время или временные файлы, при параллельном выполнении тесты легко становятся нестабильными. Надёжнее строить результат макроса только на основе синтаксического дерева и явно переданных аргументов. Если внешние данные действительно необходимы, вынесите логику их чтения в отдельный тип и передавайте ему фиксированные значения в тестах.
Сначала выполните минимальный набор тестов последовательно и только затем включайте параллельный режим. Если сбои появляются лишь при параллельном выполнении, в первую очередь проверьте фиксированные имена файлов, общие каталоги кеша, глобальные изменяемые переменные и порядок очистки после тестов. Не увеличивайте сразу число повторных попыток: они маскируют состояние гонки под периодически успешные запуски.
Тесты макросов также не должны изменять реальный исходный код проекта. Временные входные данные следует записывать в отдельный каталог для каждого сценария и удалять после завершения. При сбое сохраняйте только обезличенные входные данные, результаты раскрытия и журналы. Это обеспечит воспроизводимость и не позволит остаткам предыдущего запуска повлиять на следующий.
Безопасно обновляйте набор инструментов
Перед обновлением Xcode или SwiftSyntax выполните полный набор тестов со старым набором инструментов и сохраните эталонные результаты. Затем переключите набор инструментов и выполните ту же команду. Проверяйте различия в следующем порядке:
- Убедитесь, что фактически выбраны ожидаемые путь к Xcode и версия Swift.
- Проверьте, изменился ли
Package.resolvedожидаемым образом. - Отделите изменения форматирования, например пробелов и отступов, от семантических изменений членов, типов и уровней доступа.
- Повторно запустите тесты поведения и убедитесь, что сгенерированный интерфейс можно вызвать.
- Обновляйте ожидаемые результаты раскрытия только после завершения проверки.
Если похожие различия в форматировании одновременно появились во всех сценариях, сначала следует изучить изменения правил печати или набора инструментов. Если изменился только один пограничный сценарий, причина, скорее всего, находится в самой реализации макроса. Не обновляйте снимки массовой заменой: среди большого объёма текстовых изменений может затеряться настоящая регрессия API.
В итоге необходимо сохранить три вида артефактов: машиночитаемые результаты тестов, различия между фактическим и ожидаемым раскрытием при сбоях и отпечаток набора инструментов. Когда следующая сборка на облачном Mac завершится с ошибкой, расследование больше не придётся начинать с предположения, что окружение могло отличаться. Вместо этого можно будет точно определить, на каком уровне контракта и начиная с какой версии произошло изменение.
Часто задаваемые вопросы
Достаточно ли успешной компиляции для проверки макроса Swift?
Нет. Компиляция подтверждает лишь допустимость результата. Отдельно проверяйте раскрытый исходный код, ошибки, предупреждения и предлагаемые исправления.
Что делать, если после обновления Xcode изменились все эталоны?
Сначала сравните путь Xcode, версию Swift и файл блокировки зависимостей. Затем отделите форматирование от изменений API и обновляйте эталоны только после проверки поведения.
Перенесите проверенный рабочий процесс в облачный Mac, работающий онлайн 365 дней в году
Выберите BookaMac M4 или BookaMac M4 Pro под свои задачи и при оформлении заказа проверьте регион, срок аренды и дополнительные варианты хранилища.