Skip to content

Commit fe905c2

Browse files
committed
Trigger Codename One main from iOS UI tests
1 parent ce3db3f commit fe905c2

File tree

1 file changed

+142
-0
lines changed

1 file changed

+142
-0
lines changed

scripts/ios/tests/HelloCodenameOneUITests.swift.tmpl

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import XCTest
22
import UIKit
33
import CoreGraphics
4+
import Darwin
45

56
final class HelloCodenameOneUITests: XCTestCase {
67
private var app: XCUIApplication!
@@ -41,6 +42,7 @@ final class HelloCodenameOneUITests: XCTestCase {
4142
print("CN1SS:INFO:ui_test_launch_arguments=\(app.launchArguments.joined(separator: " "))")
4243

4344
ensureAppLaunched()
45+
triggerCodenameOneMainIfPossible()
4446
waitForStableFrame()
4547
}
4648

@@ -133,6 +135,7 @@ final class HelloCodenameOneUITests: XCTestCase {
133135

134136
private func captureScreenshot(named name: String) throws {
135137
ensureAppLaunched()
138+
triggerCodenameOneMainIfPossible()
136139
waitForStableFrame()
137140
let result = pollForRenderableContent(label: name, timeout: 30, poll: 0.6)
138141
let shot = result.screenshot
@@ -283,6 +286,14 @@ final class HelloCodenameOneUITests: XCTestCase {
283286
return nil
284287
}
285288

289+
private func triggerCodenameOneMainIfPossible() {
290+
guard let bundleID = resolveBundleIdentifier(), !bundleID.isEmpty else {
291+
print("CN1SS:WARN:codenameone_main_skipped reason=no_bundle_identifier")
292+
return
293+
}
294+
CodenameOneMainInvoker.shared.invokeIfNeeded(app: app, bundleIdentifier: bundleID)
295+
}
296+
286297
private func sanitizeTestName(_ name: String) -> String {
287298
let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-")
288299
let underscore: UnicodeScalar = "_"
@@ -361,3 +372,134 @@ final class HelloCodenameOneUITests: XCTestCase {
361372
print("\(prefix):END:\(name)")
362373
}
363374
}
375+
376+
private final class CodenameOneMainInvoker {
377+
static let shared = CodenameOneMainInvoker()
378+
379+
private let queue = DispatchQueue(label: "codenameone.main.invoker")
380+
private var invokedBundles: Set<String> = []
381+
private var handles: [String: UnsafeMutableRawPointer] = [:]
382+
383+
private init() {}
384+
385+
func invokeIfNeeded(app: XCUIApplication, bundleIdentifier: String) {
386+
var alreadyInvoked = false
387+
queue.sync {
388+
alreadyInvoked = invokedBundles.contains(bundleIdentifier)
389+
}
390+
if alreadyInvoked {
391+
return
392+
}
393+
394+
guard let context = prepareInvocation(app: app, bundleIdentifier: bundleIdentifier) else {
395+
return
396+
}
397+
398+
context.invoke()
399+
400+
queue.sync {
401+
invokedBundles.insert(bundleIdentifier)
402+
handles[bundleIdentifier] = context.handle
403+
}
404+
print("CN1SS:INFO:codenameone_main_invoked bundle=\(bundleIdentifier)")
405+
}
406+
407+
private func prepareInvocation(app: XCUIApplication, bundleIdentifier: String) -> InvocationContext? {
408+
guard let container = locateAppContainer(app: app, bundleIdentifier: bundleIdentifier) else {
409+
print("CN1SS:WARN:codenameone_main_skipped reason=container_missing bundle=\(bundleIdentifier)")
410+
return nil
411+
}
412+
413+
guard let executable = readExecutableName(appContainer: container) else {
414+
print("CN1SS:WARN:codenameone_main_skipped reason=executable_missing bundle=\(bundleIdentifier)")
415+
return nil
416+
}
417+
418+
let binaryPath = (container as NSString).appendingPathComponent(executable)
419+
guard let handle = dlopen(binaryPath, RTLD_NOW | RTLD_GLOBAL) else {
420+
if let error = dlerror() {
421+
print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier) error=\(String(cString: error))")
422+
} else {
423+
print("CN1SS:WARN:codenameone_main_skipped reason=dlopen_failed bundle=\(bundleIdentifier)")
424+
}
425+
return nil
426+
}
427+
428+
guard let initPtr = dlsym(handle, "initConstantPool") else {
429+
print("CN1SS:WARN:codenameone_main_skipped reason=missing_initConstantPool bundle=\(bundleIdentifier)")
430+
return nil
431+
}
432+
433+
guard let threadPtr = dlsym(handle, "getThreadLocalData") else {
434+
print("CN1SS:WARN:codenameone_main_skipped reason=missing_getThreadLocalData bundle=\(bundleIdentifier)")
435+
return nil
436+
}
437+
438+
let mainSymbol = "com_codenameone_examples_HelloCodenameOne_main___java_lang_String_1ARRAY"
439+
guard let mainPtr = dlsym(handle, mainSymbol) else {
440+
print("CN1SS:WARN:codenameone_main_skipped reason=missing_main_symbol bundle=\(bundleIdentifier) symbol=\(mainSymbol)")
441+
return nil
442+
}
443+
444+
let initFn = unsafeBitCast(initPtr, to: InvocationContext.InitConstantPoolFn.self)
445+
let threadFn = unsafeBitCast(threadPtr, to: InvocationContext.GetThreadLocalDataFn.self)
446+
let mainFn = unsafeBitCast(mainPtr, to: InvocationContext.CodenameOneMainFn.self)
447+
448+
return InvocationContext(handle: handle, initConstantPool: initFn, getThreadLocalData: threadFn, mainFunction: mainFn)
449+
}
450+
451+
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)")
458+
}
459+
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)")
466+
}
467+
468+
if let fallback = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
469+
print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier) fallbackExecutable=\(fallback)")
470+
} else {
471+
print("CN1SS:WARN:codenameone_main_skipped reason=bundle_path_unavailable bundle=\(bundleIdentifier)")
472+
}
473+
return nil
474+
}
475+
476+
private func readExecutableName(appContainer: String) -> String? {
477+
let infoPath = (appContainer as NSString).appendingPathComponent("Info.plist")
478+
guard let info = NSDictionary(contentsOfFile: infoPath) as? [String: Any] else {
479+
print("CN1SS:WARN:codenameone_main_skipped reason=info_plist_unreadable path=\(infoPath)")
480+
return nil
481+
}
482+
guard let executable = info["CFBundleExecutable"] as? String, !executable.isEmpty else {
483+
print("CN1SS:WARN:codenameone_main_skipped reason=cfbundleexecutablenotfound path=\(infoPath)")
484+
return nil
485+
}
486+
return executable
487+
}
488+
489+
private struct InvocationContext {
490+
typealias InitConstantPoolFn = @convention(c) () -> Void
491+
typealias GetThreadLocalDataFn = @convention(c) () -> UnsafeMutableRawPointer?
492+
typealias CodenameOneMainFn = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void
493+
494+
let handle: UnsafeMutableRawPointer
495+
let initConstantPool: InitConstantPoolFn
496+
let getThreadLocalData: GetThreadLocalDataFn
497+
let mainFunction: CodenameOneMainFn
498+
499+
func invoke() {
500+
initConstantPool()
501+
let threadState = getThreadLocalData()
502+
mainFunction(threadState, nil)
503+
}
504+
}
505+
}

0 commit comments

Comments
 (0)