macOS CLI の標準ストリームと終了コードをリリース前に検証する

CI/CD ·約 10 分

macOS CLI の標準ストリームと終了コードをリリース前に検証する

クラウド Mac でコマンドラインプログラムをビルドした後、見落としやすいのはコンパイルエラーではなく、呼び出し時の取り決めです。ヘルプがデータ用の出力に混ざる、失敗しても終了コードがゼロになる、開発者の現在のディレクトリでしか動かない、といった問題があります。ここでは小さな Swift ツールを使い、引き渡し前の受け入れテストを組み立てます。この例はグラフィカルセッションに依存せず、無人のビルドジョブにも組み込めます。

呼び出し時の取り決めを先に定める

サンプルツール taskprobe は --output とテキストを受け取り、指定されたファイルにそのテキストを書き込みます。ファイルパスは必須です。ヘルプの要求は正常終了し、引数の誤りは失敗として終了します。機能を実装する前に、呼び出し側が結果をどう判定するか決めておきます。

場面 標準出力 標準エラー出力 終了コード ファイル
--help 使い方 空 0 作成しない
引数不足 空 エラー説明 0 以外 作成しない
正常な書き込み 完了を示す文字列 空 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 を使います。各チェックでは二つの出力ストリームを別々に保存します。テストでは意図的に一時的な作業ディレクトリへ移動し、引き継いだ環境変数の大半を取り除きます。引き渡すツールに特定の変数が必要なら、対話シェルにたまたま設定されていることを当てにせず、呼び出し時の取り決めに明記してください。

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 で「書き込めないディレクトリ」を作るのは避けてください。テストプロセスに特別な権限があると、結果が変わる可能性があります。存在しない親ディレクトリを使うほうが直接的ですが、それでも検証できるのは I/O エラーの一種類だけです。あらゆるディスク障害が期待どおりに処理される証明にはなりません。

誤ってテストを通過する三つの原因を調べる

パイプラインが終了コードを隠す

taskprobe ... | tee output.log の終了コードは通常、ツール自体ではなくパイプラインの最後のコマンドに由来します。受け入れテストのスクリプトでは、まず二つのストリームを直接リダイレクトし、その後でコマンドの終了状態を確認します。どうしてもパイプラインが必要なら、対応するシェルで pipefail を有効にし、使用したシェルを記録してください。

現在のディレクトリが偶然合っている

相対パスは起動時のディレクトリに左右されます。上のテストで home からバイナリを実行し、絶対パスで出力先を渡すのは、作業ディレクトリに関する暗黙の前提を見つけるためです。実際のツールが同梱リソースを読む必要があるなら、明示されたリソースの場所を基準に探してください。ジョブが常にソースコードのルートディレクトリから起動するとは想定できません。

エラーメッセージがデータとして扱われる

「コマンドが何かを出力した」だけの確認には意味がありません。後続のジョブが標準出力をそのまま結果ファイルとして保存することもあります。受け入れテストでは、失敗時に標準出力が空、標準エラー出力には内容があり、終了コードが 0 以外であることを同時に確認します。成功時は標準エラー出力が空であることに加え、生成されたファイルの実際の内容を確認します。ツールが構造化データを出力する場合は、ファイルサイズを比べるだけでなく、その形式も検証してください。

引き渡し前にテストの適用範囲を確認する

受け入れテストのスクリプトはビルド成果物のそばに置き、同じビルドジョブで実行します。合格後は、引き継ぎ時に照合できるよう、バイナリのチェックサムと選択したツールチェーンの情報を記録します。実行するアカウントには、実際のジョブで使うアカウントと同じディレクトリへのアクセス範囲が必要です。ファイルのテストには毎回新しい一時ディレクトリを使い、古い成果物のせいで test -e が意図せず成功するのを防ぎます。

この記事のテストでは、停電、ディスク容量の不足、他のプロセスによる対象ファイルの同時変更は再現できません。また、未テストのシステムバージョンでプログラムが動作することも証明できません。これらの保証が必要なら、それぞれ障害注入、並行処理の調整、対象システムでの受け入れテストを追加してください。レンタル期間が終わる前にソースコード、スクリプト、受け入れテストの記録を移し、引き継ぐ担当者が新しい環境で一連のチェックを再実行します。「以前は通った」は、新しいマシンでの受け入れ結果にはなりません。

よくある質問

エラーを標準出力に書くと何が問題ですか?

呼び出し側が標準出力をデータとして保存する場合があります。エラーは標準エラーに書き、ゼロ以外の終了コードを返します。

出力ファイルの検査だけでアトミックな書き込みを保証できますか?

できません。テストで分かるのは実行後の状態です。一時ファイルと対象ファイルが同じファイルシステム上にあることも実装側で確認します。

専用のクラウド Mac をお探しですか?

機種、ロケーション、利用期間を選ぶだけ。専有の物理マシンを提供します。実際の利用可否は、コントロールパネルに表示される最新情報をご確認ください。

構成を確認して注文