From a5d4dc16c249a80f88cf469e79796663da587385 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 14 Jul 2026 19:46:12 +0100 Subject: [PATCH 01/31] feat(simulator): Support Xcode 27 Device Hub automation Move AXe to the investigated IDB candidate and preserve the existing command contract across Indigo and DTUHID transports. Add accessibility bootstrap, resilient HID broker recovery, media adaptation, and regression coverage for Xcode 26 and Xcode 27. Co-Authored-By: Codex --- .gitattributes | 1 + Package.swift | 22 +- Sources/AXe/Commands/Button.swift | 8 +- Sources/AXe/Commands/Gesture.swift | 2 +- Sources/AXe/Commands/HIDBrokerCommand.swift | 19 + Sources/AXe/Commands/Key.swift | 8 +- Sources/AXe/Commands/KeyCombo.swift | 6 +- Sources/AXe/Commands/KeySequence.swift | 2 +- Sources/AXe/Commands/StreamVideo.swift | 58 +-- Sources/AXe/Commands/Swipe.swift | 2 +- Sources/AXe/Commands/Tap.swift | 2 +- Sources/AXe/Commands/Touch.swift | 34 +- Sources/AXe/Commands/Type.swift | 15 +- .../AXe/Commands/VideoCommandSupport.swift | 78 ++-- .../AsyncParsableCommand+Setup.swift | 6 +- .../AXe/Utilities/AccessibilityFetcher.swift | 260 ++++++++++- .../AXe/Utilities/Batch/BatchPlanRunner.swift | 3 +- .../Batch/Command+BatchConvertible.swift | 31 +- Sources/AXe/Utilities/BridgeQueues.swift | 31 -- Sources/AXe/Utilities/FutureBridge.swift | 145 ------ Sources/AXe/Utilities/GlobalSetup.swift | 18 +- Sources/AXe/Utilities/HIDBroker.swift | 417 ++++++++++++++++++ .../AXe/Utilities/HIDBrokerReadiness.swift | 68 +++ Sources/AXe/Utilities/HIDInteractor.swift | 53 +-- Sources/AXe/Utilities/TextToHIDEvents.swift | 14 +- Sources/AXe/main.swift | 3 +- Tests/AccessibilityFetcherTests.swift | 285 ++++++++++++ Tests/ButtonTests.swift | 143 +++++- Tests/HIDBrokerTests.swift | 195 ++++++++ Tests/KeyComboTests.swift | 2 +- Tests/StreamVideoTests.swift | 67 ++- Tests/TestUIState.swift | 184 ++++++++ Tests/TestUtilities.swift | 229 ++-------- Tests/VideoFrameUtilitiesTests.swift | 46 ++ ...accessibility-client-type-for-xctest.patch | 12 + ...developer-dir-environment-precedence.patch | 68 +++ .../idb/e682506/dtuhid-event-semantics.patch | 281 ++++++++++++ .../expose-selected-hid-transport.patch | 37 ++ ...ccessibility-private-framework-types.patch | 179 ++++++++ .../xcode27-accessibility-bootstrap.patch | 365 +++++++++++++++ scripts/build.sh | 224 ++++++++-- 41 files changed, 2990 insertions(+), 633 deletions(-) create mode 100644 .gitattributes create mode 100644 Sources/AXe/Commands/HIDBrokerCommand.swift delete mode 100644 Sources/AXe/Utilities/BridgeQueues.swift delete mode 100644 Sources/AXe/Utilities/FutureBridge.swift create mode 100644 Sources/AXe/Utilities/HIDBroker.swift create mode 100644 Sources/AXe/Utilities/HIDBrokerReadiness.swift create mode 100644 Tests/AccessibilityFetcherTests.swift create mode 100644 Tests/HIDBrokerTests.swift create mode 100644 Tests/TestUIState.swift create mode 100644 Tests/VideoFrameUtilitiesTests.swift create mode 100644 patches/idb/e682506/accessibility-client-type-for-xctest.patch create mode 100644 patches/idb/e682506/developer-dir-environment-precedence.patch create mode 100644 patches/idb/e682506/dtuhid-event-semantics.patch create mode 100644 patches/idb/e682506/expose-selected-hid-transport.patch create mode 100644 patches/idb/e682506/hide-accessibility-private-framework-types.patch create mode 100644 patches/idb/e682506/xcode27-accessibility-bootstrap.patch diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a0848b6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.patch whitespace=-blank-at-eol diff --git a/Package.swift b/Package.swift index 7d43852..da528d1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,26 @@ // swift-tools-version:5.10 +import Foundation import PackageDescription +let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent() +let idbCheckoutDirectory = ProcessInfo.processInfo.environment["IDB_CHECKOUT_DIR"] + .map { URL(fileURLWithPath: $0) } + ?? packageRoot.appendingPathComponent("idb_checkout", isDirectory: true) +let idbPrivateHeadersDirectory = idbCheckoutDirectory.appendingPathComponent( + "PrivateHeaders", + isDirectory: true +) +// Compile-only module-map inputs: never copy these headers into release artifacts or runtime rpaths. +let idbPrivateHeaderSearchFlags = [ + idbPrivateHeadersDirectory, + idbPrivateHeadersDirectory.appendingPathComponent("AccessibilityPlatformTranslation", isDirectory: true), + idbPrivateHeadersDirectory.appendingPathComponent("AXRuntime", isDirectory: true), + idbPrivateHeadersDirectory.appendingPathComponent("CoreSimDeviceIO", isDirectory: true), + idbPrivateHeadersDirectory.appendingPathComponent("CoreSimulator", isDirectory: true), + idbPrivateHeadersDirectory.appendingPathComponent("CoreSimulatorUtilities", isDirectory: true), + idbPrivateHeadersDirectory.appendingPathComponent("SimulatorKit", isDirectory: true), +].flatMap { ["-I", $0.path] } + let package = Package( name: "AXe", platforms: [ @@ -39,7 +59,7 @@ let package = Package( .copy("Resources/skills") ], swiftSettings: [ - .unsafeFlags(["-parse-as-library"]) + .unsafeFlags(["-parse-as-library"] + idbPrivateHeaderSearchFlags) ], linkerSettings: [ // For XCFrameworks, rpath can often be just @executable_path diff --git a/Sources/AXe/Commands/Button.swift b/Sources/AXe/Commands/Button.swift index b410d20..6b02cc9 100644 --- a/Sources/AXe/Commands/Button.swift +++ b/Sources/AXe/Commands/Button.swift @@ -91,11 +91,11 @@ struct Button: AsyncParsableCommand { if let duration = duration { // For duration-based presses, we need to create separate down/up events with delay - let buttonDownEvent = FBSimulatorHIDEvent.buttonDown(buttonType.hidButton) + let buttonDownEvent = FBSimulatorHIDEvent.button(direction: .down, button: buttonType.hidButton) let delayEvent = FBSimulatorHIDEvent.delay(duration) - let buttonUpEvent = FBSimulatorHIDEvent.buttonUp(buttonType.hidButton) - - buttonEvent = FBSimulatorHIDEvent(events: [ + let buttonUpEvent = FBSimulatorHIDEvent.button(direction: .up, button: buttonType.hidButton) + + buttonEvent = FBSimulatorHIDEvent.composite([ buttonDownEvent, delayEvent, buttonUpEvent diff --git a/Sources/AXe/Commands/Gesture.swift b/Sources/AXe/Commands/Gesture.swift index 8add73c..e512099 100644 --- a/Sources/AXe/Commands/Gesture.swift +++ b/Sources/AXe/Commands/Gesture.swift @@ -212,7 +212,7 @@ struct Gesture: AsyncParsableCommand { } // Execute the gesture sequence - let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent(events: events) + let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent.composite(events) try await HIDInteractor .performHIDEvent( diff --git a/Sources/AXe/Commands/HIDBrokerCommand.swift b/Sources/AXe/Commands/HIDBrokerCommand.swift new file mode 100644 index 0000000..7f8256b --- /dev/null +++ b/Sources/AXe/Commands/HIDBrokerCommand.swift @@ -0,0 +1,19 @@ +import ArgumentParser +import FBControlCore + +struct HIDBrokerCommand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "hid-broker", + shouldDisplay: false + ) + + @Option(name: .customLong("udid")) + var simulatorUDID: String + + func run() async throws { + let logger = AxeLogger() + try await setup(logger: logger) + try await performGlobalSetup(logger: logger) + try await HIDBroker.serve(simulatorUDID: simulatorUDID, logger: logger) + } +} diff --git a/Sources/AXe/Commands/Key.swift b/Sources/AXe/Commands/Key.swift index 06cbd6e..6df6d72 100644 --- a/Sources/AXe/Commands/Key.swift +++ b/Sources/AXe/Commands/Key.swift @@ -66,11 +66,11 @@ struct Key: AsyncParsableCommand { if let duration = duration { // For duration-based presses, we need to create separate down/up events with delay - let keyDownEvent = FBSimulatorHIDEvent.keyDown(UInt32(keycode)) + let keyDownEvent = FBSimulatorHIDEvent.keyboard(direction: .down, keyCode: UInt32(keycode)) let delayEvent = FBSimulatorHIDEvent.delay(duration) - let keyUpEvent = FBSimulatorHIDEvent.keyUp(UInt32(keycode)) - - keyEvent = FBSimulatorHIDEvent(events: [ + let keyUpEvent = FBSimulatorHIDEvent.keyboard(direction: .up, keyCode: UInt32(keycode)) + + keyEvent = FBSimulatorHIDEvent.composite([ keyDownEvent, delayEvent, keyUpEvent diff --git a/Sources/AXe/Commands/KeyCombo.swift b/Sources/AXe/Commands/KeyCombo.swift index 7add888..8be1c31 100644 --- a/Sources/AXe/Commands/KeyCombo.swift +++ b/Sources/AXe/Commands/KeyCombo.swift @@ -76,7 +76,7 @@ struct KeyCombo: AsyncParsableCommand { // Press modifiers down in order for modifier in parsedModifiers { - events.append(FBSimulatorHIDEvent.keyDown(UInt32(modifier))) + events.append(FBSimulatorHIDEvent.keyboard(direction: .down, keyCode: UInt32(modifier))) } // Press and release the target key @@ -84,10 +84,10 @@ struct KeyCombo: AsyncParsableCommand { // Release modifiers in reverse order for modifier in parsedModifiers.reversed() { - events.append(FBSimulatorHIDEvent.keyUp(UInt32(modifier))) + events.append(FBSimulatorHIDEvent.keyboard(direction: .up, keyCode: UInt32(modifier))) } - let comboEvent = FBSimulatorHIDEvent(events: events) + let comboEvent = FBSimulatorHIDEvent.composite(events) try await HIDInteractor .performHIDEvent( diff --git a/Sources/AXe/Commands/KeySequence.swift b/Sources/AXe/Commands/KeySequence.swift index b732ad9..9a5cb10 100644 --- a/Sources/AXe/Commands/KeySequence.swift +++ b/Sources/AXe/Commands/KeySequence.swift @@ -85,7 +85,7 @@ struct KeySequence: AsyncParsableCommand { } // Create composite event - let sequenceEvent = FBSimulatorHIDEvent(events: events) + let sequenceEvent = FBSimulatorHIDEvent.composite(events) // Perform the key sequence event try await HIDInteractor diff --git a/Sources/AXe/Commands/StreamVideo.swift b/Sources/AXe/Commands/StreamVideo.swift index 6acec44..73f9532 100644 --- a/Sources/AXe/Commands/StreamVideo.swift +++ b/Sources/AXe/Commands/StreamVideo.swift @@ -165,7 +165,7 @@ struct StreamVideo: AsyncParsableCommand { } } - // MARK: - Legacy BGRA streaming + // MARK: - BGRA streaming private func streamBGRA( to simulator: FBSimulator, @@ -177,27 +177,23 @@ struct StreamVideo: AsyncParsableCommand { FileHandle.standardError.write(Data(" axe stream-video --format bgra --udid | ffmpeg -f rawvideo -pixel_format bgra -video_size WIDTHxHEIGHT -i - output.mp4\n".utf8)) FileHandle.standardError.write(Data("Press Ctrl+C to stop streaming\n".utf8)) - do { - let config = FBVideoStreamConfiguration( - encoding: .BGRA, - framesPerSecond: nil, - compressionQuality: NSNumber(value: Double(quality) / 100.0), - scaleFactor: NSNumber(value: scale), - avgBitrate: nil, - keyFrameRate: nil - ) - - let stdoutConsumer = FBFileWriter.syncWriter(withFileDescriptor: STDOUT_FILENO, closeOnEndOfFile: false) - let videoStreamFuture = simulator.createStream(with: config) - let videoStream = try await FutureBridge.value(videoStreamFuture) - let startFuture = videoStream.startStreaming(stdoutConsumer) - - startFuture.onQueue(BridgeQueues.videoStreamQueue, notifyOfCompletion: { future in - if let error = future.error { - FileHandle.standardError.write(Data("Stream initialization error: \(error)\n".utf8)) - } - }) + let config = FBVideoStreamConfiguration( + format: .bgra(), + framesPerSecond: NSNumber(value: fps), + rateControl: .quality(NSNumber(value: Double(quality) / 100.0)), + scaleFactor: NSNumber(value: scale), + keyFrameRate: nil + ) + let stdoutConsumer = FBFileWriter.syncWriter(withFileDescriptor: STDOUT_FILENO, closeOnEndOfFile: false) + var videoStream: (any FBVideoStream)? + var isStreaming = false + + do { + let stream = try await simulator.createStream(configuration: config) + videoStream = stream + try await stream.startStreamingAsync(stdoutConsumer) + isStreaming = true try await Task.sleep(nanoseconds: 1_000_000_000) FileHandle.standardError.write(Data("BGRA stream is now running...\n".utf8)) @@ -212,20 +208,14 @@ struct StreamVideo: AsyncParsableCommand { } FileHandle.standardError.write(Data("\nStopping BGRA stream...\n".utf8)) - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - BridgeQueues.videoStreamQueue.async { - let stopFuture = videoStream.stopStreaming() - stopFuture.onQueue(BridgeQueues.videoStreamQueue, notifyOfCompletion: { future in - FileHandle.standardError.write(Data("BGRA stream stopped\n".utf8)) - if let error = future.error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: ()) - } - }) - } - } + isStreaming = false + try await stream.stopStreamingAsync() + FileHandle.standardError.write(Data("BGRA stream stopped\n".utf8)) } catch { + if isStreaming, let videoStream { + isStreaming = false + try? await videoStream.stopStreamingAsync() + } throw CLIError(errorDescription: "Failed to stream BGRA video: \(error.localizedDescription)") } } diff --git a/Sources/AXe/Commands/Swipe.swift b/Sources/AXe/Commands/Swipe.swift index 231de39..f7b5c59 100644 --- a/Sources/AXe/Commands/Swipe.swift +++ b/Sources/AXe/Commands/Swipe.swift @@ -137,7 +137,7 @@ struct Swipe: AsyncParsableCommand { } // Execute the swipe sequence - let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent(events: events) + let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent.composite(events) // Perform the swipe event try await HIDInteractor diff --git a/Sources/AXe/Commands/Tap.swift b/Sources/AXe/Commands/Tap.swift index 7e26fb6..06f7184 100644 --- a/Sources/AXe/Commands/Tap.swift +++ b/Sources/AXe/Commands/Tap.swift @@ -166,7 +166,7 @@ struct Tap: AsyncParsableCommand { events.append(.delay(postDelay)) } - let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent(events: events) + let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent.composite(events) try await HIDInteractor.performHIDEvent(finalEvent, for: simulatorUDID, logger: logger) case .automatic: throw CLIError(errorDescription: "Unexpected tap style resolution.") diff --git a/Sources/AXe/Commands/Touch.swift b/Sources/AXe/Commands/Touch.swift index 48c360d..1ffc421 100644 --- a/Sources/AXe/Commands/Touch.swift +++ b/Sources/AXe/Commands/Touch.swift @@ -78,49 +78,31 @@ struct Touch: AsyncParsableCommand { logger: logger ) + var primitives: [HIDBrokerPrimitive] = [] if touchDown && touchUp { // Send down and up as separate HID submissions so iOS recognizers // observe a real hold duration for long-press gestures. let touchDelay = delay ?? TapTiming.defaultHoldDuration logger.info().log("Touch down") - try await HIDInteractor - .performHIDEvent( - FBSimulatorHIDEvent.touchDownAt(x: physicalPoint.x, y: physicalPoint.y), - for: simulatorUDID, - logger: logger - ) + primitives.append(.touch(.down, x: physicalPoint.x, y: physicalPoint.y)) if touchDelay > 0 { logger.info().log("Delay: \(touchDelay) seconds") - let delayNanoseconds = UInt64(touchDelay * 1_000_000_000) - try await Task.sleep(nanoseconds: delayNanoseconds) + primitives.append(.delay(touchDelay)) } logger.info().log("Touch up") - try await HIDInteractor - .performHIDEvent( - FBSimulatorHIDEvent.touchUpAt(x: physicalPoint.x, y: physicalPoint.y), - for: simulatorUDID, - logger: logger - ) + primitives.append(.touch(.up, x: physicalPoint.x, y: physicalPoint.y)) } else if touchDown { logger.info().log("Touch down") - try await HIDInteractor - .performHIDEvent( - FBSimulatorHIDEvent.touchDownAt(x: physicalPoint.x, y: physicalPoint.y), - for: simulatorUDID, - logger: logger - ) + primitives.append(.touch(.down, x: physicalPoint.x, y: physicalPoint.y)) } else { logger.info().log("Touch up") - try await HIDInteractor - .performHIDEvent( - FBSimulatorHIDEvent.touchUpAt(x: physicalPoint.x, y: physicalPoint.y), - for: simulatorUDID, - logger: logger - ) + primitives.append(.touch(.up, x: physicalPoint.x, y: physicalPoint.y)) } + + try HIDBroker.sendTouchPrimitives(primitives, simulatorUDID: simulatorUDID) logger.info().log("Touch events completed successfully") } diff --git a/Sources/AXe/Commands/Type.swift b/Sources/AXe/Commands/Type.swift index 7abc7d1..b16d1cc 100644 --- a/Sources/AXe/Commands/Type.swift +++ b/Sources/AXe/Commands/Type.swift @@ -129,14 +129,13 @@ struct Type: AsyncParsableCommand { logger.info().log("Performing HID event sequence for text typing") - // Perform HID events sequentially - for event in hidEvents { - try await HIDInteractor - .performHIDEvent( - event, - for: simulatorUDID, - logger: logger - ) + if !hidEvents.isEmpty { + try await HIDInteractor + .performHIDEvent( + .composite(hidEvents), + for: simulatorUDID, + logger: logger + ) } logger.info().log("Text typing completed successfully") diff --git a/Sources/AXe/Commands/VideoCommandSupport.swift b/Sources/AXe/Commands/VideoCommandSupport.swift index d042f85..60eabd8 100644 --- a/Sources/AXe/Commands/VideoCommandSupport.swift +++ b/Sources/AXe/Commands/VideoCommandSupport.swift @@ -48,19 +48,13 @@ final class SignalObserver { } enum VideoProcessingError: Error { - case emptyScreenshot case failedToDecodeImage case failedToAllocatePixelBuffer } struct VideoFrameUtilities { static func captureScreenshotData(from simulator: FBSimulator) async throws -> Data { - let screenshotFuture = simulator.takeScreenshot(.PNG) - let nsData = try await FutureBridge.value(screenshotFuture) - guard let data = nsData as Data? else { - throw VideoProcessingError.emptyScreenshot - } - return data + try await simulator.takeScreenshot(format: .png) } static func makeCGImage(from data: Data) -> CGImage? { @@ -89,27 +83,12 @@ struct VideoFrameUtilities { private static func scaleJPEGData(_ data: Data, scale: Double, quality: Int) async throws -> Data { #if os(macOS) - guard let image = NSImage(data: data) else { - throw VideoProcessingError.failedToDecodeImage - } - - let newSize = NSSize( - width: image.size.width * scale, - height: image.size.height * scale - ) - - let newImage = NSImage(size: newSize) - newImage.lockFocus() - image.draw(in: NSRect(origin: .zero, size: newSize)) - newImage.unlockFocus() - - guard let tiffData = newImage.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData), - let jpegData = bitmap.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor: Double(quality) / 100.0]) else { + guard let image = makeCGImage(from: data) else { throw VideoProcessingError.failedToDecodeImage } - - return jpegData + let width = max(1, Int(Double(image.width) * scale)) + let height = max(1, Int(Double(image.height) * scale)) + return try encodeJPEG(image, width: width, height: height, quality: quality) #else return data #endif @@ -117,18 +96,53 @@ struct VideoFrameUtilities { private static func reencodeJPEGData(_ data: Data, quality: Int) async throws -> Data { #if os(macOS) - guard let image = NSImage(data: data), - let tiffData = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData), - let jpegData = bitmap.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor: Double(quality) / 100.0]) else { + guard let image = makeCGImage(from: data) else { throw VideoProcessingError.failedToDecodeImage } - - return jpegData + return try encodeJPEG(image, width: image.width, height: image.height, quality: quality) #else return data #endif } + + #if os(macOS) + private static func encodeJPEG( + _ image: CGImage, + width: Int, + height: Int, + quality: Int + ) throws -> Data { + guard let bitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: width, + pixelsHigh: height, + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0 + ), let context = NSGraphicsContext(bitmapImageRep: bitmap) else { + throw VideoProcessingError.failedToAllocatePixelBuffer + } + + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = context + context.imageInterpolation = .high + context.cgContext.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height)) + context.flushGraphics() + NSGraphicsContext.restoreGraphicsState() + + guard let data = bitmap.representation( + using: .jpeg, + properties: [.compressionFactor: Double(quality) / 100.0] + ) else { + throw VideoProcessingError.failedToDecodeImage + } + return data + } + #endif } final class H264StreamRecorder: @unchecked Sendable { diff --git a/Sources/AXe/Extensions/AsyncParsableCommand+Setup.swift b/Sources/AXe/Extensions/AsyncParsableCommand+Setup.swift index 2ba4d0c..7d6bf7c 100644 --- a/Sources/AXe/Extensions/AsyncParsableCommand+Setup.swift +++ b/Sources/AXe/Extensions/AsyncParsableCommand+Setup.swift @@ -7,8 +7,8 @@ extension AsyncParsableCommand { func setup(logger: AxeLogger) async throws { // Check Xcode availability do { - let isXcodeAvailable: NSString = try await FutureBridge.value(FBXcodeDirectory.xcodeSelectDeveloperDirectory()) - if isXcodeAvailable.length == 0 { + let developerDirectory = try FBXcodeDirectory.resolveDeveloperDirectory() + if developerDirectory.isEmpty { logger.error().log("Xcode is not available, idb will not be able to use Simulators") throw CLIError(errorDescription: "Xcode is not available, idb will not be able to use Simulators") } @@ -25,4 +25,4 @@ extension AsyncParsableCommand { throw CLIError(errorDescription: "Essential private frameworks failed to loaded.") } } -} \ No newline at end of file +} diff --git a/Sources/AXe/Utilities/AccessibilityFetcher.swift b/Sources/AXe/Utilities/AccessibilityFetcher.swift index e81da5c..184f06a 100644 --- a/Sources/AXe/Utilities/AccessibilityFetcher.swift +++ b/Sources/AXe/Utilities/AccessibilityFetcher.swift @@ -2,6 +2,23 @@ import Foundation import FBControlCore import FBSimulatorControl +struct AccessibilityRecoveryDependencies { + typealias ProcessRunner = @MainActor ( + _ executableURL: URL, + _ arguments: [String], + _ timeout: TimeInterval + ) async throws -> Int32 + typealias Waiter = @MainActor (_ duration: Duration) async throws -> Void + + let runProcess: ProcessRunner + let wait: Waiter + + static let live = AccessibilityRecoveryDependencies( + runProcess: AccessibilityFetcher.runProcess, + wait: { duration in try await Task.sleep(for: duration) } + ) +} + struct AccessibilityPoint: Equatable { let x: Double let y: Double @@ -17,7 +34,8 @@ struct AccessibilityFetcher { static func fetchAccessibilityInfoJSONData( for simulatorUDID: String, point: AccessibilityPoint? = nil, - logger: AxeLogger + logger: AxeLogger, + recoveryDependencies: AccessibilityRecoveryDependencies = .live ) async throws -> Data { let simulatorSet = try await getSimulatorSet(deviceSetPath: nil, logger: logger, reporter: EmptyEventReporter.shared) @@ -25,16 +43,188 @@ struct AccessibilityFetcher { throw CLIError(errorDescription: "Simulator with UDID \(simulatorUDID) not found in set.") } - // FBSimulator conforms to FBAccessibilityCommands. - let accessibilityInfoFuture: FBFuture - if let point { - accessibilityInfoFuture = target.accessibilityElement(at: point.cgPoint, nestedFormat: true) - } else { - accessibilityInfoFuture = target.accessibilityElements(withNestedFormat: true) + return try await retryingAfterTestManagerRecovery( + simulatorUDID: simulatorUDID, + logger: logger, + dependencies: recoveryDependencies + ) { + if let point { + let accessibilityElement = try await target.accessibilityElement(at: point.cgPoint) + defer { accessibilityElement.close() } + return try serializedAccessibilityData(from: accessibilityElement) + } + return try await fetchFrontmostAccessibilityInfoJSONData(from: target) + } + } + + private static func fetchFrontmostAccessibilityInfoJSONData(from target: FBSimulator) async throws -> Data { + var latestData: Data? + for attempt in 0..<5 { + let accessibilityElement = try await target.accessibilityElementForFrontmostApplication() + let data: Data + do { + data = try serializedAccessibilityData(from: accessibilityElement) + accessibilityElement.close() + } catch { + accessibilityElement.close() + throw error + } + latestData = data + if try containsAccessibilityDescendant(in: data) { + return data + } + if attempt < 4 { + try await Task.sleep(for: .milliseconds(50 * (1 << attempt))) + } + } + guard let latestData else { + throw CLIError(errorDescription: "Accessibility hierarchy could not be serialized.") } + return latestData + } - let infoAnyObject: AnyObject = try await FutureBridge.value(accessibilityInfoFuture) - return try serializeAccessibilityInfo(infoAnyObject) + static func retryingAfterTestManagerRecovery( + simulatorUDID: String, + logger: AxeLogger, + dependencies: AccessibilityRecoveryDependencies, + operation: @MainActor () async throws -> T + ) async throws -> T { + do { + return try await operation() + } catch { + guard shouldRecoverTestManagerDaemon(from: error) else { + throw error + } + logger.info().log("Accessibility transport failed; restarting testmanagerd and retrying once") + try await recoverTestManagerDaemon( + simulatorUDID: simulatorUDID, + dependencies: dependencies + ) + return try await operation() + } + } + + static func shouldRecoverTestManagerDaemon(from error: Error) -> Bool { + let errors = errorChain(from: error) + let details = errors.flatMap { error in + [ + error.domain, + error.localizedDescription, + error.localizedFailureReason, + error.localizedRecoverySuggestion, + error.userInfo[NSDebugDescriptionErrorKey] as? String, + ].compactMap { $0?.lowercased() } + } + if details.contains(where: { normalizedErrorText($0) == "channel disconnected" }) { + return true + } + let identifiesDTX = details.contains(where: { $0.contains("dtx") }) + let identifiesFileDescriptorExhaustion = errors.contains { error in + error.code == 24 + || error.localizedDescription.localizedCaseInsensitiveContains("errno 24") + || error.localizedDescription.localizedCaseInsensitiveContains("too many open files") + } + return identifiesDTX && identifiesFileDescriptorExhaustion + } + + private static func normalizedErrorText(_ text: String) -> String { + text.lowercased() + .components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + .joined(separator: " ") + } + + static func recoverTestManagerDaemon( + simulatorUDID: String, + dependencies: AccessibilityRecoveryDependencies + ) async throws { + let arguments = [ + "simctl", + "spawn", + simulatorUDID, + "launchctl", + "kickstart", + "-k", + "user/foreground/com.apple.testmanagerd", + ] + let status = try await dependencies.runProcess( + URL(fileURLWithPath: "/usr/bin/xcrun"), + arguments, + 3 + ) + guard status == 0 else { + throw CLIError(errorDescription: "Could not restart testmanagerd for simulator \(simulatorUDID) (simctl exited with status \(status)).") + } + try await dependencies.wait(.milliseconds(250)) + } + + private static func errorChain(from error: Error) -> [NSError] { + var errors: [NSError] = [] + var current: NSError? = error as NSError + var visited: Set = [] + while let next = current, visited.insert(ObjectIdentifier(next)).inserted { + errors.append(next) + current = next.userInfo[NSUnderlyingErrorKey] as? NSError + } + return errors + } + + static func runProcess( + executableURL: URL, + arguments: [String], + timeout: TimeInterval + ) async throws -> Int32 { + let process = Process() + process.executableURL = executableURL + process.arguments = arguments + process.standardOutput = FileHandle.nullDevice + process.standardError = FileHandle.nullDevice + try process.run() + + let deadline = Date().addingTimeInterval(timeout) + while process.isRunning, Date() < deadline { + try? await Task.sleep(for: .milliseconds(10)) + } + if process.isRunning { + process.terminate() + let terminationDeadline = Date().addingTimeInterval(0.5) + while process.isRunning, Date() < terminationDeadline { + try? await Task.sleep(for: .milliseconds(10)) + } + if process.isRunning { + kill(process.processIdentifier, SIGKILL) + } + process.waitUntilExit() + throw CLIError(errorDescription: "Timed out waiting for \(executableURL.path) to finish.") + } + process.waitUntilExit() + return process.terminationStatus + } + + private static func serializedAccessibilityData(from accessibilityElement: FBAccessibilityElement) throws -> Data { + let response = try accessibilityElement.serialize( + with: FBAccessibilityRequestOptions( + nestedFormat: true, + keys: accessibilityRequestKeys + ) + ) + return try serializeAccessibilityInfo(addingCompatibilityDefaults(to: response.elements)) + } + + static func containsAccessibilityDescendant(in data: Data) throws -> Bool { + let object = try JSONSerialization.jsonObject(with: data) + let roots: [[String: Any]] + if let array = object as? [[String: Any]] { + roots = array + } else if let root = object as? [String: Any] { + roots = [root] + } else { + return false + } + return roots.contains { root in + guard let children = root["children"] as? [Any] else { return false } + return !children.isEmpty + } } static func fetchAccessibilityElements(for simulatorUDID: String, logger: AxeLogger) async throws -> [AccessibilityElement] { @@ -49,14 +239,50 @@ struct AccessibilityFetcher { return [root] } - private static func serializeAccessibilityInfo(_ accessibilityInfo: AnyObject) throws -> Data { - if let nsDict = accessibilityInfo as? NSDictionary { - return try JSONSerialization.data(withJSONObject: nsDict, options: [.prettyPrinted]) + static func serializeAccessibilityInfo(_ accessibilityInfo: Any) throws -> Data { + guard accessibilityInfo is [String: Any] || accessibilityInfo is [[String: Any]] else { + throw CLIError(errorDescription: "Accessibility info was not a dictionary or array as expected.") + } + return try JSONSerialization.data(withJSONObject: accessibilityInfo, options: [.prettyPrinted]) + } + + static func addingCompatibilityDefaults(to accessibilityInfo: Any) -> Any { + if let elements = accessibilityInfo as? [Any] { + return elements.map(addingCompatibilityDefaults) } - if let nsArray = accessibilityInfo as? NSArray { - return try JSONSerialization.data(withJSONObject: nsArray, options: [.prettyPrinted]) + guard var element = accessibilityInfo as? [String: Any] else { + return accessibilityInfo } - - throw CLIError(errorDescription: "Accessibility info was not a dictionary or array as expected.") + for (key, value) in element { + element[key] = addingCompatibilityDefaults(to: value) + } + if element["type"] != nil, element["traits"] == nil { + element["traits"] = [String]() + } + return element } -} + + // This key set preserves AXe's legacy public JSON schema; update it only with explicit schema coverage. + static let accessibilityOutputKeys: Set = [ + .label, + .frame, + .value, + .uniqueID, + .type, + .title, + .frameDict, + .help, + .enabled, + .customActions, + .role, + .roleDescription, + .subrole, + .contentRequired, + .pid, + .traits, + ] + + // Xcode 27's private nested serializer returns an empty hierarchy when `.traits` is requested. + // Preserve the public schema by defaulting the missing field after requesting the safe subset. + static let accessibilityRequestKeys = accessibilityOutputKeys.subtracting([.traits]) +} diff --git a/Sources/AXe/Utilities/Batch/BatchPlanRunner.swift b/Sources/AXe/Utilities/Batch/BatchPlanRunner.swift index b912228..f3cdf7f 100644 --- a/Sources/AXe/Utilities/Batch/BatchPlanRunner.swift +++ b/Sources/AXe/Utilities/Batch/BatchPlanRunner.swift @@ -11,7 +11,7 @@ struct BatchPlanRunner { func flushPending() async throws { guard !pendingMergeable.isEmpty else { return } - let event = pendingMergeable.count == 1 ? pendingMergeable[0] : FBSimulatorHIDEvent(events: pendingMergeable) + let event = pendingMergeable.count == 1 ? pendingMergeable[0] : FBSimulatorHIDEvent.composite(pendingMergeable) try await HIDInteractor.performHIDEvent(event, in: session, logger: logger) pendingMergeable.removeAll(keepingCapacity: true) } @@ -43,4 +43,3 @@ struct BatchPlanRunner { try await flushPending() } } - diff --git a/Sources/AXe/Utilities/Batch/Command+BatchConvertible.swift b/Sources/AXe/Utilities/Batch/Command+BatchConvertible.swift index 5918f5e..95acc51 100644 --- a/Sources/AXe/Utilities/Batch/Command+BatchConvertible.swift +++ b/Sources/AXe/Utilities/Batch/Command+BatchConvertible.swift @@ -20,7 +20,7 @@ private func buildDelayedEvent( if let postDelay, postDelay > 0 { events.append(.delay(postDelay)) } - return events.count == 1 ? events[0] : FBSimulatorHIDEvent(events: events) + return events.count == 1 ? events[0] : FBSimulatorHIDEvent.composite(events) } private func resolveBatchTapPoint( @@ -186,8 +186,8 @@ extension Touch: BatchConvertible { logger: logger ) - let touchDownEvent = FBSimulatorHIDEvent.touchDownAt(x: physicalPoint.x, y: physicalPoint.y) - let touchUpEvent = FBSimulatorHIDEvent.touchUpAt(x: physicalPoint.x, y: physicalPoint.y) + let touchDownEvent = FBSimulatorHIDEvent.touch(direction: .down, x: physicalPoint.x, y: physicalPoint.y) + let touchUpEvent = FBSimulatorHIDEvent.touch(direction: .up, x: physicalPoint.x, y: physicalPoint.y) if touchDown && touchUp { let holdDelay = delay ?? TapTiming.defaultHoldDuration @@ -209,10 +209,10 @@ extension Touch: BatchConvertible { extension Button: BatchConvertible { func toBatchPrimitives(context: BatchContext, logger: AxeLogger) async throws -> [BatchPrimitive] { if let duration { - let composite = FBSimulatorHIDEvent(events: [ - .buttonDown(buttonType.hidButton), + let composite = FBSimulatorHIDEvent.composite([ + .button(direction: .down, button: buttonType.hidButton), .delay(duration), - .buttonUp(buttonType.hidButton) + .button(direction: .up, button: buttonType.hidButton) ]) return [.hidMergeable(composite)] } @@ -224,10 +224,10 @@ extension Button: BatchConvertible { extension Key: BatchConvertible { func toBatchPrimitives(context: BatchContext, logger: AxeLogger) async throws -> [BatchPrimitive] { if let duration { - let composite = FBSimulatorHIDEvent(events: [ - .keyDown(UInt32(keycode)), + let composite = FBSimulatorHIDEvent.composite([ + .keyboard(direction: .down, keyCode: UInt32(keycode)), .delay(duration), - .keyUp(UInt32(keycode)) + .keyboard(direction: .up, keyCode: UInt32(keycode)) ]) return [.hidMergeable(composite)] } @@ -249,7 +249,7 @@ extension KeySequence: BatchConvertible { } } - return [.hidMergeable(FBSimulatorHIDEvent(events: events))] + return [.hidMergeable(FBSimulatorHIDEvent.composite(events))] } } @@ -259,14 +259,14 @@ extension KeyCombo: BatchConvertible { var events: [FBSimulatorHIDEvent] = [] for modifier in parsedModifiers { - events.append(.keyDown(UInt32(modifier))) + events.append(.keyboard(direction: .down, keyCode: UInt32(modifier))) } events.append(.shortKeyPress(UInt32(key))) for modifier in parsedModifiers.reversed() { - events.append(.keyUp(UInt32(modifier))) + events.append(.keyboard(direction: .up, keyCode: UInt32(modifier))) } - return [.hidMergeable(FBSimulatorHIDEvent(events: events))] + return [.hidMergeable(FBSimulatorHIDEvent.composite(events))] } } @@ -299,7 +299,7 @@ extension Type: BatchConvertible { switch context.typeSubmissionMode { case .composite: - return [.hidMergeable(FBSimulatorHIDEvent(events: hidEvents))] + return [.hidMergeable(FBSimulatorHIDEvent.composite(hidEvents))] case .chunked: let chunkSize = max(1, context.typeChunkSize) var primitives: [BatchPrimitive] = [] @@ -307,11 +307,10 @@ extension Type: BatchConvertible { while start < hidEvents.count { let end = min(start + chunkSize, hidEvents.count) let chunkEvents = Array(hidEvents[start..(_ futures: FBFuture...) async throws -> [T] { - let futuresArr: [FBFuture] = futures - return try await values(futuresArr) - } - - /// Use this to receive results from multiple futures. The results are **ordered in the same order as passed futures**, so you can safely access them from - /// array by indexes. - /// - Note: We should *not* use @discardableResult, results should be dropped explicitly by the callee. - public static func values(_ futures: [FBFuture]) async throws -> [T] { - try await withThrowingTaskGroup(of: T?.self) { group in - for future in futures { - group.addTask { - try await value(future) - } - } - var results = [T]() - for try await result in group { - guard let result = result else { - throw FBFutureError.taskGroupReceivedNilResultInternalError - } - results.append(result) - } - return results - } - } - - /// Awaitable value that waits for publishing from the wrapped future - /// - Note: We should *not* use @discardableResult, results should be dropped explicitly by the callee. - public static func value(_ future: FBFuture) async throws -> T { - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - future.onQueue(BridgeQueues.futureSerialFullfillmentQueue, notifyOfCompletion: { resolvedFuture in - if let error = resolvedFuture.error { - continuation.resume(throwing: error) - } else if let value = resolvedFuture.result { - continuation.resume(returning: value as! T) - } else { - continuation.resume(throwing: FBFutureError.continuationFulfilledWithoutValues) - } - }) - } - } onCancel: { - future.cancel() - } - } - - /// Awaitable value that waits for publishing from the wrapped future. - /// This is convenient bridgeable overload for dealing with objc `NSArray`. - /// - Warning: This operation not safe (as most of objc bridge). That means you should be sure that type bridging will succeed. - public static func value(_ future: FBFuture) async throws -> [T] { - let objcValue = try await value(future as FBFuture) - // swiftlint:disable force_cast - return objcValue as! [T] - // swiftlint:enable force_cast - } - - /// Awaitable value that waits for publishing from the wrapped future. - /// This is convenient bridgeable overload for dealing with objc `NSDictionary`. - /// - Warning: This operation not safe (as most of objc bridge). That means you should be sure that type bridging will succeed. - public static func value(_ future: FBFuture) async throws -> [T: U] { - let objcValue = try await value(future) - // swiftlint:disable force_cast - return objcValue as! [T: U] - } - - /// Special overload for FBFuture representing void. - public static func value(_ future: FBFuture) async throws { - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - future.onQueue(BridgeQueues.futureSerialFullfillmentQueue, notifyOfCompletion: { resolvedFuture in - if let error = resolvedFuture.error { - continuation.resume(throwing: error) - } else { - // For NSNull futures, we just complete with void - continuation.resume(returning: ()) - } - }) - } - } onCancel: { - future.cancel() - } - } - - /// Special overload for FBFuture representing Bool. - public static func value(_ future: FBFuture) async throws -> Bool { - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - future.onQueue(BridgeQueues.futureSerialFullfillmentQueue, notifyOfCompletion: { resolvedFuture in - if let error = resolvedFuture.error { - continuation.resume(throwing: error) - } else if let value = resolvedFuture.result { - continuation.resume(returning: value.boolValue) - } else { - continuation.resume(throwing: FBFutureError.continuationFulfilledWithoutValues) - } - }) - } - } onCancel: { - future.cancel() - } - } - - /// Awaitable value that waits for publishing from the wrapped futureContext. - /// This is convenient bridgeable overload for dealing with objc `NSArray`. - public static func value(_ futureContext: FBFutureContext) async throws -> T { - return try await value(futureContext.future) - } -} - -// Typealias HACKS to satisfy AnyObject conformance for FBFuture generic constraints -// FBFuture requires T to be AnyObject. -// NSArray, NSNull, NSNumber are AnyObjects. -private typealias NSArray_TypeHack = NSArray -private typealias NSNull_TypeHack = NSNull -private typealias NSNumber_TypeHack = NSNumber \ No newline at end of file diff --git a/Sources/AXe/Utilities/GlobalSetup.swift b/Sources/AXe/Utilities/GlobalSetup.swift index 985f0a3..183174b 100644 --- a/Sources/AXe/Utilities/GlobalSetup.swift +++ b/Sources/AXe/Utilities/GlobalSetup.swift @@ -1,7 +1,6 @@ import Foundation import FBControlCore import FBSimulatorControl -import ObjectiveC // For objc_lookUpClass @MainActor func performGlobalSetup(logger: AxeLogger) async throws { @@ -10,9 +9,8 @@ func performGlobalSetup(logger: AxeLogger) async throws { // Check Xcode availability logger.info().log("Checking Xcode availability...") do { - let xcodePathFuture = FBXcodeDirectory.xcodeSelectDeveloperDirectory() - let xcodePath = try await FutureBridge.value(xcodePathFuture) - if xcodePath.length == 0 { + let xcodePath = try FBXcodeDirectory.resolveDeveloperDirectory() + if xcodePath.isEmpty { let errorMessage = "Xcode is not available (xcode-select path is empty). FBSimulatorControl may not function correctly." logger.error().log(errorMessage) throw CLIError(errorDescription: errorMessage) @@ -34,18 +32,6 @@ func performGlobalSetup(logger: AxeLogger) async throws { logger.info().log("Loading Xcode frameworks (including SimulatorKit)...") try FBSimulatorControlFrameworkLoader.xcodeFrameworks.loadPrivateFrameworks(logger) logger.info().log("Successfully loaded Xcode frameworks.") - - // Explicitly check if the critical class is now available - let clientClassName = "SimulatorKit.SimDeviceLegacyHIDClient" - let clientClass: AnyClass? = objc_lookUpClass(clientClassName) - if clientClass == nil { - let criticalMessage = "CRITICAL FAILURE: Class '\(clientClassName)' NOT FOUND by objc_lookUpClass even after FBSimulatorControlFrameworkLoader call." - logger.error().log(criticalMessage) - throw CLIError(errorDescription: criticalMessage) // This should halt execution if class isn't found - } else { - logger.info().log("CONFIRMED: Class '\(clientClassName)' was successfully found by objc_lookUpClass.") - } - } catch { let errorMessage = "Failed to load essential private frameworks: \(error.localizedDescription)" logger.error().log(errorMessage) diff --git a/Sources/AXe/Utilities/HIDBroker.swift b/Sources/AXe/Utilities/HIDBroker.swift new file mode 100644 index 0000000..90a0c40 --- /dev/null +++ b/Sources/AXe/Utilities/HIDBroker.swift @@ -0,0 +1,417 @@ +import Darwin +import Foundation +import FBControlCore +import FBSimulatorControl + +struct HIDBrokerPrimitive: Codable, Equatable { + enum Kind: String, Codable { + case down + case up + case delay + } + + let kind: Kind + let x: Double? + let y: Double? + let duration: Double? + + static func touch(_ kind: Kind, x: Double, y: Double) -> Self { + Self(kind: kind, x: x, y: y, duration: nil) + } + + static func delay(_ duration: Double) -> Self { + Self(kind: .delay, x: nil, y: nil, duration: duration) + } +} + +private struct HIDBrokerRequest: Codable { + let primitives: [HIDBrokerPrimitive] +} + +private struct HIDBrokerResponse: Codable { + let error: String? +} + +struct HIDBrokerBootIdentity: Equatable { + let processIdentifier: pid_t + let startSeconds: UInt64 + let startMicroseconds: UInt64 +} + +enum HIDBroker { + static let dtuhidMinimumBootUptime: TimeInterval = 10 + private static let maximumMessageBytes = 64 * 1024 + private static let idleTimeoutMilliseconds: Int32 = 60_000 + private static let serverIOTimeoutMilliseconds: Int = 2_000 + private static let clientWriteTimeoutMilliseconds: Int = 2_000 + private static let clientResponseTimeoutMilliseconds: Int = 30_000 + + static func sendTouchPrimitives( + _ primitives: [HIDBrokerPrimitive], + simulatorUDID: String + ) throws { + let endpoint = try endpointPath(simulatorUDID: simulatorUDID) + let request = HIDBrokerRequest(primitives: primitives) + var requestData = try JSONEncoder().encode(request) + requestData.append(0x0A) + guard requestData.count <= maximumMessageBytes else { + throw CLIError(errorDescription: "HID broker request exceeds the maximum size.") + } + + var lastError: Error? + for _ in 0..<50 { + do { + let descriptor = try connect(to: endpoint) + defer { Darwin.close(descriptor) } + try writeAll(requestData, to: descriptor) + let responseData = try readMessage(from: descriptor) + let response = try JSONDecoder().decode(HIDBrokerResponse.self, from: responseData) + if let error = response.error { + throw CLIError(errorDescription: error) + } + return + } catch { + lastError = error + try removeStaleEndpointIfSafe(endpoint) + try spawnBrokerIfNeeded(simulatorUDID: simulatorUDID, endpoint: endpoint) + usleep(100_000) + } + } + throw lastError ?? CLIError(errorDescription: "Unable to connect to the HID broker.") + } + + @MainActor + static func serve(simulatorUDID: String, logger: AxeLogger) async throws { + let endpoint = try endpointPath(simulatorUDID: simulatorUDID) + let listener = try makeListener(at: endpoint) + defer { + Darwin.close(listener) + try? removeOwnedSocket(endpoint) + } + + let bootIdentity = try currentBootIdentity(simulatorUDID: simulatorUDID) + let session = try await HIDInteractor.makeSession(for: simulatorUDID, logger: logger) + while true { + var pollDescriptor = pollfd(fd: listener, events: Int16(POLLIN), revents: 0) + let result = Darwin.poll(&pollDescriptor, 1, idleTimeoutMilliseconds) + if result == 0 { + return + } + guard result > 0 else { + if errno == EINTR { continue } + throw posixError("poll") + } + + let client = Darwin.accept(listener, nil, nil) + guard client >= 0 else { + if errno == EINTR { continue } + throw posixError("accept") + } + configureNoSignalPipe(client) + try configureSocketTimeouts( + client, + readMilliseconds: serverIOTimeoutMilliseconds, + writeMilliseconds: serverIOTimeoutMilliseconds + ) + guard shouldReuseSession( + sessionBootIdentity: bootIdentity, + currentBootIdentity: try currentBootIdentity(simulatorUDID: simulatorUDID) + ) else { + Darwin.close(client) + return + } + let shouldContinue = await handle(client: client, session: session, logger: logger) + Darwin.close(client) + if !shouldContinue { + return + } + } + } + + @MainActor + private static func handle( + client: Int32, + session: HIDInteractor.Session, + logger: AxeLogger + ) async -> Bool { + var peerUID: uid_t = 0 + var peerGID: gid_t = 0 + guard getpeereid(client, &peerUID, &peerGID) == 0, peerUID == getuid() else { + try? writeResponse(error: "HID broker rejected a client owned by another user.", to: client) + return true + } + + var shouldContinue = true + do { + let data = try readMessage(from: client) + let request = try JSONDecoder().decode(HIDBrokerRequest.self, from: data) + for primitive in request.primitives { + switch primitive.kind { + case .down, .up: + guard let x = primitive.x, let y = primitive.y else { + throw CLIError(errorDescription: "Touch primitive is missing coordinates.") + } + let direction: FBSimulatorHIDDirection = primitive.kind == .down ? .down : .up + do { + try await HIDInteractor.performHIDEvent( + .touch(direction: direction, x: x, y: y), + in: session, + logger: logger + ) + } catch { + shouldContinue = false + throw error + } + case .delay: + guard let duration = primitive.duration, duration >= 0, duration <= 10 else { + throw CLIError(errorDescription: "HID broker delay is invalid.") + } + try await Task.sleep(for: .seconds(duration)) + } + } + try writeResponse(error: nil, to: client) + } catch { + try? writeResponse(error: error.localizedDescription, to: client) + } + return shouldContinue + } + + static func endpointPath(simulatorUDID: String) throws -> String { + let uid = getuid() + let root = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + .resolvingSymlinksInPath() + .appendingPathComponent("axe-hid-\(uid)", isDirectory: true) + try ensurePrivateDirectory(root.path, uid: uid) + let developerDirectory = ProcessInfo.processInfo.environment["DEVELOPER_DIR"] ?? "default" + let identity = String(fnv1a64(developerDirectory), radix: 16) + let simulatorIdentity = String(fnv1a64(simulatorUDID), radix: 16) + let filename = "\(simulatorIdentity)-\(identity).sock" + let path = root.appendingPathComponent(filename).path + guard path.utf8.count < MemoryLayout.size(ofValue: sockaddr_un().sun_path) else { + throw CLIError(errorDescription: "HID broker socket path is too long.") + } + return path + } + + private static func ensurePrivateDirectory(_ path: String, uid: uid_t) throws { + var info = stat() + if lstat(path, &info) == 0 { + guard (info.st_mode & S_IFMT) == S_IFDIR, + info.st_uid == uid, + info.st_mode & (S_IRWXG | S_IRWXO) == 0 else { + throw CLIError(errorDescription: "HID broker directory is not a private owned directory.") + } + return + } + guard errno == ENOENT else { throw posixError("lstat") } + guard mkdir(path, S_IRWXU) == 0 || errno == EEXIST else { throw posixError("mkdir") } + guard chmod(path, S_IRWXU) == 0 else { throw posixError("chmod") } + } + + private static func makeAddress(path: String) throws -> sockaddr_un { + var address = sockaddr_un() + address.sun_family = sa_family_t(AF_UNIX) + address.sun_len = UInt8(MemoryLayout.size) + let bytes = Array(path.utf8) + [0] + guard bytes.count <= MemoryLayout.size(ofValue: address.sun_path) else { + throw CLIError(errorDescription: "HID broker socket path is too long.") + } + withUnsafeMutableBytes(of: &address.sun_path) { destination in + destination.copyBytes(from: bytes) + } + return address + } + + private static func makeListener(at path: String) throws -> Int32 { + let descriptor = socket(AF_UNIX, SOCK_STREAM, 0) + guard descriptor >= 0 else { throw posixError("socket") } + configureNoSignalPipe(descriptor) + do { + var address = try makeAddress(path: path) + let bindResult = withUnsafePointer(to: &address) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + Darwin.bind(descriptor, $0, socklen_t(MemoryLayout.size)) + } + } + guard bindResult == 0 else { throw posixError("bind") } + guard chmod(path, S_IRUSR | S_IWUSR) == 0 else { throw posixError("chmod") } + guard listen(descriptor, 8) == 0 else { throw posixError("listen") } + return descriptor + } catch { + Darwin.close(descriptor) + throw error + } + } + + private static func connect(to path: String) throws -> Int32 { + let descriptor = socket(AF_UNIX, SOCK_STREAM, 0) + guard descriptor >= 0 else { throw posixError("socket") } + configureNoSignalPipe(descriptor) + var address = try makeAddress(path: path) + let result = withUnsafePointer(to: &address) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + Darwin.connect(descriptor, $0, socklen_t(MemoryLayout.size)) + } + } + guard result == 0 else { + let error = posixError("connect") + Darwin.close(descriptor) + throw error + } + do { + try configureSocketTimeouts( + descriptor, + readMilliseconds: clientResponseTimeoutMilliseconds, + writeMilliseconds: clientWriteTimeoutMilliseconds + ) + } catch { + Darwin.close(descriptor) + throw error + } + return descriptor + } + + private static func spawnBroker(simulatorUDID: String) throws { + guard let executable = Bundle.main.executableURL else { + throw CLIError(errorDescription: "Unable to locate the AXe executable for the HID broker.") + } + let process = Process() + process.executableURL = executable + process.arguments = ["hid-broker", "--udid", simulatorUDID] + process.standardInput = FileHandle.nullDevice + process.standardOutput = FileHandle.nullDevice + process.standardError = FileHandle.nullDevice + try process.run() + } + + private static func spawnBrokerIfNeeded(simulatorUDID: String, endpoint: String) throws { + do { + let descriptor = try connect(to: endpoint) + Darwin.close(descriptor) + } catch { + try spawnBroker(simulatorUDID: simulatorUDID) + } + } + + private static func removeStaleEndpointIfSafe(_ path: String) throws { + do { + let descriptor = try connect(to: path) + Darwin.close(descriptor) + return + } catch { + if (error as NSError).code != Int(ECONNREFUSED) && (error as NSError).code != Int(ENOENT) { + throw error + } + } + try removeOwnedSocket(path) + } + + private static func removeOwnedSocket(_ path: String) throws { + var info = stat() + guard lstat(path, &info) == 0 else { + if errno == ENOENT { return } + throw posixError("lstat") + } + guard (info.st_mode & S_IFMT) == S_IFSOCK, info.st_uid == getuid() else { + throw CLIError(errorDescription: "Refusing to remove an unowned HID broker endpoint.") + } + guard unlink(path) == 0 || errno == ENOENT else { throw posixError("unlink") } + } + + static func readMessage(from descriptor: Int32) throws -> Data { + var data = Data() + var buffer = [UInt8](repeating: 0, count: 4096) + while data.count <= maximumMessageBytes { + let count = Darwin.read(descriptor, &buffer, buffer.count) + if count < 0 { + if errno == EINTR { continue } + if errno == EAGAIN || errno == EWOULDBLOCK { + throw CLIError(errorDescription: "HID broker read timed out.") + } + throw posixError("read") + } + if count == 0 { break } + if let newline = buffer[.. UInt64 { + string.utf8.reduce(14_695_981_039_346_656_037) { hash, byte in + (hash ^ UInt64(byte)) &* 1_099_511_628_211 + } + } + + private static func configureNoSignalPipe(_ descriptor: Int32) { + var enabled: Int32 = 1 + setsockopt(descriptor, SOL_SOCKET, SO_NOSIGPIPE, &enabled, socklen_t(MemoryLayout.size)) + } + + static func configureSocketTimeouts( + _ descriptor: Int32, + readMilliseconds: Int, + writeMilliseconds: Int + ) throws { + var readTimeout = socketTimeout(milliseconds: readMilliseconds) + guard setsockopt( + descriptor, + SOL_SOCKET, + SO_RCVTIMEO, + &readTimeout, + socklen_t(MemoryLayout.size) + ) == 0 else { + throw posixError("setsockopt(SO_RCVTIMEO)") + } + + var writeTimeout = socketTimeout(milliseconds: writeMilliseconds) + guard setsockopt( + descriptor, + SOL_SOCKET, + SO_SNDTIMEO, + &writeTimeout, + socklen_t(MemoryLayout.size) + ) == 0 else { + throw posixError("setsockopt(SO_SNDTIMEO)") + } + } + + private static func socketTimeout(milliseconds: Int) -> timeval { + timeval( + tv_sec: milliseconds / 1_000, + tv_usec: Int32((milliseconds % 1_000) * 1_000) + ) + } + + private static func posixError(_ operation: String) -> NSError { + NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [ + NSLocalizedDescriptionKey: "HID broker \(operation) failed: \(String(cString: strerror(errno)))" + ]) + } +} diff --git a/Sources/AXe/Utilities/HIDBrokerReadiness.swift b/Sources/AXe/Utilities/HIDBrokerReadiness.swift new file mode 100644 index 0000000..0a03c72 --- /dev/null +++ b/Sources/AXe/Utilities/HIDBrokerReadiness.swift @@ -0,0 +1,68 @@ +import Darwin +import Foundation +import FBControlCore + +extension HIDBroker { + static func currentBootIdentity(simulatorUDID: String) throws -> HIDBrokerBootIdentity { + guard let process = FBProcessFetcher().processes(withProcessName: "launchd_sim").first(where: { + $0.arguments.contains { $0.contains(simulatorUDID) } + }) else { + throw CLIError(errorDescription: "Simulator with UDID \(simulatorUDID) has no active launchd_sim process.") + } + + var processInfo = proc_bsdinfo() + let expectedSize = MemoryLayout.size + let actualSize = proc_pidinfo( + process.processIdentifier, + PROC_PIDTBSDINFO, + 0, + &processInfo, + Int32(expectedSize) + ) + guard actualSize == Int32(expectedSize) else { + throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [ + NSLocalizedDescriptionKey: "HID broker proc_pidinfo failed: \(String(cString: strerror(errno)))" + ]) + } + + return HIDBrokerBootIdentity( + processIdentifier: process.processIdentifier, + startSeconds: processInfo.pbi_start_tvsec, + startMicroseconds: processInfo.pbi_start_tvusec + ) + } + + static func dtuhidReadinessDelay( + bootIdentity: HIDBrokerBootIdentity, + now: Date, + minimumUptime: TimeInterval = dtuhidMinimumBootUptime + ) -> TimeInterval { + let startTime = TimeInterval(bootIdentity.startSeconds) + + (TimeInterval(bootIdentity.startMicroseconds) / 1_000_000) + let uptime = max(0, now.timeIntervalSince1970 - startTime) + return max(0, minimumUptime - uptime) + } + + static func waitForHIDReadiness( + bootIdentity: HIDBrokerBootIdentity, + isDTUHIDSelected: Bool, + now: () -> Date, + sleep: (TimeInterval) async throws -> Void + ) async throws { + guard isDTUHIDSelected else { + return + } + let delay = dtuhidReadinessDelay(bootIdentity: bootIdentity, now: now()) + guard delay > 0 else { + return + } + try await sleep(delay) + } + + static func shouldReuseSession( + sessionBootIdentity: HIDBrokerBootIdentity, + currentBootIdentity: HIDBrokerBootIdentity + ) -> Bool { + sessionBootIdentity == currentBootIdentity + } +} diff --git a/Sources/AXe/Utilities/HIDInteractor.swift b/Sources/AXe/Utilities/HIDInteractor.swift index 4b83c6f..df381a8 100644 --- a/Sources/AXe/Utilities/HIDInteractor.swift +++ b/Sources/AXe/Utilities/HIDInteractor.swift @@ -1,7 +1,6 @@ import Foundation import FBControlCore import FBSimulatorControl -import ObjectiveC // MARK: - HID Interactor @MainActor @@ -13,11 +12,8 @@ struct HIDInteractor { let hid: FBSimulatorHID } - - // Cache for HID connections per simulator private static var hidConnections: [String: FBSimulatorHID] = [:] - /// Configurable stabilization delay to ensure HID events are fully processed /// Can be set via AXE_HID_STABILIZATION_MS environment variable private static var stabilizationDelayMs: UInt64 { @@ -54,14 +50,28 @@ struct HIDInteractor { } logger.info().log("Simulator state verified: booted") + let bootIdentity = try HIDBroker.currentBootIdentity(simulatorUDID: simulatorUDID) let hid = try await getOrCreateHIDConnection(for: simulator, logger: logger) + let connectedBootIdentity = try HIDBroker.currentBootIdentity(simulatorUDID: simulatorUDID) + guard HIDBroker.shouldReuseSession( + sessionBootIdentity: bootIdentity, + currentBootIdentity: connectedBootIdentity + ) else { + hidConnections.removeValue(forKey: simulatorUDID) + throw CLIError(errorDescription: "Simulator rebooted while AXe was connecting to HID.") + } + try await HIDBroker.waitForHIDReadiness( + bootIdentity: connectedBootIdentity, + isDTUHIDSelected: hid.transportType == .dtuhid, + now: Date.init, + sleep: { delay in try await Task.sleep(for: .seconds(delay)) } + ) return Session(simulatorUDID: simulatorUDID, simulator: simulator, hid: hid) } static func performHIDEvent(_ event: FBSimulatorHIDEvent, in session: Session, logger: AxeLogger) async throws { logger.info().log("Performing HID event...") - let eventFuture = event.perform(on: session.hid) - _ = try await FutureBridge.value(eventFuture) + try await session.hid.send(event: event, logger: logger) logger.info().log("HID event performed successfully.") if stabilizationDelayMs > 0 { @@ -96,19 +106,19 @@ struct HIDInteractor { let movePoints = try compositeDragMovePoints(from: start, to: end, steps: steps) let stepDelay = duration / Double(steps) var events: [FBSimulatorHIDEvent] = [ - .touchDownAt(x: start.x, y: start.y), + .touch(direction: .down, x: start.x, y: start.y), .delay(initialHold) ] for point in movePoints { events.append(.delay(stepDelay)) - events.append(try touchMoveEvent(x: point.x, y: point.y)) + events.append(.touch(direction: .down, x: point.x, y: point.y)) } events.append(.delay(finalHold)) - events.append(.touchUpAt(x: end.x, y: end.y)) + events.append(.touch(direction: .up, x: end.x, y: end.y)) - return FBSimulatorHIDEvent(events: events) + return .composite(events) } static func compositeDragMovePoints( @@ -129,19 +139,6 @@ struct HIDInteractor { } } - private static func touchMoveEvent(x: Double, y: Double) throws -> FBSimulatorHIDEvent { - typealias TouchMoveIMP = @convention(c) (AnyClass, Selector, Double, Double) -> FBSimulatorHIDEvent - - let selector = NSSelectorFromString("touchMoveAtX:y:") - guard let method = class_getClassMethod(FBSimulatorHIDEvent.self, selector) else { - throw CLIError(errorDescription: "FBSimulatorHIDEvent does not support touch move events.") - } - - let implementation = method_getImplementation(method) - let touchMove = unsafeBitCast(implementation, to: TouchMoveIMP.self) - return touchMove(FBSimulatorHIDEvent.self, selector, x, y) - } - static func performCompositeDrag( from start: (x: Double, y: Double), to end: (x: Double, y: Double), @@ -186,8 +183,8 @@ struct HIDInteractor { try await Task.sleep(for: .seconds(preDelay)) } - let touchDownEvent = FBSimulatorHIDEvent.touchDownAt(x: point.x, y: point.y) - let touchUpEvent = FBSimulatorHIDEvent.touchUpAt(x: point.x, y: point.y) + let touchDownEvent = FBSimulatorHIDEvent.touch(direction: .down, x: point.x, y: point.y) + let touchUpEvent = FBSimulatorHIDEvent.touch(direction: .up, x: point.x, y: point.y) var didTouchDown = false do { @@ -209,7 +206,6 @@ struct HIDInteractor { } } - // Get or create a cached HID connection (matching CompanionLib's connectToHID behavior) private static func getOrCreateHIDConnection(for simulator: FBSimulator, logger: AxeLogger) async throws -> FBSimulatorHID { if let existingHID = hidConnections[simulator.udid] { @@ -218,8 +214,7 @@ struct HIDInteractor { } logger.info().log("Creating new HID connection for simulator \(simulator.udid)...") - let hidFuture = simulator.connectToHID() - let hid = try await FutureBridge.value(hidFuture) + let hid = try await simulator.connectToHID() hidConnections[simulator.udid] = hid logger.info().log("HID connection created and cached for simulator \(simulator.udid)") @@ -230,4 +225,4 @@ struct HIDInteractor { static func clearHIDConnections() { hidConnections.removeAll() } -} +} diff --git a/Sources/AXe/Utilities/TextToHIDEvents.swift b/Sources/AXe/Utilities/TextToHIDEvents.swift index b096346..d28e9aa 100644 --- a/Sources/AXe/Utilities/TextToHIDEvents.swift +++ b/Sources/AXe/Utilities/TextToHIDEvents.swift @@ -22,18 +22,18 @@ struct TextToHIDEvents { /// Creates key events for a character that doesn't require shift private static func simpleKeyEvent(keyCode: Int) -> [FBSimulatorHIDEvent] { return [ - .keyDown(UInt32(keyCode)), - .keyUp(UInt32(keyCode)) + .keyboard(direction: .down, keyCode: UInt32(keyCode)), + .keyboard(direction: .up, keyCode: UInt32(keyCode)) ] } /// Creates key events for a character that requires shift private static func shiftedKeyEvent(keyCode: Int) -> [FBSimulatorHIDEvent] { return [ - .keyDown(225), // Left Shift key down - .keyDown(UInt32(keyCode)), // Target key down - .keyUp(UInt32(keyCode)), // Target key up - .keyUp(225) // Left Shift key up + .keyboard(direction: .down, keyCode: 225), + .keyboard(direction: .down, keyCode: UInt32(keyCode)), + .keyboard(direction: .up, keyCode: UInt32(keyCode)), + .keyboard(direction: .up, keyCode: 225) ] } @@ -86,4 +86,4 @@ struct TextToHIDEvents { return events } -} \ No newline at end of file +} diff --git a/Sources/AXe/main.swift b/Sources/AXe/main.swift index 0b41ab4..47efd0d 100644 --- a/Sources/AXe/main.swift +++ b/Sources/AXe/main.swift @@ -31,7 +31,8 @@ struct Axe: AsyncParsableCommand { StreamVideo.self, RecordVideo.self, Screenshot.self, - Batch.self + Batch.self, + HIDBrokerCommand.self ] ) } diff --git a/Tests/AccessibilityFetcherTests.swift b/Tests/AccessibilityFetcherTests.swift new file mode 100644 index 0000000..d481cf8 --- /dev/null +++ b/Tests/AccessibilityFetcherTests.swift @@ -0,0 +1,285 @@ +import Foundation +import FBControlCore +import Testing +@testable import AXe + +@Suite("Accessibility Fetcher Tests") +@MainActor +struct AccessibilityFetcherTests { + @Test("Serializes nested accessibility arrays without changing their schema") + func serializesNestedAccessibilityArrays() throws { + let hierarchy: [[String: Any]] = [ + [ + "AXLabel": "Fixture", + "type": "Application", + "frame": ["x": 0, "y": 0, "width": 390, "height": 844], + "traits": ["Button"], + "children": [["AXLabel": "Tap Count: 0", "type": "StaticText"]], + ], + ] + + let data = try AccessibilityFetcher.serializeAccessibilityInfo(hierarchy) + let serialized = try #require(JSONSerialization.jsonObject(with: data) as? [[String: Any]]) + let root = try #require(serialized.first) + + #expect(root["AXLabel"] as? String == "Fixture") + #expect(root["type"] as? String == "Application") + #expect(root["traits"] as? [String] == ["Button"]) + #expect((root["children"] as? [[String: Any]])?.first?["AXLabel"] as? String == "Tap Count: 0") + #expect(root["elements"] == nil) + } + + @Test("Requests the complete legacy accessibility compatibility key set") + func requestsLegacyAccessibilityCompatibilityKeys() { + #expect(AccessibilityFetcher.accessibilityOutputKeys == [ + .label, + .frame, + .value, + .uniqueID, + .type, + .title, + .frameDict, + .help, + .enabled, + .customActions, + .role, + .roleDescription, + .subrole, + .contentRequired, + .pid, + .traits, + ]) + #expect(!AccessibilityFetcher.accessibilityRequestKeys.contains(.traits)) + #expect( + AccessibilityFetcher.accessibilityRequestKeys.union([.traits]) + == AccessibilityFetcher.accessibilityOutputKeys + ) + } + + @Test("Defaults unavailable traits recursively without changing non-element dictionaries") + func defaultsUnavailableTraitsRecursively() throws { + let hierarchy: [[String: Any]] = [[ + "type": "Application", + "frame": ["x": 0, "y": 0, "width": 390, "height": 844], + "children": [["type": "Button", "traits": ["Button"]]], + ]] + + let compatible = try #require( + AccessibilityFetcher.addingCompatibilityDefaults(to: hierarchy) as? [[String: Any]] + ) + let root = try #require(compatible.first) + let frame = try #require(root["frame"] as? [String: Any]) + let child = try #require((root["children"] as? [[String: Any]])?.first) + + #expect(root["traits"] as? [String] == []) + #expect(frame["traits"] == nil) + #expect(child["traits"] as? [String] == ["Button"]) + } + + @Test("Distinguishes transient root-only responses from populated hierarchies") + func detectsAccessibilityDescendants() throws { + let rootOnly = try JSONSerialization.data(withJSONObject: [[ + "type": "Application", + "children": [], + ]]) + let populated = try JSONSerialization.data(withJSONObject: [[ + "type": "Application", + "children": [["type": "Button", "children": []]], + ]]) + + #expect(try !AccessibilityFetcher.containsAccessibilityDescendant(in: rootOnly)) + #expect(try AccessibilityFetcher.containsAccessibilityDescendant(in: populated)) + } + + @Test("Serializes point lookup dictionaries without adding a response envelope") + func serializesPointLookupDictionary() throws { + let element: [String: Any] = [ + "AXLabel": "Tap Count: 0", + "AXUniqueId": "tap-count", + "type": "StaticText", + ] + + let data = try AccessibilityFetcher.serializeAccessibilityInfo(element) + let serialized = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + + #expect(serialized["AXLabel"] as? String == "Tap Count: 0") + #expect(serialized["AXUniqueId"] as? String == "tap-count") + #expect(serialized["elements"] == nil) + } + + @Test("Rejects accessibility payloads that are not dictionaries or arrays") + func rejectsInvalidAccessibilityPayload() { + #expect(throws: CLIError.self) { + try AccessibilityFetcher.serializeAccessibilityInfo("invalid") + } + } + + @Test("Restarts the canonical testmanagerd service with direct simctl arguments") + func restartsCanonicalTestManagerService() async throws { + var executableURL: URL? + var arguments: [String] = [] + var timeout: TimeInterval? + var waits: [Duration] = [] + let dependencies = AccessibilityRecoveryDependencies( + runProcess: { receivedURL, receivedArguments, receivedTimeout in + executableURL = receivedURL + arguments = receivedArguments + timeout = receivedTimeout + return 0 + }, + wait: { duration in waits.append(duration) } + ) + + try await AccessibilityFetcher.recoverTestManagerDaemon( + simulatorUDID: "TEST-UDID", + dependencies: dependencies + ) + + #expect(executableURL?.path == "/usr/bin/xcrun") + #expect(arguments == [ + "simctl", + "spawn", + "TEST-UDID", + "launchctl", + "kickstart", + "-k", + "user/foreground/com.apple.testmanagerd", + ]) + #expect(timeout == 3) + #expect(waits == [.milliseconds(250)]) + } + + @Test("Classifies only confirmed accessibility channel failures as recoverable") + func classifiesRecoverableChannelFailures() { + let disconnected = NSError( + domain: "Accessibility", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Channel disconnected"] + ) + let fileDescriptorExhaustion = NSError( + domain: "DTXConnectionServicesErrorDomain", + code: 24, + userInfo: nil + ) + let unrelatedPOSIXError = NSError(domain: NSPOSIXErrorDomain, code: 24) + let unrelatedDTXError = NSError(domain: "DTXConnectionServicesErrorDomain", code: 7) + let nearMatch = NSError( + domain: "Accessibility", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Previous channel disconnected unexpectedly"] + ) + let normalizedMatch = NSError( + domain: "Accessibility", + code: 1, + userInfo: [NSLocalizedDescriptionKey: " CHANNEL\n DISCONNECTED "] + ) + + #expect(AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: disconnected)) + #expect(AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: normalizedMatch)) + #expect(AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: fileDescriptorExhaustion)) + #expect(!AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: unrelatedPOSIXError)) + #expect(!AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: unrelatedDTXError)) + #expect(!AccessibilityFetcher.shouldRecoverTestManagerDaemon(from: nearMatch)) + } + + @Test("Recovers and reacquires the frontmost hierarchy once") + func retriesFrontmostHierarchyOnce() async throws { + var operationCount = 0 + var recoveryCount = 0 + let dependencies = AccessibilityRecoveryDependencies( + runProcess: { _, _, _ in + recoveryCount += 1 + return 0 + }, + wait: { _ in } + ) + let disconnected = NSError( + domain: "Accessibility", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Channel disconnected"] + ) + + let result = try await AccessibilityFetcher.retryingAfterTestManagerRecovery( + simulatorUDID: "TEST-UDID", + logger: AxeLogger(), + dependencies: dependencies + ) { + operationCount += 1 + if operationCount == 1 { + throw disconnected + } + return "reacquired" + } + + #expect(result == "reacquired") + #expect(operationCount == 2) + #expect(recoveryCount == 1) + } + + @Test("Does not loop when the retry also disconnects") + func boundsRecoveryToOneRetry() async { + var operationCount = 0 + var recoveryCount = 0 + let dependencies = AccessibilityRecoveryDependencies( + runProcess: { _, _, _ in + recoveryCount += 1 + return 0 + }, + wait: { _ in } + ) + let disconnected = NSError( + domain: "Accessibility", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Channel disconnected"] + ) + + do { + _ = try await AccessibilityFetcher.retryingAfterTestManagerRecovery( + simulatorUDID: "TEST-UDID", + logger: AxeLogger(), + dependencies: dependencies + ) { + operationCount += 1 + throw disconnected + } as String + Issue.record("Expected the retry to fail") + } catch { + #expect(error.localizedDescription == "Channel disconnected") + } + + #expect(operationCount == 2) + #expect(recoveryCount == 1) + } + + @Test("Preserves unrelated errors without recovery") + func preservesUnrelatedErrors() async { + var recoveryCount = 0 + let dependencies = AccessibilityRecoveryDependencies( + runProcess: { _, _, _ in + recoveryCount += 1 + return 0 + }, + wait: { _ in } + ) + let unrelated = NSError( + domain: "Accessibility", + code: 99, + userInfo: [NSLocalizedDescriptionKey: "Unrelated failure"] + ) + + do { + _ = try await AccessibilityFetcher.retryingAfterTestManagerRecovery( + simulatorUDID: "TEST-UDID", + logger: AxeLogger(), + dependencies: dependencies + ) { + throw unrelated + } as String + Issue.record("Expected the operation to fail") + } catch { + #expect(error.localizedDescription == "Unrelated failure") + } + + #expect(recoveryCount == 0) + } +} diff --git a/Tests/ButtonTests.swift b/Tests/ButtonTests.swift index 63d8d00..bb46f43 100644 --- a/Tests/ButtonTests.swift +++ b/Tests/ButtonTests.swift @@ -3,6 +3,109 @@ import Foundation @Suite("Button Command Tests", .serialized, .enabled(if: isE2EEnabled)) struct ButtonTests { + private func simulatorUDID() throws -> String { + try #require(defaultSimulatorUDID, "AXE_E2E_SIMULATOR_UDID is required for button E2E tests") + } + + private func springBoardState(_ name: String) async throws -> Int { + let simulatorUDID = try simulatorUDID() + let result = try await CommandRunner.run( + "xcrun simctl spawn \(simulatorUDID) notifyutil -g com.apple.springboard.\(name)" + ) + return try #require( + result.output.split(whereSeparator: { $0.isWhitespace }).last.flatMap { Int($0) }, + "SpringBoard \(name) state must be observable" + ) + } + + private func waitForSpringBoardState(_ name: String, expectedState: Int) async throws -> Int { + let deadline = Date().addingTimeInterval(5) + var currentState = try await springBoardState(name) + while currentState != expectedState && Date() < deadline { + try await Task.sleep(for: .milliseconds(100)) + currentState = try await springBoardState(name) + } + return currentState + } + + private func isXcode27() async throws -> Bool { + let version = try await CommandRunner.run("xcodebuild -version") + let majorVersion = version.output + .split(whereSeparator: { $0.isWhitespace }) + .dropFirst() + .first + .flatMap { Int($0.split(separator: ".").first ?? "") } + return majorVersion == 27 + } + + private func waitForDeviceHIDServiceIfRequired() async throws { + guard try await isXcode27() else { + return + } + + let simulatorUDID = try simulatorUDID() + let deadline = Date().addingTimeInterval(10) + while Date() < deadline { + let result = try await CommandRunner.run( + "launchd_sim_pid=$(pgrep -f '^launchd_sim .*/Devices/\(simulatorUDID)/data/var/run/launchd_bootstrap.plist$' | head -1); " + + "test -n \"$launchd_sim_pid\" && pgrep -P \"$launchd_sim_pid\" -x dtuhidd", + allowFailure: true + ) + if result.exitCode == 0 { + return + } + try await Task.sleep(for: .milliseconds(100)) + } + + throw TestError.unexpectedState("Device Hub simulator DTUHID service did not become ready") + } + + private func waitForDeviceHIDAttachmentAfterBootIfRequired() async throws { + try await waitForDeviceHIDServiceIfRequired() + guard try await isXcode27() else { + return + } + try await Task.sleep(for: .seconds(10)) + } + + private func rebootSimulator() async throws { + let simulatorUDID = try simulatorUDID() + _ = try await CommandRunner.run("xcrun simctl shutdown \(simulatorUDID)", allowFailure: true) + _ = try await CommandRunner.run("xcrun simctl boot \(simulatorUDID)") + _ = try await CommandRunner.run("xcrun simctl bootstatus \(simulatorUDID) -b", timeout: 120) + try await waitForDeviceHIDAttachmentAfterBootIfRequired() + #expect( + try await waitForSpringBoardState("lockstate", expectedState: 0) == 0, + "A freshly booted simulator should be unlocked" + ) + } + + private func prepareUnlockedSimulator() async throws { + if try await springBoardState("lockstate") != 0 { + try await rebootSimulator() + } else { + try await waitForDeviceHIDServiceIfRequired() + } + } + + private func runButtonCommand(_ command: String) async throws -> Duration { + try await prepareUnlockedSimulator() + let startTime = ContinuousClock.now + do { + try await TestHelpers.runAxeCommand(command, simulatorUDID: defaultSimulatorUDID) + } catch { + try? await rebootSimulator() + throw error + } + return ContinuousClock.now - startTime + } + + private func pressLockingButton(_ command: String) async throws { + _ = try await runButtonCommand(command) + #expect(try await waitForSpringBoardState("lockstate", expectedState: 1) == 1) + try await rebootSimulator() + } + @Test("Home button press") func homeButtonPress() async throws { // Arrange @@ -17,31 +120,35 @@ struct ButtonTests { @Test("Lock button press") func lockButtonPress() async throws { - // Act - try await TestHelpers.runAxeCommand("button lock", simulatorUDID: defaultSimulatorUDID) - - // Note: Cannot assert UI state as lock button locks the device - // This test verifies the command executes without error + try await pressLockingButton("button lock") } @Test("Side button press") func sideButtonPress() async throws { - // Act - try await TestHelpers.runAxeCommand("button side-button", simulatorUDID: defaultSimulatorUDID) - - // Note: Side button behavior varies by device - // This test verifies the command executes without error + _ = try await runButtonCommand("button side-button") + #expect( + try await waitForSpringBoardState("hasBlankedScreen", expectedState: 1) == 1, + "A short side-button press should blank the simulator display" + ) + try await rebootSimulator() + } + + @Test("Apple Pay button command executes") + func applePayButtonPress() async throws { + try await waitForDeviceHIDServiceIfRequired() + try await TestHelpers.runAxeCommand("button apple-pay", simulatorUDID: defaultSimulatorUDID) + } + + @Test("Siri button command executes") + func siriButtonPress() async throws { + try await waitForDeviceHIDServiceIfRequired() + try await TestHelpers.runAxeCommand("button siri", simulatorUDID: defaultSimulatorUDID) } @Test("Button press with duration") func buttonPressWithDuration() async throws { - // Act - let startTime = Date() - try await TestHelpers.runAxeCommand("button lock --duration 2", simulatorUDID: defaultSimulatorUDID) - let endTime = Date() - - // Assert timing - let duration = endTime.timeIntervalSince(startTime) - #expect(duration >= 2.0, "Command should take at least 2 seconds with delays") + let duration = try await runButtonCommand("button lock --duration 2") + #expect(duration >= .seconds(2), "Command should hold the button for at least 2 seconds") + try await rebootSimulator() } } diff --git a/Tests/HIDBrokerTests.swift b/Tests/HIDBrokerTests.swift new file mode 100644 index 0000000..d673001 --- /dev/null +++ b/Tests/HIDBrokerTests.swift @@ -0,0 +1,195 @@ +import Darwin +import Foundation +import Testing +@testable import AXe + +@Suite("HID Broker Tests") +struct HIDBrokerTests { + @Test("Broker endpoint is deterministic, bounded, and simulator-specific") + func endpointIdentity() throws { + let first = try HIDBroker.endpointPath(simulatorUDID: "simulator-a") + let repeated = try HIDBroker.endpointPath(simulatorUDID: "simulator-a") + let second = try HIDBroker.endpointPath(simulatorUDID: "simulator-b") + + #expect(first == repeated) + #expect(first != second) + #expect(first.utf8.count < MemoryLayout.size(ofValue: sockaddr_un().sun_path)) + + let attributes = try FileManager.default.attributesOfItem( + atPath: URL(fileURLWithPath: first).deletingLastPathComponent().path + ) + #expect(attributes[.ownerAccountID] as? NSNumber == NSNumber(value: getuid())) + #expect((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o700) + } + + @Test("Touch primitives preserve their wire values") + func primitiveRoundTrip() throws { + let primitives = [ + HIDBrokerPrimitive.touch(.down, x: 12.5, y: 42), + HIDBrokerPrimitive.delay(0.25), + HIDBrokerPrimitive.touch(.up, x: 13, y: 43.5), + ] + + let data = try JSONEncoder().encode(primitives) + #expect(try JSONDecoder().decode([HIDBrokerPrimitive].self, from: data) == primitives) + } + + @Test("Broker sessions are scoped to one simulator boot") + func bootIdentityScopesSession() { + let original = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 100, + startMicroseconds: 200 + ) + let rebootedWithNewPID = HIDBrokerBootIdentity( + processIdentifier: 43, + startSeconds: 101, + startMicroseconds: 200 + ) + let rebootedWithReusedPID = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 101, + startMicroseconds: 200 + ) + + #expect(HIDBroker.shouldReuseSession(sessionBootIdentity: original, currentBootIdentity: original)) + #expect(!HIDBroker.shouldReuseSession( + sessionBootIdentity: original, + currentBootIdentity: rebootedWithNewPID + )) + #expect(!HIDBroker.shouldReuseSession( + sessionBootIdentity: original, + currentBootIdentity: rebootedWithReusedPID + )) + } + + @Test("DTUHID waits only for the remaining simulator boot readiness window") + func dtuhidReadinessDelay() { + let bootIdentity = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 100, + startMicroseconds: 250_000 + ) + + #expect(HIDBroker.dtuhidReadinessDelay( + bootIdentity: bootIdentity, + now: Date(timeIntervalSince1970: 105.25) + ) == 5) + #expect(HIDBroker.dtuhidReadinessDelay( + bootIdentity: bootIdentity, + now: Date(timeIntervalSince1970: 110.25) + ) == 0) + #expect(HIDBroker.dtuhidReadinessDelay( + bootIdentity: bootIdentity, + now: Date(timeIntervalSince1970: 120.25) + ) == 0) + } + + @Test("DTUHID readiness timing handles a clock earlier than process start") + func dtuhidReadinessDelayWithEarlierClock() { + let bootIdentity = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 100, + startMicroseconds: 0 + ) + + #expect(HIDBroker.dtuhidReadinessDelay( + bootIdentity: bootIdentity, + now: Date(timeIntervalSince1970: 99) + ) == HIDBroker.dtuhidMinimumBootUptime) + } + + @Test("DTUHID readiness uses the injected boot identity and clock") + func dtuhidReadinessWait() async throws { + let bootIdentity = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 100, + startMicroseconds: 250_000 + ) + var delays: [TimeInterval] = [] + + try await HIDBroker.waitForHIDReadiness( + bootIdentity: bootIdentity, + isDTUHIDSelected: true, + now: { Date(timeIntervalSince1970: 106.25) }, + sleep: { delays.append($0) } + ) + + #expect(delays == [4]) + } + + @Test("Legacy Indigo transport is never delayed") + func indigoReadinessDoesNotWait() async throws { + let bootIdentity = HIDBrokerBootIdentity( + processIdentifier: 42, + startSeconds: 100, + startMicroseconds: 0 + ) + var didSleep = false + + try await HIDBroker.waitForHIDReadiness( + bootIdentity: bootIdentity, + isDTUHIDSelected: false, + now: { Date(timeIntervalSince1970: 100) }, + sleep: { _ in didSleep = true } + ) + + #expect(!didSleep) + } + + @Test("Partial messages cannot block a broker read indefinitely") + func partialMessageReadTimesOut() throws { + var descriptors = [Int32](repeating: -1, count: 2) + #expect(socketpair(AF_UNIX, SOCK_STREAM, 0, &descriptors) == 0) + defer { + Darwin.close(descriptors[0]) + Darwin.close(descriptors[1]) + } + + try HIDBroker.configureSocketTimeouts( + descriptors[0], + readMilliseconds: 100, + writeMilliseconds: 100 + ) + var partialByte: UInt8 = 0x7B + #expect(Darwin.write(descriptors[1], &partialByte, 1) == 1) + + let start = Date() + do { + _ = try HIDBroker.readMessage(from: descriptors[0]) + Issue.record("A partial message should time out") + } catch {} + #expect(Date().timeIntervalSince(start) < 1) + } + + @Test("A client that does not read cannot block a broker write indefinitely") + func unreadResponseWriteTimesOut() throws { + var descriptors = [Int32](repeating: -1, count: 2) + #expect(socketpair(AF_UNIX, SOCK_STREAM, 0, &descriptors) == 0) + defer { + Darwin.close(descriptors[0]) + Darwin.close(descriptors[1]) + } + + var bufferBytes: Int32 = 4_096 + #expect(setsockopt( + descriptors[0], + SOL_SOCKET, + SO_SNDBUF, + &bufferBytes, + socklen_t(MemoryLayout.size) + ) == 0) + try HIDBroker.configureSocketTimeouts( + descriptors[0], + readMilliseconds: 100, + writeMilliseconds: 100 + ) + + let start = Date() + do { + try HIDBroker.writeAll(Data(repeating: 0x41, count: 1_048_576), to: descriptors[0]) + Issue.record("An unread response should time out") + } catch {} + #expect(Date().timeIntervalSince(start) < 1) + } +} diff --git a/Tests/KeyComboTests.swift b/Tests/KeyComboTests.swift index 925f1e9..bf614b0 100644 --- a/Tests/KeyComboTests.swift +++ b/Tests/KeyComboTests.swift @@ -21,7 +21,7 @@ struct KeyComboTests { let textField = UIStateParser.findElement(in: uiState) { $0.type == "TextField" } #expect(textField != nil) #expect( - textField?.value == nil || textField?.value == "", + textField?.value == nil || textField?.value == "" || textField?.value == "empty", "Text field should be cleared after Cmd+A then Backspace" ) } diff --git a/Tests/StreamVideoTests.swift b/Tests/StreamVideoTests.swift index 6458399..625e2b6 100644 --- a/Tests/StreamVideoTests.swift +++ b/Tests/StreamVideoTests.swift @@ -1,5 +1,19 @@ -import Testing +import Darwin import Foundation +import Testing + +private final class ProcessIdentifierBox: @unchecked Sendable { + private let lock = NSLock() + private var storedValue: pid_t? + + var value: pid_t? { + lock.withLock { storedValue } + } + + func store(_ value: pid_t) { + lock.withLock { storedValue = value } + } +} @Suite("Stream Video Command Tests", .serialized, .enabled(if: isE2EEnabled)) struct StreamVideoTests { @@ -64,13 +78,22 @@ struct StreamVideoTests { @Test("Stream video can be cancelled gracefully") func streamVideoCancellation() async throws { + let processIdentifier = ProcessIdentifierBox() let task = Task { - try await streamVideoForDuration(format: "mjpeg", fps: 30, duration: 60.0) + try await streamVideoForDuration( + format: "mjpeg", + fps: 30, + duration: 60.0, + onStart: { processIdentifier.store($0) } + ) } try await Task.sleep(nanoseconds: 500_000_000) task.cancel() _ = await task.result + + let pid = try #require(processIdentifier.value, "Stream process should have started") + #expect(kill(pid, 0) == -1 && errno == ESRCH, "Cancelling the test task must not leak the AXe subprocess") } @Test("Stream video rejects invalid formats") @@ -102,21 +125,22 @@ struct StreamVideoTests { fps: Int = 10, quality: Int = 80, scale: Double = 1.0, - duration: TimeInterval = 2.0 + duration: TimeInterval = 2.0, + onStart: @escaping @Sendable (pid_t) -> Void = { _ in } ) async throws -> (output: String, data: Data, dataString: String, dataSize: Int, exitCode: Int32) { - var command = "stream-video" - command += " --format \(format)" - command += " --fps \(fps)" - command += " --quality \(quality) --scale \(scale)" - let udid = try TestHelpers.requireSimulatorUDID() let axePath = try TestHelpers.getAxePath() - let fullCommand = "\(axePath) \(command) --udid \(udid)" - let process = Process() - process.executableURL = URL(fileURLWithPath: "/bin/bash") - process.arguments = ["-c", fullCommand] + process.executableURL = URL(fileURLWithPath: axePath) + process.arguments = [ + "stream-video", + "--format", format, + "--fps", String(fps), + "--quality", String(quality), + "--scale", String(scale), + "--udid", udid, + ] let outputPipe = Pipe() let errorPipe = Pipe() @@ -128,8 +152,23 @@ struct StreamVideoTests { } try process.run() - - try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) + onStart(process.processIdentifier) + + do { + try await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000)) + } catch { + process.terminate() + let cleanupTask = Task { + try await TestHelpers.waitForProcessExit( + process, + timeout: 10.0, + description: "cancelled stream-video process did not exit" + ) + } + try await cleanupTask.value + _ = try? await stdoutReadTask.value + throw error + } process.terminate() diff --git a/Tests/TestUIState.swift b/Tests/TestUIState.swift new file mode 100644 index 0000000..6c79ba3 --- /dev/null +++ b/Tests/TestUIState.swift @@ -0,0 +1,184 @@ +import Foundation + +struct UIElement: Decodable { + let type: String + let frame: Frame? + let children: [UIElement]? + let role: String? + let enabled: Bool? + let title: String? + let subrole: String? + let contentRequired: Bool? + let roleDescription: String? + let helpText: String? + let AXFrame: String? + let customActions: [String]? + let AXLabel: String? + let AXValue: String? + let AXUniqueId: String? + let AXIdentifier: String? + + enum CodingKeys: String, CodingKey { + case type + case frame + case children + case role + case enabled + case title + case subrole + case contentRequired = "content_required" + case roleDescription = "role_description" + case helpText = "help" + case AXFrame + case customActions = "custom_actions" + case AXLabel + case AXValue + case AXUniqueId + case AXIdentifier + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + type = try container.decode(String.self, forKey: .type) + frame = try container.decodeIfPresent(Frame.self, forKey: .frame) + children = try container.decodeIfPresent([UIElement].self, forKey: .children) + role = try Self.decodeOptionalScalarString(from: container, forKey: .role) + enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) + title = try Self.decodeOptionalScalarString(from: container, forKey: .title) + subrole = try Self.decodeOptionalScalarString(from: container, forKey: .subrole) + contentRequired = try container.decodeIfPresent(Bool.self, forKey: .contentRequired) + roleDescription = try Self.decodeOptionalScalarString(from: container, forKey: .roleDescription) + helpText = try Self.decodeOptionalScalarString(from: container, forKey: .helpText) + AXFrame = try Self.decodeOptionalScalarString(from: container, forKey: .AXFrame) + customActions = try container.decodeIfPresent([String].self, forKey: .customActions) + AXLabel = try Self.decodeOptionalScalarString(from: container, forKey: .AXLabel) + AXValue = try Self.decodeOptionalScalarString(from: container, forKey: .AXValue) + AXUniqueId = try Self.decodeOptionalScalarString(from: container, forKey: .AXUniqueId) + AXIdentifier = try Self.decodeOptionalScalarString(from: container, forKey: .AXIdentifier) + } + + private static func decodeOptionalScalarString( + from container: KeyedDecodingContainer, + forKey key: CodingKeys + ) throws -> String? { + if !container.contains(key) { + return nil + } + if try container.decodeNil(forKey: key) { + return nil + } + if let value = try? container.decode(String.self, forKey: key) { + return value + } + if let value = try? container.decode(Int.self, forKey: key) { + return String(value) + } + if let value = try? container.decode(Double.self, forKey: key) { + return String(value) + } + if let value = try? container.decode(Bool.self, forKey: key) { + return String(value) + } + return nil + } + + struct Frame: Decodable { + let x: Double + let y: Double + let width: Double + let height: Double + } + + var label: String? { + AXLabel + } + + var value: String? { + AXValue + } + + var identifier: String? { + AXUniqueId ?? AXIdentifier + } +} + +struct UIStateParser { + static func parseDescribeUIRoots(_ jsonString: String) throws -> [UIElement] { + var jsonContent = jsonString + + if let jsonStart = jsonString.firstIndex(where: { $0 == "[" || $0 == "{" }) { + jsonContent = String(jsonString[jsonStart...]) + } + + guard let data = jsonContent.data(using: .utf8) else { + throw TestError.invalidJSON("Could not convert string to data") + } + + let decoder = JSONDecoder() + if let elements = try? decoder.decode([UIElement].self, from: data) { + return elements + } + + let element = try decoder.decode(UIElement.self, from: data) + return [element] + } + + static func parseDescribeUIOutput(_ jsonString: String) throws -> UIElement { + let elements = try parseDescribeUIRoots(jsonString) + guard let firstElement = elements.first else { + throw TestError.invalidJSON("No UI elements found") + } + return firstElement + } + + static func findElement(in root: UIElement, matching predicate: (UIElement) -> Bool) -> UIElement? { + if predicate(root) { + return root + } + + if let children = root.children { + for child in children { + if let found = findElement(in: child, matching: predicate) { + return found + } + } + } + + return nil + } + + static func findElement(in root: UIElement, withIdentifier identifier: String) -> UIElement? { + findElement(in: root) { element in + element.identifier == identifier + } + } + + static func findElementByLabel(in root: UIElement, label: String) -> UIElement? { + findElement(in: root) { element in + element.label == label + } + } + + static func findElementContainingLabel(in root: UIElement, containing: String) -> UIElement? { + findElement(in: root) { element in + element.label?.contains(containing) == true + } + } + + static func findElement(in roots: [UIElement], matching predicate: (UIElement) -> Bool) -> UIElement? { + for root in roots { + if let element = findElement(in: root, matching: predicate) { + return element + } + } + + return nil + } + + static func findElement(in roots: [UIElement], withIdentifier identifier: String) -> UIElement? { + findElement(in: roots) { element in + element.identifier == identifier + } + } +} diff --git a/Tests/TestUtilities.swift b/Tests/TestUtilities.swift index 2246165..ea42c62 100644 --- a/Tests/TestUtilities.swift +++ b/Tests/TestUtilities.swift @@ -95,194 +95,6 @@ struct CommandRunner { } } -// MARK: - UI State Parsing - -struct UIElement: Decodable { - let type: String - let frame: Frame? - let children: [UIElement]? - let role: String? - let enabled: Bool? - let title: String? - let subrole: String? - let contentRequired: Bool? - let roleDescription: String? - let helpText: String? - let AXFrame: String? - let customActions: [String]? - - // The actual JSON uses AX prefixed fields - let AXLabel: String? - let AXValue: String? - let AXUniqueId: String? - let AXIdentifier: String? - - enum CodingKeys: String, CodingKey { - case type - case frame - case children - case role - case enabled - case title - case subrole - case contentRequired = "content_required" - case roleDescription = "role_description" - case helpText = "help" - case AXFrame - case customActions = "custom_actions" - case AXLabel - case AXValue - case AXUniqueId - case AXIdentifier - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - type = try container.decode(String.self, forKey: .type) - frame = try container.decodeIfPresent(Frame.self, forKey: .frame) - children = try container.decodeIfPresent([UIElement].self, forKey: .children) - role = try Self.decodeOptionalScalarString(from: container, forKey: .role) - enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) - title = try Self.decodeOptionalScalarString(from: container, forKey: .title) - subrole = try Self.decodeOptionalScalarString(from: container, forKey: .subrole) - contentRequired = try container.decodeIfPresent(Bool.self, forKey: .contentRequired) - roleDescription = try Self.decodeOptionalScalarString(from: container, forKey: .roleDescription) - helpText = try Self.decodeOptionalScalarString(from: container, forKey: .helpText) - AXFrame = try Self.decodeOptionalScalarString(from: container, forKey: .AXFrame) - customActions = try container.decodeIfPresent([String].self, forKey: .customActions) - AXLabel = try Self.decodeOptionalScalarString(from: container, forKey: .AXLabel) - AXValue = try Self.decodeOptionalScalarString(from: container, forKey: .AXValue) - AXUniqueId = try Self.decodeOptionalScalarString(from: container, forKey: .AXUniqueId) - AXIdentifier = try Self.decodeOptionalScalarString(from: container, forKey: .AXIdentifier) - } - - private static func decodeOptionalScalarString( - from container: KeyedDecodingContainer, - forKey key: CodingKeys - ) throws -> String? { - if !container.contains(key) { - return nil - } - if try container.decodeNil(forKey: key) { - return nil - } - if let value = try? container.decode(String.self, forKey: key) { - return value - } - if let value = try? container.decode(Int.self, forKey: key) { - return String(value) - } - if let value = try? container.decode(Double.self, forKey: key) { - return String(value) - } - if let value = try? container.decode(Bool.self, forKey: key) { - return String(value) - } - return nil - } - - struct Frame: Decodable { - let x: Double - let y: Double - let width: Double - let height: Double - } - - // Provide convenient accessors - var label: String? { - return AXLabel - } - - var value: String? { - return AXValue - } - - var identifier: String? { - return AXUniqueId ?? AXIdentifier - } -} - -struct UIStateParser { - static func parseDescribeUIRoots(_ jsonString: String) throws -> [UIElement] { - var jsonContent = jsonString - - if let jsonStart = jsonString.firstIndex(where: { $0 == "[" || $0 == "{" }) { - jsonContent = String(jsonString[jsonStart...]) - } - - guard let data = jsonContent.data(using: .utf8) else { - throw TestError.invalidJSON("Could not convert string to data") - } - - let decoder = JSONDecoder() - if let elements = try? decoder.decode([UIElement].self, from: data) { - return elements - } - - let element = try decoder.decode(UIElement.self, from: data) - return [element] - } - - static func parseDescribeUIOutput(_ jsonString: String) throws -> UIElement { - let elements = try parseDescribeUIRoots(jsonString) - guard let firstElement = elements.first else { - throw TestError.invalidJSON("No UI elements found") - } - return firstElement - } - - static func findElement(in root: UIElement, matching predicate: (UIElement) -> Bool) -> UIElement? { - if predicate(root) { - return root - } - - if let children = root.children { - for child in children { - if let found = findElement(in: child, matching: predicate) { - return found - } - } - } - - return nil - } - - static func findElement(in root: UIElement, withIdentifier identifier: String) -> UIElement? { - return findElement(in: root) { element in - element.identifier == identifier - } - } - - static func findElementByLabel(in root: UIElement, label: String) -> UIElement? { - return findElement(in: root) { element in - element.label == label - } - } - - static func findElementContainingLabel(in root: UIElement, containing: String) -> UIElement? { - return findElement(in: root) { element in - element.label?.contains(containing) == true - } - } - - static func findElement(in roots: [UIElement], matching predicate: (UIElement) -> Bool) -> UIElement? { - for root in roots { - if let element = findElement(in: root, matching: predicate) { - return element - } - } - - return nil - } - - static func findElement(in roots: [UIElement], withIdentifier identifier: String) -> UIElement? { - findElement(in: roots) { element in - element.identifier == identifier - } - } -} - // MARK: - Test Helpers struct TestHelpers { @@ -419,6 +231,36 @@ struct TestHelpers { // Launch to specific screen _ = try await CommandRunner.run("xcrun simctl launch \(udid) com.cameroncooke.AxePlayground --launch-arg \"screen=\(screen)\"") + if screen == "text-input" { + let deadline = Date().addingTimeInterval(10) + var lastFocusRequest: Date? + while Date() < deadline { + if let state = try? await getUIState(simulatorUDID: udid) { + let focusIndicator = UIStateParser.findElement(in: state) { element in + element.identifier == "text-input-screen" && element.label == "✏️ Typing active" + } + if focusIndicator != nil { + return + } + + let shouldRequestFocus = lastFocusRequest.map { + Date().timeIntervalSince($0) >= 1 + } ?? true + if shouldRequestFocus, + let textFieldFrame = UIStateParser.findElement(in: state, matching: { $0.type == "TextField" })?.frame { + lastFocusRequest = Date() + let centerX = textFieldFrame.x + (textFieldFrame.width / 2) + let centerY = textFieldFrame.y + (textFieldFrame.height / 2) + _ = try? await runAxeCommand( + "tap -x \(centerX) -y \(centerY)", + simulatorUDID: udid + ) + } + } + try await Task.sleep(nanoseconds: 200_000_000) + } + throw TestError.unexpectedState("Text input fixture did not become focused and ready") + } try await Task.sleep(nanoseconds: 2_000_000_000) } @@ -436,7 +278,16 @@ struct TestHelpers { throw TestError.unexpectedState("axe describe-ui command failed with exit code \(result.exitCode). Output: \(result.output)") } - return try UIStateParser.parseDescribeUIOutput(result.output) + let roots = try UIStateParser.parseDescribeUIRoots(result.output) + if let playgroundRoot = roots.first(where: { root in + root.type == "Application" && root.label == "AxePlayground" + }) { + return playgroundRoot + } + guard let firstRoot = roots.first else { + throw TestError.invalidJSON("No UI elements found") + } + return firstRoot } static func waitForLandscapeCoordinateFixtureLayout(timeout: TimeInterval) async throws -> UIElement { diff --git a/Tests/VideoFrameUtilitiesTests.swift b/Tests/VideoFrameUtilitiesTests.swift new file mode 100644 index 0000000..f5c9ccb --- /dev/null +++ b/Tests/VideoFrameUtilitiesTests.swift @@ -0,0 +1,46 @@ +import CoreGraphics +import Foundation +import ImageIO +import Testing +import UniformTypeIdentifiers +@testable import AXe + +@Suite("Video Frame Utilities Tests") +struct VideoFrameUtilitiesTests { + @Test("Compressed stream scaling uses pixel dimensions") + func compressedStreamScalingUsesPixelDimensions() async throws { + let source = try makePNG(width: 1206, height: 2622) + + let scaled = try await VideoFrameUtilities.processJPEGData(source, scale: 0.5, quality: 75) + let image = try #require(VideoFrameUtilities.makeCGImage(from: scaled)) + + #expect(image.width == 603) + #expect(image.height == 1311) + } + + private func makePNG(width: Int, height: Int) throws -> Data { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = try #require(CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + )) + context.setFillColor(CGColor(red: 0.2, green: 0.4, blue: 0.8, alpha: 1)) + context.fill(CGRect(x: 0, y: 0, width: width, height: height)) + let image = try #require(context.makeImage()) + let data = NSMutableData() + let destination = try #require(CGImageDestinationCreateWithData( + data, + UTType.png.identifier as CFString, + 1, + nil + )) + CGImageDestinationAddImage(destination, image, nil) + #expect(CGImageDestinationFinalize(destination)) + return data as Data + } +} diff --git a/patches/idb/e682506/accessibility-client-type-for-xctest.patch b/patches/idb/e682506/accessibility-client-type-for-xctest.patch new file mode 100644 index 0000000..0de5f54 --- /dev/null +++ b/patches/idb/e682506/accessibility-client-type-for-xctest.patch @@ -0,0 +1,12 @@ +diff --git a/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift b/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift +index 53a6d67cb..d7d3e209f 100644 +--- a/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift ++++ b/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift +@@ -130,6 +130,7 @@ public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDeleg + let box = AXPResponseBox() + + let xpcStart = CFAbsoluteTimeGetCurrent() ++ axRequest?.clientType = 2 + device?.sendAccessibilityRequestAsync(axRequest, completionQueue: callbackQueue) { innerResponse in + box.response = innerResponse + group.leave() diff --git a/patches/idb/e682506/developer-dir-environment-precedence.patch b/patches/idb/e682506/developer-dir-environment-precedence.patch new file mode 100644 index 0000000..17c48a2 --- /dev/null +++ b/patches/idb/e682506/developer-dir-environment-precedence.patch @@ -0,0 +1,68 @@ +diff --git a/FBControlCore/Utility/FBXcodeDirectory.swift b/FBControlCore/Utility/FBXcodeDirectory.swift +index fcf2a61a7..e3fd12a14 100644 +--- a/FBControlCore/Utility/FBXcodeDirectory.swift ++++ b/FBControlCore/Utility/FBXcodeDirectory.swift +@@ -12,6 +12,18 @@ public struct FBXcodeDirectory { + // MARK: Public + + public static func resolveDeveloperDirectory() throws -> String { ++ try resolveDeveloperDirectory(environment: ProcessInfo.processInfo.environment) ++ } ++ ++ static func resolveDeveloperDirectory(environment: [String: String]) throws -> String { ++ if let directory = environment["DEVELOPER_DIR"], ++ !directory.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ++ { ++ let resolved = (directory as NSString).resolvingSymlinksInPath ++ try validateXcodeDirectory(resolved) ++ return resolved ++ } ++ + let directory: String + do { + directory = try symlinkedDeveloperDirectory() +diff --git a/FBControlCoreTests/Tests/Unit/FBXcodeDirectoryTests.swift b/FBControlCoreTests/Tests/Unit/FBXcodeDirectoryTests.swift +index 1ed675985..ecf99e552 100644 +--- a/FBControlCoreTests/Tests/Unit/FBXcodeDirectoryTests.swift ++++ b/FBControlCoreTests/Tests/Unit/FBXcodeDirectoryTests.swift +@@ -9,6 +9,40 @@ + import XCTest + + final class FBXcodeDirectoryTests: XCTestCase { ++ func testResolveUsesProcessDeveloperDirectoryEnvironment() throws { ++ guard let directory = ProcessInfo.processInfo.environment["DEVELOPER_DIR"] else { ++ throw XCTSkip("DEVELOPER_DIR is not set") ++ } ++ ++ XCTAssertEqual( ++ try FBXcodeDirectory.resolveDeveloperDirectory(), ++ (directory as NSString).resolvingSymlinksInPath) ++ } ++ ++ func testDeveloperDirectoryEnvironmentTakesPrecedence() throws { ++ let directory = FileManager.default.temporaryDirectory ++ .appendingPathComponent(UUID().uuidString, isDirectory: true) ++ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) ++ defer { try? FileManager.default.removeItem(at: directory) } ++ ++ let resolved = try FBXcodeDirectory.resolveDeveloperDirectory( ++ environment: ["DEVELOPER_DIR": directory.path]) ++ ++ XCTAssertEqual(resolved, directory.path) ++ } ++ ++ func testResolveWithoutEnvironmentRetainsLegacyPrecedence() throws { ++ let expected: String ++ do { ++ expected = try FBXcodeDirectory.symlinkedDeveloperDirectory() ++ } catch { ++ expected = try FBXcodeDirectory.xcodeSelectDeveloperDirectory() ++ } ++ let fallback = try FBXcodeDirectory.resolveDeveloperDirectory(environment: [:]) ++ ++ XCTAssertEqual(fallback, expected) ++ } ++ + func testXcodeSelect() throws { + let directory = try FBXcodeDirectory.xcodeSelectDeveloperDirectory() + assertDirectory(directory) diff --git a/patches/idb/e682506/dtuhid-event-semantics.patch b/patches/idb/e682506/dtuhid-event-semantics.patch new file mode 100644 index 0000000..19691c7 --- /dev/null +++ b/patches/idb/e682506/dtuhid-event-semantics.patch @@ -0,0 +1,281 @@ +diff --git a/FBSimulatorControl/HID/FBSimulatorHIDEvent.swift b/FBSimulatorControl/HID/FBSimulatorHIDEvent.swift +index 9e16a9bf7..d205f1f2b 100644 +--- a/FBSimulatorControl/HID/FBSimulatorHIDEvent.swift ++++ b/FBSimulatorControl/HID/FBSimulatorHIDEvent.swift +@@ -84,7 +84,8 @@ public extension FBSimulatorHID { + /// gesture intact before the connection is torn down while avoiding a per-primitive stall (e.g. + /// slow typing on the DTUHID transport). + func send(event: FBSimulatorHIDEvent, logger: FBControlCoreLogger) async throws { +- for subEvent in event.subEvents ?? [event] { ++ let transportEvent = event.event(for: transportType) ++ for subEvent in transportEvent.subEvents ?? [transportEvent] { + switch subEvent { + case let .delay(duration): + logger.log("Delay \(duration)s") +@@ -99,3 +100,110 @@ public extension FBSimulatorHID { + } + ++// MARK: - Transport adaptation ++ ++extension FBSimulatorHIDEvent { ++ private static let dtuhidApplePayInterPressDelay: TimeInterval = 0.15 ++ private static let dtuhidKeyboardEventDelay: TimeInterval = 0.01 ++ private static let dtuhidKeyboardSettleDelay: TimeInterval = 0.05 ++ ++ func event(for transportType: FBSimulatorHIDTransportType) -> FBSimulatorHIDEvent { ++ guard transportType == .dtuhid else { ++ return self ++ } ++ ++ let transportEvent = addingDTUHIDKeyboardPacing(to: replacingApplePayPresses(in: self)) ++ guard containsKeyboardEvent(transportEvent) else { ++ return transportEvent ++ } ++ ++ return .composite([transportEvent, .delay(Self.dtuhidKeyboardSettleDelay)]) ++ } ++ ++ private func replacingApplePayPresses(in event: FBSimulatorHIDEvent) -> FBSimulatorHIDEvent { ++ guard case let .composite(events) = event else { ++ return event ++ } ++ ++ var rewritten: [FBSimulatorHIDEvent] = [] ++ var index = events.startIndex ++ ++ while index < events.endIndex { ++ guard case .button(direction: .down, button: .applePay) = events[index], ++ let releaseIndex = events[index...].firstIndex(where: { ++ if case .button(direction: .up, button: .applePay) = $0 { ++ return true ++ } ++ return false ++ }), ++ events[events.index(after: index).. FBSimulatorHIDEvent { ++ switch event { ++ case let .button(direction, button) where button == .applePay: ++ return .button(direction: direction, button: .sideButton) ++ case let .delay(duration): ++ return .delay(duration / 2) ++ case let .composite(events): ++ return .composite(events.map { applePayPressEvent(from: $0) }) ++ default: ++ return event ++ } ++ } ++ ++ private func containsKeyboardEvent(_ event: FBSimulatorHIDEvent) -> Bool { ++ switch event { ++ case .keyboard: ++ return true ++ case let .composite(events): ++ return events.contains(where: containsKeyboardEvent) ++ default: ++ return false ++ } ++ } ++ ++ private func isDurationOnly(_ event: FBSimulatorHIDEvent) -> Bool { ++ switch event { ++ case .delay: ++ return true ++ case let .composite(events): ++ return events.allSatisfy(isDurationOnly) ++ default: ++ return false ++ } ++ } ++ ++ private func addingDTUHIDKeyboardPacing(to event: FBSimulatorHIDEvent) -> FBSimulatorHIDEvent { ++ if case .keyboard = event { ++ return .composite([event, .delay(Self.dtuhidKeyboardEventDelay)]) ++ } ++ guard case let .composite(events) = event else { ++ return event ++ } ++ ++ let pacedEvents = events.flatMap { event -> [FBSimulatorHIDEvent] in ++ if case .keyboard = event { ++ return [event, .delay(Self.dtuhidKeyboardEventDelay)] ++ } ++ return [addingDTUHIDKeyboardPacing(to: event)] ++ } ++ return .composite(pacedEvents) ++ } ++} ++ + // MARK: - Factories +diff --git a/project.yml b/project.yml +index 4b5a730eb..a5cbf4161 100644 +--- a/project.yml ++++ b/project.yml +@@ -309,3 +309,26 @@ targets: + - target: IDBCompanionUtilities + embed: true + - sdk: Foundation.framework ++ ++ FBSimulatorHIDEventTransportTests: ++ type: bundle.unit-test ++ platform: macOS ++ sources: ++ - path: FBSimulatorHIDEventTransportTests ++ settings: ++ base: ++ PRODUCT_BUNDLE_IDENTIFIER: com.facebook.FBSimulatorHIDEventTransportTests ++ PRODUCT_NAME: FBSimulatorHIDEventTransportTests ++ GENERATE_INFOPLIST_FILE: YES ++ dependencies: ++ - target: FBSimulatorControl ++ - sdk: XCTest.framework ++ ++schemes: ++ FBSimulatorControl: ++ build: ++ targets: ++ FBSimulatorControl: all ++ test: ++ targets: ++ - FBSimulatorHIDEventTransportTests +diff --git a/FBSimulatorHIDEventTransportTests/FBSimulatorHIDEventTransportTests.swift b/FBSimulatorHIDEventTransportTests/FBSimulatorHIDEventTransportTests.swift +new file mode 100644 +index 000000000..0b6df41b1 +--- /dev/null ++++ b/FBSimulatorHIDEventTransportTests/FBSimulatorHIDEventTransportTests.swift +@@ -0,0 +1,119 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++@testable import FBSimulatorControl ++import XCTest ++ ++final class FBSimulatorHIDEventTransportTests: XCTestCase { ++ ++ func testApplePayBecomesTwoSideButtonPressesOnDTUHID() { ++ XCTAssertEqual( ++ FBSimulatorHIDEvent.shortButtonPress(.applePay).event(for: .dtuhid), ++ .composite([ ++ .button(direction: .down, button: .sideButton), ++ .button(direction: .up, button: .sideButton), ++ .delay(0.15), ++ .button(direction: .down, button: .sideButton), ++ .button(direction: .up, button: .sideButton), ++ ])) ++ } ++ ++ func testApplePaySplitsCommandDurationAcrossSideButtonPresses() { ++ let event = FBSimulatorHIDEvent.composite([ ++ .button(direction: .down, button: .applePay), ++ .delay(2), ++ .button(direction: .up, button: .applePay), ++ ]) ++ ++ let transportEvent = event.event(for: .dtuhid) ++ XCTAssertEqual( ++ transportEvent, ++ .composite([ ++ .button(direction: .down, button: .sideButton), ++ .delay(1), ++ .button(direction: .up, button: .sideButton), ++ .delay(0.15), ++ .button(direction: .down, button: .sideButton), ++ .delay(1), ++ .button(direction: .up, button: .sideButton), ++ ])) ++ XCTAssertEqual(totalDelay(in: transportEvent), 2.15, accuracy: 1e-9) ++ } ++ ++ func testIndigoPreservesApplePayEvent() { ++ let event = FBSimulatorHIDEvent.shortButtonPress(.applePay) ++ XCTAssertEqual(event.event(for: .indigo), event) ++ } ++ ++ func testUnmatchedApplePayPrimitiveRemainsUnsupported() { ++ let event = FBSimulatorHIDEvent.button(direction: .down, button: .applePay) ++ XCTAssertEqual(event.event(for: .dtuhid), event) ++ } ++ ++ func testApplePayRewritePreservesNestedDurationBudget() { ++ let event = FBSimulatorHIDEvent.composite([ ++ .button(direction: .down, button: .applePay), ++ .composite([.delay(1), .delay(1)]), ++ .button(direction: .up, button: .applePay), ++ ]) ++ ++ let transportEvent = event.event(for: .dtuhid) ++ XCTAssertEqual(totalDelay(in: transportEvent), 2.15, accuracy: 1e-9) ++ } ++ ++ func testApplePayRewriteDoesNotDuplicateMixedEvents() { ++ let event = FBSimulatorHIDEvent.composite([ ++ .button(direction: .down, button: .applePay), ++ .touch(direction: .down, x: 10, y: 20), ++ .button(direction: .up, button: .applePay), ++ ]) ++ ++ XCTAssertEqual(event.event(for: .dtuhid), event) ++ } ++ ++ func testDTUHIDPacesKeyboardCompositeBeforeFlush() { ++ XCTAssertEqual( ++ FBSimulatorHIDEvent.shortKeyPress(4).event(for: .dtuhid), ++ .composite([ ++ .composite([ ++ .keyboard(direction: .down, keyCode: 4), ++ .delay(0.01), ++ .keyboard(direction: .up, keyCode: 4), ++ .delay(0.01), ++ ]), ++ .delay(0.05), ++ ])) ++ } ++ ++ func testDTUHIDPacesDirectKeyboardEventBeforeFlush() { ++ XCTAssertEqual( ++ FBSimulatorHIDEvent.keyboard(direction: .down, keyCode: 4).event(for: .dtuhid), ++ .composite([ ++ .composite([ ++ .keyboard(direction: .down, keyCode: 4), ++ .delay(0.01), ++ ]), ++ .delay(0.05), ++ ])) ++ } ++ ++ func testDTUHIDPreservesNonKeyboardEvent() { ++ let event = FBSimulatorHIDEvent.tapAt(x: 10, y: 20) ++ XCTAssertEqual(event.event(for: .dtuhid), event) ++ } ++ ++ private func totalDelay(in event: FBSimulatorHIDEvent) -> TimeInterval { ++ switch event { ++ case let .delay(duration): ++ return duration ++ case let .composite(events): ++ return events.reduce(0) { $0 + totalDelay(in: $1) } ++ default: ++ return 0 ++ } ++ } ++} diff --git a/patches/idb/e682506/expose-selected-hid-transport.patch b/patches/idb/e682506/expose-selected-hid-transport.patch new file mode 100644 index 0000000..e368a67 --- /dev/null +++ b/patches/idb/e682506/expose-selected-hid-transport.patch @@ -0,0 +1,37 @@ +diff --git a/FBSimulatorControl/HID/FBSimulatorHID.swift b/FBSimulatorControl/HID/FBSimulatorHID.swift +index 1b84ff788..9e230012f 100644 +--- a/FBSimulatorControl/HID/FBSimulatorHID.swift ++++ b/FBSimulatorControl/HID/FBSimulatorHID.swift +@@ -42,6 +42,8 @@ public final class FBSimulatorHID: CustomStringConvertible, @unchecked Sendable + public let purple: FBSimulatorPurpleHID + + private weak var simulator: FBSimulator? ++ /// The transport selected for touch, button, and keyboard primitives. ++ public let transportType: FBSimulatorHIDTransportType + + // MARK: Initializers + +@@ -58,18 +60,20 @@ public final class FBSimulatorHID: CustomStringConvertible, @unchecked Sendable + public convenience init( + for simulator: FBSimulator, transport transportType: FBSimulatorHIDTransportType? = nil + ) throws { ++ let resolvedTransportType = transportType ?? simulator.defaultHIDTransport + let transport: FBSimulatorHIDTransport +- switch transportType ?? simulator.defaultHIDTransport { ++ switch resolvedTransportType { + case .indigo: + transport = try FBSimulatorIndigoHIDTransport.indigo(for: simulator) + case .dtuhid: + transport = try FBSimulatorDTUHIDTransport.dtuhid(for: simulator) + } +- self.init(transport: transport, purple: FBSimulatorPurpleHID(), simulator: simulator) ++ self.init(transport: transport, transportType: resolvedTransportType, purple: FBSimulatorPurpleHID(), simulator: simulator) + } + +- private init(transport: FBSimulatorHIDTransport, purple: FBSimulatorPurpleHID, simulator: FBSimulator) { ++ private init(transport: FBSimulatorHIDTransport, transportType: FBSimulatorHIDTransportType, purple: FBSimulatorPurpleHID, simulator: FBSimulator) { + self.transport = transport ++ self.transportType = transportType + self.purple = purple + self.simulator = simulator + } diff --git a/patches/idb/e682506/hide-accessibility-private-framework-types.patch b/patches/idb/e682506/hide-accessibility-private-framework-types.patch new file mode 100644 index 0000000..1e83774 --- /dev/null +++ b/patches/idb/e682506/hide-accessibility-private-framework-types.patch @@ -0,0 +1,179 @@ +diff --git a/FBSimulatorControl/Commands/FBAXPlatformElement.swift b/FBSimulatorControl/Commands/FBAXPlatformElement.swift +--- a/FBSimulatorControl/Commands/FBAXPlatformElement.swift ++++ b/FBSimulatorControl/Commands/FBAXPlatformElement.swift +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. + */ + +-import AccessibilityPlatformTranslation ++@_implementationOnly import AccessibilityPlatformTranslation + import AppKit + import FBControlCore + import Foundation +diff --git a/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift b/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift +--- a/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift ++++ b/FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. + */ + +-@preconcurrency import AccessibilityPlatformTranslation ++@_implementationOnly @preconcurrency import AccessibilityPlatformTranslation + import CoreSimulator + import FBControlCore + import Foundation +@@ -24,11 +24,10 @@ private final class AXPResponseBox: @unchecked Sendable { + /// attribute callbacks into synchronous CoreSimulator XPC round-trips. + /// + /// Created and driven entirely from Swift in this module (see +-/// `FBSimulatorAccessibilityCommands`). It remains an `@objc`/`NSObject` class +-/// only because it conforms to the Objective-C `AXPTranslationTokenDelegateHelper` +-/// protocol and is installed as `AXPTranslator`'s bridge-token delegate. +-@objc(FBAXTranslationDispatcher) +-public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDelegateHelper { ++/// `FBSimulatorAccessibilityCommands`). It remains an `NSObject` class because ++/// it conforms to the Objective-C `AXPTranslationTokenDelegateHelper` protocol ++/// and is installed as `AXPTranslator`'s bridge-token delegate. ++final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDelegateHelper { + + private weak var translator: AXPTranslator? + private let logger: FBControlCoreLogger? +@@ -36,7 +35,7 @@ public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDeleg + private let lock = NSLock() + private var tokenToRequest: [String: FBAXTranslationRequest] = [:] + +- public init(translator: AXPTranslator, logger: FBControlCoreLogger?) { ++ init(translator: AXPTranslator, logger: FBControlCoreLogger?) { + self.translator = translator + self.logger = logger + self.callbackQueue = DispatchQueue(label: "com.facebook.fbsimulatorcontrol.accessibility_translator.callback") +@@ -82,7 +81,7 @@ public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDeleg + logger?.log("Registered request with token \(request.token)") + } + +- public func popRequest(_ request: FBAXTranslationRequest) { ++ func popRequest(_ request: FBAXTranslationRequest) { + lock.lock() + let present = tokenToRequest[request.token] != nil + if present { +@@ -111,7 +110,7 @@ public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDeleg + // Since the CoreSimulator accessibility API is asynchronous but AXPTranslator's + // delegation is synchronous, a DispatchGroup acts as a mutex to wait on the + // result. The wait must never run on the main queue. +- public func accessibilityTranslationDelegateBridgeCallback(withToken token: String) -> AXPTranslationCallback { ++ func accessibilityTranslationDelegateBridgeCallback(withToken token: String) -> AXPTranslationCallback { + guard let request = request(forToken: token) else { + return { [weak self] _ in + self?.logger?.log("Request with token \(token) is gone. Returning empty response") +@@ -146,11 +145,11 @@ public final class FBAXTranslationDispatcher: NSObject, AXPTranslationTokenDeleg + } + } + +- public func accessibilityTranslationConvertPlatformFrame(toSystem rect: CGRect, withToken token: String) -> CGRect { ++ func accessibilityTranslationConvertPlatformFrame(toSystem rect: CGRect, withToken token: String) -> CGRect { + rect + } + +- public func accessibilityTranslationRootParent(withToken token: String) -> Any? { ++ func accessibilityTranslationRootParent(withToken token: String) -> Any? { + logger?.log("Delegate method 'accessibilityTranslationRootParentWithToken:', with unknown implementation called with token \(token). Returning nil.") + return nil + } +diff --git a/FBSimulatorControl/Commands/FBAXTranslationRequest.swift b/FBSimulatorControl/Commands/FBAXTranslationRequest.swift +--- a/FBSimulatorControl/Commands/FBAXTranslationRequest.swift ++++ b/FBSimulatorControl/Commands/FBAXTranslationRequest.swift +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. + */ + +-import AccessibilityPlatformTranslation ++@_implementationOnly import AccessibilityPlatformTranslation + import CoreSimulator + import FBControlCore + import Foundation +@@ -18,10 +18,10 @@ import Foundation + /// + /// Created and driven entirely from Swift in this module (the dispatcher, the + /// element handle, and the facade), so it is a plain Swift class. +-public final class FBAXTranslationRequest { ++final class FBAXTranslationRequest { + + /// What the request resolves and how it serializes. +- public enum Kind { ++ enum Kind { + /// The frontmost application's element tree, with frame-coverage calculation + /// and remote (separate-process) content discovery. + case frontmostApplication +@@ -36,19 +36,19 @@ public final class FBAXTranslationRequest { + // indefinitely on a `DispatchGroup.wait`. + private static let defaultRequestTimeoutSeconds: TimeInterval = 5.0 + +- public let kind: Kind +- public let token: String +- public var device: SimDevice? +- public var collector: FBAccessibilityProfilingCollector? +- public var logger: FBControlCoreLogger? +- public var translator: AXPTranslator? ++ let kind: Kind ++ let token: String ++ var device: SimDevice? ++ var collector: FBAccessibilityProfilingCollector? ++ var logger: FBControlCoreLogger? ++ var translator: AXPTranslator? + + /// Per-request timeout (seconds) applied to each synchronous CoreSimulator + /// accessibility XPC round-trip. `0` (or negative) is "wait nothing". There is + /// no "wait forever" mode — a stalled XPC service never hangs the caller. +- public var requestTimeoutSeconds: TimeInterval ++ var requestTimeoutSeconds: TimeInterval + +- public init(kind: Kind) { ++ init(kind: Kind) { + self.kind = kind + self.token = UUID().uuidString + self.requestTimeoutSeconds = Self.defaultRequestTimeoutSeconds +@@ -56,12 +56,12 @@ public final class FBAXTranslationRequest { + + /// A fresh request of the same kind with a new token, used to retry after + /// SpringBoard remediation. +- public func cloneWithNewToken() -> FBAXTranslationRequest { ++ func cloneWithNewToken() -> FBAXTranslationRequest { + FBAXTranslationRequest(kind: kind) + } + + /// Resolves the root translation object for this request's kind. +- public func perform(withTranslator translator: AXPTranslator) -> AXPTranslationObject? { ++ func perform(withTranslator translator: AXPTranslator) -> AXPTranslationObject? { + switch kind { + case .frontmostApplication: + return translator.frontmostApplication(withDisplayId: 0, bridgeDelegateToken: token) +diff --git a/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift b/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift +--- a/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift ++++ b/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. + */ + +-@preconcurrency import AccessibilityPlatformTranslation ++@_implementationOnly @preconcurrency import AccessibilityPlatformTranslation + import AppKit + import CoreSimulator + import FBControlCore +@@ -20,7 +20,7 @@ extension FBSimulator { + /// fixture's mock `AXPTranslator` (mirrors the original `id` parameter). Swift-only + /// (not `@objc`): an `@objc` `Any` parameter double-visions as `Any`/`Any!` and + /// makes the call ambiguous; nothing in Objective-C calls this anymore. +- public static func createAccessibilityTranslationDispatcher(withTranslator translator: Any) -> FBAXTranslationDispatcher { ++ static func createAccessibilityTranslationDispatcher(withTranslator translator: Any) -> FBAXTranslationDispatcher { + let axTranslator = unsafeBitCast(translator as AnyObject, to: AXPTranslator.self) + let dispatcher = FBAXTranslationDispatcher(translator: axTranslator, logger: nil) + axTranslator.bridgeTokenDelegate = dispatcher +@@ -37,7 +37,7 @@ extension FBSimulator { + return FBSimulator.createAccessibilityTranslationDispatcher(withTranslator: translator) + }() + +- @objc public var accessibilityTranslationDispatcher: FBAXTranslationDispatcher { ++ var accessibilityTranslationDispatcher: FBAXTranslationDispatcher { + FBSimulator.sharedAccessibilityTranslationDispatcher + } + } diff --git a/patches/idb/e682506/xcode27-accessibility-bootstrap.patch b/patches/idb/e682506/xcode27-accessibility-bootstrap.patch new file mode 100644 index 0000000..f2a5b18 --- /dev/null +++ b/patches/idb/e682506/xcode27-accessibility-bootstrap.patch @@ -0,0 +1,365 @@ +diff --git a/FBControlCore/Utility/FBWeakFramework+ApplePrivateFrameworks.swift b/FBControlCore/Utility/FBWeakFramework+ApplePrivateFrameworks.swift +index 3a119597..7fe4f7ee 100644 +--- a/FBControlCore/Utility/FBWeakFramework+ApplePrivateFrameworks.swift ++++ b/FBControlCore/Utility/FBWeakFramework+ApplePrivateFrameworks.swift +@@ -27,6 +27,14 @@ extension FBWeakFramework { + FBWeakFramework.xcodeFramework(withRelativePath: "../SharedFrameworks/DTXConnectionServices.framework", requiredClassNames: ["DTXConnection", "DTXRemoteInvocationReceipt"]) + } + ++ @objc(XCTDaemonControl) public class var xctDaemonControl: FBWeakFramework { ++ FBWeakFramework.xcodeFramework(withRelativePath: "../SharedFrameworks/XCTDaemonControl.framework", requiredClassNames: ["XCTDaemonConnectionConfiguration"]) ++ } ++ ++ @objc(XCUIAutomation) public class var xcuiAutomation: FBWeakFramework { ++ FBWeakFramework.xcodeFramework(withRelativePath: "../SharedFrameworks/XCUIAutomation.framework", requiredClassNames: ["XCUIDeviceRemoteDaemonConnectionProvider", "XCUIDeviceRemoteAutomationSession"]) ++ } ++ + @objc(XCTest) public class var xcTest: FBWeakFramework { + FBWeakFramework.xcodeFramework(withRelativePath: "Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework", requiredClassNames: ["XCTestConfiguration"]) + } +diff --git a/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift b/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift +index b54a6b9c..d153f42d 100644 +--- a/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift ++++ b/FBSimulatorControl/Commands/FBSimulatorAccessibilityCommands.swift +@@ -116,6 +116,13 @@ public final class FBSimulatorAccessibilityCommands: NSObject, AsyncAccessibilit + throw FBAccessibilityError.accessibilityUnavailable + } + try FBSimulatorControlFrameworkLoader.accessibilityFrameworks.loadPrivateFrameworks(simulator.logger) ++ if FBXcodeConfiguration.xcodeVersion.majorVersion >= 27 { ++ try FBSimulatorControlFrameworkLoader.bootstrapAccessibility( ++ forSimulatorDevice: simulator.device, ++ timeout: 5, ++ logger: simulator.logger ++ ) ++ } + } + + // Returns an FBAccessibilityElement wrapping the platform element for the given request. +diff --git a/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.h b/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.h +index be9d601d..a9e8a838 100644 +--- a/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.h ++++ b/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.h +@@ -24,6 +24,19 @@ + */ + @property (class, nonnull, nonatomic, readonly, strong) FBSimulatorControlFrameworkLoader *accessibilityFrameworks; + ++/** ++ The Xcode frameworks needed to bootstrap simulator Accessibility on Xcode 27+. ++ */ ++@property (class, nonnull, nonatomic, readonly, strong) FBSimulatorControlFrameworkLoader *accessibilityAutomationFrameworks; ++ ++/** ++ Starts a short-lived remote automation session and asks it to load Accessibility. ++ */ +++ (BOOL)bootstrapAccessibilityForSimulatorDevice:(nonnull id)simulatorDevice ++ timeout:(NSTimeInterval)timeout ++ logger:(nullable id)logger ++ error:(NSError * _Nullable * _Nullable)error; ++ + /** + All of the Frameworks for operations involving the HID and Framebuffer. + */ +diff --git a/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.m b/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.m +index b7bd9def..02bbe138 100644 +--- a/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.m ++++ b/FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.m +@@ -9,6 +9,125 @@ + + #import + #import ++#import ++ ++static NSString *const FBSimulatorAccessibilityBootstrapErrorDomain = @"com.facebook.FBSimulatorControl.AccessibilityBootstrap"; ++ ++static NSError *FBSimulatorAccessibilityBootstrapError(NSInteger code, NSString *description) ++{ ++ return [NSError errorWithDomain:FBSimulatorAccessibilityBootstrapErrorDomain ++ code:code ++ userInfo:@{NSLocalizedDescriptionKey: description}]; ++} ++ ++static void FBSimulatorInvalidateAutomationSession(id session) ++{ ++ SEL invalidateSelector = NSSelectorFromString(@"invalidate"); ++ if ([session respondsToSelector:invalidateSelector]) { ++ ((void (*)(id, SEL))objc_msgSend)(session, invalidateSelector); ++ } ++} ++ ++@interface FBSimulatorAccessibilityBootstrapAttempt : NSObject ++ ++@property (nonatomic, strong, readonly) dispatch_group_t group; ++@property (nonatomic, assign) BOOL success; ++@property (nonatomic, copy, nullable) NSError *error; ++ ++@end ++ ++@implementation FBSimulatorAccessibilityBootstrapAttempt ++ ++- (instancetype)init ++{ ++ self = [super init]; ++ if (!self) { ++ return nil; ++ } ++ _group = dispatch_group_create(); ++ dispatch_group_enter(_group); ++ return self; ++} ++ ++@end ++ ++@interface FBSimulatorAccessibilitySessionRequest : NSObject ++ ++- (void)completeWithSession:(nullable id)session error:(nullable NSError *)error; ++- (nullable id)sessionByMarkingTimedOut; ++- (nullable id)session; ++- (nullable NSError *)error; ++ ++@end ++ ++@implementation FBSimulatorAccessibilitySessionRequest ++{ ++ NSLock *_lock; ++ id _session; ++ NSError *_error; ++ BOOL _timedOut; ++} ++ ++- (instancetype)init ++{ ++ self = [super init]; ++ if (!self) { ++ return nil; ++ } ++ _lock = [NSLock new]; ++ return self; ++} ++ ++- (void)completeWithSession:(id)session error:(NSError *)error ++{ ++ [_lock lock]; ++ BOOL timedOut = _timedOut; ++ if (!timedOut) { ++ _session = session; ++ _error = error; ++ } ++ [_lock unlock]; ++ if (timedOut && session) { ++ FBSimulatorInvalidateAutomationSession(session); ++ } ++} ++ ++- (id)sessionByMarkingTimedOut ++{ ++ [_lock lock]; ++ _timedOut = YES; ++ id session = _session; ++ _session = nil; ++ [_lock unlock]; ++ return session; ++} ++ ++- (id)session ++{ ++ [_lock lock]; ++ id session = _session; ++ [_lock unlock]; ++ return session; ++} ++ ++- (NSError *)error ++{ ++ [_lock lock]; ++ NSError *error = _error; ++ [_lock unlock]; ++ return error; ++} ++ ++@end ++ ++@interface FBSimulatorControlFrameworkLoader () ++ +++ (BOOL)performAccessibilityBootstrapForSimulatorDevice:(id)simulatorDevice ++ timeout:(NSTimeInterval)timeout ++ logger:(nullable id)logger ++ error:(NSError **)error; ++ ++@end + + static void FBSimulatorControl_SimLogHandler(int level, const char *function, int lineNumber, NSString *format, ...) + { +@@ -54,6 +173,173 @@ + (FBSimulatorControlFrameworkLoader *)accessibilityFrameworks + return loader; + } + +++ (FBSimulatorControlFrameworkLoader *)accessibilityAutomationFrameworks ++{ ++ static dispatch_once_t onceToken; ++ static FBSimulatorControlFrameworkLoader *loader; ++ dispatch_once(&onceToken, ^{ ++ loader = [FBSimulatorControlFrameworkLoader loaderWithName:@"FBSimulatorControl" ++ frameworks:@[ ++ FBWeakFramework.XCTDaemonControl, ++ FBWeakFramework.XCUIAutomation, ++ ]]; ++ }); ++ return loader; ++} ++ +++ (BOOL)bootstrapAccessibilityForSimulatorDevice:(id)simulatorDevice timeout:(NSTimeInterval)timeout logger:(id)logger error:(NSError **)error ++{ ++ static dispatch_once_t onceToken; ++ static dispatch_queue_t stateQueue; ++ static NSMapTable *states; ++ dispatch_once(&onceToken, ^{ ++ stateQueue = dispatch_queue_create("com.facebook.FBSimulatorControl.AccessibilityBootstrap", DISPATCH_QUEUE_SERIAL); ++ states = [NSMapTable weakToStrongObjectsMapTable]; ++ }); ++ ++ __block FBSimulatorAccessibilityBootstrapAttempt *attempt; ++ __block BOOL ownsAttempt = NO; ++ dispatch_sync(stateQueue, ^{ ++ id state = [states objectForKey:simulatorDevice]; ++ if ([state isKindOfClass:FBSimulatorAccessibilityBootstrapAttempt.class]) { ++ attempt = state; ++ } else { ++ attempt = [FBSimulatorAccessibilityBootstrapAttempt new]; ++ [states setObject:attempt forKey:simulatorDevice]; ++ ownsAttempt = YES; ++ } ++ }); ++ if (!ownsAttempt) { ++ dispatch_group_wait(attempt.group, DISPATCH_TIME_FOREVER); ++ if (!attempt.success && error) { ++ *error = attempt.error; ++ } ++ return attempt.success; ++ } ++ ++ NSError *bootstrapError = nil; ++ BOOL success = [self performAccessibilityBootstrapForSimulatorDevice:simulatorDevice ++ timeout:timeout ++ logger:logger ++ error:&bootstrapError]; ++ attempt.success = success; ++ attempt.error = bootstrapError; ++ dispatch_sync(stateQueue, ^{ ++ if ([states objectForKey:simulatorDevice] == attempt) { ++ [states removeObjectForKey:simulatorDevice]; ++ } ++ }); ++ dispatch_group_leave(attempt.group); ++ if (!success && error) { ++ *error = bootstrapError; ++ } ++ return success; ++} ++ +++ (BOOL)performAccessibilityBootstrapForSimulatorDevice:(id)simulatorDevice timeout:(NSTimeInterval)timeout logger:(id)logger error:(NSError **)error ++{ ++ if (![self.accessibilityAutomationFrameworks loadPrivateFrameworks:logger error:error]) { ++ return NO; ++ } ++ ++ Class providerClass = NSClassFromString(@"XCUIDeviceRemoteDaemonConnectionProvider"); ++ SEL providerSelector = NSSelectorFromString(@"connectionProviderForSimDevice:"); ++ if (![providerClass respondsToSelector:providerSelector]) { ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(1, @"XCUIDeviceRemoteDaemonConnectionProvider.connectionProviderForSimDevice: is unavailable"); ++ } ++ return NO; ++ } ++ id provider = ((id (*)(id, SEL, id))objc_msgSend)(providerClass, providerSelector, simulatorDevice); ++ if (!provider) { ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(2, @"Could not create the simulator remote daemon connection provider"); ++ } ++ return NO; ++ } ++ ++ Class sessionClass = NSClassFromString(@"XCUIDeviceRemoteAutomationSession"); ++ SEL requestSelector = NSSelectorFromString(@"requestSessionWithDaemonConnectionProvider:completion:"); ++ if (![sessionClass respondsToSelector:requestSelector]) { ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(3, @"XCUIDeviceRemoteAutomationSession request API is unavailable"); ++ } ++ return NO; ++ } ++ ++ dispatch_semaphore_t sessionSemaphore = dispatch_semaphore_create(0); ++ FBSimulatorAccessibilitySessionRequest *sessionRequest = [FBSimulatorAccessibilitySessionRequest new]; ++ id sessionCompletion = ^(id returnedSession, NSError *returnedError) { ++ [sessionRequest completeWithSession:returnedSession error:returnedError]; ++ dispatch_semaphore_signal(sessionSemaphore); ++ }; ++ ((void (*)(id, SEL, id, id))objc_msgSend)(sessionClass, requestSelector, provider, sessionCompletion); ++ if (dispatch_semaphore_wait(sessionSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) != 0) { ++ id lateSession = [sessionRequest sessionByMarkingTimedOut]; ++ if (lateSession) { ++ FBSimulatorInvalidateAutomationSession(lateSession); ++ } ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(4, @"Timed out creating the simulator remote automation session"); ++ } ++ return NO; ++ } ++ id session = sessionRequest.session; ++ if (!session) { ++ if (error) { ++ *error = sessionRequest.error ?: FBSimulatorAccessibilityBootstrapError(5, @"Could not create the simulator remote automation session"); ++ } ++ return NO; ++ } ++ ++ @try { ++ SEL enableSelector = NSSelectorFromString(@"enableAutomationModeWithError:"); ++ if ([session respondsToSelector:enableSelector]) { ++ NSError *enableError = nil; ++ BOOL enabled = ((BOOL (*)(id, SEL, NSError **))objc_msgSend)(session, enableSelector, &enableError); ++ if (!enabled) { ++ if (error) { ++ *error = enableError ?: FBSimulatorAccessibilityBootstrapError(6, @"Could not enable simulator automation mode"); ++ } ++ return NO; ++ } ++ } ++ ++ SEL loadSelector = NSSelectorFromString(@"loadAccessibilityWithTimeout:reply:"); ++ if (![session respondsToSelector:loadSelector]) { ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(7, @"Remote automation session cannot load Accessibility"); ++ } ++ return NO; ++ } ++ dispatch_semaphore_t loadSemaphore = dispatch_semaphore_create(0); ++ __block BOOL loaded = NO; ++ __block NSError *loadError = nil; ++ id loadReply = ^(BOOL returnedLoaded, NSError *returnedError) { ++ loaded = returnedLoaded; ++ loadError = returnedError; ++ dispatch_semaphore_signal(loadSemaphore); ++ }; ++ ((void (*)(id, SEL, double, id))objc_msgSend)(session, loadSelector, timeout, loadReply); ++ if (dispatch_semaphore_wait(loadSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)((timeout + 1) * NSEC_PER_SEC))) != 0) { ++ if (error) { ++ *error = FBSimulatorAccessibilityBootstrapError(8, @"Timed out loading simulator Accessibility"); ++ } ++ return NO; ++ } ++ if (!loaded) { ++ if (error) { ++ *error = loadError ?: FBSimulatorAccessibilityBootstrapError(9, @"The simulator rejected the Accessibility load request"); ++ } ++ return NO; ++ } ++ [logger log:@"Bootstrapped simulator Accessibility through a short-lived remote automation session"]; ++ return YES; ++ } @finally { ++ FBSimulatorInvalidateAutomationSession(session); ++ } ++} ++ + + (FBSimulatorControlFrameworkLoader *)xcodeFrameworks + { + static dispatch_once_t onceToken; diff --git a/scripts/build.sh b/scripts/build.sh index 960f86c..10084d4 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -48,13 +48,21 @@ load_env_file "${ENV_FILE}" # Environment and Configuration IDB_CHECKOUT_DIR="${IDB_CHECKOUT_DIR:-./idb_checkout}" -IDB_GIT_REF="${IDB_GIT_REF:-76639e4d0e1741adf391cab36f19fbc59378153e}" -IDB_PATCHES_DIR="${IDB_PATCHES_DIR:-./patches/idb}" +IDB_GIT_REF="${IDB_GIT_REF:-e682506725e9efefb9c43b8b917c0b12eb2a5939}" +IDB_PATCHES_DIR="${IDB_PATCHES_DIR:-./patches/idb/e682506}" BUILD_OUTPUT_DIR="${BUILD_OUTPUT_DIR:-./build_products}" DERIVED_DATA_PATH="${DERIVED_DATA_PATH:-./build_derived_data}" BUILD_XCFRAMEWORK_DIR="${BUILD_XCFRAMEWORK_DIR:-${BUILD_OUTPUT_DIR}/XCFrameworks}" FBSIMCONTROL_PROJECT="${IDB_CHECKOUT_DIR}/FBSimulatorControl.xcodeproj" TEMP_DIR="${TEMP_DIR:-$(mktemp -d)}" +IDB_REQUIRED_PATCHES=( + "accessibility-client-type-for-xctest.patch" + "developer-dir-environment-precedence.patch" + "expose-selected-hid-transport.patch" + "hide-accessibility-private-framework-types.patch" + "xcode27-accessibility-bootstrap.patch" + "dtuhid-event-semantics.patch" +) # Shared payload helper # shellcheck source=./release-payload.sh @@ -166,22 +174,74 @@ function verify_macho_has_arch() { fi } -function verify_fbsimulatorcontrol_has_xctest_client_type_patch() { +function verify_fbsimulatorcontrol_candidate_patches() { local binary_path="$1" if [[ ! -f "$binary_path" ]]; then - echo "❌ Error: FBSimulatorControl binary not found for accessibility patch verification: $binary_path" + echo "❌ Error: FBSimulatorControl binary not found for candidate patch verification: $binary_path" exit 1 fi - local client_type_references - client_type_references=$(strings -a "$binary_path" | grep -F "setClientType:" || true) - if [[ -z "$client_type_references" ]]; then - echo "❌ Error: FBSimulatorControl is missing the XCTest accessibility client type patch" - echo " Expected to find selector reference 'setClientType:' in: $binary_path" - echo " Rebuild IDB frameworks after applying patches/idb/accessibility-client-type-for-xctest.patch." + local required_references=( + "setClientType:" + "XCUIDeviceRemoteAutomationSession" + "enableAutomationModeWithError:" + "loadAccessibilityWithTimeout:reply:" + "transportType" + ) + local reference + for reference in "${required_references[@]}"; do + if ! strings -a "$binary_path" | grep -F "$reference" >/dev/null; then + echo "❌ Error: FBSimulatorControl is missing candidate patch evidence: ${reference}" + echo " Checked binary: $binary_path" + echo " Expected patch set: ${IDB_PATCHES_DIR}" + echo " Re-run setup, generation, and the framework build at ${IDB_GIT_REF}." + exit 1 + fi + done + + local framework_contents + framework_contents=$(dirname "$binary_path") + local public_module_artifacts=( + "${framework_contents}/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftinterface" + "${framework_contents}/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftinterface" + "${framework_contents}/Headers/FBSimulatorControl-Swift.h" + ) + local compiled_swift_modules=( + "${framework_contents}/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftmodule" + "${framework_contents}/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftmodule" + ) + local compiled_swift_module + for compiled_swift_module in "${compiled_swift_modules[@]}"; do + if [[ ! -f "${compiled_swift_module}" ]]; then + echo "❌ Error: FBSimulatorControl compiled Swift module is missing: ${compiled_swift_module}" + echo " Textual reconstruction is not viable because the module and public class share a name." + exit 1 + fi + done + local artifact + for artifact in "${public_module_artifacts[@]}"; do + if [[ ! -f "$artifact" ]]; then + echo "❌ Error: FBSimulatorControl public module artifact is missing: ${artifact}" + exit 1 + fi + if grep -E 'AccessibilityPlatformTranslation|AXP[A-Za-z]' "$artifact" >/dev/null; then + echo "❌ Error: FBSimulatorControl leaks AccessibilityPlatformTranslation through its public interface" + echo " Leaking artifact: ${artifact}" + echo " Rebuild after applying hide-accessibility-private-framework-types.patch." + exit 1 + fi + done + + if ! grep -F 'FBAccessibilityElement' "${public_module_artifacts[0]}" >/dev/null || + ! grep -F 'accessibilityElementForFrontmostApplication' "${public_module_artifacts[0]}" >/dev/null + then + echo "❌ Error: FBSimulatorControl lost the public AXe accessibility command surface" + echo " Checked: ${public_module_artifacts[0]}" exit 1 fi + + print_success "FBSimulatorControl contains candidate diagnostics without leaking private accessibility framework types" } # Function to invoke xcodebuild, optionally with xcpretty @@ -235,15 +295,15 @@ function copy_resource_bundle() { } function clone_idb_repo() { - if [ ! -d $IDB_CHECKOUT_DIR ]; then - print_info "Creating $IDB_DIRECTORY directory and cloning idb repository..." - git clone https://github.com/facebook/idb.git $IDB_CHECKOUT_DIR - (cd $IDB_CHECKOUT_DIR && git checkout "$IDB_GIT_REF") + if [ ! -d "$IDB_CHECKOUT_DIR/.git" ]; then + print_info "Creating $IDB_CHECKOUT_DIR directory and cloning idb repository..." + git clone https://github.com/facebook/idb.git "$IDB_CHECKOUT_DIR" + (cd "$IDB_CHECKOUT_DIR" && git checkout --detach "$IDB_GIT_REF") print_success "idb repository cloned at $IDB_GIT_REF." apply_idb_patches else print_info "Updating idb repository to $IDB_GIT_REF..." - (cd $IDB_CHECKOUT_DIR && git fetch --all --tags --prune && git reset --hard "$IDB_GIT_REF") + (cd "$IDB_CHECKOUT_DIR" && git fetch --all --tags --prune && git checkout -- . && git clean -fd && git checkout --detach "$IDB_GIT_REF") print_success "idb repository updated to $IDB_GIT_REF." apply_idb_patches fi @@ -254,19 +314,13 @@ function apply_idb_patches() { return fi - shopt -s nullglob - local patches=("$IDB_PATCHES_DIR"/*.patch) - if [ ${#patches[@]} -eq 0 ]; then - shopt -u nullglob - return - fi - print_info "Applying local patches to idb repository..." # Ensure we start from a clean working tree so patches apply consistently. (cd "$IDB_CHECKOUT_DIR" && git checkout -- . >/dev/null 2>&1 && git clean -fd >/dev/null 2>&1) || true - local patch_file - for patch_file in "${patches[@]}"; do + local patch_name patch_file + for patch_name in "${IDB_REQUIRED_PATCHES[@]}"; do + patch_file="${IDB_PATCHES_DIR}/${patch_name}" local patch_abs patch_abs="$(cd "$(dirname "$patch_file")" && pwd)/$(basename "$patch_file")" print_info " → $(basename "$patch_file")" @@ -282,7 +336,91 @@ function apply_idb_patches() { exit 1 fi done - shopt -u nullglob + verify_idb_source_state +} + +function verify_idb_source_state() { + local actual_ref + actual_ref=$(git -C "$IDB_CHECKOUT_DIR" rev-parse HEAD) + if [[ "$actual_ref" != "$IDB_GIT_REF" ]]; then + echo "❌ Error: IDB checkout SHA mismatch" + echo " Expected: $IDB_GIT_REF" + echo " Actual: $actual_ref" + exit 1 + fi + + local patch_name + for patch_name in "${IDB_REQUIRED_PATCHES[@]}"; do + if [[ ! -f "${IDB_PATCHES_DIR}/${patch_name}" ]]; then + echo "❌ Error: Required candidate patch is missing: ${IDB_PATCHES_DIR}/${patch_name}" + exit 1 + fi + done + + local source_checks=( + "FBControlCore/Utility/FBXcodeDirectory.swift|environment[\"DEVELOPER_DIR\"]" + "FBSimulatorControl/Commands/FBAXTranslationDispatcher.swift|clientType = 2" + "FBSimulatorControl/Commands/FBAXTranslationRequest.swift|@_implementationOnly import AccessibilityPlatformTranslation" + "FBSimulatorControl/HID/FBSimulatorHID.swift|public let transportType" + "FBSimulatorControl/HID/FBSimulatorHIDEvent.swift|event.event(for: transportType)" + "FBSimulatorControl/Utility/FBSimulatorControlFrameworkLoader.m|XCUIDeviceRemoteAutomationSession" + ) + local check source_file source_marker + for check in "${source_checks[@]}"; do + source_file="${check%%|*}" + source_marker="${check#*|}" + if ! grep -Fq "$source_marker" "${IDB_CHECKOUT_DIR}/${source_file}"; then + echo "❌ Error: Candidate patch verification failed" + echo " Missing '${source_marker}' in ${source_file}" + echo " Patch directory: ${IDB_PATCHES_DIR}" + exit 1 + fi + done + + if ! git -C "$IDB_CHECKOUT_DIR" diff --check; then + echo "❌ Error: Candidate IDB patch set introduced whitespace errors" + exit 1 + fi + + print_success "Verified IDB source SHA ${actual_ref} and candidate patch set ${IDB_PATCHES_DIR}" + for patch_name in "${IDB_REQUIRED_PATCHES[@]}"; do + print_info " $(shasum -a 256 "${IDB_PATCHES_DIR}/${patch_name}" | awk '{print $1}') ${patch_name}" + done +} + +function generate_idb_projects() { + if ! command -v xcodegen >/dev/null 2>&1; then + echo "❌ Error: XcodeGen is required to generate the candidate IDB projects." + echo " Install XcodeGen or make an existing installation available in PATH." + exit 1 + fi + + verify_idb_source_state + print_info "Generating candidate IDB projects with $(xcodegen --version)..." + (cd "$IDB_CHECKOUT_DIR" && ./build.sh generate) + if [[ ! -f "${FBSIMCONTROL_PROJECT}/project.pbxproj" ]]; then + echo "❌ Error: Candidate project generation did not produce ${FBSIMCONTROL_PROJECT}/project.pbxproj" + exit 1 + fi + print_success "Generated candidate IDB projects before framework compilation" +} + +function write_idb_build_evidence() { + local evidence_path="${BUILD_XCFRAMEWORK_DIR}/IDB_BUILD_EVIDENCE.txt" + mkdir -p "$BUILD_XCFRAMEWORK_DIR" + { + echo "IDB_SHA=${IDB_GIT_REF}" + echo "IDB_PATCH_DIRECTORY=${IDB_PATCHES_DIR}" + echo "DEVELOPER_DIR=${DEVELOPER_DIR:-}" + echo "XCODE_VERSION=$(xcodebuild -version | tr '\n' ' ')" + echo "SWIFT_VERSION=$(swiftc --version 2>&1 | head -1)" + echo "XCODEGEN_VERSION=$(xcodegen --version)" + local patch_name + for patch_name in "${IDB_REQUIRED_PATCHES[@]}"; do + echo "PATCH_SHA256=$(shasum -a 256 "${IDB_PATCHES_DIR}/${patch_name}" | awk '{print $1}') ${patch_name}" + done + } > "$evidence_path" + print_success "Recorded candidate build evidence at ${evidence_path}" } # Function to build a single framework @@ -372,6 +510,22 @@ function create_xcframework() { local xcframework_exit_code=$? if [ $xcframework_exit_code -eq 0 ]; then + local source_swiftmodule_dir="${signed_framework_path}/Modules/${scheme_name}.swiftmodule" + if [[ -d "${source_swiftmodule_dir}" ]]; then + local library_identifier + library_identifier=$(/usr/libexec/PlistBuddy \ + -c "Print :AvailableLibraries:0:LibraryIdentifier" \ + "${xcframework_path}/Info.plist") + local packaged_swiftmodule_dir="${xcframework_path}/${library_identifier}/${scheme_name}.framework/Modules/${scheme_name}.swiftmodule" + mkdir -p "${packaged_swiftmodule_dir}" + + local compiled_swiftmodule + for compiled_swiftmodule in "${source_swiftmodule_dir}"/*.swiftmodule; do + [[ -f "${compiled_swiftmodule}" ]] || continue + cp "${compiled_swiftmodule}" "${packaged_swiftmodule_dir}/" + done + print_info "Preserved compiled Swift modules for ${scheme_name}.xcframework" + fi print_success "XCFramework ${scheme_name}.xcframework created at ${xcframework_path}" else echo "❌ Error: XCFramework creation for ${scheme_name} failed with exit code ${xcframework_exit_code}" @@ -649,7 +803,7 @@ function verify_xcframework_inputs() { verify_macho_has_arch "${framework_binary}" "arm64" verify_macho_has_arch "${framework_binary}" "x86_64" if [[ "${framework_name}" == "FBSimulatorControl" ]]; then - verify_fbsimulatorcontrol_has_xctest_client_type_patch "${framework_binary}" + verify_fbsimulatorcontrol_candidate_patches "${framework_binary}" fi done @@ -686,7 +840,7 @@ function verify_release_architectures() { verify_macho_has_arch "${framework_binary}" "arm64" verify_macho_has_arch "${framework_binary}" "x86_64" if [[ "${framework_name}" == "FBSimulatorControl" ]]; then - verify_fbsimulatorcontrol_has_xctest_client_type_patch "${framework_binary}" + verify_fbsimulatorcontrol_candidate_patches "${framework_binary}" fi done @@ -964,7 +1118,10 @@ Commands: Clean previous build products and derived data. frameworks - Build all IDB frameworks (FBControlCore, XCTestBootstrap, FBSimulatorControl, FBDeviceControl). + Generate the candidate project, then build all IDB frameworks (FBControlCore, XCTestBootstrap, FBSimulatorControl, FBDeviceControl). + + generate + Verify the pinned candidate and patches, then regenerate IDB projects using XcodeGen. install Install built frameworks to the Frameworks directory. @@ -1006,6 +1163,8 @@ Environment Variables (set inline, exported, or via a git-ignored .env file): AXE_ENV_FILE Path to the .env file to load (default: /.env) AXE_CODESIGN_IDENTITY Code-signing identity (required for signing; no default) IDB_CHECKOUT_DIR Directory for IDB repository (default: ./idb_checkout) + IDB_GIT_REF Exact IDB revision (default: e682506725e9efefb9c43b8b917c0b12eb2a5939) + IDB_PATCHES_DIR Candidate patch directory (default: ./patches/idb/e682506) BUILD_OUTPUT_DIR Directory for build outputs (default: ./build_products) DERIVED_DATA_PATH Directory for derived data (default: ./build_derived_data) TEMP_DIR Temporary directory for final packages (default: system temp) @@ -1044,6 +1203,7 @@ function cmd_clean() { function cmd_frameworks() { print_section "🔧" "Building Frameworks" + generate_idb_projects framework_build "FBControlCore" "${FBSIMCONTROL_PROJECT}" "${BUILD_OUTPUT_DIR}" framework_build "XCTestBootstrap" "${FBSIMCONTROL_PROJECT}" "${BUILD_OUTPUT_DIR}" framework_build "FBSimulatorControl" "${FBSIMCONTROL_PROJECT}" "${BUILD_OUTPUT_DIR}" @@ -1085,6 +1245,12 @@ function cmd_xcframeworks() { create_xcframework "XCTestBootstrap" "${BUILD_OUTPUT_DIR}" create_xcframework "FBSimulatorControl" "${BUILD_OUTPUT_DIR}" create_xcframework "FBDeviceControl" "${BUILD_OUTPUT_DIR}" + write_idb_build_evidence +} + +function cmd_generate() { + print_section "🧬" "Generating Candidate IDB Projects" + generate_idb_projects } function cmd_sign_xcframeworks() { @@ -1184,6 +1350,8 @@ case $COMMAND in exit 0;; setup) cmd_setup;; + generate) + cmd_generate;; clean) cmd_clean;; frameworks) From d4d464b46fe9311589307884c16001bf966247f1 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 14 Jul 2026 19:46:20 +0100 Subject: [PATCH 02/31] test(compatibility): Capture Xcode 26 and 27 CLI contracts Store matrix-scoped command, hierarchy, and provenance goldens so release-shaped payloads can be compared across the supported Xcode environments. Co-Authored-By: Codex --- Package.swift | 3 +- Tests/Goldens/README.md | 58 + .../contract.json | 8 + .../provenance.json | 9 + .../cases/error-batch-unknown-option/argv.txt | 1 + .../error-batch-unknown-option/exit-code.txt | 1 + .../error-batch-unknown-option/stderr.txt | 4 + .../error-batch-unknown-option/stdout.txt | 0 .../error-button-unknown-option/argv.txt | 1 + .../error-button-unknown-option/exit-code.txt | 1 + .../error-button-unknown-option/stderr.txt | 4 + .../error-button-unknown-option/stdout.txt | 0 .../error-describe-ui-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-drag-unknown-option/argv.txt | 1 + .../error-drag-unknown-option/exit-code.txt | 1 + .../error-drag-unknown-option/stderr.txt | 4 + .../error-drag-unknown-option/stdout.txt | 0 .../error-gesture-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../error-gesture-unknown-option/stderr.txt | 4 + .../error-gesture-unknown-option/stdout.txt | 0 .../cases/error-init-unknown-option/argv.txt | 1 + .../error-init-unknown-option/exit-code.txt | 1 + .../error-init-unknown-option/stderr.txt | 3 + .../error-init-unknown-option/stdout.txt | 0 .../error-key-combo-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../error-key-combo-unknown-option/stderr.txt | 4 + .../error-key-combo-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-key-unknown-option/argv.txt | 1 + .../error-key-unknown-option/exit-code.txt | 1 + .../cases/error-key-unknown-option/stderr.txt | 4 + .../cases/error-key-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 3 + .../stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../error-screenshot-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../error-slider-unknown-option/argv.txt | 1 + .../error-slider-unknown-option/exit-code.txt | 1 + .../error-slider-unknown-option/stderr.txt | 4 + .../error-slider-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-swipe-unknown-option/argv.txt | 1 + .../error-swipe-unknown-option/exit-code.txt | 1 + .../error-swipe-unknown-option/stderr.txt | 4 + .../error-swipe-unknown-option/stdout.txt | 0 .../cases/error-tap-unknown-option/argv.txt | 1 + .../error-tap-unknown-option/exit-code.txt | 1 + .../cases/error-tap-unknown-option/stderr.txt | 4 + .../cases/error-tap-unknown-option/stdout.txt | 0 .../cases/error-touch-unknown-option/argv.txt | 1 + .../error-touch-unknown-option/exit-code.txt | 1 + .../error-touch-unknown-option/stderr.txt | 4 + .../error-touch-unknown-option/stdout.txt | 0 .../cases/error-type-unknown-option/argv.txt | 1 + .../error-type-unknown-option/exit-code.txt | 1 + .../error-type-unknown-option/stderr.txt | 4 + .../error-type-unknown-option/stdout.txt | 0 .../stable/cases/help-batch/argv.txt | 1 + .../stable/cases/help-batch/exit-code.txt | 1 + .../stable/cases/help-batch/stderr.txt | 0 .../stable/cases/help-batch/stdout.txt | 49 + .../stable/cases/help-button/argv.txt | 1 + .../stable/cases/help-button/exit-code.txt | 1 + .../stable/cases/help-button/stderr.txt | 0 .../stable/cases/help-button/stdout.txt | 21 + .../stable/cases/help-describe-ui/argv.txt | 1 + .../cases/help-describe-ui/exit-code.txt | 1 + .../stable/cases/help-describe-ui/stderr.txt | 0 .../stable/cases/help-describe-ui/stdout.txt | 12 + .../stable/cases/help-drag/argv.txt | 1 + .../stable/cases/help-drag/exit-code.txt | 1 + .../stable/cases/help-drag/stderr.txt | 0 .../stable/cases/help-drag/stdout.txt | 21 + .../stable/cases/help-gesture/argv.txt | 1 + .../stable/cases/help-gesture/exit-code.txt | 1 + .../stable/cases/help-gesture/stderr.txt | 0 .../stable/cases/help-gesture/stdout.txt | 39 + .../stable/cases/help-init/argv.txt | 1 + .../stable/cases/help-init/exit-code.txt | 1 + .../stable/cases/help-init/stderr.txt | 0 .../stable/cases/help-init/stdout.txt | 16 + .../stable/cases/help-key-combo/argv.txt | 1 + .../stable/cases/help-key-combo/exit-code.txt | 1 + .../stable/cases/help-key-combo/stderr.txt | 0 .../stable/cases/help-key-combo/stdout.txt | 37 + .../stable/cases/help-key-sequence/argv.txt | 1 + .../cases/help-key-sequence/exit-code.txt | 1 + .../stable/cases/help-key-sequence/stderr.txt | 0 .../stable/cases/help-key-sequence/stdout.txt | 23 + .../stable/cases/help-key/argv.txt | 1 + .../stable/cases/help-key/exit-code.txt | 1 + .../stable/cases/help-key/stderr.txt | 0 .../stable/cases/help-key/stdout.txt | 29 + .../cases/help-list-simulators/argv.txt | 1 + .../cases/help-list-simulators/exit-code.txt | 1 + .../cases/help-list-simulators/stderr.txt | 0 .../cases/help-list-simulators/stdout.txt | 8 + .../stable/cases/help-record-video/argv.txt | 1 + .../cases/help-record-video/exit-code.txt | 1 + .../stable/cases/help-record-video/stderr.txt | 0 .../stable/cases/help-record-video/stdout.txt | 15 + .../stable/cases/help-screenshot/argv.txt | 1 + .../cases/help-screenshot/exit-code.txt | 1 + .../stable/cases/help-screenshot/stderr.txt | 0 .../stable/cases/help-screenshot/stdout.txt | 13 + .../stable/cases/help-slider/argv.txt | 1 + .../stable/cases/help-slider/exit-code.txt | 1 + .../stable/cases/help-slider/stderr.txt | 0 .../stable/cases/help-slider/stdout.txt | 24 + .../stable/cases/help-stream-video/argv.txt | 1 + .../cases/help-stream-video/exit-code.txt | 1 + .../stable/cases/help-stream-video/stderr.txt | 0 .../stable/cases/help-stream-video/stdout.txt | 14 + .../stable/cases/help-swipe/argv.txt | 1 + .../stable/cases/help-swipe/exit-code.txt | 1 + .../stable/cases/help-swipe/stderr.txt | 0 .../stable/cases/help-swipe/stdout.txt | 18 + .../stable/cases/help-tap/argv.txt | 1 + .../stable/cases/help-tap/exit-code.txt | 1 + .../stable/cases/help-tap/stderr.txt | 0 .../stable/cases/help-tap/stdout.txt | 42 + .../stable/cases/help-touch/argv.txt | 1 + .../stable/cases/help-touch/exit-code.txt | 1 + .../stable/cases/help-touch/stderr.txt | 0 .../stable/cases/help-touch/stdout.txt | 28 + .../stable/cases/help-type/argv.txt | 1 + .../stable/cases/help-type/exit-code.txt | 1 + .../stable/cases/help-type/stderr.txt | 0 .../stable/cases/help-type/stdout.txt | 38 + .../stable/cases/help/argv.txt | 1 + .../stable/cases/help/exit-code.txt | 1 + .../stable/cases/help/stderr.txt | 0 .../stable/cases/help/stdout.txt | 42 + .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../output-screenshot-missing-value/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../stable/cases/stdin-batch-empty/argv.txt | 1 + .../cases/stdin-batch-empty/exit-code.txt | 1 + .../stable/cases/stdin-batch-empty/stderr.txt | 3 + .../stable/cases/stdin-batch-empty/stdin.txt | 0 .../stable/cases/stdin-batch-empty/stdout.txt | 0 .../cases/stdin-type-unsupported/argv.txt | 1 + .../stdin-type-unsupported/exit-code.txt | 1 + .../cases/stdin-type-unsupported/stderr.txt | 1 + .../cases/stdin-type-unsupported/stdin.txt | 1 + .../cases/stdin-type-unsupported/stdout.txt | 0 .../cases/validation-batch-source/argv.txt | 1 + .../validation-batch-source/exit-code.txt | 1 + .../cases/validation-batch-source/stderr.txt | 3 + .../cases/validation-batch-source/stdout.txt | 0 .../cases/validation-button-value/argv.txt | 1 + .../validation-button-value/exit-code.txt | 1 + .../cases/validation-button-value/stderr.txt | 4 + .../cases/validation-button-value/stdout.txt | 0 .../validation-describe-ui-point/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-describe-ui-point/stderr.txt | 3 + .../validation-describe-ui-point/stdout.txt | 0 .../cases/validation-drag-duration/argv.txt | 1 + .../validation-drag-duration/exit-code.txt | 1 + .../cases/validation-drag-duration/stderr.txt | 4 + .../cases/validation-drag-duration/stdout.txt | 0 .../cases/validation-gesture-value/argv.txt | 1 + .../validation-gesture-value/exit-code.txt | 1 + .../cases/validation-gesture-value/stderr.txt | 12 + .../cases/validation-gesture-value/stdout.txt | 0 .../cases/validation-init-client/argv.txt | 1 + .../validation-init-client/exit-code.txt | 1 + .../cases/validation-init-client/stderr.txt | 4 + .../cases/validation-init-client/stdout.txt | 0 .../cases/validation-key-combo-value/argv.txt | 1 + .../validation-key-combo-value/exit-code.txt | 1 + .../validation-key-combo-value/stderr.txt | 3 + .../validation-key-combo-value/stdout.txt | 0 .../validation-key-sequence-value/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-key-sequence-value/stderr.txt | 3 + .../validation-key-sequence-value/stdout.txt | 0 .../cases/validation-key-value/argv.txt | 1 + .../cases/validation-key-value/exit-code.txt | 1 + .../cases/validation-key-value/stderr.txt | 3 + .../cases/validation-key-value/stdout.txt | 0 .../validation-list-simulators-value/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 3 + .../stdout.txt | 0 .../validation-record-video-fps/argv.txt | 1 + .../validation-record-video-fps/exit-code.txt | 1 + .../validation-record-video-fps/stderr.txt | 3 + .../validation-record-video-fps/stdout.txt | 0 .../validation-screenshot-output/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-screenshot-output/stderr.txt | 4 + .../validation-screenshot-output/stdout.txt | 0 .../cases/validation-slider-value/argv.txt | 1 + .../validation-slider-value/exit-code.txt | 1 + .../cases/validation-slider-value/stderr.txt | 3 + .../cases/validation-slider-value/stdout.txt | 0 .../validation-stream-video-format/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-stream-video-format/stderr.txt | 4 + .../validation-stream-video-format/stdout.txt | 0 .../cases/validation-swipe-duration/argv.txt | 1 + .../validation-swipe-duration/exit-code.txt | 1 + .../validation-swipe-duration/stderr.txt | 4 + .../validation-swipe-duration/stdout.txt | 0 .../cases/validation-tap-coordinates/argv.txt | 1 + .../validation-tap-coordinates/exit-code.txt | 1 + .../validation-tap-coordinates/stderr.txt | 4 + .../validation-tap-coordinates/stdout.txt | 0 .../cases/validation-touch-mode/argv.txt | 1 + .../cases/validation-touch-mode/exit-code.txt | 1 + .../cases/validation-touch-mode/stderr.txt | 3 + .../cases/validation-touch-mode/stdout.txt | 0 .../cases/validation-type-source/argv.txt | 1 + .../validation-type-source/exit-code.txt | 1 + .../cases/validation-type-source/stderr.txt | 3 + .../cases/validation-type-source/stdout.txt | 0 .../stable/cases/version/argv.txt | 1 + .../stable/cases/version/exit-code.txt | 1 + .../stable/cases/version/stderr.txt | 0 .../stable/cases/version/stdout.txt | 1 + .../stable/hierarchy/exit-code.txt | 1 + .../stable/hierarchy/schema-types.json | 1082 ++++++++++++++++ .../stable/hierarchy/stderr.txt | 0 .../contract.json | 8 + .../provenance.json | 9 + .../cases/error-batch-unknown-option/argv.txt | 1 + .../error-batch-unknown-option/exit-code.txt | 1 + .../error-batch-unknown-option/stderr.txt | 4 + .../error-batch-unknown-option/stdout.txt | 0 .../error-button-unknown-option/argv.txt | 1 + .../error-button-unknown-option/exit-code.txt | 1 + .../error-button-unknown-option/stderr.txt | 4 + .../error-button-unknown-option/stdout.txt | 0 .../error-describe-ui-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-drag-unknown-option/argv.txt | 1 + .../error-drag-unknown-option/exit-code.txt | 1 + .../error-drag-unknown-option/stderr.txt | 4 + .../error-drag-unknown-option/stdout.txt | 0 .../error-gesture-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../error-gesture-unknown-option/stderr.txt | 4 + .../error-gesture-unknown-option/stdout.txt | 0 .../cases/error-init-unknown-option/argv.txt | 1 + .../error-init-unknown-option/exit-code.txt | 1 + .../error-init-unknown-option/stderr.txt | 3 + .../error-init-unknown-option/stdout.txt | 0 .../error-key-combo-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../error-key-combo-unknown-option/stderr.txt | 4 + .../error-key-combo-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-key-unknown-option/argv.txt | 1 + .../error-key-unknown-option/exit-code.txt | 1 + .../cases/error-key-unknown-option/stderr.txt | 4 + .../cases/error-key-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 3 + .../stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../error-screenshot-unknown-option/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../error-slider-unknown-option/argv.txt | 1 + .../error-slider-unknown-option/exit-code.txt | 1 + .../error-slider-unknown-option/stderr.txt | 4 + .../error-slider-unknown-option/stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../cases/error-swipe-unknown-option/argv.txt | 1 + .../error-swipe-unknown-option/exit-code.txt | 1 + .../error-swipe-unknown-option/stderr.txt | 4 + .../error-swipe-unknown-option/stdout.txt | 0 .../cases/error-tap-unknown-option/argv.txt | 1 + .../error-tap-unknown-option/exit-code.txt | 1 + .../cases/error-tap-unknown-option/stderr.txt | 4 + .../cases/error-tap-unknown-option/stdout.txt | 0 .../cases/error-touch-unknown-option/argv.txt | 1 + .../error-touch-unknown-option/exit-code.txt | 1 + .../error-touch-unknown-option/stderr.txt | 4 + .../error-touch-unknown-option/stdout.txt | 0 .../cases/error-type-unknown-option/argv.txt | 1 + .../error-type-unknown-option/exit-code.txt | 1 + .../error-type-unknown-option/stderr.txt | 4 + .../error-type-unknown-option/stdout.txt | 0 .../stable/cases/help-batch/argv.txt | 1 + .../stable/cases/help-batch/exit-code.txt | 1 + .../stable/cases/help-batch/stderr.txt | 0 .../stable/cases/help-batch/stdout.txt | 49 + .../stable/cases/help-button/argv.txt | 1 + .../stable/cases/help-button/exit-code.txt | 1 + .../stable/cases/help-button/stderr.txt | 0 .../stable/cases/help-button/stdout.txt | 21 + .../stable/cases/help-describe-ui/argv.txt | 1 + .../cases/help-describe-ui/exit-code.txt | 1 + .../stable/cases/help-describe-ui/stderr.txt | 0 .../stable/cases/help-describe-ui/stdout.txt | 12 + .../stable/cases/help-drag/argv.txt | 1 + .../stable/cases/help-drag/exit-code.txt | 1 + .../stable/cases/help-drag/stderr.txt | 0 .../stable/cases/help-drag/stdout.txt | 21 + .../stable/cases/help-gesture/argv.txt | 1 + .../stable/cases/help-gesture/exit-code.txt | 1 + .../stable/cases/help-gesture/stderr.txt | 0 .../stable/cases/help-gesture/stdout.txt | 39 + .../stable/cases/help-init/argv.txt | 1 + .../stable/cases/help-init/exit-code.txt | 1 + .../stable/cases/help-init/stderr.txt | 0 .../stable/cases/help-init/stdout.txt | 16 + .../stable/cases/help-key-combo/argv.txt | 1 + .../stable/cases/help-key-combo/exit-code.txt | 1 + .../stable/cases/help-key-combo/stderr.txt | 0 .../stable/cases/help-key-combo/stdout.txt | 37 + .../stable/cases/help-key-sequence/argv.txt | 1 + .../cases/help-key-sequence/exit-code.txt | 1 + .../stable/cases/help-key-sequence/stderr.txt | 0 .../stable/cases/help-key-sequence/stdout.txt | 23 + .../stable/cases/help-key/argv.txt | 1 + .../stable/cases/help-key/exit-code.txt | 1 + .../stable/cases/help-key/stderr.txt | 0 .../stable/cases/help-key/stdout.txt | 29 + .../cases/help-list-simulators/argv.txt | 1 + .../cases/help-list-simulators/exit-code.txt | 1 + .../cases/help-list-simulators/stderr.txt | 0 .../cases/help-list-simulators/stdout.txt | 8 + .../stable/cases/help-record-video/argv.txt | 1 + .../cases/help-record-video/exit-code.txt | 1 + .../stable/cases/help-record-video/stderr.txt | 0 .../stable/cases/help-record-video/stdout.txt | 15 + .../stable/cases/help-screenshot/argv.txt | 1 + .../cases/help-screenshot/exit-code.txt | 1 + .../stable/cases/help-screenshot/stderr.txt | 0 .../stable/cases/help-screenshot/stdout.txt | 13 + .../stable/cases/help-slider/argv.txt | 1 + .../stable/cases/help-slider/exit-code.txt | 1 + .../stable/cases/help-slider/stderr.txt | 0 .../stable/cases/help-slider/stdout.txt | 24 + .../stable/cases/help-stream-video/argv.txt | 1 + .../cases/help-stream-video/exit-code.txt | 1 + .../stable/cases/help-stream-video/stderr.txt | 0 .../stable/cases/help-stream-video/stdout.txt | 14 + .../stable/cases/help-swipe/argv.txt | 1 + .../stable/cases/help-swipe/exit-code.txt | 1 + .../stable/cases/help-swipe/stderr.txt | 0 .../stable/cases/help-swipe/stdout.txt | 18 + .../stable/cases/help-tap/argv.txt | 1 + .../stable/cases/help-tap/exit-code.txt | 1 + .../stable/cases/help-tap/stderr.txt | 0 .../stable/cases/help-tap/stdout.txt | 42 + .../stable/cases/help-touch/argv.txt | 1 + .../stable/cases/help-touch/exit-code.txt | 1 + .../stable/cases/help-touch/stderr.txt | 0 .../stable/cases/help-touch/stdout.txt | 28 + .../stable/cases/help-type/argv.txt | 1 + .../stable/cases/help-type/exit-code.txt | 1 + .../stable/cases/help-type/stderr.txt | 0 .../stable/cases/help-type/stdout.txt | 38 + .../stable/cases/help/argv.txt | 1 + .../stable/cases/help/exit-code.txt | 1 + .../stable/cases/help/stderr.txt | 0 .../stable/cases/help/stdout.txt | 42 + .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../output-screenshot-missing-value/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 4 + .../stdout.txt | 0 .../stable/cases/stdin-batch-empty/argv.txt | 1 + .../cases/stdin-batch-empty/exit-code.txt | 1 + .../stable/cases/stdin-batch-empty/stderr.txt | 3 + .../stable/cases/stdin-batch-empty/stdin.txt | 0 .../stable/cases/stdin-batch-empty/stdout.txt | 0 .../cases/stdin-type-unsupported/argv.txt | 1 + .../stdin-type-unsupported/exit-code.txt | 1 + .../cases/stdin-type-unsupported/stderr.txt | 1 + .../cases/stdin-type-unsupported/stdin.txt | 1 + .../cases/stdin-type-unsupported/stdout.txt | 0 .../cases/validation-batch-source/argv.txt | 1 + .../validation-batch-source/exit-code.txt | 1 + .../cases/validation-batch-source/stderr.txt | 3 + .../cases/validation-batch-source/stdout.txt | 0 .../cases/validation-button-value/argv.txt | 1 + .../validation-button-value/exit-code.txt | 1 + .../cases/validation-button-value/stderr.txt | 4 + .../cases/validation-button-value/stdout.txt | 0 .../validation-describe-ui-point/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-describe-ui-point/stderr.txt | 3 + .../validation-describe-ui-point/stdout.txt | 0 .../cases/validation-drag-duration/argv.txt | 1 + .../validation-drag-duration/exit-code.txt | 1 + .../cases/validation-drag-duration/stderr.txt | 4 + .../cases/validation-drag-duration/stdout.txt | 0 .../cases/validation-gesture-value/argv.txt | 1 + .../validation-gesture-value/exit-code.txt | 1 + .../cases/validation-gesture-value/stderr.txt | 12 + .../cases/validation-gesture-value/stdout.txt | 0 .../cases/validation-init-client/argv.txt | 1 + .../validation-init-client/exit-code.txt | 1 + .../cases/validation-init-client/stderr.txt | 4 + .../cases/validation-init-client/stdout.txt | 0 .../cases/validation-key-combo-value/argv.txt | 1 + .../validation-key-combo-value/exit-code.txt | 1 + .../validation-key-combo-value/stderr.txt | 3 + .../validation-key-combo-value/stdout.txt | 0 .../validation-key-sequence-value/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-key-sequence-value/stderr.txt | 3 + .../validation-key-sequence-value/stdout.txt | 0 .../cases/validation-key-value/argv.txt | 1 + .../cases/validation-key-value/exit-code.txt | 1 + .../cases/validation-key-value/stderr.txt | 3 + .../cases/validation-key-value/stdout.txt | 0 .../validation-list-simulators-value/argv.txt | 1 + .../exit-code.txt | 1 + .../stderr.txt | 3 + .../stdout.txt | 0 .../validation-record-video-fps/argv.txt | 1 + .../validation-record-video-fps/exit-code.txt | 1 + .../validation-record-video-fps/stderr.txt | 3 + .../validation-record-video-fps/stdout.txt | 0 .../validation-screenshot-output/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-screenshot-output/stderr.txt | 4 + .../validation-screenshot-output/stdout.txt | 0 .../cases/validation-slider-value/argv.txt | 1 + .../validation-slider-value/exit-code.txt | 1 + .../cases/validation-slider-value/stderr.txt | 3 + .../cases/validation-slider-value/stdout.txt | 0 .../validation-stream-video-format/argv.txt | 1 + .../exit-code.txt | 1 + .../validation-stream-video-format/stderr.txt | 4 + .../validation-stream-video-format/stdout.txt | 0 .../cases/validation-swipe-duration/argv.txt | 1 + .../validation-swipe-duration/exit-code.txt | 1 + .../validation-swipe-duration/stderr.txt | 4 + .../validation-swipe-duration/stdout.txt | 0 .../cases/validation-tap-coordinates/argv.txt | 1 + .../validation-tap-coordinates/exit-code.txt | 1 + .../validation-tap-coordinates/stderr.txt | 4 + .../validation-tap-coordinates/stdout.txt | 0 .../cases/validation-touch-mode/argv.txt | 1 + .../cases/validation-touch-mode/exit-code.txt | 1 + .../cases/validation-touch-mode/stderr.txt | 3 + .../cases/validation-touch-mode/stdout.txt | 0 .../cases/validation-type-source/argv.txt | 1 + .../validation-type-source/exit-code.txt | 1 + .../cases/validation-type-source/stderr.txt | 3 + .../cases/validation-type-source/stdout.txt | 0 .../stable/cases/version/argv.txt | 1 + .../stable/cases/version/exit-code.txt | 1 + .../stable/cases/version/stderr.txt | 0 .../stable/cases/version/stdout.txt | 1 + .../stable/hierarchy/exit-code.txt | 1 + .../stable/hierarchy/schema-types.json | 1086 +++++++++++++++++ .../stable/hierarchy/stderr.txt | 0 scripts/regenerate-goldens.sh | 334 +++++ 505 files changed, 4136 insertions(+), 1 deletion(-) create mode 100644 Tests/Goldens/README.md create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/contract.json create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/provenance.json create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-stream-video-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-stream-video-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-stream-video-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-stream-video-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-swipe-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-swipe-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-swipe-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-swipe-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-tap-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-tap-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-tap-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-tap-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-touch-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-touch-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-touch-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-touch-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-type-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-type-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-type-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-type-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-batch/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-batch/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-batch/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-batch/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-button/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-button/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-button/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-button/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-describe-ui/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-describe-ui/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-describe-ui/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-describe-ui/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-drag/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-drag/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-drag/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-drag/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-gesture/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-gesture/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-gesture/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-gesture/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-init/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-init/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-init/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-init/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-combo/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-combo/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-combo/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-combo/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-sequence/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-sequence/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-sequence/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key-sequence/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-key/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-list-simulators/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-list-simulators/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-list-simulators/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-list-simulators/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-record-video/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-record-video/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-record-video/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-record-video/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-screenshot/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-screenshot/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-screenshot/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-screenshot/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-slider/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-slider/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-slider/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-slider/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-stream-video/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-stream-video/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-stream-video/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-stream-video/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-swipe/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-swipe/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-swipe/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-swipe/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-tap/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-tap/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-tap/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-tap/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-touch/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-touch/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-touch/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-touch/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-type/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-type/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-type/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help-type/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/help/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-record-video-missing-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-record-video-missing-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-record-video-missing-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-record-video-missing-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-screenshot-missing-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-screenshot-missing-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-screenshot-missing-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-screenshot-missing-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-stream-video-stdout-format/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-stream-video-stdout-format/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-stream-video-stdout-format/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/output-stream-video-stdout-format/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-batch-empty/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-batch-empty/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-batch-empty/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-batch-empty/stdin.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-batch-empty/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-type-unsupported/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-type-unsupported/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-type-unsupported/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-type-unsupported/stdin.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/stdin-type-unsupported/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-batch-source/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-batch-source/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-batch-source/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-batch-source/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-button-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-button-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-button-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-button-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-describe-ui-point/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-describe-ui-point/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-describe-ui-point/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-describe-ui-point/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-drag-duration/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-drag-duration/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-drag-duration/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-drag-duration/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-gesture-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-gesture-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-gesture-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-gesture-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-init-client/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-init-client/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-init-client/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-init-client/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-combo-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-combo-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-combo-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-combo-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-sequence-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-sequence-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-sequence-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-sequence-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-key-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-list-simulators-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-list-simulators-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-list-simulators-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-list-simulators-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-record-video-fps/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-record-video-fps/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-record-video-fps/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-record-video-fps/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-screenshot-output/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-screenshot-output/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-screenshot-output/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-screenshot-output/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-slider-value/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-slider-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-slider-value/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-slider-value/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-stream-video-format/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-stream-video-format/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-stream-video-format/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-stream-video-format/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-swipe-duration/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-swipe-duration/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-swipe-duration/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-swipe-duration/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-tap-coordinates/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-tap-coordinates/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-tap-coordinates/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-tap-coordinates/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-touch-mode/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-touch-mode/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-touch-mode/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-touch-mode/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-type-source/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-type-source/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-type-source/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/validation-type-source/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/version/argv.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/version/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/version/stderr.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/version/stdout.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/hierarchy/exit-code.txt create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/hierarchy/schema-types.json create mode 100644 Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/hierarchy/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/contract.json create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/provenance.json create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-batch-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-batch-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-batch-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-batch-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-button-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-button-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-button-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-button-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-describe-ui-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-describe-ui-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-describe-ui-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-describe-ui-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-drag-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-drag-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-drag-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-drag-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-gesture-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-gesture-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-gesture-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-gesture-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-init-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-init-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-init-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-init-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-combo-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-combo-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-combo-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-combo-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-sequence-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-sequence-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-sequence-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-sequence-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-key-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-list-simulators-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-list-simulators-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-list-simulators-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-list-simulators-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-record-video-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-record-video-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-record-video-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-record-video-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-screenshot-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-screenshot-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-screenshot-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-screenshot-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-slider-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-slider-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-slider-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-slider-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-stream-video-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-stream-video-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-stream-video-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-stream-video-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-swipe-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-swipe-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-swipe-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-swipe-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-tap-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-tap-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-tap-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-tap-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-touch-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-touch-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-touch-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-touch-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-type-unknown-option/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-type-unknown-option/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-type-unknown-option/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/error-type-unknown-option/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-batch/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-batch/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-batch/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-batch/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-button/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-button/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-button/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-button/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-describe-ui/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-describe-ui/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-describe-ui/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-describe-ui/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-drag/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-drag/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-drag/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-drag/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-gesture/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-gesture/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-gesture/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-gesture/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-init/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-init/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-init/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-init/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-combo/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-combo/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-combo/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-combo/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-sequence/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-sequence/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-sequence/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key-sequence/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-key/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-list-simulators/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-list-simulators/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-list-simulators/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-list-simulators/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-record-video/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-record-video/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-record-video/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-record-video/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-screenshot/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-screenshot/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-screenshot/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-screenshot/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-slider/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-slider/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-slider/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-slider/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-stream-video/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-stream-video/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-stream-video/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-stream-video/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-swipe/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-swipe/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-swipe/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-swipe/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-tap/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-tap/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-tap/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-tap/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-touch/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-touch/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-touch/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-touch/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-type/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-type/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-type/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help-type/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/help/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-record-video-missing-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-record-video-missing-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-record-video-missing-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-record-video-missing-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-screenshot-missing-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-screenshot-missing-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-screenshot-missing-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-screenshot-missing-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-stream-video-stdout-format/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-stream-video-stdout-format/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-stream-video-stdout-format/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/output-stream-video-stdout-format/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-batch-empty/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-batch-empty/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-batch-empty/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-batch-empty/stdin.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-batch-empty/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-type-unsupported/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-type-unsupported/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-type-unsupported/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-type-unsupported/stdin.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/stdin-type-unsupported/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-batch-source/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-batch-source/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-batch-source/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-batch-source/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-button-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-button-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-button-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-button-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-describe-ui-point/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-describe-ui-point/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-describe-ui-point/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-describe-ui-point/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-drag-duration/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-drag-duration/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-drag-duration/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-drag-duration/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-gesture-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-gesture-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-gesture-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-gesture-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-init-client/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-init-client/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-init-client/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-init-client/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-combo-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-combo-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-combo-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-combo-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-sequence-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-sequence-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-sequence-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-sequence-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-key-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-list-simulators-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-list-simulators-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-list-simulators-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-list-simulators-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-record-video-fps/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-record-video-fps/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-record-video-fps/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-record-video-fps/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-screenshot-output/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-screenshot-output/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-screenshot-output/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-screenshot-output/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-slider-value/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-slider-value/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-slider-value/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-slider-value/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-stream-video-format/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-stream-video-format/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-stream-video-format/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-stream-video-format/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-swipe-duration/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-swipe-duration/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-swipe-duration/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-swipe-duration/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-tap-coordinates/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-tap-coordinates/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-tap-coordinates/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-tap-coordinates/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-touch-mode/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-touch-mode/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-touch-mode/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-touch-mode/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-type-source/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-type-source/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-type-source/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/validation-type-source/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/version/argv.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/version/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/version/stderr.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/cases/version/stdout.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/hierarchy/exit-code.txt create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/hierarchy/schema-types.json create mode 100644 Tests/Goldens/xcode-27.0-beta3-27A5218g_ios-27.0-24A5380g/stable/hierarchy/stderr.txt create mode 100755 scripts/regenerate-goldens.sh diff --git a/Package.swift b/Package.swift index da528d1..0b749ee 100644 --- a/Package.swift +++ b/Package.swift @@ -76,7 +76,8 @@ let package = Package( .testTarget( name: "AXeTests", dependencies: ["AXe", "AXeCore"], - path: "Tests" + path: "Tests", + exclude: ["Goldens"] ), .plugin( name: "VersionPlugin", diff --git a/Tests/Goldens/README.md b/Tests/Goldens/README.md new file mode 100644 index 0000000..ef311c3 --- /dev/null +++ b/Tests/Goldens/README.md @@ -0,0 +1,58 @@ +# Compatibility Goldens + +Each child directory is one explicit Xcode/runtime matrix cell. The directory name must identify the selected Xcode and simulator runtime builds, for example `xcode-26.5-17F42_ios-26.5-23F77`. + +Regenerate a cell with the exact release-shaped payload and a booted simulator containing AxePlayground: + +```bash +scripts/regenerate-goldens.sh \ + --axe /absolute/path/to/axe \ + --developer-dir /Applications/Xcode-26.5.0.app/Contents/Developer \ + --udid SIMULATOR_UDID \ + --matrix-id xcode-26.5-17F42_ios-26.5-23F77 \ + --update +``` + +Use `--check` with the same arguments to regenerate into a temporary directory and compare it with the checked-in cell. + +Each cell has three parts: + +- `contract.json` records stable matrix identity: schema version, Xcode version/build, runtime build, and fixture. +- `stable/` records argv, stdout, stderr, stdin where applicable, and exit status. It covers help and unknown-option behavior for every public subcommand, plus command-specific typed validation, stdin, and output-path contracts. Hierarchy values are intentionally excluded because labels, frames, and identifiers can be fixture- or host-specific; `stable/hierarchy/schema-types.json` records normalized JSON paths and types. +- `provenance.json` records volatile run identity: exact AXe payload SHA-256, simulator UDID/device name, and the SHA-256 of the stable contract. `--check` validates the newly generated provenance but compares only `contract.json` and `stable/`, so a different simulator or byte-identical rebuilt payload does not create golden churn. + +The local release matrix script must write `AXE_MATRIX_EVIDENCE_PATH` using schema version 1. The release gate accepts exactly the required Xcode 26.5/iOS 26.5 and Xcode 27 Beta 3/iOS 27 cells, binds them to `AXE_PAYLOAD_SHA256`, and verifies retained patch hashes against the repository: + +```json +{ + "schema_version": 1, + "result": "pass", + "source": {"axe_commit": "40-character commit SHA"}, + "payload": {"sha256": "64-character payload SHA-256"}, + "archives": {"universal_sha256": "64-character archive SHA-256"}, + "idb": { + "sha": "40-character pinned IDB SHA", + "retained_patches": [ + {"path": "patches/idb/e682506/example.patch", "sha256": "64-character patch SHA-256"} + ] + }, + "cells": [ + { + "xcode": {"version": "26.5", "build": "17F42"}, + "runtime": {"platform": "iOS", "version": "26.5", "build": "23F77"}, + "simulator": {"udid": "simulator UDID", "device_type": "iPhone model"}, + "commands": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"}, + "media": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"}, + "goldens": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"} + }, + { + "xcode": {"version": "27.0 Beta 3", "build": "27A5218g"}, + "runtime": {"platform": "iOS", "version": "27.0", "build": "24A5380g"}, + "simulator": {"udid": "simulator UDID", "device_type": "iPhone model"}, + "commands": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"}, + "media": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"}, + "goldens": {"result": "pass", "artifact_sha256": "64-character evidence SHA-256"} + } + ] +} +``` diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/contract.json b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/contract.json new file mode 100644 index 0000000..b569406 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/contract.json @@ -0,0 +1,8 @@ +{ + "fixture_screen": "tap-test", + "matrix_id": "xcode-26.5-17F42_ios-26.5-23F77", + "runtime_build": "23F77", + "schema_version": 1, + "xcode_build": "17F42", + "xcode_version": "Xcode 26.5" +} diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/provenance.json b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/provenance.json new file mode 100644 index 0000000..7e1066d --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/provenance.json @@ -0,0 +1,9 @@ +{ + "axe_payload_sha256": "583bc18685e9e8f57b8ddb00366c5659f2ed8998d918b6eef2172c4139eb30fd", + "schema_version": 1, + "simulator": { + "device_name": "iPhone 17 Pro", + "udid": "3E654A95-E95D-4023-ACDA-8064CC90FBDA" + }, + "stable_contract_sha256": "0791e86c72f7fe55d50a29ad8b5f0e48f593651307b037a7a3d03a2a1bb218ec" +} diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/argv.txt new file mode 100644 index 0000000..590bdc1 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/argv.txt @@ -0,0 +1 @@ +axe batch --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stderr.txt new file mode 100644 index 0000000..daa0782 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--udid ' +Help: --udid The UDID of the simulator. +Usage: axe batch --udid [--step ...] [--file ] [--stdin] [--ax-cache ] [--type-submission ] [--type-chunk-size ] [--tap-style ] [--continue-on-error] [--wait-timeout ] [--poll-interval ] [--verbose] + See 'axe batch --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-batch-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/argv.txt new file mode 100644 index 0000000..06550bc --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/argv.txt @@ -0,0 +1 @@ +axe button --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stderr.txt new file mode 100644 index 0000000..4496eb0 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '' +Help: The button to press. +Usage: axe button [--duration ] --udid + See 'axe button --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-button-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/argv.txt new file mode 100644 index 0000000..70ec796 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/argv.txt @@ -0,0 +1 @@ +axe describe-ui --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stderr.txt new file mode 100644 index 0000000..15b2c53 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--udid ' +Help: --udid The UDID of the simulator. +Usage: axe describe-ui --udid [--point ] + See 'axe describe-ui --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-describe-ui-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/argv.txt new file mode 100644 index 0000000..63fd2e1 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/argv.txt @@ -0,0 +1 @@ +axe drag --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stderr.txt new file mode 100644 index 0000000..a22d6cc --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--start-x ' +Help: --start-x The X coordinate of the starting point. +Usage: axe drag --start-x --start-y --end-x --end-y [--duration ] [--steps ] [--pre-delay ] [--post-delay ] --udid + See 'axe drag --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-drag-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/argv.txt new file mode 100644 index 0000000..9159aeb --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/argv.txt @@ -0,0 +1 @@ +axe gesture --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stderr.txt new file mode 100644 index 0000000..1355563 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '' +Help: The gesture preset to perform. +Usage: axe gesture [--screen-width ] [--screen-height ] [--duration ] [--delta ] [--pre-delay ] [--post-delay ] --udid + See 'axe gesture --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-gesture-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/argv.txt new file mode 100644 index 0000000..903fc11 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/argv.txt @@ -0,0 +1 @@ +axe init --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stderr.txt new file mode 100644 index 0000000..3f21d4c --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stderr.txt @@ -0,0 +1,3 @@ +Error: Unknown option '--axe-invalid-option' +Usage: axe init [--client ] [--dest ] [--force] [--uninstall] [--print] + See 'axe init --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-init-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/argv.txt new file mode 100644 index 0000000..bc24760 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/argv.txt @@ -0,0 +1 @@ +axe key-combo --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stderr.txt new file mode 100644 index 0000000..c4bee4c --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--modifiers ' +Help: --modifiers Comma-separated list of modifier keycodes to hold (0-255). +Usage: axe key-combo --modifiers --key --udid + See 'axe key-combo --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-combo-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/argv.txt new file mode 100644 index 0000000..6014848 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/argv.txt @@ -0,0 +1 @@ +axe key-sequence --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stderr.txt new file mode 100644 index 0000000..41aacdc --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--keycodes ' +Help: --keycodes Comma-separated list of HID keycodes to press in sequence. +Usage: axe key-sequence --keycodes [--delay ] --udid + See 'axe key-sequence --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-sequence-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/argv.txt new file mode 100644 index 0000000..8f09033 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/argv.txt @@ -0,0 +1 @@ +axe key --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stderr.txt new file mode 100644 index 0000000..d8875ba --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '' +Help: The HID keycode to press (0-255). +Usage: axe key [--duration ] --udid + See 'axe key --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-key-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/argv.txt new file mode 100644 index 0000000..56be9b5 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/argv.txt @@ -0,0 +1 @@ +axe list-simulators --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stderr.txt new file mode 100644 index 0000000..b9933cd --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stderr.txt @@ -0,0 +1,3 @@ +Error: Unknown option '--axe-invalid-option' +Usage: axe list-simulators + See 'axe list-simulators --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-list-simulators-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/argv.txt new file mode 100644 index 0000000..2cb7359 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/argv.txt @@ -0,0 +1 @@ +axe record-video --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stderr.txt new file mode 100644 index 0000000..41aeaec --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--udid ' +Help: --udid The UDID of the simulator. +Usage: axe record-video --udid [--fps ] [--quality ] [--scale ] [--output ] + See 'axe record-video --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-record-video-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/argv.txt new file mode 100644 index 0000000..0aaae25 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/argv.txt @@ -0,0 +1 @@ +axe screenshot --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stderr.txt new file mode 100644 index 0000000..cd9bb7d --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--udid ' +Help: --udid The UDID of the simulator. +Usage: axe screenshot --udid [--output ] + See 'axe screenshot --help' for more information. diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stdout.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-screenshot-unknown-option/stdout.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/argv.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/argv.txt new file mode 100644 index 0000000..30e199d --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/argv.txt @@ -0,0 +1 @@ +axe slider --axe-invalid-option diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/exit-code.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/exit-code.txt new file mode 100644 index 0000000..900731f --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/exit-code.txt @@ -0,0 +1 @@ +64 diff --git a/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/stderr.txt b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/stderr.txt new file mode 100644 index 0000000..c987b20 --- /dev/null +++ b/Tests/Goldens/xcode-26.5-17F42_ios-26.5-23F77/stable/cases/error-slider-unknown-option/stderr.txt @@ -0,0 +1,4 @@ +Error: Missing expected argument '--value ' +Help: --value Target slider value as a percentage from 0 to 100. +Usage: axe slider [--id ] [--label