From 10fd323bb084c853f29887ee08d610e52db1dc31 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 23 Mar 2026 02:05:07 -0700 Subject: [PATCH] Fix display regression UI test launch gating --- .../DisplayResolutionRegressionUITests.swift | 158 +++--------------- 1 file changed, 20 insertions(+), 138 deletions(-) diff --git a/cmuxUITests/DisplayResolutionRegressionUITests.swift b/cmuxUITests/DisplayResolutionRegressionUITests.swift index 35529f3a..7342c52f 100644 --- a/cmuxUITests/DisplayResolutionRegressionUITests.swift +++ b/cmuxUITests/DisplayResolutionRegressionUITests.swift @@ -11,9 +11,7 @@ final class DisplayResolutionRegressionUITests: XCTestCase { private var displayDonePath = "" private var helperBinaryPath = "" private var helperLogPath = "" - private var appLogPath = "" - private var launchedAppProcess: Process? - private var launchedAppBundlePath = "" + private var launchedApp: XCUIApplication? private var helperProcess: Process? override func setUp() { @@ -32,7 +30,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase { displayDonePath = "\(tempPrefix).done" helperBinaryPath = "\(tempPrefix)-helper" helperLogPath = "\(tempPrefix)-helper.log" - appLogPath = "\(tempPrefix)-app.log" removeTestArtifacts() } @@ -243,38 +240,18 @@ final class DisplayResolutionRegressionUITests: XCTestCase { } private func launchAppProcess(targetDisplayID: String) throws { - let executablePath = try resolveAppExecutablePath() - launchedAppBundlePath = URL(fileURLWithPath: executablePath) - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - .path - - let proc = Process() - proc.executableURL = URL(fileURLWithPath: executablePath) - var environment = ProcessInfo.processInfo.environment + let app = XCUIApplication() for (key, value) in launchEnvironment(targetDisplayID: targetDisplayID) { - environment[key] = value + app.launchEnvironment[key] = value } - proc.environment = environment + app.launch() + launchedApp = app - let logHandle = FileHandle(forWritingAtPath: appLogPath) ?? { - FileManager.default.createFile(atPath: appLogPath, contents: nil) - return FileHandle(forWritingAtPath: appLogPath) - }() - proc.standardOutput = logHandle - proc.standardError = logHandle - - try proc.run() - launchedAppProcess = proc - - if !waitForAppLaunchDiagnostics(pid: proc.processIdentifier, timeout: 12.0) { + if !waitForAppLaunchDiagnostics(timeout: 12.0) { throw NSError(domain: "DisplayResolutionRegressionUITests", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "App process failed to write launch diagnostics. pid=\(proc.processIdentifier) log=\(readTrimmedFile(atPath: appLogPath) ?? "")" + NSLocalizedDescriptionKey: "App failed to write launch diagnostics. state=\(app.state.rawValue) diagnostics=\(loadDiagnostics() ?? [:])" ]) } - - reopenAppBundleIfNeeded() } private func launchEnvironment(targetDisplayID: String) -> [String: String] { @@ -288,21 +265,20 @@ final class DisplayResolutionRegressionUITests: XCTestCase { } private func terminateLaunchedAppIfNeeded() { - guard let launchedAppProcess else { return } - defer { - self.launchedAppProcess = nil - self.launchedAppBundlePath = "" + guard let launchedApp else { return } + defer { self.launchedApp = nil } + + if launchedApp.state == .notRunning { + return } - if launchedAppProcess.isRunning { - launchedAppProcess.terminate() - launchedAppProcess.waitUntilExit() - } + launchedApp.terminate() + _ = launchedApp.wait(for: .notRunning, timeout: 5.0) } private func launchedAppDiagnostics() -> String { - guard let launchedAppProcess else { return "not-launched" } - return "pid=\(launchedAppProcess.processIdentifier) running=\(launchedAppProcess.isRunning) bundle=\(launchedAppBundlePath)" + guard let launchedApp else { return "not-launched" } + return "state=\(launchedApp.state.rawValue)" } private func waitForTargetDisplayMove(targetDisplayID: String, timeout: TimeInterval) -> Bool { @@ -369,108 +345,15 @@ final class DisplayResolutionRegressionUITests: XCTestCase { .deletingLastPathComponent() } - private func resolveAppExecutablePath() throws -> String { - let fileManager = FileManager.default - let env = ProcessInfo.processInfo.environment - var productDirectories: [String] = [] - - if let builtProductsDir = env["BUILT_PRODUCTS_DIR"], !builtProductsDir.isEmpty { - productDirectories.append(builtProductsDir) - } - - if let hostPath = env["TEST_HOST"], !hostPath.isEmpty { - let hostURL = URL(fileURLWithPath: hostPath) - let productsDir = hostURL - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - .path - productDirectories.append(productsDir) - } - - productDirectories.append(contentsOf: inferredBuildProductsDirectories()) - - var candidates: [String] = [] - for productsDir in uniquePaths(productDirectories) { - appendAppExecutableCandidates(fromProductsDirectory: productsDir, to: &candidates) - } - - for path in uniquePaths(candidates) where fileManager.isExecutableFile(atPath: path) { - return URL(fileURLWithPath: path).resolvingSymlinksInPath().path - } - - throw NSError(domain: "DisplayResolutionRegressionUITests", code: 4, userInfo: [ - NSLocalizedDescriptionKey: "Unable to locate cmux app executable. searched=\(uniquePaths(candidates))" - ]) - } - - private func inferredBuildProductsDirectories() -> [String] { - let bundleURLs = [ - Bundle.main.bundleURL, - Bundle(for: Self.self).bundleURL, - ] - - return bundleURLs.compactMap { bundleURL in - let standardizedPath = bundleURL.standardizedFileURL.path - let components = standardizedPath.split(separator: "/") - guard let productsIndex = components.firstIndex(of: "Products"), - productsIndex + 1 < components.count else { - return nil - } - let prefixComponents = components.prefix(productsIndex + 2) - return "/" + prefixComponents.joined(separator: "/") - } - } - - private func appendAppExecutableCandidates(fromProductsDirectory productsDir: String, to candidates: inout [String]) { - candidates.append("\(productsDir)/cmux DEV.app/Contents/MacOS/cmux DEV") - candidates.append("\(productsDir)/cmux.app/Contents/MacOS/cmux") - - guard let entries = try? FileManager.default.contentsOfDirectory(atPath: productsDir) else { - return - } - - for entry in entries.sorted() where entry.hasSuffix(".app") { - let executableName = (entry as NSString).deletingPathExtension - let executablePath = URL(fileURLWithPath: productsDir) - .appendingPathComponent(entry) - .appendingPathComponent("Contents/MacOS") - .appendingPathComponent(executableName) - .path - candidates.append(executablePath) - } - } - - private func uniquePaths(_ paths: [String]) -> [String] { - var seen = Set() - var ordered: [String] = [] - for path in paths { - let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { continue } - if seen.insert(trimmed).inserted { - ordered.append(trimmed) - } - } - return ordered - } - - private func waitForAppLaunchDiagnostics(pid: Int32, timeout: TimeInterval) -> Bool { + private func waitForAppLaunchDiagnostics(timeout: TimeInterval) -> Bool { waitForCondition(timeout: timeout) { guard let diagnostics = self.loadDiagnostics() else { return false } - return diagnostics["pid"] == String(pid) + guard let pid = diagnostics["pid"], !pid.isEmpty else { return false } + guard let stage = diagnostics["stage"], !stage.isEmpty else { return false } + return true } } - private func reopenAppBundleIfNeeded() { - guard !launchedAppBundlePath.isEmpty else { return } - - let proc = Process() - proc.executableURL = URL(fileURLWithPath: "/usr/bin/open") - proc.arguments = [launchedAppBundlePath] - try? proc.run() - } - private func removeTestArtifacts() { for path in [ diagnosticsPath, @@ -480,7 +363,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase { displayDonePath, helperBinaryPath, helperLogPath, - appLogPath, ] { guard !path.isEmpty else { continue } try? FileManager.default.removeItem(atPath: path)