雲端 Mac 上的 iOS 彙總進度驗收:權重、取消與完成語意

雲端 Mac 上的 iOS 彙總進度驗收:權重、取消與完成語意

雲端 Mac 上的 iOS 彙總進度驗收:權重、取消與完成語意

批次處理檔案時,進度列先跑到百分之百,接著卻跳出失敗訊息;使用者按下取消,背景工作仍然提交結果。這通常不是繪製問題,而是把工作量、取消訊號與業務終態混在了一起。在 GNUMac 雲端 Mac 上,可以先用不依賴網路與模擬器的單元測試確立這些契約,再驗證 iOS 介面。

先約定進度代表什麼

本文以「轉換四份輸入,再驗證兩份輸出」作為測試情境。轉換占總工作量的六成,驗證占四成。這是測試指定的權重,不是效能量測結果,也不代表剩餘時間。

父進度的總量設為一百,子進度則分別以檔案數計量。不要直接把轉換檔案數與上傳位元組數相加;不同單位必須先對應到父層權重。

業務狀態另行區分為等待、執行中、取消中、已取消、失敗與成功。進度物件只負責描述數量,不負責錯誤分類。

百分比回答「約定的工作量完成了多少」,業務狀態回答「結果是否已可靠交付」。兩者不能互相取代。

實作前先寫明三項約束:每個子進度只能掛接一次;完成量不得超過總量;提交結果前必須檢查是否已取消。父子物件由單一任務持有,不要放進所有任務共用的全域變數。

用確定性測試驗收權重

在現有專案的單元測試目標中新增測試檔案,加入以下程式碼。測試只使用 Foundation 與 XCTest,不需要等待真正的檔案轉換,也不靠休眠猜測執行時序。

import Foundation
import XCTest

final class WeightedProgressTests: XCTestCase {
    func testWeightedStages() {
        let root = Progress(totalUnitCount: 100)
        let convert = Progress(totalUnitCount: 4)
        let verify = Progress(totalUnitCount: 2)

        root.addChild(convert, withPendingUnitCount: 60)
        root.addChild(verify, withPendingUnitCount: 40)

        XCTAssertEqual(root.fractionCompleted, 0, accuracy: 0.000001)

        convert.completedUnitCount = 2
        XCTAssertEqual(root.fractionCompleted, 0.3, accuracy: 0.000001)

        verify.completedUnitCount = 1
        XCTAssertEqual(root.fractionCompleted, 0.5, accuracy: 0.000001)

        convert.completedUnitCount = 4
        verify.completedUnitCount = 2
        XCTAssertEqual(root.fractionCompleted, 1, accuracy: 0.000001)
    }
}

這裡刻意讓兩個階段交錯推進,確認父進度不會只反映目前的階段。浮點數結果應以容許誤差比較,不要把百分比格式化成字串後再做斷言。

在雲端 Mac 的測試導覽器執行這個類別,保存提交雜湊值、測試結果與使用的工具鏈資訊。如果已有命令列測試入口,只需加入這個測試目標,不必為進度測試另外建立一套模擬器矩陣。

取消必須由執行器配合

Progress.cancel() 是訊號,不會強制終止正在執行的迴圈。每處理一個工作單元前都要檢查狀態,確實完成後才增加計數;最終替換輸出檔案前,也必須再次檢查。

以下測試驗證「取消後不啟動下一個工作單元」。它不代表已涵蓋並行競爭條件,可以直接加入前面的測試類別。

func testCancellationStopsNextUnit() {
    let progress = Progress(totalUnitCount: 4)
    progress.isCancellable = true
    var processed = 0

    func runNextUnit() {
        guard !progress.isCancelled else { return }
        processed += 1
        progress.completedUnitCount += 1
    }

    runNextUnit()
    progress.cancel()
    runNextUnit()

    XCTAssertTrue(progress.isCancelled)
    XCTAssertEqual(processed, 1)
    XCTAssertEqual(progress.completedUnitCount, 1)
}

非同步執行器還需要兩個可控制的暫停點:開始耗時操作之前,以及提交結果之前。測試先暫停任務、發出取消訊號,再放行,並斷言沒有啟動新工作,也沒有發布最終結果。應使用測試閘門協調執行,不要固定等待幾秒來碰運氣。

如果底層操作提供取消介面,應同步轉送訊號。若無法中斷,就等它返回後捨棄結果,並清理暫存檔案;介面維持「取消中」,不要提前承諾工作已經停止。

不要掩飾未知總量與重試

尚未掃描出檔案總數時,先顯示「正在統計」,不要拿零總量硬算百分比。掃描結束後固定任務清單,再建立完整的父子關係,開始處理。

執行中持續增加分母,會讓進度倒退。若確實需要動態探索任務,應顯示階段狀態或已處理數量,並清楚說明目前的百分比只適用於工作量已確定的階段。

重試時也不要反覆將同一個子進度歸零。業務層可以保留「第幾次嘗試」,進度層則依成功完成的邏輯單元累計。一次失敗的嘗試,不等於又完成了一份檔案。

也要為零輸入另外訂定規則:若空輸入合法,直接回傳「無需處理」的成功結果;若呼叫端必須提供輸入,就在啟動前回報錯誤,不要建立一個永遠停在起點的任務。

落實到介面與持續驗收

觀察到進度變更後,應切回主執行器更新介面狀態。每次執行都分配獨立的任務識別碼,回寫時核對識別碼,避免上一輪任務的延遲通知覆寫新進度。

顯示百分比時可以取整,但不要用取整後的數值判斷成功。最後一個檔案處理完後,若還需要驗證與原子提交,應事先為這些步驟保留工作量;失敗時保留錯誤狀態,不要強制補滿計數。

合併前逐項檢查:

  • 中間權重與交錯推進的斷言皆通過,結果不受執行速度影響。
  • 開始前取消、處理中取消、提交前取消都有獨立測試案例。
  • 錯誤路徑不會被對應為成功,暫存輸出能夠清理。
  • 新任務啟動後,舊任務的通知無法修改目前的介面。
  • 空輸入、未知總量與重試都有明確的顯示規則。

最終交付的不是一條看起來平順的進度列,而是一份可驗證的狀態契約。先讓計數可信,再讓取消可控,最後讓成功只代表結果真正可用。

常見問題

Progress 到達百分之百就能顯示成功嗎?

不能。計數完成只描述工作量,業務成功仍須確認最終寫入、驗證或交付成功。介面應另外檢查業務狀態。

呼叫 cancel() 會自動停止正在執行的工作嗎?

不會。執行端必須檢查取消狀態,並將取消傳遞給實際的網路或運算任務;提交最終結果前也要再次檢查。

為你的工作流程配置一台專屬 Mac mini

依照專案需求挑選機型、節點與租用期間。下單前先確認記憶體、儲存空間和連線需求;實際可用狀態以控制台即時回報為準。

先確認工作流程,再啟用你的設備。

專屬實體 Mac mini。選好機型、節點與租期後,再依照專案需求配置儲存空間。