mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
XCTest records a refused launch as a test issue that never throws, so the companion returned ok for an app that never started. Check the state the app actually reached and report the refusal instead.
102 lines
3.6 KiB
Swift
102 lines
3.6 KiB
Swift
import XCTest
|
|
|
|
// Launches and terminates apps through the automation session. Lifecycle
|
|
// performed outside the session leaves its cached app proxies bound to dead
|
|
// processes, after which snapshots hang and typing asserts.
|
|
enum AppLifecycle {
|
|
enum LifecycleError: Error {
|
|
case failed(String)
|
|
}
|
|
|
|
static func launch(bundleIdentifier: String, foregroundIfRunning: Bool) throws {
|
|
var reached = XCUIApplication.State.unknown
|
|
try onMainCatching {
|
|
let application = XCUIApplication(bundleIdentifier: bundleIdentifier)
|
|
if foregroundIfRunning {
|
|
// activate starts the app when needed and foregrounds it when
|
|
// already running, without a forced relaunch.
|
|
application.activate()
|
|
} else {
|
|
application.launch()
|
|
}
|
|
reached = application.state
|
|
}
|
|
// A refused launch is recorded as a test issue that never throws, so
|
|
// without this the runner answers ok for an app that is not running
|
|
// and the host learns nothing until its own bound expires.
|
|
switch reached {
|
|
case .runningForeground, .runningBackground, .runningBackgroundSuspended:
|
|
return
|
|
default:
|
|
throw LifecycleError.failed(
|
|
"\(bundleIdentifier) is \(name(of: reached)) after launch")
|
|
}
|
|
}
|
|
|
|
static func terminate(bundleIdentifier: String) throws {
|
|
try onMainCatching {
|
|
let application = XCUIApplication(bundleIdentifier: bundleIdentifier)
|
|
if application.state == .notRunning {
|
|
return
|
|
}
|
|
application.terminate()
|
|
}
|
|
}
|
|
|
|
// state reports the app's run state in the vocabulary the Go transport maps
|
|
// to a process state: foreground, background, or notRunning. On the
|
|
// simulator the hybrid never calls it; on device it backs ForegroundApp.
|
|
static func state(bundleIdentifier: String) -> String {
|
|
var result = "notRunning"
|
|
let collect = {
|
|
switch XCUIApplication(bundleIdentifier: bundleIdentifier).state {
|
|
case .runningForeground:
|
|
result = "foreground"
|
|
case .runningBackground, .runningBackgroundSuspended:
|
|
result = "background"
|
|
default:
|
|
result = "notRunning"
|
|
}
|
|
}
|
|
if Thread.isMainThread {
|
|
collect()
|
|
} else {
|
|
DispatchQueue.main.sync(execute: collect)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private static func name(of state: XCUIApplication.State) -> String {
|
|
switch state {
|
|
case .runningForeground:
|
|
return "foreground"
|
|
case .runningBackground:
|
|
return "background"
|
|
case .runningBackgroundSuspended:
|
|
return "suspended"
|
|
case .notRunning:
|
|
return "not running"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// onMainCatching runs automation work on the main thread and converts a
|
|
// framework assertion into a thrown error so the server survives it.
|
|
private static func onMainCatching(_ work: @escaping () -> Void) throws {
|
|
var caughtError: NSError?
|
|
var completed = false
|
|
let run = {
|
|
completed = CompanionRunCatching(work, &caughtError)
|
|
}
|
|
if Thread.isMainThread {
|
|
run()
|
|
} else {
|
|
DispatchQueue.main.sync(execute: run)
|
|
}
|
|
if !completed {
|
|
throw LifecycleError.failed(caughtError?.localizedDescription ?? "unknown")
|
|
}
|
|
}
|
|
}
|