180 lines
5.3 KiB
Swift
180 lines
5.3 KiB
Swift
// AnalyticsUploadSchedulingTests.swift
|
|
// OSGKeyboardTests
|
|
//
|
|
// Foreground upload policy regression tests for batching and suspension safety.
|
|
|
|
@testable import OSGKeyboard
|
|
import XCTest
|
|
|
|
final class AnalyticsUploadSchedulingTests: XCTestCase {
|
|
func testCountThresholdCoalescesSignalsIntoOneUpload() async throws {
|
|
let probe = AnalyticsUploadProbe()
|
|
let signal = AnalyticsUploadSignal(
|
|
policy: AnalyticsUploadPolicy(
|
|
eventThreshold: 3,
|
|
flushInterval: .seconds(10),
|
|
activationDelay: .zero,
|
|
maximumBatches: 1
|
|
)
|
|
)
|
|
await signal.install {
|
|
await probe.recordUpload()
|
|
}
|
|
await signal.setActive(true)
|
|
|
|
await signal.requestUpload()
|
|
await signal.requestUpload()
|
|
try await Task.sleep(for: .milliseconds(30))
|
|
let countBeforeThreshold = await probe.uploadCount()
|
|
XCTAssertEqual(countBeforeThreshold, 0)
|
|
|
|
await signal.requestUpload()
|
|
try await waitUntil { await probe.uploadCount() == 1 }
|
|
let countAfterThreshold = await probe.uploadCount()
|
|
XCTAssertEqual(countAfterThreshold, 1)
|
|
}
|
|
|
|
func testIntervalFlushesAQueueBelowThreshold() async throws {
|
|
let probe = AnalyticsUploadProbe()
|
|
let signal = AnalyticsUploadSignal(
|
|
policy: AnalyticsUploadPolicy(
|
|
eventThreshold: 20,
|
|
flushInterval: .milliseconds(20),
|
|
activationDelay: .zero,
|
|
maximumBatches: 1
|
|
)
|
|
)
|
|
await signal.install {
|
|
await probe.recordUpload()
|
|
}
|
|
await signal.setActive(true)
|
|
|
|
await signal.requestUpload()
|
|
|
|
try await waitUntil { await probe.uploadCount() == 1 }
|
|
let uploadCount = await probe.uploadCount()
|
|
XCTAssertEqual(uploadCount, 1)
|
|
}
|
|
|
|
func testInactiveSignalDefersUntilNextActivation() async throws {
|
|
let probe = AnalyticsUploadProbe()
|
|
let signal = AnalyticsUploadSignal(
|
|
policy: AnalyticsUploadPolicy(
|
|
eventThreshold: 2,
|
|
flushInterval: .milliseconds(20),
|
|
activationDelay: .milliseconds(20),
|
|
maximumBatches: 1
|
|
)
|
|
)
|
|
await signal.install {
|
|
await probe.recordUpload()
|
|
}
|
|
|
|
await signal.requestUpload()
|
|
await signal.requestUpload()
|
|
try await Task.sleep(for: .milliseconds(40))
|
|
let inactiveUploadCount = await probe.uploadCount()
|
|
XCTAssertEqual(inactiveUploadCount, 0)
|
|
|
|
await signal.setActive(true)
|
|
try await waitUntil { await probe.uploadCount() == 1 }
|
|
let activeUploadCount = await probe.uploadCount()
|
|
XCTAssertEqual(activeUploadCount, 1)
|
|
}
|
|
|
|
func testLateActionInstallationDoesNotLoseActivationUpload() async throws {
|
|
let probe = AnalyticsUploadProbe()
|
|
let signal = AnalyticsUploadSignal(
|
|
policy: AnalyticsUploadPolicy(
|
|
eventThreshold: 20,
|
|
flushInterval: .seconds(10),
|
|
activationDelay: .milliseconds(20),
|
|
maximumBatches: 1
|
|
)
|
|
)
|
|
await signal.setActive(true)
|
|
await signal.requestActivationUpload()
|
|
|
|
await signal.install {
|
|
await probe.recordUpload()
|
|
}
|
|
|
|
try await waitUntil { await probe.uploadCount() == 1 }
|
|
let uploadCount = await probe.uploadCount()
|
|
XCTAssertEqual(uploadCount, 1)
|
|
}
|
|
|
|
func testPauseCancelsAndWaitsForInFlightUpload() async throws {
|
|
let probe = AnalyticsUploadProbe()
|
|
let signal = AnalyticsUploadSignal(
|
|
policy: AnalyticsUploadPolicy(
|
|
eventThreshold: 1,
|
|
flushInterval: .seconds(10),
|
|
activationDelay: .zero,
|
|
maximumBatches: 1
|
|
)
|
|
)
|
|
await signal.install {
|
|
await probe.recordStarted()
|
|
do {
|
|
try await Task.sleep(for: .seconds(30))
|
|
} catch {
|
|
await probe.recordCancellation()
|
|
}
|
|
}
|
|
await signal.setActive(true)
|
|
await signal.requestUpload()
|
|
try await waitUntil { await probe.didStart() }
|
|
|
|
await signal.pauseAndWait()
|
|
|
|
let wasCancelled = await probe.wasCancelled()
|
|
XCTAssertTrue(wasCancelled)
|
|
}
|
|
|
|
private func waitUntil(
|
|
timeout: Duration = .seconds(1),
|
|
condition: @escaping @Sendable () async -> Bool
|
|
) async throws {
|
|
let clock = ContinuousClock()
|
|
let deadline = clock.now.advanced(by: timeout)
|
|
while clock.now < deadline {
|
|
if await condition() {
|
|
return
|
|
}
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
XCTFail("Timed out waiting for analytics upload state")
|
|
}
|
|
}
|
|
|
|
private actor AnalyticsUploadProbe {
|
|
private var uploads = 0
|
|
private var started = false
|
|
private var cancelled = false
|
|
|
|
func recordUpload() {
|
|
uploads += 1
|
|
}
|
|
|
|
func uploadCount() -> Int {
|
|
uploads
|
|
}
|
|
|
|
func recordStarted() {
|
|
started = true
|
|
}
|
|
|
|
func didStart() -> Bool {
|
|
started
|
|
}
|
|
|
|
func recordCancellation() {
|
|
cancelled = true
|
|
}
|
|
|
|
func wasCancelled() -> Bool {
|
|
cancelled
|
|
}
|
|
}
|