雲端 Mac 命令列工具驗收:標準串流、結束碼與原子寫入

CI/CD 實踐 ·約 7 分鐘閱讀

雲端 Mac 命令列工具驗收:標準串流、結束碼與原子寫入

在雲端 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 版本上實際執行。驗收應針對即將交付的同一份二進位檔案;不要驗收後重新編譯,卻交付另一份產出檔案。

在獨立目錄驗收成功與失敗情境

以下腳本沿用上一段 shell 工作階段中的 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 製造「無法寫入的目錄」:如果測試程序具有特殊權限,結果可能不同。使用不存在的上層目錄更直接,但仍只涵蓋一種 I/O 失敗,不能證明所有磁碟異常都會如預期處理。

排除三種常見的假通過

管線掩蓋結束碼

taskprobe ... | tee output.log 的結束碼通常來自管線末端,而不是工具本身。驗收腳本應優先直接重新導向兩個串流,再檢查命令的結束狀態。如果確實需要管線,請在支援該選項的 shell 中啟用 pipefail,並記錄使用的 shell。

工作目錄恰好正確

相對路徑會受到啟動目錄影響。上述測試從 home 執行二進位檔,並傳入絕對輸出路徑,就是為了找出隱含的工作目錄假設。實際工具若要讀取隨套件提供的資源,應依據明確的資源位置尋找,不能假設工作一律從原始碼根目錄啟動。

把錯誤文字當成資料

只檢查「命令有印出東西」沒有意義。下游工作可能把標準輸出直接存成結果檔案。驗收時應同時斷言:失敗時標準輸出為空、標準錯誤有內容、結束碼非零;成功時標準錯誤為空,並檢查產生檔案的實際內容。若工具輸出結構化資料,還應驗證格式,而不只是比較檔案大小。

交付前再次檢查邊界

將驗收腳本放在建置產出檔案旁,由同一個建置工作執行。通過後記錄二進位檔的校驗值與所選工具鏈資訊,供後續交接核對;執行時使用的帳號,應與實際工作帳號具有相同的目錄存取範圍。檔案測試應使用一次性目錄,避免舊產出檔案讓 test -e 意外通過。

本文的測試無法模擬斷電、磁碟寫滿,或其他程序同時修改目標檔案,也無法證明程式能在未經測試的系統版本上執行。若需要這些保證,應分別補上故障注入、並行存取協調,以及目標系統上的驗收。租期結束前,將原始碼、腳本與驗收記錄移出,並請接手者在新環境重新執行全套檢查;「以前通過」不等於在新機器上驗收通過。

常見問題

為何錯誤訊息不能只印在標準輸出?

下游腳本可能將標準輸出直接存成資料檔。錯誤應寫入標準錯誤,並回傳非零結束碼,才不會混入有效資料。

測試成功後的檔案內容,就能證明寫入是原子的嗎?

不能。這只能驗證可觀察結果;實作仍須確保暫存檔與目標檔位於同一檔案系統,並檢查寫入失敗時的行為。

需要一台獨享雲端 Mac 嗎?

選擇機型、節點和計費週期。交付的是獨享實體機,實際可用狀態以控制台即時回報為準。

查看規格並下單