在雲端 Mac 驗收 iOS SQLite WAL 當機復原

在雲端 Mac 驗收 iOS SQLite WAL 當機復原

一款 iOS App 將離線草稿寫入 SQLite:使用者看到「儲存成功」後,程序卻在下一次寫入時當機。重新啟動後,草稿究竟還在嗎?在雲端 Mac 的獨立模擬器上,可以把這個問題設計成可重複的實驗:先提交一筆基準紀錄,再於第二筆交易尚未提交時刻意製造當機,最後直接檢查資料庫。這裡驗證的是交易邊界,不是建置速度,也不能以「App 能重新開啟」取代資料檢查。

先定義合格結果

準備一張只有 id 和 phase 兩欄的測試表。紀錄 1|committed 必須在當機前完成提交;紀錄 2|pending 必須寫入新開啟的交易,但不能提交。復原後,完整性檢查應回傳 ok,查詢結果只能有第一筆紀錄。如果第二筆也存在,應先排查故障點是否設在提交之後;如果第一筆消失,則要確認它是否真的寫入同一個資料庫檔案。

WAL 並不保證「所有寫入都不會遺失」。這項測試只要求成功提交的交易在這次程序當機後仍可讀取,而未提交的交易不會變成已儲存的資料;它不能取代斷電、儲存裝置損壞或業務層級的同步測試。

實驗使用一次性的模擬器與測試套件識別碼。先固定 Xcode 版本與模擬器執行環境,記錄 xcodebuild -version 的輸出和模擬器識別碼;每一輪都使用相同條件。不要在個人開發用模擬器或真實使用者的資料庫上執行故障注入。

在提交邊界注入故障

將故障入口限定在除錯建置中,並要求明確的啟動參數,避免一般執行時誤觸。以下函式假設呼叫端已開啟 Documents/Recovery.sqlite,而且每次 SQL 呼叫都會檢查回傳值;範例使用 precondition,讓準備階段的失敗立即顯現,而不是繼續產生誤導性的查詢結果。

#if DEBUG
import Darwin
import SQLite3

func runRecoveryProbe(_ db: OpaquePointer) {
    func execute(_ sql: String) {
        precondition(sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK)
    }

    execute("PRAGMA journal_mode=WAL")
    execute("CREATE TABLE IF NOT EXISTS probe (id INTEGER PRIMARY KEY, phase TEXT NOT NULL)")
    execute("INSERT OR REPLACE INTO probe VALUES (1, 'committed')")
    execute("BEGIN IMMEDIATE")
    execute("INSERT OR REPLACE INTO probe VALUES (2, 'pending')")
    abort()
}
#endif

只有啟動參數包含專用測試標記時才呼叫此函式;測試入口應先確認使用的是實驗資料庫,且先前沒有尚未結束的交易。PRAGMA journal_mode=WAL 必須在交易之外執行。abort() 會讓這一輪的程序異常終止,因此程式碼後面刻意沒有 COMMIT。如果實際專案使用資料庫封裝層,就應透過該封裝層建立相同的提交邊界;不要繞過它,卻仍宣稱已驗收正式環境的寫入路徑。

避免舊紀錄干擾

每一輪先重設一次性模擬器中的測試 App 資料,或安裝到新建的專用模擬器;不要只執行 DELETE FROM probe,卻留下其他業務資料表和檔案。首次執行前,確認資料庫確實位於 App 的資料容器內。注入故障前後,記錄資料庫路徑、建置版本和模擬器識別碼,但不要將實際專案資料複製進實驗容器。

當機後檢查原始現場

啟動 App 後,先確認它因預設故障而異常終止,再進行查詢。只看到啟動命令返回,並不足以證明故障已發生:App 可能還沒執行到測試入口。可以交叉比對模擬器中的程序狀態與當機報告;沒有當機證據的這一輪應判定為「測試未完成」,不能算通過。

確認程序已結束後,在雲端 Mac 的終端機讀取 App 資料容器。將變數替換為這一輪的模擬器識別碼和測試套件識別碼:

UDID="your-simulator-udid"
BUNDLE_ID="com.example.RecoveryProbe"
DATA="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)"
sqlite3 "$DATA/Documents/Recovery.sqlite" \
  'PRAGMA integrity_check; SELECT id, phase FROM probe ORDER BY id;'

預期輸出依序為 ok 和 1|committed。檢查前,不要刪除或單獨移走資料庫旁的 -wal、-shm 檔案;讓 SQLite 按原狀開啟資料庫。檢查時也不要啟動會再次寫入的 App 程序。如果專案原本就有多個資料庫,先核對檔名和容器路徑,避免把「查錯檔案」誤判為復原失敗。

將失敗歸類以利定位

輸出包含 2|pending 時,優先檢查是否發生自動提交、封裝層是否提前結束交易,或故障標記是否導向另一條程式路徑。缺少 1|committed 時,確認第一筆寫入是否成功,並核對測試是否在同一個連線、同一個檔案中執行。如果查詢顯示「沒有該資料表」,先確認安裝的是包含測試入口的建置版本,以及容器路徑沒有指向上一輪的模擬器。

integrity_check 不是業務資料斷言:即使回傳 ok,仍須逐筆核對紀錄。反過來,程序當機報告只能證明程序異常結束,不能證明交易復原正確。將這兩類證據連同 Xcode 版本、模擬器識別碼和測試套件的建置識別碼一起保存,才能在重現失敗時比較相同條件。

接入流水線時維持測試邊界

讓這項測試獨占模擬器;故障注入結束並確認程序退出後,再執行唯讀檢查步驟。不要與一般 UI 測試共用同一個 App 資料容器:平行寫入會改變觀察結果。故障入口只存在於除錯建置,發布建置不得包含它;流水線應明確檢查這一點。

最後再進行一輪不注入當機的對照測試:在正常路徑提交第二筆紀錄,並確認可以讀出。這樣既能驗證「未提交的寫入會回滾」,也能確認測試本身沒有擋下所有寫入。取得這兩組結果後,才能把一次偶然的重新啟動現象,轉化為可複核的持久化驗收。

常見問題

正常關閉 App 能代替當機測試嗎?

不能。正常關閉可能提交交易或執行清理;測試必須在提交前終止程序,並確認發生異常結束。

檢查前要清除 WAL 伴隨檔案嗎?

不要。保留資料庫及 -wal、-shm 檔案,由 SQLite 開啟資料庫並檢查完整性與紀錄。

UDPMac 雲端 Mac

將下一次 Xcode 建置作業移至獨享實體主機。

選擇 Apple Silicon 機型、節點與租期,透過遠端連線執行自己的建置流程。實際可用情況以訂購時控制台顯示為準。

查看雲端 Mac 租用方案