Skip to content

Commit 1532f17

Browse files
committed
Launch display regression app with NSWorkspace
1 parent 8bc0ed1 commit 1532f17

1 file changed

Lines changed: 60 additions & 57 deletions

File tree

cmuxUITests/DisplayResolutionRegressionUITests.swift

Lines changed: 60 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import XCTest
22
import Foundation
3+
import AppKit
34
import Darwin
45

56
final class DisplayResolutionRegressionUITests: XCTestCase {
@@ -12,8 +13,8 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
1213
private var displayDonePath = ""
1314
private var helperBinaryPath = ""
1415
private var helperLogPath = ""
15-
private var appLogPath = ""
16-
private var launchedAppProcess: Process?
16+
private var launchedApp: NSRunningApplication?
17+
private var launchedAppBundleURL: URL?
1718
private var helperProcess: Process?
1819

1920
override func setUp() {
@@ -32,7 +33,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
3233
displayDonePath = "\(tempPrefix).done"
3334
helperBinaryPath = "\(tempPrefix)-helper"
3435
helperLogPath = "\(tempPrefix)-helper.log"
35-
appLogPath = "\(tempPrefix)-app.log"
3636

3737
removeTestArtifacts()
3838
}
@@ -241,28 +241,41 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
241241
}
242242

243243
private func launchAppProcess(targetDisplayID: String) throws {
244-
let proc = Process()
245-
proc.executableURL = URL(fileURLWithPath: try resolveAppExecutablePath())
246-
proc.currentDirectoryURL = repoRootURL
247-
248-
var environment = sanitizedAppProcessEnvironment()
249-
for (key, value) in launchEnvironment(targetDisplayID: targetDisplayID) {
250-
environment[key] = value
244+
let appBundleURL = try resolveAppBundleURL()
245+
let configuration = NSWorkspace.OpenConfiguration()
246+
configuration.activates = false
247+
configuration.createsNewApplicationInstance = true
248+
configuration.environment = launchEnvironment(targetDisplayID: targetDisplayID)
249+
250+
var launchedApp: NSRunningApplication?
251+
var launchError: Error?
252+
NSWorkspace.shared.openApplication(at: appBundleURL, configuration: configuration) { app, error in
253+
launchedApp = app
254+
launchError = error
251255
}
252-
proc.environment = environment
253256

254-
let logHandle = FileHandle(forWritingAtPath: appLogPath) ?? {
255-
FileManager.default.createFile(atPath: appLogPath, contents: nil)
256-
return FileHandle(forWritingAtPath: appLogPath)
257-
}()
258-
proc.standardOutput = logHandle
259-
proc.standardError = logHandle
257+
let launchCompleted = waitForCondition(timeout: 15.0) {
258+
launchedApp != nil || launchError != nil
259+
}
260+
guard launchCompleted else {
261+
throw NSError(domain: "DisplayResolutionRegressionUITests", code: 5, userInfo: [
262+
NSLocalizedDescriptionKey: "Timed out waiting for NSWorkspace launch completion"
263+
])
264+
}
265+
if let launchError {
266+
throw launchError
267+
}
268+
guard let launchedApp else {
269+
throw NSError(domain: "DisplayResolutionRegressionUITests", code: 6, userInfo: [
270+
NSLocalizedDescriptionKey: "NSWorkspace launch returned no app instance"
271+
])
272+
}
260273

261-
try proc.run()
262-
launchedAppProcess = proc
274+
self.launchedApp = launchedApp
275+
launchedAppBundleURL = appBundleURL
263276
}
264277

265-
private func resolveAppExecutablePath() throws -> String {
278+
private func resolveAppBundleURL() throws -> URL {
266279
let env = ProcessInfo.processInfo.environment
267280
let fileManager = FileManager.default
268281
var candidateProductsDirectories: [String] = []
@@ -286,17 +299,15 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
286299
candidateProductsDirectories.append(contentsOf: inferredBuildProductsDirectories())
287300

288301
for productsDir in uniquePaths(candidateProductsDirectories) {
289-
let appExecutablePath = URL(fileURLWithPath: productsDir)
302+
let appBundleURL = URL(fileURLWithPath: productsDir)
290303
.appendingPathComponent("cmux DEV.app")
291-
.appendingPathComponent("Contents/MacOS/cmux DEV")
292-
.path
293-
if fileManager.isExecutableFile(atPath: appExecutablePath) {
294-
return appExecutablePath
304+
if fileManager.fileExists(atPath: appBundleURL.path) {
305+
return appBundleURL
295306
}
296307
}
297308

298309
throw NSError(domain: "DisplayResolutionRegressionUITests", code: 4, userInfo: [
299-
NSLocalizedDescriptionKey: "Unable to resolve cmux DEV executable from UI test environment"
310+
NSLocalizedDescriptionKey: "Unable to resolve cmux DEV app bundle from UI test environment"
300311
])
301312
}
302313

@@ -331,26 +342,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
331342
return ordered
332343
}
333344

334-
private func sanitizedAppProcessEnvironment() -> [String: String] {
335-
let blockedPrefixes = [
336-
"XCTest",
337-
"XCInject",
338-
"DYLD_",
339-
"__XPC_DYLD_",
340-
]
341-
let blockedKeys: Set<String> = [
342-
"LLVM_PROFILE_FILE",
343-
"OS_ACTIVITY_DT_MODE",
344-
]
345-
346-
return ProcessInfo.processInfo.environment.filter { key, _ in
347-
if blockedKeys.contains(key) {
348-
return false
349-
}
350-
return !blockedPrefixes.contains(where: { key.hasPrefix($0) })
351-
}
352-
}
353-
354345
private func launchEnvironment(targetDisplayID: String) -> [String: String] {
355346
[
356347
"CMUX_UI_TEST_MODE": "1",
@@ -362,7 +353,26 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
362353
}
363354

364355
private func terminateLaunchedAppIfNeeded() {
365-
terminateProcessIfNeeded(&launchedAppProcess, timeout: 5.0)
356+
guard let launchedApp else { return }
357+
defer {
358+
self.launchedApp = nil
359+
launchedAppBundleURL = nil
360+
}
361+
362+
guard !launchedApp.isTerminated else { return }
363+
364+
_ = launchedApp.terminate()
365+
let terminatedAfterTerminate = waitForCondition(timeout: 5.0) {
366+
launchedApp.isTerminated
367+
}
368+
if terminatedAfterTerminate {
369+
return
370+
}
371+
372+
_ = launchedApp.forceTerminate()
373+
_ = waitForCondition(timeout: 1.0) {
374+
launchedApp.isTerminated
375+
}
366376
}
367377

368378
private func terminateHelperProcessIfNeeded() {
@@ -397,15 +407,9 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
397407
}
398408

399409
private func launchedAppDiagnostics() -> String {
400-
guard let launchedAppProcess else { return "not-launched" }
401-
let log = readTrimmedFile(atPath: appLogPath) ?? "<missing>"
402-
let status: String
403-
if launchedAppProcess.isRunning {
404-
status = "running"
405-
} else {
406-
status = String(launchedAppProcess.terminationStatus)
407-
}
408-
return "pid=\(launchedAppProcess.processIdentifier) running=\(launchedAppProcess.isRunning) status=\(status) log=\(log)"
410+
guard let launchedApp else { return "not-launched" }
411+
let bundlePath = launchedAppBundleURL?.path ?? "<unknown>"
412+
return "pid=\(launchedApp.processIdentifier) terminated=\(launchedApp.isTerminated) bundle=\(bundlePath)"
409413
}
410414

411415
private func waitForTargetDisplayMove(targetDisplayID: String, timeout: TimeInterval) -> Bool {
@@ -481,7 +485,6 @@ final class DisplayResolutionRegressionUITests: XCTestCase {
481485
displayDonePath,
482486
helperBinaryPath,
483487
helperLogPath,
484-
appLogPath,
485488
] {
486489
guard !path.isEmpty else { continue }
487490
try? FileManager.default.removeItem(atPath: path)

0 commit comments

Comments
 (0)