Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,120 @@ import Network

#if AGENT_DEVICE_RUNNER_UNIT_TESTS
extension RunnerTests {
func testUptimeHttpRemainsResponsiveWhileCommandQueueIsOccupied() throws {
let listener = try NWListener(using: .tcp, on: .any)
let ready = expectation(description: "HTTP listener ready")
listener.stateUpdateHandler = { state in
if case .ready = state { ready.fulfill() }
}
listener.newConnectionHandler = { connection in
connection.start(queue: self.transportQueue)
self.handle(connection: connection)
}
listener.start(queue: transportQueue)
defer { listener.cancel() }
wait(for: [ready], timeout: 2)
let port = try XCTUnwrap(listener.port)
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 3
configuration.timeoutIntervalForResource = 3
let client = URLSession(configuration: configuration)
defer { client.invalidateAndCancel() }

let occupied = expectation(description: "command queue occupied")
let drained = DispatchSemaphore(value: 0)
let releaseWork = DispatchSemaphore(value: 0)
commandExecutionQueue.async {
occupied.fulfill()
releaseWork.wait()
drained.signal()
}
defer {
releaseWork.signal()
XCTAssertEqual(drained.wait(timeout: .now() + 2), .success)
}
wait(for: [occupied], timeout: 2)

let started = ProcessInfo.processInfo.systemUptime
let uptime = try transportHttpResponse(
client: client,
port: port,
payload: #"{"command":"uptime","commandId":"occupied-preflight"}"#
)
let elapsed = ProcessInfo.processInfo.systemUptime - started
XCTAssertTrue(uptime.ok)
XCTAssertNotNil(uptime.data?.currentUptimeMs)
XCTAssertLessThan(elapsed, 1)
XCTAssertEqual(
commandJournal.status(normalizedCommandId: "occupied-preflight").lifecycleState,
RunnerCommandLifecycleState.notAccepted.rawValue
)

let queued = transportHttpRequest(
client: client,
port: port,
payload: #"{"command":"appState","commandId":"after-preflight"}"#
)
let acceptedDeadline = Date().addingTimeInterval(1)
var state: String?
repeat {
state = try transportHttpResponse(
client: client,
port: port,
payload: #"{"command":"status","statusCommandId":"after-preflight"}"#
).data?.lifecycleState
} while state == RunnerCommandLifecycleState.notAccepted.rawValue && Date() < acceptedDeadline
XCTAssertEqual(state, RunnerCommandLifecycleState.accepted.rawValue)
releaseWork.signal()
wait(for: [queued.finished], timeout: 2)
let response = try queued.response()
XCTAssertEqual(response.error?.message, "appState requires appBundleId")
XCTAssertEqual(
commandJournal.status(normalizedCommandId: "after-preflight").lifecycleState,
RunnerCommandLifecycleState.failed.rawValue
)
NSLog("AGENT_DEVICE_RUNNER_OCCUPIED_UPTIME_MS=%.1f", elapsed * 1000)
}

private final class TransportHttpReply {
let finished = XCTestExpectation(description: "HTTP response received")
var data: Data?
var error: Error?

func response() throws -> Response {
if let error { throw error }
return try JSONDecoder().decode(Response.self, from: XCTUnwrap(data))
}
}

private func transportHttpRequest(
client: URLSession,
port: NWEndpoint.Port,
payload: String
) -> TransportHttpReply {
let reply = TransportHttpReply()
var request = URLRequest(url: URL(string: "http://127.0.0.1:\(port.rawValue)/command")!)
request.httpMethod = "POST"
request.httpBody = Data(payload.utf8)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
client.dataTask(with: request) { data, _, error in
reply.data = data
reply.error = error
reply.finished.fulfill()
}.resume()
return reply
}

private func transportHttpResponse(
client: URLSession,
port: NWEndpoint.Port,
payload: String
) throws -> Response {
let reply = transportHttpRequest(client: client, port: port, payload: payload)
wait(for: [reply.finished], timeout: 2)
return try reply.response()
}

func testDuplicateCommandIdCoalescesOntoInFlightExecution() throws {
let command = try JSONDecoder().decode(
Command.self,
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/__tests__/wait-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ test.for(['runner-start', 'target-discovery'] as const)(
retriable: true,
readableCaptures: 0,
captures: 1,
polls: [{ startedMs: 0, outcome: 'readiness' }],
polls: [{ startedMs: expect.any(Number), outcome: 'readiness' }],
});
expect(response.error.details?.captureStalled).toBeUndefined();
},
Expand Down
Loading