@@ -2,6 +2,7 @@ import XCTest
22import UIKit
33import CoreGraphics
44import Darwin
5+ import Foundation
56
67final class HelloCodenameOneUITests: XCTestCase {
78 private var app: XCUIApplication!
@@ -275,13 +276,10 @@ final class HelloCodenameOneUITests: XCTestCase {
275276 if let explicit = targetBundleIdentifier, !explicit.isEmpty {
276277 return explicit
277278 }
278- do {
279- let value = try app.value(forKey: "bundleID")
280- if let actual = value as? String, !actual.isEmpty {
281- return actual
279+ if let bundle: String = dynamicAppValue("bundleID") {
280+ if !bundle.isEmpty {
281+ return bundle
282282 }
283- } catch {
284- print("CN1SS:WARN:ui_test_bundle_resolution_failed error=\(error)")
285283 }
286284 return nil
287285 }
@@ -449,20 +447,16 @@ private final class CodenameOneMainInvoker {
449447 }
450448
451449 private func locateAppContainer(app: XCUIApplication, bundleIdentifier: String) -> String? {
452- do {
453- if let bundleURL = try app.value(forKey: "bundleURL") as? URL {
454- return bundleURL.path
455- }
456- } catch {
457- print("CN1SS:WARN:codenameone_main_kvc_failed key=bundleURL bundle=\(bundleIdentifier) error=\(error)")
450+ if let bundleURL: URL = dynamicAppValue("bundleURL"), !bundleURL.path.isEmpty {
451+ return bundleURL.path
458452 }
459453
460- do {
461- if let bundlePath = try app.value(forKey: "bundlePath") as? String {
462- return bundlePath
463- }
464- } catch {
465- print("CN1SS:WARN:codenameone_main_kvc_failed key=bundlePath bundle=\(bundleIdentifier) error=\(error)")
454+ if let bundlePath: String = dynamicAppValue("bundlePath"), !bundlePath.isEmpty {
455+ return bundlePath
456+ }
457+
458+ if let container = locateViaSimctl(bundleIdentifier: bundleIdentifier) {
459+ return container
466460 }
467461
468462 if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
@@ -486,6 +480,71 @@ private final class CodenameOneMainInvoker {
486480 return executable
487481 }
488482
483+ private func locateViaSimctl(bundleIdentifier: String) -> String? {
484+ let env = ProcessInfo.processInfo.environment
485+ guard let udid = env["SIMULATOR_UDID"], !udid.isEmpty else {
486+ return nil
487+ }
488+
489+ let task = Process()
490+ task.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
491+ task.arguments = ["simctl", "get_app_container", udid, bundleIdentifier]
492+
493+ let stdoutPipe = Pipe()
494+ let stderrPipe = Pipe()
495+ task.standardOutput = stdoutPipe
496+ task.standardError = stderrPipe
497+
498+ do {
499+ try task.run()
500+ } catch {
501+ print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) error=\(error)")
502+ return nil
503+ }
504+
505+ task.waitUntilExit()
506+ if task.terminationStatus != 0 {
507+ let data = stderrPipe.fileHandleForReading.readDataToEndOfFile()
508+ if let message = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !message.isEmpty {
509+ print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus) stderr=\(message)")
510+ } else {
511+ print("CN1SS:WARN:codenameone_main_simctl_failed bundle=\(bundleIdentifier) status=\(task.terminationStatus)")
512+ }
513+ return nil
514+ }
515+
516+ let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
517+ guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty else {
518+ print("CN1SS:WARN:codenameone_main_simctl_empty bundle=\(bundleIdentifier)")
519+ return nil
520+ }
521+
522+ return output
523+ }
524+
525+ private func dynamicAppValue<T>(_ selectorName: String) -> T? {
526+ let selector = NSSelectorFromString(selectorName)
527+ guard app.responds(to: selector) else {
528+ return nil
529+ }
530+ guard let unmanaged = app.perform(selector) else {
531+ return nil
532+ }
533+ let value = unmanaged.takeUnretainedValue()
534+ switch value {
535+ case let typed as T:
536+ return typed
537+ case let number as NSNumber where T.self == Bool.self:
538+ return (number.boolValue as? T)
539+ case let string as NSString where T.self == String.self:
540+ return (string as String) as? T
541+ case let url as NSURL where T.self == URL.self:
542+ return (url as URL) as? T
543+ default:
544+ return nil
545+ }
546+ }
547+
489548 private struct InvocationContext {
490549 typealias InitConstantPoolFn = @convention(c) () -> Void
491550 typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer?
0 commit comments