fix iOS
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#if os(iOS)
|
||||
import Testing
|
||||
import CoreGraphics
|
||||
@testable import TVBox
|
||||
|
||||
/// Property 7: Channel overlay group width is proportional
|
||||
/// For any screen width w > 0, computeGroupWidth(screenWidth: w) returns a value in [w * 0.30, w * 0.35]
|
||||
///
|
||||
/// **Validates: Requirements 8.3, 8.4**
|
||||
|
||||
@Suite("Property 7: Channel overlay group width is proportional")
|
||||
struct ChannelOverlayGroupWidthPropertyTests {
|
||||
|
||||
// MARK: - Parameterized tests with representative screen widths
|
||||
|
||||
/// Common iOS device screen widths and edge cases
|
||||
static let screenWidths: [CGFloat] = [
|
||||
// Small values
|
||||
1.0, 10.0, 50.0,
|
||||
// iPhone SE / small phones
|
||||
320.0, 375.0,
|
||||
// iPhone standard
|
||||
390.0, 393.0,
|
||||
// iPhone Plus / Max
|
||||
414.0, 428.0, 430.0,
|
||||
// iPad
|
||||
744.0, 768.0, 810.0, 834.0, 1024.0, 1194.0,
|
||||
// Large values
|
||||
1366.0, 2048.0, 5000.0
|
||||
]
|
||||
|
||||
@Test("computeGroupWidth returns value in [w*0.30, w*0.35] for common screen widths",
|
||||
arguments: screenWidths)
|
||||
func groupWidthInProportionalRange(screenWidth: CGFloat) {
|
||||
let result = ChannelOverlayView.computeGroupWidth(screenWidth: screenWidth)
|
||||
let lowerBound = screenWidth * 0.30
|
||||
let upperBound = screenWidth * 0.35
|
||||
|
||||
#expect(result >= lowerBound,
|
||||
"computeGroupWidth(\(screenWidth)) = \(result) should be >= \(lowerBound) (w * 0.30)")
|
||||
#expect(result <= upperBound,
|
||||
"computeGroupWidth(\(screenWidth)) = \(result) should be <= \(upperBound) (w * 0.35)")
|
||||
}
|
||||
|
||||
// MARK: - Randomized property test
|
||||
|
||||
@Test("computeGroupWidth satisfies proportional invariant for random positive widths")
|
||||
func groupWidthProportionalRandomized() {
|
||||
for _ in 0..<1000 {
|
||||
// Generate random positive screen widths across different magnitudes
|
||||
let screenWidth = CGFloat.random(in: 0.001...10000.0)
|
||||
|
||||
let result = ChannelOverlayView.computeGroupWidth(screenWidth: screenWidth)
|
||||
let lowerBound = screenWidth * 0.30
|
||||
let upperBound = screenWidth * 0.35
|
||||
|
||||
#expect(result >= lowerBound,
|
||||
"Random width \(screenWidth): result \(result) should be >= \(lowerBound)")
|
||||
#expect(result <= upperBound,
|
||||
"Random width \(screenWidth): result \(result) should be <= \(upperBound)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Proportionality: result scales linearly with screen width
|
||||
|
||||
@Test("computeGroupWidth scales linearly with screen width")
|
||||
func groupWidthScalesLinearly() {
|
||||
let widths: [CGFloat] = [100.0, 200.0, 400.0, 800.0]
|
||||
|
||||
for i in 0..<(widths.count - 1) {
|
||||
let w1 = widths[i]
|
||||
let w2 = widths[i + 1]
|
||||
let result1 = ChannelOverlayView.computeGroupWidth(screenWidth: w1)
|
||||
let result2 = ChannelOverlayView.computeGroupWidth(screenWidth: w2)
|
||||
|
||||
let ratio = w2 / w1
|
||||
let resultRatio = result2 / result1
|
||||
|
||||
// The ratio of results should equal the ratio of inputs (linear scaling)
|
||||
#expect(abs(resultRatio - ratio) < 0.001,
|
||||
"Group width should scale linearly: ratio of widths = \(ratio), ratio of results = \(resultRatio)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge case: very small positive widths
|
||||
|
||||
@Test("computeGroupWidth handles very small positive widths", arguments: [
|
||||
CGFloat(0.001),
|
||||
CGFloat(0.01),
|
||||
CGFloat(0.1),
|
||||
CGFloat(0.5),
|
||||
])
|
||||
func groupWidthSmallPositiveWidths(screenWidth: CGFloat) {
|
||||
let result = ChannelOverlayView.computeGroupWidth(screenWidth: screenWidth)
|
||||
let lowerBound = screenWidth * 0.30
|
||||
let upperBound = screenWidth * 0.35
|
||||
|
||||
#expect(result >= lowerBound,
|
||||
"Small width \(screenWidth): result \(result) should be >= \(lowerBound)")
|
||||
#expect(result <= upperBound,
|
||||
"Small width \(screenWidth): result \(result) should be <= \(upperBound)")
|
||||
}
|
||||
|
||||
// MARK: - Edge case: very large widths
|
||||
|
||||
@Test("computeGroupWidth handles very large widths", arguments: [
|
||||
CGFloat(10000.0),
|
||||
CGFloat(50000.0),
|
||||
CGFloat(100000.0),
|
||||
])
|
||||
func groupWidthLargeWidths(screenWidth: CGFloat) {
|
||||
let result = ChannelOverlayView.computeGroupWidth(screenWidth: screenWidth)
|
||||
let lowerBound = screenWidth * 0.30
|
||||
let upperBound = screenWidth * 0.35
|
||||
|
||||
#expect(result >= lowerBound,
|
||||
"Large width \(screenWidth): result \(result) should be >= \(lowerBound)")
|
||||
#expect(result <= upperBound,
|
||||
"Large width \(screenWidth): result \(result) should be <= \(upperBound)")
|
||||
}
|
||||
|
||||
// MARK: - Result is always positive for positive input
|
||||
|
||||
@Test("computeGroupWidth always returns positive value for positive input")
|
||||
func groupWidthAlwaysPositive() {
|
||||
for _ in 0..<500 {
|
||||
let screenWidth = CGFloat.random(in: 0.001...10000.0)
|
||||
let result = ChannelOverlayView.computeGroupWidth(screenWidth: screenWidth)
|
||||
#expect(result > 0, "computeGroupWidth(\(screenWidth)) = \(result) should be > 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,271 @@
|
||||
#if os(iOS)
|
||||
import Testing
|
||||
import CoreGraphics
|
||||
@testable import TVBox
|
||||
|
||||
/// Property-based tests for PlayerGestureLayer pure computation functions.
|
||||
/// These tests validate correctness properties across a wide range of inputs.
|
||||
|
||||
// MARK: - Property 3: Vertical gesture direction mapping preserves monotonicity
|
||||
|
||||
/// **Validates: Requirements 2.2, 3.2**
|
||||
///
|
||||
/// For any vertical translation `dy`, the computed adjustment delta SHALL be positive when
|
||||
/// `dy < 0` (upward swipe) and negative when `dy > 0` (downward swipe), ensuring that
|
||||
/// upward gestures always increase the controlled value and downward gestures always decrease it.
|
||||
|
||||
@Suite("Property 3: Vertical gesture direction mapping preserves monotonicity")
|
||||
struct VerticalGestureMonotonicityPropertyTests {
|
||||
|
||||
// Use a container width and threshold that ensure vertical gesture classification
|
||||
static let containerWidth: CGFloat = 400.0
|
||||
static let threshold: CGFloat = 10.0
|
||||
|
||||
// MARK: - Parameterized tests with representative dy values
|
||||
|
||||
/// Negative dy values (upward swipes) that exceed the threshold
|
||||
static let negativeDyValues: [CGFloat] = [
|
||||
-11.0, -20.0, -50.0, -100.0, -300.0, -500.0, -1000.0, -1500.0
|
||||
]
|
||||
|
||||
/// Positive dy values (downward swipes) that exceed the threshold
|
||||
static let positiveDyValues: [CGFloat] = [
|
||||
11.0, 20.0, 50.0, 100.0, 300.0, 500.0, 1000.0
|
||||
]
|
||||
|
||||
/// Start positions covering both left half (brightness) and right half (volume)
|
||||
static let startPositions: [CGFloat] = [0.0, 50.0, 100.0, 199.0, 200.0, 250.0, 350.0, 399.0]
|
||||
|
||||
@Test("Upward swipe (negative dy) produces positive delta",
|
||||
arguments: negativeDyValues, startPositions)
|
||||
func upwardSwipeProducesPositiveDelta(dy: CGFloat, startX: CGFloat) {
|
||||
// Use dx = 0 to ensure vertical classification (|dy| > |dx|)
|
||||
let translation = CGSize(width: 0, height: dy)
|
||||
let result = PlayerGestureLayer.classifyGesture(
|
||||
startX: startX,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: translation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .adjustingBrightness(let delta):
|
||||
#expect(delta > 0,
|
||||
"Upward swipe (dy=\(dy)) on left side should produce positive delta, got \(delta)")
|
||||
case .adjustingVolume(let delta):
|
||||
#expect(delta > 0,
|
||||
"Upward swipe (dy=\(dy)) on right side should produce positive delta, got \(delta)")
|
||||
default:
|
||||
Issue.record("Expected brightness or volume adjustment for dy=\(dy), startX=\(startX), got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Downward swipe (positive dy) produces negative delta",
|
||||
arguments: positiveDyValues, startPositions)
|
||||
func downwardSwipeProducesNegativeDelta(dy: CGFloat, startX: CGFloat) {
|
||||
// Use dx = 0 to ensure vertical classification (|dy| > |dx|)
|
||||
let translation = CGSize(width: 0, height: dy)
|
||||
let result = PlayerGestureLayer.classifyGesture(
|
||||
startX: startX,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: translation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .adjustingBrightness(let delta):
|
||||
#expect(delta < 0,
|
||||
"Downward swipe (dy=\(dy)) on left side should produce negative delta, got \(delta)")
|
||||
case .adjustingVolume(let delta):
|
||||
#expect(delta < 0,
|
||||
"Downward swipe (dy=\(dy)) on right side should produce negative delta, got \(delta)")
|
||||
default:
|
||||
Issue.record("Expected brightness or volume adjustment for dy=\(dy), startX=\(startX), got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Randomized property test
|
||||
|
||||
@Test("Monotonicity holds for random vertical translations")
|
||||
func monotonicityRandomized() {
|
||||
for _ in 0..<1000 {
|
||||
let startX = CGFloat.random(in: 0.0..<Self.containerWidth)
|
||||
// Generate dy that exceeds threshold (either positive or negative)
|
||||
let dyMagnitude = CGFloat.random(in: (Self.threshold + 1)...1000.0)
|
||||
let isUpward = Bool.random()
|
||||
let dy = isUpward ? -dyMagnitude : dyMagnitude
|
||||
|
||||
// Use small dx to ensure vertical classification
|
||||
let dx = CGFloat.random(in: -Self.threshold...Self.threshold) * 0.5
|
||||
let translation = CGSize(width: dx, height: dy)
|
||||
|
||||
let result = PlayerGestureLayer.classifyGesture(
|
||||
startX: startX,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: translation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
|
||||
switch result {
|
||||
case .adjustingBrightness(let delta), .adjustingVolume(let delta):
|
||||
if dy < 0 {
|
||||
#expect(delta > 0,
|
||||
"Upward swipe (dy=\(dy)) should produce positive delta, got \(delta)")
|
||||
} else {
|
||||
#expect(delta < 0,
|
||||
"Downward swipe (dy=\(dy)) should produce negative delta, got \(delta)")
|
||||
}
|
||||
case .seeking:
|
||||
// If |dx| > |dy|, it classifies as seeking - this is acceptable
|
||||
// since we're testing the vertical case
|
||||
break
|
||||
case .none:
|
||||
// Below threshold - acceptable for edge cases
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delta magnitude proportionality
|
||||
|
||||
@Test("Larger vertical displacement produces larger absolute delta")
|
||||
func deltaMagnitudeProportional() {
|
||||
let startX: CGFloat = 100.0 // Left half for brightness
|
||||
|
||||
let smallDy: CGFloat = -20.0
|
||||
let largeDy: CGFloat = -200.0
|
||||
|
||||
let smallTranslation = CGSize(width: 0, height: smallDy)
|
||||
let largeTranslation = CGSize(width: 0, height: largeDy)
|
||||
|
||||
let smallResult = PlayerGestureLayer.classifyGesture(
|
||||
startX: startX,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: smallTranslation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
let largeResult = PlayerGestureLayer.classifyGesture(
|
||||
startX: startX,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: largeTranslation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
|
||||
if case .adjustingBrightness(let smallDelta) = smallResult,
|
||||
case .adjustingBrightness(let largeDelta) = largeResult {
|
||||
#expect(largeDelta > smallDelta,
|
||||
"Larger upward swipe should produce larger positive delta: small=\(smallDelta), large=\(largeDelta)")
|
||||
} else {
|
||||
Issue.record("Expected brightness adjustments for both translations")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Zero dy produces no vertical gesture
|
||||
|
||||
@Test("Zero dy does not produce vertical adjustment")
|
||||
func zeroDyNoVerticalAdjustment() {
|
||||
let translation = CGSize(width: 0, height: 0)
|
||||
let result = PlayerGestureLayer.classifyGesture(
|
||||
startX: 100.0,
|
||||
containerWidth: Self.containerWidth,
|
||||
translation: translation,
|
||||
threshold: Self.threshold
|
||||
)
|
||||
#expect(result == .none,
|
||||
"Zero translation should produce .none, got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property 4: Adjustment value clamping invariant
|
||||
|
||||
/// **Validates: Requirements 2.4, 3.4**
|
||||
///
|
||||
/// For any currentValue in [0, 1] and any delta (positive, negative, very large, very small),
|
||||
/// the result of `clampAdjustment(currentValue:delta:)` must always be in [0.0, 1.0].
|
||||
|
||||
@Suite("Property 4: Adjustment value clamping invariant")
|
||||
struct AdjustmentClampingPropertyTests {
|
||||
|
||||
// MARK: - Parameterized boundary values
|
||||
|
||||
static let boundaryCurrentValues: [CGFloat] = [0.0, 0.001, 0.25, 0.5, 0.75, 0.999, 1.0]
|
||||
static let boundaryDeltas: [CGFloat] = [
|
||||
-1000.0, -100.0, -10.0, -1.0, -0.5, -0.001,
|
||||
0.0,
|
||||
0.001, 0.5, 1.0, 10.0, 100.0, 1000.0
|
||||
]
|
||||
|
||||
@Test("Clamped result is always within [0, 1] for boundary values",
|
||||
arguments: boundaryCurrentValues, boundaryDeltas)
|
||||
func clampedResultInRange(currentValue: CGFloat, delta: CGFloat) {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: currentValue, delta: delta)
|
||||
#expect(result >= 0.0, "Result \(result) should be >= 0.0 for currentValue=\(currentValue), delta=\(delta)")
|
||||
#expect(result <= 1.0, "Result \(result) should be <= 1.0 for currentValue=\(currentValue), delta=\(delta)")
|
||||
}
|
||||
|
||||
// MARK: - Randomized property test
|
||||
|
||||
@Test("Clamped result is always within [0, 1] for random inputs")
|
||||
func clampedResultInRangeRandomized() {
|
||||
// Generate many random test cases to approximate property-based testing
|
||||
for _ in 0..<1000 {
|
||||
let currentValue = CGFloat.random(in: 0.0...1.0)
|
||||
let delta = CGFloat.random(in: -1000.0...1000.0)
|
||||
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: currentValue, delta: delta)
|
||||
#expect(result >= 0.0, "Result \(result) should be >= 0.0 for currentValue=\(currentValue), delta=\(delta)")
|
||||
#expect(result <= 1.0, "Result \(result) should be <= 1.0 for currentValue=\(currentValue), delta=\(delta)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge cases with extreme deltas
|
||||
|
||||
@Test("Clamped result handles extreme positive deltas")
|
||||
func extremePositiveDeltas() {
|
||||
let extremeDeltas: [CGFloat] = [CGFloat.greatestFiniteMagnitude / 2, 1e10, 1e6]
|
||||
for delta in extremeDeltas {
|
||||
for currentValue in [0.0, 0.5, 1.0] as [CGFloat] {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: currentValue, delta: delta)
|
||||
#expect(result >= 0.0 && result <= 1.0,
|
||||
"Result \(result) out of range for currentValue=\(currentValue), delta=\(delta)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Clamped result handles extreme negative deltas")
|
||||
func extremeNegativeDeltas() {
|
||||
let extremeDeltas: [CGFloat] = [-CGFloat.greatestFiniteMagnitude / 2, -1e10, -1e6]
|
||||
for delta in extremeDeltas {
|
||||
for currentValue in [0.0, 0.5, 1.0] as [CGFloat] {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: currentValue, delta: delta)
|
||||
#expect(result >= 0.0 && result <= 1.0,
|
||||
"Result \(result) out of range for currentValue=\(currentValue), delta=\(delta)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Zero delta preserves value
|
||||
|
||||
@Test("Zero delta preserves the current value",
|
||||
arguments: boundaryCurrentValues)
|
||||
func zeroDeltaPreservesValue(currentValue: CGFloat) {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: currentValue, delta: 0.0)
|
||||
#expect(result == currentValue,
|
||||
"Zero delta should preserve value, got \(result) instead of \(currentValue)")
|
||||
}
|
||||
|
||||
// MARK: - Idempotence at boundaries
|
||||
|
||||
@Test("Result at lower bound stays at lower bound with negative delta")
|
||||
func lowerBoundIdempotence() {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: 0.0, delta: -0.5)
|
||||
#expect(result == 0.0, "Should clamp to 0.0 at lower bound, got \(result)")
|
||||
}
|
||||
|
||||
@Test("Result at upper bound stays at upper bound with positive delta")
|
||||
func upperBoundIdempotence() {
|
||||
let result = PlayerGestureLayer.clampAdjustment(currentValue: 1.0, delta: 0.5)
|
||||
#expect(result == 1.0, "Should clamp to 1.0 at upper bound, got \(result)")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,130 @@
|
||||
#if os(iOS)
|
||||
import Testing
|
||||
import CoreGraphics
|
||||
@testable import TVBox
|
||||
|
||||
/// Property 6: Pinch zoom clamping invariant
|
||||
/// For any scale s > 0, clampZoom returns a value in [1.0, 3.0]
|
||||
///
|
||||
/// **Validates: Requirements 5.2, 5.3**
|
||||
|
||||
@Suite("Property 6: Pinch zoom clamping invariant")
|
||||
struct PlayerGestureLayerZoomClampingTests {
|
||||
|
||||
// MARK: - Property Test: Default bounds [1.0, 3.0]
|
||||
|
||||
/// Property: For any positive scale, clampZoom with default bounds always returns a value in [1.0, 3.0]
|
||||
@Test("clampZoom clamps to [1.0, 3.0] for various positive scales", arguments: [
|
||||
// Boundary values
|
||||
CGFloat(0.001), // very small positive
|
||||
CGFloat(0.5), // below min
|
||||
CGFloat(0.99), // just below min
|
||||
CGFloat(1.0), // exactly min
|
||||
CGFloat(1.001), // just above min
|
||||
CGFloat(1.5), // mid-range
|
||||
CGFloat(2.0), // mid-range
|
||||
CGFloat(2.5), // mid-range
|
||||
CGFloat(2.99), // just below max
|
||||
CGFloat(3.0), // exactly max
|
||||
CGFloat(3.001), // just above max
|
||||
CGFloat(3.5), // above max
|
||||
CGFloat(5.0), // well above max
|
||||
CGFloat(10.0), // far above max
|
||||
CGFloat(100.0), // extreme above max
|
||||
CGFloat(1000.0), // very extreme
|
||||
CGFloat(0.0001), // near-zero positive
|
||||
])
|
||||
func clampZoomDefaultBounds(scale: CGFloat) {
|
||||
let result = PlayerGestureLayer.clampZoom(scale: scale)
|
||||
#expect(result >= 1.0, "clampZoom(\(scale)) = \(result) should be >= 1.0")
|
||||
#expect(result <= 3.0, "clampZoom(\(scale)) = \(result) should be <= 3.0")
|
||||
}
|
||||
|
||||
// MARK: - Property Test: Custom bounds
|
||||
|
||||
/// Property: For any positive scale and valid custom bounds, clampZoom returns a value in [minZoom, maxZoom]
|
||||
@Test("clampZoom respects custom min/max bounds", arguments: [
|
||||
(CGFloat(0.5), CGFloat(0.5), CGFloat(2.0)), // scale below custom min
|
||||
(CGFloat(1.0), CGFloat(0.5), CGFloat(2.0)), // scale within custom range
|
||||
(CGFloat(2.5), CGFloat(0.5), CGFloat(2.0)), // scale above custom max
|
||||
(CGFloat(0.1), CGFloat(0.2), CGFloat(5.0)), // scale below wide range min
|
||||
(CGFloat(3.0), CGFloat(0.2), CGFloat(5.0)), // scale within wide range
|
||||
(CGFloat(6.0), CGFloat(0.2), CGFloat(5.0)), // scale above wide range max
|
||||
(CGFloat(2.0), CGFloat(2.0), CGFloat(2.0)), // min equals max
|
||||
(CGFloat(1.0), CGFloat(2.0), CGFloat(2.0)), // below when min equals max
|
||||
(CGFloat(3.0), CGFloat(2.0), CGFloat(2.0)), // above when min equals max
|
||||
(CGFloat(0.01), CGFloat(0.5), CGFloat(10.0)), // very small scale, wide range
|
||||
(CGFloat(100.0), CGFloat(0.5), CGFloat(10.0)), // very large scale, wide range
|
||||
])
|
||||
func clampZoomCustomBounds(scale: CGFloat, minZoom: CGFloat, maxZoom: CGFloat) {
|
||||
let result = PlayerGestureLayer.clampZoom(scale: scale, minZoom: minZoom, maxZoom: maxZoom)
|
||||
#expect(result >= minZoom, "clampZoom(\(scale), min: \(minZoom), max: \(maxZoom)) = \(result) should be >= \(minZoom)")
|
||||
#expect(result <= maxZoom, "clampZoom(\(scale), min: \(minZoom), max: \(maxZoom)) = \(result) should be <= \(maxZoom)")
|
||||
}
|
||||
|
||||
// MARK: - Property Test: Idempotency
|
||||
|
||||
/// Property: Clamping an already-clamped value should return the same value
|
||||
@Test("clampZoom is idempotent - clamping twice gives same result", arguments: [
|
||||
CGFloat(0.5),
|
||||
CGFloat(1.0),
|
||||
CGFloat(2.0),
|
||||
CGFloat(3.0),
|
||||
CGFloat(5.0),
|
||||
])
|
||||
func clampZoomIdempotent(scale: CGFloat) {
|
||||
let firstClamp = PlayerGestureLayer.clampZoom(scale: scale)
|
||||
let secondClamp = PlayerGestureLayer.clampZoom(scale: firstClamp)
|
||||
#expect(firstClamp == secondClamp, "clampZoom should be idempotent: clampZoom(clampZoom(\(scale))) = \(secondClamp) != clampZoom(\(scale)) = \(firstClamp)")
|
||||
}
|
||||
|
||||
// MARK: - Property Test: Values within range are unchanged
|
||||
|
||||
/// Property: If scale is already within [minZoom, maxZoom], it should be returned unchanged
|
||||
@Test("clampZoom preserves values already within bounds", arguments: [
|
||||
CGFloat(1.0),
|
||||
CGFloat(1.5),
|
||||
CGFloat(2.0),
|
||||
CGFloat(2.5),
|
||||
CGFloat(3.0),
|
||||
])
|
||||
func clampZoomPreservesValidValues(scale: CGFloat) {
|
||||
let result = PlayerGestureLayer.clampZoom(scale: scale)
|
||||
#expect(result == scale, "clampZoom(\(scale)) should return \(scale) unchanged, got \(result)")
|
||||
}
|
||||
|
||||
// MARK: - Randomized Property Test
|
||||
|
||||
/// Property: Randomized test with many generated values to approximate property-based testing
|
||||
@Test("clampZoom satisfies clamping invariant for generated positive scales")
|
||||
func clampZoomRandomizedProperty() {
|
||||
// Generate a wide range of positive scale values to test the property
|
||||
var rng = SystemRandomNumberGenerator()
|
||||
for _ in 0..<200 {
|
||||
// Generate random positive CGFloat values across different magnitudes
|
||||
let magnitude = CGFloat.random(in: 0..<4, using: &rng) // 0 to 4 for exponent
|
||||
let scale = CGFloat(pow(10.0, Double(magnitude))) * CGFloat.random(in: 0.001..<1.0, using: &rng)
|
||||
|
||||
let result = PlayerGestureLayer.clampZoom(scale: scale)
|
||||
#expect(result >= 1.0, "Random scale \(scale): result \(result) should be >= 1.0")
|
||||
#expect(result <= 3.0, "Random scale \(scale): result \(result) should be <= 3.0")
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: Randomized test with custom bounds
|
||||
@Test("clampZoom satisfies clamping invariant for generated scales with custom bounds")
|
||||
func clampZoomRandomizedCustomBounds() {
|
||||
var rng = SystemRandomNumberGenerator()
|
||||
for _ in 0..<200 {
|
||||
// Generate random bounds where minZoom <= maxZoom
|
||||
let minZoom = CGFloat.random(in: 0.1..<5.0, using: &rng)
|
||||
let maxZoom = minZoom + CGFloat.random(in: 0.0..<10.0, using: &rng)
|
||||
let scale = CGFloat.random(in: 0.001..<20.0, using: &rng)
|
||||
|
||||
let result = PlayerGestureLayer.clampZoom(scale: scale, minZoom: minZoom, maxZoom: maxZoom)
|
||||
#expect(result >= minZoom, "scale=\(scale), min=\(minZoom), max=\(maxZoom): result \(result) should be >= \(minZoom)")
|
||||
#expect(result <= maxZoom, "scale=\(scale), min=\(minZoom), max=\(maxZoom): result \(result) should be <= \(maxZoom)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user