Test iOS Floating-Point Boundaries on a Cloud Mac
When a progress bar stops updating or a chart suddenly goes blank, the UI thread may not be the culprit: NaN may have entered the calculation upstream. Dividing a sensor reading by its full-scale range, or completed work by the total, can produce invalid results. On an ArmShell cloud Mac, you can first isolate this calculation in a standalone Swift package, catch invalid values with boundary tests, and then integrate it into your iOS project.
Define the numeric contract before adjusting tolerances
This article covers one specific use case: normalizing a nonnegative reading to a value between zero and one. Inputs must be finite, the scale must be greater than zero, and the result must be finite and within range. Invalid data produces an error rather than being automatically clamped. A zero reading is valid, and negative zero is always returned as positive zero.
One condition is easy to miss: value < 0 || value > 1 does not rule out NaN. Ordered comparisons with NaN all return false, so the program may continue treating it as a valid result.
Finiteness, the valid business range, and approximate equality are three separate checks. Increasing a test's tolerance cannot fix invalid input.
It is also important to decide how errors will be handled up front. The UI can display “Data unavailable,” but it must not silently turn a failure into zero. Otherwise, users cannot distinguish an actual zero reading from a failed read.
Create a test package that does not need a simulator
First, confirm that the command-line tools are available, then create a package in a temporary directory. These steps neither modify your app project nor require a graphical session:
xcrun swift --version
root="$(mktemp -d)"
cd "$root"
swift package init --type library --name NumericBoundary
Replace the contents of Sources/NumericBoundary/NumericBoundary.swift with:
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
}
Validating only the inputs is not enough. Dividing two finite numbers can still produce infinity—for example, when a very large numerator is divided by a very small positive denominator. A finite result outside the allowed range must also fail; the calculation layer should not rely on UI components to catch it.
This implementation allows normal floating-point rounding. If your requirements also specify a minimum valid reading, define a separate threshold. Dividing very small positive numbers can underflow to zero; this example accepts that outcome rather than confusing “finite” with “no loss of precision whatsoever.”
Cover failure paths with counterexamples
Replace the contents of the default test file with the following code. Each case checks the error category so that an error thrown for some other reason cannot make the test pass incorrectly:
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)
}
}
Negative zero equals positive zero in an ordinary equality comparison, so you must also check sign to prove that the normalization policy actually took effect. Do not test exceptional cases by asserting that “the result is not equal to NaN,” either. NaN is not even equal to itself, so that assertion proves nothing.
Tie tolerances to the scale of the problem
The output in this example is always between zero and one, making an absolute-error comparison straightforward. 1e-12 is a test tolerance chosen for this small calculation, not a universal precision standard for every sensor, graphics, or statistics task.
If calculated values span a wide range of magnitudes, first confirm that both values are finite, then use a combination of absolute and relative error suited to your requirements:
abs(actual - expected)
<= max(absoluteTolerance,
relativeTolerance * max(abs(actual), abs(expected)))
Both tolerances must also be finite and nonnegative. At extreme magnitudes, the difference or the multiplication on the right-hand side can overflow, so this formula should not be treated as a universally safe library implementation.
Near zero, absolute tolerance is the main control; for large numbers, relative tolerance dominates. Test code should explain where the thresholds come from, such as device resolution or an algorithm's error budget, rather than repeatedly widening them until the tests pass. Do not format values as decimal strings before comparing them, either: that can hide real differences.
Integrate with the pipeline and verify the test gate
Run these commands in the package directory:
swift test
swift test -c release
Run both debug and release configurations. Production validation uses guard and thrown errors, not assert, which may be removed in optimized builds. The pipeline must preserve command exit codes and stop subsequent delivery steps when tests fail.
Next, deliberately break the implementation: temporarily remove raw.isFinite from the input checks, confirm that the NaN case fails because the error category changes, and then restore the code. This proves that the tests genuinely cover input validation, rather than merely executing the function.
When integrating with the app, have the application code call the same implementation instead of maintaining a copy in the test package. Recording the toolchain version, commit identifier, and failing cases is enough; there is no need to upload real user readings. The final acceptance criteria are clear failures for invalid inputs, consistently passing valid boundaries, tolerances with a documented basis, and the same contract enforced in release builds.
Frequently asked questions
Will checking for values below zero or above one reject NaN?
No. Both ordered comparisons return false for NaN. Check isFinite first, then validate the allowed range.
Do finite inputs guarantee a finite result?
No. Division of finite values can overflow to infinity. Check the result for finiteness and the required business range.
Get a Cloud Mac Ready for Your Next Build
Choose a model and rental term that fit your workload. Get exclusive access to a physical Apple Silicon node, not a virtual machine.
Rent a Cloud Mac