11import XCTest
22import Foundation
3+ import AppKit
34import Darwin
45
56final 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