|
1 | 1 | import Foundation |
| 2 | +import XCTest |
2 | 3 |
|
3 | 4 | enum RunnerCommandLifecycleState: String { |
4 | 5 | case notAccepted |
@@ -50,7 +51,7 @@ final class RunnerCommandJournal { |
50 | 51 | command: command, |
51 | 52 | state: response.ok ? .completed : .failed, |
52 | 53 | responseOk: response.ok, |
53 | | - responseJson: encodeResponseJson(response), |
| 54 | + responseJson: encodeResponseJson(command: command, response: response), |
54 | 55 | error: response.error |
55 | 56 | ) |
56 | 57 | } |
@@ -131,10 +132,147 @@ final class RunnerCommandJournal { |
131 | 132 | return trimmed.isEmpty ? nil : trimmed |
132 | 133 | } |
133 | 134 |
|
134 | | - private func encodeResponseJson(_ response: Response) -> String? { |
135 | | - guard response.data?.nodes == nil else { return nil } |
| 135 | + private func encodeResponseJson(command: Command, response: Response) -> String? { |
| 136 | + guard shouldRetainResponseJson(command: command) else { return nil } |
136 | 137 | guard let data = try? JSONEncoder().encode(response) else { return nil } |
137 | 138 | guard data.count <= maxResponseJsonBytes else { return nil } |
138 | 139 | return String(data: data, encoding: .utf8) |
139 | 140 | } |
| 141 | + |
| 142 | + private func shouldRetainResponseJson(command: Command) -> Bool { |
| 143 | + switch command.command { |
| 144 | + case .snapshot, .screenshot: |
| 145 | + return false |
| 146 | + default: |
| 147 | + return true |
| 148 | + } |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +extension RunnerTests { |
| 153 | + func testCommandJournalRetentionPolicy() throws { |
| 154 | + let journal = RunnerCommandJournal() |
| 155 | + |
| 156 | + let uptime = runnerJournalCommand("uptime", id: "small-scalar") |
| 157 | + journal.accept(command: uptime) |
| 158 | + journal.finish( |
| 159 | + command: uptime, |
| 160 | + response: Response(ok: true, data: DataPayload(currentUptimeMs: 12.5)) |
| 161 | + ) |
| 162 | + |
| 163 | + let scalarStatus = journal.status(commandId: "small-scalar") |
| 164 | + XCTAssertEqual(scalarStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 165 | + XCTAssertEqual(scalarStatus.lifecycleResponseOk, true) |
| 166 | + XCTAssertNotNil(scalarStatus.lifecycleResponseJson) |
| 167 | + let scalarResponse = try decodeRunnerJournalResponse(scalarStatus.lifecycleResponseJson) |
| 168 | + XCTAssertEqual(scalarResponse.data?.currentUptimeMs, 12.5) |
| 169 | + |
| 170 | + let querySelector = runnerJournalCommand("querySelector", id: "small-object") |
| 171 | + journal.accept(command: querySelector) |
| 172 | + journal.finish( |
| 173 | + command: querySelector, |
| 174 | + response: Response(ok: true, data: DataPayload(found: true, nodes: [runnerJournalNode()])) |
| 175 | + ) |
| 176 | + |
| 177 | + let objectStatus = journal.status(commandId: "small-object") |
| 178 | + XCTAssertNotNil(objectStatus.lifecycleResponseJson) |
| 179 | + let objectResponse = try decodeRunnerJournalResponse(objectStatus.lifecycleResponseJson) |
| 180 | + XCTAssertEqual(objectResponse.data?.found, true) |
| 181 | + XCTAssertEqual(objectResponse.data?.nodes?.count, 1) |
| 182 | + |
| 183 | + let snapshot = runnerJournalCommand("snapshot", id: "snapshot-tree") |
| 184 | + journal.accept(command: snapshot) |
| 185 | + journal.finish( |
| 186 | + command: snapshot, |
| 187 | + response: Response(ok: true, data: DataPayload(nodes: [runnerJournalNode()], truncated: false)) |
| 188 | + ) |
| 189 | + |
| 190 | + let snapshotStatus = journal.status(commandId: "snapshot-tree") |
| 191 | + XCTAssertEqual(snapshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 192 | + XCTAssertEqual(snapshotStatus.lifecycleResponseOk, true) |
| 193 | + XCTAssertNil(snapshotStatus.lifecycleResponseJson) |
| 194 | + |
| 195 | + let screenshot = runnerJournalCommand("screenshot", id: "screenshot-artifact") |
| 196 | + journal.accept(command: screenshot) |
| 197 | + journal.finish( |
| 198 | + command: screenshot, |
| 199 | + response: Response(ok: true, data: DataPayload(message: "tmp/screenshot-1.png")) |
| 200 | + ) |
| 201 | + |
| 202 | + let screenshotStatus = journal.status(commandId: "screenshot-artifact") |
| 203 | + XCTAssertEqual(screenshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 204 | + XCTAssertEqual(screenshotStatus.lifecycleResponseOk, true) |
| 205 | + XCTAssertNil(screenshotStatus.lifecycleResponseJson) |
| 206 | + |
| 207 | + let largeRead = runnerJournalCommand("readText", id: "large-read") |
| 208 | + journal.accept(command: largeRead) |
| 209 | + journal.finish( |
| 210 | + command: largeRead, |
| 211 | + response: Response(ok: true, data: DataPayload(text: String(repeating: "x", count: 17 * 1024))) |
| 212 | + ) |
| 213 | + |
| 214 | + let largeReadStatus = journal.status(commandId: "large-read") |
| 215 | + XCTAssertEqual(largeReadStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue) |
| 216 | + XCTAssertEqual(largeReadStatus.lifecycleResponseOk, true) |
| 217 | + XCTAssertNil(largeReadStatus.lifecycleResponseJson) |
| 218 | + } |
| 219 | + |
| 220 | + func testCommandJournalKeepsErrorMetadataWhenResponseJsonIsDropped() { |
| 221 | + let journal = RunnerCommandJournal() |
| 222 | + let snapshot = runnerJournalCommand("snapshot", id: "snapshot-error") |
| 223 | + let hint = "Try a smaller read such as snapshot -s <visible label or id> -d 8." |
| 224 | + |
| 225 | + journal.accept(command: snapshot) |
| 226 | + journal.finish( |
| 227 | + command: snapshot, |
| 228 | + response: Response( |
| 229 | + ok: false, |
| 230 | + error: ErrorPayload( |
| 231 | + code: "IOS_AX_SNAPSHOT_FAILED", |
| 232 | + message: "iOS XCTest snapshot failed while serializing the accessibility tree.", |
| 233 | + hint: hint |
| 234 | + ) |
| 235 | + ) |
| 236 | + ) |
| 237 | + |
| 238 | + let status = journal.status(commandId: "snapshot-error") |
| 239 | + XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.failed.rawValue) |
| 240 | + XCTAssertEqual(status.lifecycleResponseOk, false) |
| 241 | + XCTAssertNil(status.lifecycleResponseJson) |
| 242 | + XCTAssertEqual(status.lifecycleErrorCode, "IOS_AX_SNAPSHOT_FAILED") |
| 243 | + XCTAssertEqual( |
| 244 | + status.lifecycleErrorMessage, |
| 245 | + "iOS XCTest snapshot failed while serializing the accessibility tree." |
| 246 | + ) |
| 247 | + XCTAssertEqual(status.lifecycleErrorHint, hint) |
| 248 | + } |
| 249 | + |
| 250 | + private func runnerJournalCommand(_ command: String, id: String) -> Command { |
| 251 | + let json = #"{"command":"\#(command)","commandId":"\#(id)"}"# |
| 252 | + return try! JSONDecoder().decode(Command.self, from: Data(json.utf8)) |
| 253 | + } |
| 254 | + |
| 255 | + private func runnerJournalNode() -> SnapshotNode { |
| 256 | + SnapshotNode( |
| 257 | + index: 0, |
| 258 | + type: "button", |
| 259 | + label: "Continue", |
| 260 | + identifier: "continue", |
| 261 | + value: nil, |
| 262 | + rect: SnapshotRect(x: 10, y: 20, width: 100, height: 44), |
| 263 | + enabled: true, |
| 264 | + focused: nil, |
| 265 | + selected: nil, |
| 266 | + hittable: true, |
| 267 | + depth: 0, |
| 268 | + parentIndex: nil, |
| 269 | + hiddenContentAbove: nil, |
| 270 | + hiddenContentBelow: nil |
| 271 | + ) |
| 272 | + } |
| 273 | + |
| 274 | + private func decodeRunnerJournalResponse(_ responseJson: String?) throws -> Response { |
| 275 | + let responseJson = try XCTUnwrap(responseJson) |
| 276 | + return try JSONDecoder().decode(Response.self, from: Data(responseJson.utf8)) |
| 277 | + } |
140 | 278 | } |
0 commit comments