雲端 Mac 上的 iOS 浮點資料邊界測試:NaN、無窮值與容許誤差

雲端 Mac 上的 iOS 浮點資料邊界測試:NaN、無窮值與容許誤差

雲端 Mac 上的 iOS 浮點資料邊界測試:NaN、無窮值與容許誤差

進度列偶爾不更新、統計圖突然一片空白,問題可能不在 UI 執行緒,而是上游傳入了 NaN。將感測器讀值除以量測範圍,或將完成量除以總量,都可能產生無效結果。在 ArmShell 雲端 Mac 上,可以先將這層運算拆成獨立的 Swift 套件,以邊界測試攔下異常,再整合進 iOS 專案。

先訂定數值契約,而不是先調整容許誤差

本文只處理一項明確的業務需求:將非負讀值正規化到零至一之間。輸入必須是有限值,量測範圍必須大於零,結果也必須是有限值且不得超出範圍;無效資料一律回報錯誤,不自動截斷。零讀值是有效輸入,負零則統一輸出為正零。

這裡有個容易遺漏的條件:value < 0 || value > 1 無法排除 NaN。NaN 的有序比較都會回傳 false,程式可能繼續將它視為有效結果。

有限性、業務範圍與近似相等,是三個不同層次的檢查。放寬測試的容許誤差,無法修正無效輸入。

事先明訂錯誤處理策略也很重要:介面可以顯示「資料無法使用」,但不能悄悄將失敗轉換為零,否則使用者就無法區分真正的零值與讀取失敗。

建立不依賴模擬器的測試套件

先確認命令列工具可用,再於暫存目錄建立套件。以下操作不會修改應用程式專案,也不需要啟動圖形介面工作階段:

xcrun swift --version
root="$(mktemp -d)"
cd "$root"
swift package init --type library --name NumericBoundary

將 Sources/NumericBoundary/NumericBoundary.swift 的內容替換為:

public enum LevelError: Error, Equatable {
    case invalidInput
    case invalidResult
}

public func normalizedLevel(
    _ raw: Double,
    scale: Double
) throws -> Double {
    guard raw.isFinite, scale.isFinite,
          raw >= 0, scale > 0 else {
        throw LevelError.invalidInput
    }

    let ratio = raw / scale
    guard ratio.isFinite, ratio >= 0, ratio <= 1 else {
        throw LevelError.invalidResult
    }

    return ratio == 0 ? 0.0 : ratio
}

不能只驗證輸入。兩個有限值相除,仍可能得到無窮值,例如極大的分子除以極小的正分母。另一方面,即使結果是有限值,只要超出量測範圍,也應判定為失敗,不能仰賴介面元件替運算層收拾問題。

這段實作允許正常的浮點捨入;若業務還要求最小有效讀值,就需要另設門檻。極小正數相除可能下溢為零,本例接受這種情況,不將「有限」誤當成「完全沒有精度損失」。

用反例涵蓋異常路徑

將預設測試檔案的內容替換為以下程式碼。每個案例都會檢查錯誤類別,避免函式因其他原因拋出錯誤時,測試仍被誤判為通過:

import XCTest
@testable import NumericBoundary

final class NumericBoundaryTests: XCTestCase {
    func testInvalidInputs() {
        let cases: [(Double, Double)] = [
            (.nan, 1),
            (.infinity, 1),
            (-.infinity, 1),
            (1, .nan),
            (1, .infinity),
            (1, -.infinity),
            (-1, 1),
            (1, 0),
            (1, -0.0),
            (1, -1)
        ]

        for (raw, scale) in cases {
            XCTAssertThrowsError(
                try normalizedLevel(raw, scale: scale)
            ) { error in
                XCTAssertEqual(error as? LevelError, .invalidInput)
            }
        }
    }

    func testInvalidResults() {
        let cases: [(Double, Double)] = [
            (2, 1),
            (.greatestFiniteMagnitude, .leastNormalMagnitude)
        ]

        for (raw, scale) in cases {
            XCTAssertThrowsError(
                try normalizedLevel(raw, scale: scale)
            ) { error in
                XCTAssertEqual(error as? LevelError, .invalidResult)
            }
        }
    }

    func testValidBoundaries() throws {
        XCTAssertEqual(try normalizedLevel(0, scale: 1), 0)
        XCTAssertEqual(try normalizedLevel(1, scale: 1), 1)
        XCTAssertEqual(
            try normalizedLevel(1, scale: 10),
            0.1,
            accuracy: 1e-12
        )

        let zero = try normalizedLevel(-0.0, scale: 1)
        XCTAssertEqual(zero, 0)
        XCTAssertEqual(zero.sign, .plus)
    }
}

在一般的相等比較中,負零等於正零,因此必須額外檢查 sign,才能證明正規化策略確實生效。異常案例也不要使用「結果不等於 NaN」的斷言:NaN 連自身都不相等,這種測試無法證明任何事。

容許誤差必須依據業務尺度設定

範例的輸出範圍固定為零至一,採用絕對誤差比較較為直觀。1e-12 是針對這段簡單運算選定的測試容許誤差,並非適用於所有感測器、圖形或統計工作的通用精度標準。

如果運算值的跨度很大,應先確認兩個值都是有限值,再依業務需求結合絕對誤差與相對誤差:

abs(actual - expected)
    <= max(absoluteTolerance,
           relativeTolerance * max(abs(actual), abs(expected)))

兩個容許誤差本身也必須是有限且非負的值。在極端數量級下,差值或右側的乘法可能溢位,因此不能直接將這個公式視為通用且安全的函式庫實作。

接近零時,主要由絕對誤差控制;數值很大時,則主要由相對誤差控制。測試程式碼應說明門檻的依據,例如裝置解析度或演算法的誤差預算,而不是不斷放寬門檻,直到測試通過。也不要先將數值格式化為小數字串再比較,這樣會掩蓋實際差異。

納入流水線並驗證品質關卡有效

在套件目錄執行:

swift test
swift test -c release

偵錯與發行組態都必須執行測試。正式環境的驗證應使用 guard 與拋出錯誤,而不是可能在最佳化建置中被移除的 assert。流水線必須保留命令的結束代碼,只要測試失敗,就停止後續交付。

接著做一次破壞性驗證:暫時移除輸入檢查中的 raw.isFinite,確認 NaN 案例會因錯誤類別改變而失敗,再還原程式碼。這樣才能證明測試確實涵蓋輸入端的檢查,而不只是執行過函式。

整合進應用程式時,應讓業務程式碼呼叫同一份實作,不要在測試套件中另行維護副本。記錄工具鏈版本、提交識別碼與失敗案例即可,不必上傳真實使用者的讀值。最終驗收標準是:異常輸入明確失敗、有效邊界穩定通過、容許誤差有依據,而且在發行組態下仍執行相同的契約。

常見問題

只檢查數值是否小於零或大於一,能排除 NaN 嗎?

不能。NaN 與零、一進行有序比較時都會得到 false,必須先用 isFinite 排除 NaN 和正負無窮,再檢查範圍。

輸入都是有限數,還需要檢查運算結果嗎?

需要。有限數相除仍可能溢位為無窮值,運算結果必須再次檢查 isFinite,並確認符合業務允許的範圍。

為下一次建置準備雲端 Mac

依照工作負載選擇機型與租用週期。使用獨享 Apple Silicon 實體節點,並非虛擬機。

立即租用雲端 Mac

從閱讀到第一次建置

準備好工作負載,再挑選適合自己的獨享實體節點。