在雲端 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 嗎?
選擇機型、節點和計費週期。交付的是獨享實體機,實際可用狀態以控制台即時回報為準。