From c4c705d490af26f68d54aa86d3e06f229bb8d5b9 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 13 Nov 2025 22:20:29 -0500 Subject: [PATCH 1/4] Add localized String functions --- .../Resources/Localizable.xcstrings | 20 +++++++ .../SkipAndroidBridgeSamples.swift | 4 ++ .../SkipAndroidBridgeSamplesTests.swift | 54 ++++++++++++++++--- 3 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings diff --git a/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings b/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings new file mode 100644 index 0000000..46341dc --- /dev/null +++ b/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings @@ -0,0 +1,20 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "literal" : { + + }, + "localized" : { + "comment" : "localized string", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Localized into English" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift index c4b313e..d0aaca4 100644 --- a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift +++ b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift @@ -50,6 +50,10 @@ public func localizedStringResourceInterpolatedKey() -> String { return interpolation.key } +public func localizedStringValueNS() -> String { + NSLocalizedString("localized", bundle: Bundle.module, comment: "localized string") +} + public func mainActorAsyncValue() async -> String { await Task.detached { await MainActorClass().mainActorValue() diff --git a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift index 17b10c7..ce32502 100644 --- a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift +++ b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift @@ -5,18 +5,41 @@ import SkipBridge import SkipAndroidBridge import SkipAndroidBridgeSamples import XCTest +#if SKIP +import androidx.test.platform.app.InstrumentationRegistry +#endif + +let logger: Logger = Logger(subsystem: "SkipAndroidBridgeSamplesTests", category: "Tests") final class SkipAndroidBridgeSamplesTests: XCTestCase { + static var wasSetupOnce = false + static var wasSetupOnMainThread: Bool? = nil + override func setUp() { + super.setUp() + #if SKIP + if Self.wasSetupOnce { + return // already set up once + } + Self.wasSetupOnce = true + loadPeerLibrary(packageName: "skip-android-bridge", moduleName: "SkipAndroidBridgeSamples") //try AndroidBridge.initBridge("SkipAndroidBridgeSamples") // doesn't work on Robolectric - let context = ProcessInfo.processInfo.androidContext - try AndroidBridgeBootstrap.initAndroidBridge(filesDir: context.getFilesDir().getAbsolutePath(), cacheDir: context.getCacheDir().getAbsolutePath()) + // we need to run this synchronously on the main thread in order for initAndroidBridge to setup the main looper properly + // androidx.test.runner.AndroidJUnitRunner,5,main] + InstrumentationRegistry.getInstrumentation().runOnMainSync { + logger.info("setting up tests on thread=\(java.lang.Thread.currentThread()) vs. mainLooper thread=\(android.os.Looper.getMainLooper().getThread())") + Self.wasSetupOnMainThread = java.lang.Thread.currentThread() == android.os.Looper.getMainLooper().getThread() + let context = ProcessInfo.processInfo.androidContext + try AndroidBridgeBootstrap.initAndroidBridge(filesDir: context.getFilesDir().getAbsolutePath(), cacheDir: context.getCacheDir().getAbsolutePath()) + } #endif + + logger.info("setup complete") } func testSimpleConstants() { @@ -39,7 +62,7 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { func testResourceURL() throws { if isRobolectric { // unwrap fails on Robolectric - throw XCTSkip("unknown error on Robolectric") + throw XCTSkip("bridged assets not working on Robolectric") } let url = try XCTUnwrap(getAssetURL(named: "sample.json")) @@ -87,6 +110,10 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { func testAndroidContext() throws { if !isAndroid { + throw XCTSkip("test only runs on Android") + } + + if isRobolectric { throw XCTSkip("no package name on Robolectric") } @@ -99,9 +126,20 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { XCTAssertEqual(localizedStringResourceInterpolatedKey(), "interpolated %lld!") } - // not working yet… -// func testMainActorAsync() async throws { -// let value = await mainActorAsyncValue() -// XCTAssertEqual("MainActor!", value) -// } + func testLocalizedStringNS() throws { + if isRobolectric { + throw XCTSkip("bridged localized strings not working on Robolectric") + } + XCTAssertEqual(localizedStringValueNS(), "Localized into English") + } + + func testMainActorAsync() async throws { + #if SKIP + XCTAssertEqual(true, Self.wasSetupOnMainThread, "test case was not initialized on the main thread") + #endif + + // test hangs on Android emulator for some reason + let value = await mainActorAsyncValue() + XCTAssertEqual("MainActor!", value) + } } From ca8428a1c8bcf642693e2fed561e7f95f5dc44a7 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 13 Nov 2025 23:14:47 -0500 Subject: [PATCH 2/4] Disable localized String checking on Darwin OSS toolchain --- .../SkipAndroidBridgeSamplesTests.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift index ce32502..73f6121 100644 --- a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift +++ b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift @@ -127,9 +127,16 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { } func testLocalizedStringNS() throws { - if isRobolectric { + if isRobolectric || !isJava { throw XCTSkip("bridged localized strings not working on Robolectric") } + + if !isJava { + // we guard for !isJava because on CI we are running using the OSS Swift toolchain, which doesn't process .xcstrings files + // this _will_ pass when running using Xcode's swift version, but AFAIK there isn't any way to check for that at runtime + throw XCTSkip("xcstrings not working on Swift OSS toolchain") + } + XCTAssertEqual(localizedStringValueNS(), "Localized into English") } From 1aad80fb01a3ef0ed705937c34f4280119910282 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 14 Nov 2025 00:30:03 -0500 Subject: [PATCH 3/4] Update test cases --- .../SkipAndroidBridgeSamplesTests.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift index 73f6121..800b153 100644 --- a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift +++ b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift @@ -127,13 +127,13 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { } func testLocalizedStringNS() throws { - if isRobolectric || !isJava { + if isRobolectric { throw XCTSkip("bridged localized strings not working on Robolectric") } - if !isJava { + if !isJava && ProcessInfo.processInfo.environment["XCODE_SCHEME_NAME"] == nil { // we guard for !isJava because on CI we are running using the OSS Swift toolchain, which doesn't process .xcstrings files - // this _will_ pass when running using Xcode's swift version, but AFAIK there isn't any way to check for that at runtime + // this _will_ pass when running using Xcode's swift version, but AFAIK there isn't any way to check for that at runtime other than checking for an environment variable that is usually set by Xcode throw XCTSkip("xcstrings not working on Swift OSS toolchain") } @@ -143,6 +143,10 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { func testMainActorAsync() async throws { #if SKIP XCTAssertEqual(true, Self.wasSetupOnMainThread, "test case was not initialized on the main thread") + + if isAndroid { + throw XCTSkip("test hangs on Android") + } #endif // test hangs on Android emulator for some reason From e3043e9f9ca2506b56f95bc98ef3dd6f2c364a00 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Tue, 25 Nov 2025 10:38:47 -0500 Subject: [PATCH 4/4] Add test for @MainActor callbacks --- .../SkipAndroidBridgeSamples.swift | 36 +++++++++++++++++++ .../SkipAndroidBridgeSamplesTests.swift | 18 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift index d0aaca4..8d964e6 100644 --- a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift +++ b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift @@ -8,6 +8,8 @@ import SwiftJNI import AndroidNative #endif +let logger: Logger = Logger(subsystem: "SkipAndroidBridgeSamples", category: "Samples") + public let swiftStringConstant = "s" public func getStringValue(_ string: String?) -> String? { @@ -76,3 +78,37 @@ public func nativeAndroidContextPackageName() throws -> String? { "MainActor!" } } + +public typealias MainActorCallback = @MainActor () async -> () + +public struct MainActorCallbacks: @unchecked Sendable { + let callbackMainActor: MainActorCallback + + public init(callbackMainActor: @escaping MainActorCallback) { + self.callbackMainActor = callbackMainActor + } +} + +// disabling causes a hang when running tests +/*@MainActor*/ public class MainActorCallbackModel { + public static let shared = MainActorCallbackModel() + var callbacks: MainActorCallbacks? + + public init(callbacks: MainActorCallbacks? = nil) { + self.callbacks = callbacks + } + + public func setCallbacks(_ callbacks: MainActorCallbacks) { + logger.log("setting callbacks on thread: \(Thread.current)") + self.callbacks = callbacks + logger.log("done setting callbacks: \(String(describing: callbacks.callbackMainActor))") + } + + public func doSomething() async { + logger.log("calling callbacks on thread: \(Thread.current)") + //try? await Task.sleep(for: .seconds(2)) + //logger.log("calling callbacks: done sleeping") + await callbacks?.callbackMainActor() + logger.log("calling callbacks: done") + } +} diff --git a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift index 800b153..61541c9 100644 --- a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift +++ b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift @@ -153,4 +153,22 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { let value = await mainActorAsyncValue() XCTAssertEqual("MainActor!", value) } + + func testMainActorCallback() async throws { + var counter = 0 + logger.log("setting callbacks") + let callbacks = MainActorCallbacks(callbackMainActor: { + try? await Task.sleep(nanoseconds: 1000) + counter += 1 + }) + MainActorCallbackModel.shared.setCallbacks(callbacks) + logger.log("setting callbacks: done") + + XCTAssertEqual(0, counter) + logger.log("calling callbacks") + await MainActorCallbackModel.shared.doSomething() + logger.log("calling callbacks: done") + _ = callbacks + XCTAssertEqual(1, counter) + } }