- record analytics/usage only in the host app; the extension no longer opens the SQLite queues, so hiding the keyboard cannot hold WAL locks (0xdead10cc) - sync clipboard history writes across processes so deletes made on one side are not reverted by the other - guard memory-relief reentrancy on controller and telemetry sides to break the recursive crash loop - resolve credentials off the main thread and migrate legacy keys once per install, so Keychain reads cannot be caught in the suspend window - gate Volcengine ASR ping completion to once (SIGTRAP on double resume) - treat same-text captures as the same logical copy and dedupe auto actions by content fingerprint within 30s, stopping Universal Clipboard storms - realign version line to 2.1.0 (build 110)
169 lines
6.9 KiB
Swift
169 lines
6.9 KiB
Swift
// KeyboardExtensionMemoryBudgetTests.swift
|
|
// OSGKeyboardTests
|
|
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class KeyboardExtensionMemoryBudgetTests: XCTestCase {
|
|
func testMemoryLevelsUseDocumentedBoundaries() {
|
|
XCTAssertEqual(
|
|
KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: 35.9),
|
|
.normal
|
|
)
|
|
XCTAssertEqual(
|
|
KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: 36),
|
|
.warning
|
|
)
|
|
XCTAssertEqual(
|
|
KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: 40),
|
|
.high
|
|
)
|
|
XCTAssertEqual(
|
|
KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: 48),
|
|
.critical
|
|
)
|
|
}
|
|
|
|
func testUnavailableFootprintIsNotMisclassifiedAsSafe() {
|
|
XCTAssertEqual(
|
|
KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: -1),
|
|
.unavailable
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Covers the *acting* half of the memory guard. The budget thresholds above
|
|
/// were always correct; what was missing was anything that used them. A
|
|
/// keyboard extension gets no reliable system memory warning before jetsam,
|
|
/// so these thresholds are the only chance to shed load before the keyboard
|
|
/// is killed mid-sentence.
|
|
@MainActor
|
|
final class KeyboardExtensionMemoryReliefTests: XCTestCase {
|
|
override func tearDown() {
|
|
KeyboardExtensionMemoryTelemetry.reliefHandler = nil
|
|
KeyboardExtensionMemoryTelemetry.stopSampling()
|
|
super.tearDown()
|
|
}
|
|
|
|
private func armedLevels(
|
|
accepting: Bool = true
|
|
) -> (record: () -> [KeyboardExtensionMemoryBudget.Level], reset: () -> Void) {
|
|
let box = LevelBox()
|
|
KeyboardExtensionMemoryTelemetry.begin(context: "test")
|
|
KeyboardExtensionMemoryTelemetry.stopSampling()
|
|
KeyboardExtensionMemoryTelemetry.reliefHandler = { level in
|
|
box.levels.append(level)
|
|
return accepting
|
|
}
|
|
return ({ box.levels }, { box.levels.removeAll() })
|
|
}
|
|
|
|
@MainActor
|
|
private final class LevelBox {
|
|
var levels: [KeyboardExtensionMemoryBudget.Level] = []
|
|
}
|
|
|
|
func testNormalAndWarningFootprintsDoNotShed() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(20)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(37)
|
|
XCTAssertEqual(levels(), [])
|
|
}
|
|
|
|
func testHighFootprintRequestsSoftRelief() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(41)
|
|
XCTAssertEqual(levels(), [.high])
|
|
}
|
|
|
|
func testSustainedPressureDoesNotReshedAtTheSameLevel() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
// Ten seconds of polling above the threshold. A timed cooldown would
|
|
// have shed three more times by now; hysteresis sheds once.
|
|
for _ in 0..<10 {
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
}
|
|
XCTAssertEqual(levels(), [.critical])
|
|
}
|
|
|
|
func testPressureMustRecedeBelowWarningBeforeSheddingAgain() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
// Landing back in the warning band is where a shed leaves the keyboard;
|
|
// treating that as recovery would re-arm on the very next sample.
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(37)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
XCTAssertEqual(levels(), [.critical])
|
|
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(22)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
XCTAssertEqual(levels(), [.critical, .critical])
|
|
}
|
|
|
|
func testDeclinedReliefStaysArmedForTheNextPoll() {
|
|
let (levels, _) = armedLevels(accepting: false)
|
|
// The host declines while the user is mid-composition. Recording it as
|
|
// a completed shed would leave the keyboard at 48 MiB, having released
|
|
// nothing, until pressure receded on its own.
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
XCTAssertEqual(levels(), [.critical, .critical, .critical])
|
|
}
|
|
|
|
func testReliefIsNeverRequestedFromInsideRelief() {
|
|
let box = LevelBox()
|
|
KeyboardExtensionMemoryTelemetry.begin(context: "test")
|
|
KeyboardExtensionMemoryTelemetry.stopSampling()
|
|
KeyboardExtensionMemoryTelemetry.reliefHandler = { level in
|
|
box.levels.append(level)
|
|
// Shedding records milestones, and recording samples memory again.
|
|
// Without the reentrancy gate this re-entered the handler until the
|
|
// main thread's stack ran out — the crash this test pins down.
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
return true
|
|
}
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
XCTAssertEqual(box.levels, [.critical])
|
|
}
|
|
|
|
func testRepeatedHighFootprintShedsOnce() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(41)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(42)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(43)
|
|
// One shed per level per episode: re-tearing down the typing surface on
|
|
// every 1 s poll would make the keyboard unusable, not safer.
|
|
XCTAssertEqual(levels(), [.high])
|
|
}
|
|
|
|
func testEscalationToCriticalShedsEvenAfterASoftShed() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(41)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
// The hard shed must not wait out a soft one — 49 MiB is ~11 MiB from
|
|
// the observed jetsam boundary.
|
|
XCTAssertEqual(levels(), [.high, .critical])
|
|
}
|
|
|
|
func testCriticalDoesNotDownshiftBackToHigh() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(41)
|
|
XCTAssertEqual(levels(), [.critical])
|
|
}
|
|
|
|
func testNewSessionCanShedAgain() {
|
|
let (levels, _) = armedLevels()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(49)
|
|
XCTAssertEqual(levels(), [.critical])
|
|
// `begin` is called once per extension process; a reused process must
|
|
// not inherit the previous presentation's armed state.
|
|
KeyboardExtensionMemoryTelemetry.begin(context: "test-2")
|
|
KeyboardExtensionMemoryTelemetry.stopSampling()
|
|
KeyboardExtensionMemoryTelemetry.simulateFootprintForTesting(41)
|
|
XCTAssertEqual(levels(), [.critical, .high])
|
|
}
|
|
}
|