BookaMac 工程紀錄

雲端 Mac 的 Swift 巨集展開迴歸測試:鎖定生成程式碼

雲端 Mac 的 Swift 巨集展開迴歸測試:鎖定生成程式碼

某次巨集實作合併後,本機測試全數通過,但雲端 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。局部字串斷言會漏掉重複成員、錯誤縮排,以及意外新增的介面。若巨集允許傳入參數,還應分別為預設值、明確指定值與無效運算式建立測試案例。

將錯誤路徑分開測試

不要把無效輸入與成功展開混在同一項測試中。應分別斷言診斷文字、行列位置與修正建議,並在測試名稱中清楚寫出觸發條件。如此一來,調整一則錯誤訊息時,就不會迫使團隊重新接受整份成功快照。

記錄工具鏈指紋

BookaMac 上的雲端 Mac 可以長期執行同一條流水線,但節點穩定不代表工具鏈永遠不會改變。每次測試都應將 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

固定 LANGLC_ALL 後,診斷文字較不容易因執行帳號的語言環境而變化。xcrun 則可確保測試使用目前選取的 Xcode 工具鏈,而不是 shell 路徑中偶然出現的另一個 swift。如果儲存庫中沒有 Package.resolved,指令碼應明確略過複製步驟,而不是建立空白檔案冒充鎖定結果。

隔離並行執行與共享狀態

如果巨集實作會讀取環境變數、目前目錄、時間或暫存檔案,測試在並行執行時就很容易出現不穩定結果。更穩妥的原則是讓巨集只依據語法樹與明確參數生成結果。若確實需要外部資料,應將讀取邏輯抽離至獨立型別,並在測試中注入固定值。

先以循序模式跑通最小測試集合,再啟用並行測試。如果只有並行執行時才會失敗,應優先檢查固定檔名、共享快取目錄、全域可變變數與測試清理順序,不要直接增加重試次數。重試只會把競爭條件掩蓋成偶爾成功。

巨集測試也不應修改真實專案的原始碼。暫時輸入應寫入每個測試案例各自獨立的目錄,完成後再清理;失敗時只保留去識別化後的輸入、展開結果與日誌。這樣既能重現問題,也不會讓上一輪殘留資料影響下一輪測試。

安全升級工具鏈

升級 Xcode 或 SwiftSyntax 前,先以舊工具鏈執行完整測試並保存基準,再切換工具鏈執行相同命令。審查差異時,依照下列順序處理:

  1. 確認實際選取的 Xcode 路徑與 Swift 版本。
  2. 檢查 Package.resolved 是否發生預期內的變化。
  3. 區分空白、縮排等格式變化,以及成員、型別、存取層級等語意變更。
  4. 重新執行行為測試,確認生成的介面可被呼叫。
  5. 只有完成審查後,才更新展開結果的預期值。

如果所有測試案例同時出現相似的格式差異,通常應先調查輸出規則或工具鏈變化;如果只有一個邊界案例改變,則更可能是巨集實作本身的問題。不要使用批次取代直接刷新快照,否則真正的 API 迴歸會混在大量文字變動中。

最終應保留三類產物:機器可讀的測試結果、展開失敗時實際結果與預期結果的差異,以及工具鏈指紋。下一次雲端 Mac 建置失敗時,排查起點就不再是「環境可能不同」,而是可以明確回答哪一層契約從哪個版本開始發生了變化。

常見問題

Swift 巨集只要最終編譯成功就算通過嗎?

不夠。編譯成功只能證明生成程式碼可被接受,還要比較展開原始碼,並分別驗證錯誤、警告及修正建議,才能發現 API 形狀的意外變化。

工具鏈升級後所有展開基準都改變時該怎麼做?

先比較 Xcode 路徑、Swift 版本及依賴鎖定檔,再區分純格式差異與語意變更。確認 API、診斷與執行行為後才更新基準。

獨享實體 Mac mini

將已驗證的工作流程部署到持續在線的雲端 Mac

依用途選擇 BookaMac M4 或 BookaMac M4 Pro,下單時請確認地區、租用期限及儲存空間加購項目。

選擇租用方案