@@ -18,23 +18,6 @@ extension RunnerTests {
1818 )
1919 }
2020
21- func testAlertReplacementCommandDeadlineScalesWithMeasuredLatency( ) {
22- // A quiet host still asks for the production default: 24 reads at 10 ms is a quarter of a second.
23- XCTAssertEqual ( RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: 0.001 ) , 10_000 )
24- XCTAssertEqual ( RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: 0.01 ) , 10_000 )
25- // The linear region is where a contended host lands: one second per read buys the 24 reads
26- // twice over, which is what the red nightly nights needed and 10 s did not cover.
27- XCTAssertEqual ( RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: 1 ) , 48_000 )
28- // Past the cap the fixture gives up rather than dominate the lane, whatever the host costs.
29- XCTAssertEqual ( RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: 2.5 ) , 60_000 )
30- XCTAssertEqual ( RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: 300 ) , 60_000 )
31- XCTAssertEqual (
32- RunnerTests . alertResolutionReadsBeforeActivation,
33- 24 ,
34- " the decomposition below is the traced command path; re-trace it before changing a count "
35- )
36- }
37-
3821 func testAlertDeadlineBeforeActivationLeavesTheOriginalUntouched( ) throws {
3922 app. launchArguments = [ " --agent-device-alert-replacement-regression " ]
4023 app. launch ( )
@@ -43,7 +26,7 @@ extension RunnerTests {
4326 app. terminate ( )
4427 }
4528 XCTAssertTrue ( app. alerts. firstMatch. waitForExistence ( timeout: appExistenceTimeout) )
46- let alert = try XCTUnwrap ( resolveAlert ( app : app , deadline : Date ( ) . addingTimeInterval ( 10 ) ) )
29+ let alert = try resolveAlertBeforeTheCommand ( )
4730 let response = handleAlert ( alert, action: " accept " , deadline: . distantPast)
4831 XCTAssertFalse ( response. ok)
4932 XCTAssertEqual ( response. error? . code, " ALERT_DEADLINE_EXCEEDED " )
@@ -60,7 +43,7 @@ extension RunnerTests {
6043 app. terminate ( )
6144 }
6245 XCTAssertTrue ( app. alerts. firstMatch. waitForExistence ( timeout: appExistenceTimeout) )
63- let alert = try XCTUnwrap ( resolveAlert ( app : app , deadline : Date ( ) . addingTimeInterval ( 10 ) ) )
46+ let alert = try resolveAlertBeforeTheCommand ( )
6447 alertButtonHittabilityProbeOverrideForTesting = { probeDeadline in
6548 while Date ( ) < probeDeadline {
6649 Thread . sleep ( forTimeInterval: min ( 0.02 , max ( 0 , probeDeadline. timeIntervalSinceNow) ) )
@@ -85,28 +68,16 @@ extension RunnerTests {
8568 app. terminate ( )
8669 }
8770 XCTAssertTrue ( app. alerts. firstMatch. waitForExistence ( timeout: appExistenceTimeout) )
88- let alert = try XCTUnwrap ( resolveAlert ( app : app , deadline : Date ( ) . addingTimeInterval ( 10 ) ) )
71+ let alert = try resolveAlertBeforeTheCommand ( )
8972
90- let deadline = Date ( ) . addingTimeInterval ( 6 )
91- let startedAt = Date ( )
92- let response = handleAlert ( alert, action: " accept " , deadline: deadline)
93- let elapsed = Date ( ) . timeIntervalSince ( startedAt)
73+ let response = handleAlert ( alert, action: " accept " , deadline: Date ( ) . addingTimeInterval ( RunnerTests . alertActivationDeadline) )
9474
95- // The fixture keeps an animation in flight, which is what XCTest waits out before it synthesises
96- // an event, so this comes back early only because activation refused to wait for an app that has
97- // no intention of settling (#2546).
98- XCTAssertLessThan ( elapsed, 9 , " activation waited \( elapsed) s for a busy app to idle " )
75+ // The fixture keeps an animation in flight until a button is answered or its 20 s backstop stops
76+ // it, and an in-flight animation is what XCTest waits out before it synthesises an event. An
77+ // answer that arrives while the app is still busy is one activation did not wait to idle (#2546).
9978 XCTAssertTrue ( response. ok, String ( describing: response. error) )
100- let recordedActions = app. staticTexts [ " agent-device-alert-actions " ] . label
101- if response. ok {
102- XCTAssertEqual ( recordedActions, " First actions: 1; replacement actions: 0 " )
103- } else {
104- XCTAssertEqual (
105- recordedActions,
106- " First actions: 0; replacement actions: 0 " ,
107- " a caller told about an expired deadline must not have a button activated behind it "
108- )
109- }
79+ XCTAssertEqual ( app. staticTexts [ " agent-device-alert-actions " ] . label, " First actions: 1; replacement actions: 0 " )
80+ XCTAssertEqual ( app. staticTexts [ " agent-device-alert-busy-answer " ] . label, " Answered while busy " )
11081 }
11182
11283 func testAlertActivationDoesNotWaitOutANotificationBanner( ) throws {
@@ -127,9 +98,9 @@ extension RunnerTests {
12798 }
12899 acceptNotificationAuthorizationUntilAlertAppears ( )
129100 XCTAssertTrue ( banner. waitForExistence ( timeout: appExistenceTimeout) , " the fixture keeps a banner up " )
130- let alert = try XCTUnwrap ( resolveAlert ( app : app , deadline : Date ( ) . addingTimeInterval ( 30 ) ) )
101+ let alert = try resolveAlertBeforeTheCommand ( )
131102
132- let response = handleAlert ( alert, action: " accept " , deadline: Date ( ) . addingTimeInterval ( 30 ) )
103+ let response = handleAlert ( alert, action: " accept " , deadline: Date ( ) . addingTimeInterval ( RunnerTests . alertActivationDeadline ) )
133104
134105 XCTAssertEqual ( consultedInterruptions, [ ] , " alert activation waited on XCTest's interruption handling " )
135106 XCTAssertTrue ( response. ok, String ( describing: response. error) )
@@ -152,81 +123,45 @@ extension RunnerTests {
152123 XCTAssertTrue ( fixtureAlert. exists, " the banner fixture needs notification authorization before it presents its alert " )
153124 }
154125
155- /// The accessibility round trips `resolveAlert` spends before a button is chosen, decomposed from
156- /// the implementation rather than counted off one trace: the blocking-modal probe scans
157- /// SpringBoard's alert and sheet lists and re-reads the candidate it settles on; the app's own
158- /// alert list is resolved and then read for existence and frame; `actionableElements` issues one
159- /// fetch per member of `actionableTypes`; and every candidate then pays `exists`, `isHittable`,
160- /// `elementType`, `frame`, `label` and `isEnabled` of its own.
161- static let alertResolutionModalProbeReads = 3
162- static let alertResolutionAlertRootReads = 3
163- static let alertResolutionActionableTypeReads = 6
164- static let alertResolutionButtonCandidates = 2
165- static let alertResolutionCandidateReads = 6
166-
167- static let alertResolutionReadsBeforeActivation =
168- alertResolutionModalProbeReads + alertResolutionAlertRootReads + alertResolutionActionableTypeReads
169- + alertResolutionButtonCandidates * alertResolutionCandidateReads
170-
171- /// The reads that follow resolution — the hittability wait and the first verification observation
172- /// run on the same channel — plus the allowance for a host that gets slower mid-command.
173- private static let alertCommandSafetyFactor : TimeInterval = 2
174-
175- /// No higher than the slowest test this lane already runs (61 s on a green nightly), so a badly
176- /// starved host cannot make these fixtures the lane's worst contributor. The cap covers the worst
177- /// night traced so far, which needed 40.4 s of deadline.
178- private static let alertCommandTimeoutCap : TimeInterval = 60
179-
180- /// The alert command's deadline for the replacement fixtures.
181- ///
182- /// `timeoutMs` bounds the whole command, so it is only meaningful in units of what one
183- /// accessibility read costs this host right now: `RunnerTests+Alert.swift` starts the deadline at
184- /// dispatch and the resolution above spends all of it reading before anything is activated. The
185- /// hosted lane runs a read at about 10 ms on a healthy night and about 1.7 s on a red one, which
186- /// is why the fixed 10 s these fixtures asked for passed on green nights and expired mid-resolution
187- /// on red ones (runs 35, 36 and 37 needed 31.2 s, 30.6 s and 40.4 s). Deriving the budget from a
188- /// measured read is `docs/agents/testing.md`'s preferred answer for a timeout that only fails on a
189- /// contended host; production still defaults to `defaultAlertCommandTimeout` for real callers.
190- static func alertReplacementCommandTimeoutMs( measuredRoundTrip: TimeInterval ) -> Int {
191- let derived = measuredRoundTrip * Double( alertResolutionReadsBeforeActivation) * alertCommandSafetyFactor
192- return Int ( ( min ( max ( derived, defaultAlertCommandTimeout) , alertCommandTimeoutCap) * 1000 ) . rounded ( ) )
193- }
126+ /// Resolution reads the alert, its owner and every candidate button before anything is activated,
127+ /// and a contended hosted simulator has spent 40 s of reads there. These fixtures prove what
128+ /// activation and verification do, so they resolve first under this allowance, which only a failed
129+ /// resolution ever spends.
130+ static let alertResolutionAllowance : TimeInterval = 60
194131
195- /// Samples the read the command is about to pay 24 times, once the alert is up, so the sample comes
196- /// from the window the command runs in. Two samples and the slower one wins: a single lucky read
197- /// must not size the budget low, and this is a worst-of measurement, not a mean.
198- private func measuredAccessibilityRoundTrip( ) -> TimeInterval {
199- let recordedActions = app. staticTexts [ " agent-device-alert-actions " ]
200- var roundTrip = TimeInterval ( 0.001 )
201- for _ in 0 ..< 2 {
202- let startedAt = Date ( )
203- _ = recordedActions. label
204- roundTrip = max ( roundTrip, Date ( ) . timeIntervalSince ( startedAt) )
205- }
206- return roundTrip
132+ /// The deadline activation and verification run under once the alert is resolved: a confirmed
133+ /// answer returns as soon as verification sees the replacement, so only a fixture whose answer is
134+ /// the deadline itself pays it in full. It buys the dozen reads after resolution 2.5 s each, above the
135+ /// 1.7 s a read cost on the worst hosted nights traced (#2708).
136+ static let alertActivationDeadline : TimeInterval = 30
137+
138+ private func resolveAlertBeforeTheCommand( ) throws -> RunnerAlert {
139+ try XCTUnwrap ( resolveAlert ( app: app, deadline: Date ( ) . addingTimeInterval ( RunnerTests . alertResolutionAllowance) ) )
207140 }
208141
209142 private func assertReplacementAlertUntouched( action: String , arguments: [ String ] , confirmed: Bool ) throws {
210143 app. launchArguments = [ " --agent-device-alert-replacement-regression " ] + arguments
211144 app. launch ( )
212145 defer {
146+ alertResolutionOverrideForTesting = nil
213147 invalidateCachedTarget ( reason: " unit_test_cleanup " )
214148 app. terminate ( )
215149 }
216150 XCTAssertTrue ( app. alerts. firstMatch. waitForExistence ( timeout: appExistenceTimeout) )
217- let measuredRoundTrip = measuredAccessibilityRoundTrip ( )
218- let timeoutMs = RunnerTests . alertReplacementCommandTimeoutMs ( measuredRoundTrip: measuredRoundTrip)
151+ let original = try resolveAlertBeforeTheCommand ( )
152+ alertResolutionOverrideForTesting = { _ in original }
153+ let timeoutMs = Int ( RunnerTests . alertActivationDeadline * 1000 )
219154 let command = try runnerCommandFixture (
220155 #"{"command":"alert","commandId":"alert-replacement","action":" \#( action) ","timeoutMs": \#( timeoutMs) }"#
221156 )
222157 let response = try executeOnMainPrepared ( command: command, activeApp: app)
223- let budget = " timeoutMs \( timeoutMs ) from a \( String ( format : " %.3f " , measuredRoundTrip ) ) s read "
224- XCTAssertEqual ( response. ok, confirmed, " \( budget ) : \( String ( describing: response. error) ) " )
158+ alertResolutionOverrideForTesting = nil
159+ XCTAssertEqual ( response. ok, confirmed, String ( describing: response. error) )
225160 if !confirmed { XCTAssertEqual ( response. error? . code, " ALERT_DEADLINE_EXCEEDED " ) }
226161 XCTAssertTrue ( app. alerts. firstMatch. exists, " the replacement must remain visible " )
227162 XCTAssertEqual ( app. staticTexts [ " agent-device-alert-actions " ] . label, " First actions: 1; replacement actions: 0 " )
228- let current = try XCTUnwrap ( resolveAlert ( app : app , deadline : Date ( ) . addingTimeInterval ( 10 ) ) )
229- let inspection = handleAlert ( current, action: " get " , deadline: Date ( ) . addingTimeInterval ( 10 ) )
163+ let current = try resolveAlertBeforeTheCommand ( )
164+ let inspection = handleAlert ( current, action: " get " , deadline: Date ( ) . addingTimeInterval ( RunnerTests . alertActivationDeadline ) )
230165 XCTAssertTrue ( inspection. ok)
231166 XCTAssertEqual ( inspection. data? . items? . sorted ( ) , [ " Cancel " , " OK " ] )
232167 XCTAssertEqual ( app. staticTexts [ " agent-device-alert-actions " ] . label, " First actions: 1; replacement actions: 0 " )
0 commit comments