Как проверить контракт macOS CLI перед выпуском

DevOps и CI/CD ·~5 мин чтения

Как проверить контракт macOS CLI перед выпуском

После сборки CLI-программы на облачном Mac проще всего упустить не ошибку компиляции, а контракт её вызова: справка попадает в поток данных, программа при сбое возвращает нулевой код или работает только из текущего каталога разработчика. Ниже небольшой инструмент на Swift используется для приёмочной проверки перед выпуском. Пример не требует графического сеанса и подходит для автоматизированной сборки.

Сначала определите контракт вызова

Пример инструмента taskprobe принимает --output и текст, который записывает в указанный файл. Путь к файлу обязателен; запрос справки завершается успешно, ошибка в аргументах — с ненулевым кодом. Прежде чем реализовывать функциональность, определите, как вызывающая сторона будет распознавать результат.

Сценарий Стандартный вывод Стандартный поток ошибок Код выхода Файл
--help Справка по использованию Пуст 0 Не создаётся
Не хватает аргументов Пуст Описание ошибки Ненулевой Не создаётся
Успешная запись Сообщение о завершении Пуст 0 Полное содержимое
Путь недоступен для записи Пуст Описание ошибки Ненулевой Целевой файл не создаётся

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

Атомарная запись защищает целевой файл от замены частично записанным новым содержимым, но не гарантирует сохранность данных после отключения питания. Для этого нужна отдельная стратегия записи на диск и восстановления.

Соберите минимальный инструмент для проверки

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

WORK="$(mktemp -d)"
mkdir -p "$WORK/bin" "$WORK/home"
cat > "$WORK/main.swift" <<'SWIFT'
import Foundation

func fail(_ message: String) -> Never {
    FileHandle.standardError.write(Data((message + "\n").utf8))
    exit(2)
}

let args = Array(CommandLine.arguments.dropFirst())
if args == ["--help"] {
    print("usage: taskprobe --output PATH TEXT")
    exit(0)
}
guard args.count == 3, args[0] == "--output",
      !args[1].isEmpty else {
    fail("expected --output PATH TEXT")
}

let target = URL(fileURLWithPath: args[1])
do {
    try Data(args[2].utf8).write(to: target, options: .atomic)
    print("written")
} catch {
    fail("cannot write output: \(error.localizedDescription)")
}
SWIFT
xcrun swiftc -O "$WORK/main.swift" -o "$WORK/bin/taskprobe"

xcrun использует выбранный в данный момент набор инструментов разработки. Перед компиляцией выбор можно проверить командой xcode-select -p; это не заменяет запуск на целевой версии macOS. Приёмочную проверку нужно проводить на том же бинарном файле, который будет передан. Не пересобирайте программу после проверки, чтобы затем передать другой файл.

Проверьте успешные и ошибочные сценарии в отдельном каталоге

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

set -eu
BIN="$WORK/bin/taskprobe"
cd "$WORK/home"

env -i PATH=/usr/bin:/bin HOME="$WORK/home" \
  "$BIN" --help >"$WORK/help.out" 2>"$WORK/help.err"
grep -Fx 'usage: taskprobe --output PATH TEXT' "$WORK/help.out"
test ! -s "$WORK/help.err"

if "$BIN" --output >"$WORK/bad.out" 2>"$WORK/bad.err"; then
  echo "missing arguments returned success" >&2
  exit 1
fi
test ! -s "$WORK/bad.out"
grep -F 'expected --output' "$WORK/bad.err"

"$BIN" --output "$WORK/home/result.txt" 'hello 云端 Mac' \
  >"$WORK/good.out" 2>"$WORK/good.err"
grep -Fx 'written' "$WORK/good.out"
test ! -s "$WORK/good.err"
grep -Fx 'hello 云端 Mac' "$WORK/home/result.txt"

if "$BIN" --output "$WORK/home/absent/result.txt" test \
  >"$WORK/fail.out" 2>"$WORK/fail.err"; then
  echo "unwritable path returned success" >&2
  exit 1
fi
test ! -e "$WORK/home/absent/result.txt"
test ! -s "$WORK/fail.out"
test -s "$WORK/fail.err"

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

Исключите три распространённых ложных успеха

Конвейер скрывает код выхода

Код выхода команды taskprobe ... | tee output.log обычно принадлежит последней команде конвейера, а не самому инструменту. В приёмочном скрипте лучше напрямую перенаправлять оба потока и затем проверять статус команды. Если конвейер необходим, включите pipefail в оболочке, которая поддерживает эту опцию, и зафиксируйте, какая оболочка использовалась.

Рабочий каталог случайно оказался подходящим

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

Текст ошибки принимается за данные

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

Перед передачей проверьте границы применимости

Разместите приёмочный скрипт рядом с результатом сборки и запускайте их проверку в рамках той же задачи сборки. После успешного прохождения зафиксируйте контрольную сумму бинарного файла и сведения о выбранном наборе инструментов, чтобы при передаче их можно было сверить. Учётная запись, под которой запускается проверка, должна иметь тот же доступ к каталогам, что и учётная запись реальной задачи. Для проверки файлов используйте одноразовый каталог: иначе старый файл может привести к неожиданному успеху test -e.

Эти тесты не моделируют отключение питания, заполнение диска или одновременное изменение целевого файла другим процессом. Они также не доказывают, что программа работает на непроверенных версиях системы. Если нужны такие гарантии, добавьте соответственно тесты с внесением сбоев, координацию параллельного доступа и приёмку на целевой системе. До конца срока аренды перенесите исходный код, скрипты и результаты проверок, а принимающая сторона должна повторить весь набор тестов в новой среде. «Когда-то прошло проверку» — не результат приёмки на новой машине.

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

Почему ошибки нельзя выводить только в stdout?

Вызывающий сценарий может сохранять stdout как данные. Ошибка должна попадать в stderr и сопровождаться ненулевым кодом завершения.

Подтверждает ли проверка файла атомарность записи?

Нет. Проверка показывает только наблюдаемый результат. Реализация также должна размещать временный и целевой файлы в одной файловой системе.

Нужен выделенный Mac в облаке?

Выберите модель, локацию и срок оплаты. Вам будет предоставлен выделенный физический сервер; фактическая доступность отображается в консоли в реальном времени.

Посмотреть конфигурации и заказать