diff --git a/CHANGELOG.md b/CHANGELOG.md index a659cc213e..512d342224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ if the content is the same. Custom implementations of these methods will be resp * Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403)) * Support for automatic resolution of embedded object constraints during migration through `RealmConfiguration.Builder.migration(migration: AutomaticSchemaMigration, resolveEmbeddedObjectConstraints: Boolean)`. (Issue [#1464](https://github.com/realm/realm-kotlin/issues/1464) * [Sync] Add support for customizing authorization headers and adding additional custom headers to all Atlas App service requests with `AppConfiguration.Builder.authorizationHeaderName()` and `AppConfiguration.Builder.addCustomRequestHeader(...)`. (Issue [#1453](https://github.com/realm/realm-kotlin/pull/1453)) +* [Sync] Added support for manually triggering a reconnect attempt for Device Sync. This is done through a new `App.Sync.reconnect()` method. This method is also now called automatically when a mobile device toggles off airplane mode. (Issue [#1479](https://github.com/realm/realm-kotlin/issues/1479)) ### Fixed * None. diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 731a6a1e57..92e8181adc 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -707,6 +707,11 @@ expect object RealmInterop { callback: AppCallback ) + // Sync Client + fun realm_app_sync_client_reconnect(app: RealmAppPointer) + fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean + fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) + // Sync config fun realm_config_set_sync_config( realmConfiguration: RealmConfigurationPointer, diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 35a6a7e8b2..b42fe90b2f 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1558,6 +1558,17 @@ actual object RealmInterop { ) } + actual fun realm_app_sync_client_reconnect(app: RealmAppPointer) { + realmc.realm_app_sync_client_reconnect(app.cptr()) + } + actual fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean { + return realmc.realm_app_sync_client_has_sessions(app.cptr()) + } + + actual fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) { + realmc.realm_app_sync_client_wait_for_sessions_to_terminate(app.cptr()) + } + actual fun realm_sync_config_new(user: RealmUserPointer, partition: String): RealmSyncConfigurationPointer { return LongPointerWrapper(realmc.realm_sync_config_new(user.cptr(), partition)).also { ptr -> // Stop the session immediately when the Realm is closed, so the lifecycle of the diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 1f25cf704d..268464c636 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2948,6 +2948,17 @@ actual object RealmInterop { } } + actual fun realm_app_sync_client_reconnect(app: RealmAppPointer) { + realm_wrapper.realm_app_sync_client_reconnect(app.cptr()) + } + actual fun realm_app_sync_client_has_sessions(app: RealmAppPointer): Boolean { + return realm_wrapper.realm_app_sync_client_has_sessions(app.cptr()) + } + + actual fun realm_app_sync_client_wait_for_sessions_to_terminate(app: RealmAppPointer) { + realm_wrapper.realm_app_sync_client_wait_for_sessions_to_terminate(app.cptr()) + } + actual fun realm_config_set_sync_config(realmConfiguration: RealmConfigurationPointer, syncConfiguration: RealmSyncConfigurationPointer) { realm_wrapper.realm_config_set_sync_config(realmConfiguration.cptr(), syncConfiguration.cptr()) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt index a6fbd387aa..8f0c714fac 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmInstantImpl.kt @@ -32,7 +32,7 @@ public data class RealmInstantImpl(override val seconds: Long, override val nano } } -internal fun RealmInstant.toDuration(): Duration { +public fun RealmInstant.toDuration(): Duration { return epochSeconds.seconds + nanosecondsOfSecond.nanoseconds } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 32ce083867..bfec4f83b5 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -131,7 +131,7 @@ public expect fun epochInSeconds(): Long /** * Returns a RealmInstant representing the time that has passed since the Unix epoch. */ -internal expect fun currentTime(): RealmInstant +public expect fun currentTime(): RealmInstant /** * Returns the type of a mutable property. diff --git a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index 11ed3e1512..5f042e6de1 100644 --- a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -28,7 +28,7 @@ public actual fun epochInSeconds(): Long = * Since internalNow() should only logically return a value after the Unix epoch, it is safe to create a RealmInstant * without considering having to pass negative nanoseconds. */ -internal actual fun currentTime(): RealmInstant { +public actual fun currentTime(): RealmInstant { val jtInstant = systemUTC().instant() return RealmInstantImpl(jtInstant.epochSecond, jtInstant.nano) } diff --git a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt index c1fff3dedd..4b71b3708b 100644 --- a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt +++ b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt @@ -68,7 +68,7 @@ public actual fun epochInSeconds(): Long = * without considering having to pass negative nanoseconds. */ @Suppress("MagicNumber") -internal actual fun currentTime(): RealmInstant { +public actual fun currentTime(): RealmInstant { val secs: Double = NSDate().timeIntervalSince1970 return when { // We can't convert the MIN value to ms - it is initialized with Long.MIN_VALUE and diff --git a/packages/library-sync/src/androidMain/AndroidManifest.xml b/packages/library-sync/src/androidMain/AndroidManifest.xml index a24af63904..34c98bc70e 100644 --- a/packages/library-sync/src/androidMain/AndroidManifest.xml +++ b/packages/library-sync/src/androidMain/AndroidManifest.xml @@ -16,8 +16,22 @@ --> + + + + + + + diff --git a/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..b89325a7a1 --- /dev/null +++ b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,6 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // Registering network state listeners are done in io.realm.kotlin.mongodb.RealmSyncInitializer + // so we do not have to store the Android Context. +} diff --git a/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt new file mode 100644 index 0000000000..f5bed0c72c --- /dev/null +++ b/packages/library-sync/src/androidMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncInitializer.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.internal + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkInfo +import android.net.NetworkRequest +import android.os.Build +import androidx.startup.Initializer +import io.realm.kotlin.internal.RealmInitializer +import io.realm.kotlin.log.RealmLog + +/** + * An **initializer** for Sync specific functionality that does not fit into the `RealmInitializer` + * in cinterop.o allow Realm to access context properties. + */ +class RealmSyncInitializer : Initializer { + + companion object { + @Suppress("DEPRECATION") // Should only be called below API 21 + fun isConnected(cm: ConnectivityManager?): Boolean { + return cm?.let { + val networkInfo: NetworkInfo? = cm.activeNetworkInfo + networkInfo != null && networkInfo.isConnectedOrConnecting || isEmulator() + } ?: true + } + + // Credit: http://stackoverflow.com/questions/2799097/how-can-i-detect-when-an-android-application-is-running-in-the-emulator + fun isEmulator(): Boolean { + return Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.startsWith("unknown") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT + } + } + + @Suppress("invisible_member", "invisible_reference", "NestedBlockDepth") + override fun create(context: Context): Context { + val result: Int = context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + if (result == PackageManager.PERMISSION_GRANTED) { + try { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + // There has been a fair amount of changes and deprecations with regard to how to listen + // to the network status. ConnectivityManager#CONNECTIVITY_ACTION was deprecated in API 28 + // but ConnectivityManager.NetworkCallback became available a lot sooner in API 21, so + // we default to this as soon as possible. + // + // On later versions of Android (need reference), these callbacks will also only trigger + // if the app is in the foreground. + // + // The current implementation is a best-effort in detecting when the network is available + // again. + // + // See https://developer.android.com/training/basics/network-ops/reading-network-state + // See https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION + // See https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 21 */) { + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 23 */) { + request.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + RealmLog.info("Register ConnectivityManager network callbacks") + connectivityManager?.registerNetworkCallback( + request.build(), + object : NetworkCallback() { + override fun onAvailable(network: Network) { + NetworkStateObserver.notifyConnectionChange(true) + } + + override fun onUnavailable() { + NetworkStateObserver.notifyConnectionChange(false) + } + } + ) + } else { + RealmLog.info("Register BroadcastReceiver connectivity callbacks") + @Suppress("DEPRECATION") + context.registerReceiver( + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val isConnected: Boolean = isConnected(connectivityManager) + NetworkStateObserver.notifyConnectionChange(isConnected) + } + }, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + ) + } + } catch (ex: Exception) { + RealmLog.warn("Something went wrong trying to register a network state listener: $ex") + } + } else { + RealmLog.warn( + "It was not possible to register a network state listener. " + + "ACCESS_NETWORK_STATE was not granted." + ) + } + return context + } + + override fun dependencies(): MutableList>> { + return mutableListOf(RealmInitializer::class.java) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt index 8bf334fcf5..21a44d3831 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/App.kt @@ -23,6 +23,7 @@ import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.AppImpl +import io.realm.kotlin.mongodb.sync.Sync import kotlinx.coroutines.flow.Flow /** @@ -77,6 +78,12 @@ public interface App { */ public val currentUser: User? + /** + * Returns a Device Sync manager that control functionality across all open realms associated + * with this app. + */ + public val sync: Sync + /** * Returns all known users that are either [User.State.LOGGED_IN] or [User.State.LOGGED_OUT]. * Only users that at some point logged into this device will be returned. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt index 260a220332..ee1a151d80 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppImpl.kt @@ -20,19 +20,25 @@ import io.realm.kotlin.internal.interop.RealmAppPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmUserPointer import io.realm.kotlin.internal.interop.sync.NetworkTransport +import io.realm.kotlin.internal.toDuration import io.realm.kotlin.internal.util.DispatcherHolder import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.use +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AuthenticationChange import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.auth.EmailPasswordAuth +import io.realm.kotlin.mongodb.sync.Sync +import io.realm.kotlin.types.RealmInstant import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds internal typealias AppResources = Triple @@ -45,6 +51,37 @@ public class AppImpl( internal val appNetworkDispatcher: DispatcherHolder private val networkTransport: NetworkTransport + private var lastOnlineStateReported: Duration? = null + private var lastConnectedState: Boolean? = null // null = unknown, true = connected, false = disconnected + @Suppress("MagicNumber") + private val reconnectThreshold = 5.seconds + + @Suppress("invisible_member", "invisible_reference", "MagicNumber") + private val connectionListener = NetworkStateObserver.ConnectionListener { connectionAvailable -> + // In an ideal world, we would be able to reliably detect the network coming and + // going. Unfortunately that does not seem to be case (at least on Android). + // + // So instead of assuming that we have always detect the device going offline first, + // we just tell Realm Core to reconnect when we detect the network has come back. + // + // Due to the way network interfaces are re-enabled on Android, we might see multiple + // "isOnline" messages in short order. So in order to prevent resetting the network + // too often we throttle messages, so a reconnect can only happen ever 5 seconds. + RealmLog.debug("Network state change detected. ConnectionAvailable = $connectionAvailable") + val now: Duration = RealmInstant.now().toDuration() + if (connectionAvailable && (lastOnlineStateReported == null || now.minus(lastOnlineStateReported!!) > reconnectThreshold) + ) { + RealmLog.info("Trigger network reconnect.") + try { + sync.reconnect() + } catch (ex: Exception) { + RealmLog.error(ex.toString()) + } + lastOnlineStateReported = now + } + lastConnectedState = connectionAvailable + } + // Allow some delay between events being reported and them being consumed. // When the (somewhat arbitrary) limit is hit, we will throw an exception, since we assume the // consumer is doing something wrong. This is also needed because we don't @@ -61,6 +98,7 @@ public class AppImpl( appNetworkDispatcher = appResources.first networkTransport = appResources.second nativePointer = appResources.third + NetworkStateObserver.addListener(connectionListener) } override val emailPasswordAuth: EmailPasswordAuth by lazy { EmailPasswordAuthImpl(nativePointer) } @@ -68,6 +106,7 @@ public class AppImpl( override val currentUser: User? get() = RealmInterop.realm_app_get_current_user(nativePointer) ?.let { UserImpl(it, this) } + override val sync: Sync by lazy { SyncImpl(nativePointer) } override fun allUsers(): Map { val nativeUsers: List = @@ -130,6 +169,7 @@ public class AppImpl( // be beneficial in order to reason about the lifecycle of the Sync thread and dispatchers. networkTransport.close() nativePointer.release() + NetworkStateObserver.removeListener(connectionListener) } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..3887309801 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,64 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.interop.SynchronizableObject + +// Register a system specific network listener (if supported) +internal expect fun registerSystemNetworkObserver() + +/** + * This class is responsible for keeping track of system events related to the network so it can + * delegate them to interested parties. + */ +internal object NetworkStateObserver { + + /** + * This interface is used in a thread-safe manner, i.e. implementers do not have to think + * about race conditions. + */ + internal fun interface ConnectionListener { + fun onChange(connectionAvailable: Boolean) + } + + private val mutex = SynchronizableObject() + private val listeners = mutableListOf() + + init { + registerSystemNetworkObserver() + } + + /** + * Called by each custom network implementation whenever a network change is detected. + */ + fun notifyConnectionChange(isOnline: Boolean) { + mutex.withLock { + listeners.forEach { + it.onChange(isOnline) + } + } + } + + /** + * Add a listener to be notified about any network changes. + * This method is thread safe. + * IMPORTANT: Not removing it again will result in leaks. + * @param listener the listener to add. + */ + fun addListener(listener: ConnectionListener) { + mutex.withLock { + listeners.add(listener) + } + } + + /** + * Removes a network listener. + * This method is thread safe. + * + * @param listener the listener to remove. + * @return `true` if the listener was removed. + */ + fun removeListener(listener: ConnectionListener): Boolean { + mutex.withLock { + return listeners.remove(listener) + } + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt new file mode 100644 index 0000000000..41391cd869 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncImpl.kt @@ -0,0 +1,19 @@ +package io.realm.kotlin.mongodb.internal + +import io.realm.kotlin.internal.interop.RealmAppPointer +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.mongodb.sync.Sync + +internal class SyncImpl(private val app: RealmAppPointer) : Sync { + + override val hasSyncSessions: Boolean + get() = RealmInterop.realm_app_sync_client_has_sessions(app) + + override fun reconnect() { + RealmInterop.realm_app_sync_client_reconnect(app) + } + + override fun waitForSessionsToTerminate() { + RealmInterop.realm_app_sync_client_wait_for_sessions_to_terminate(app) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt new file mode 100644 index 0000000000..51cbdcd537 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/Sync.kt @@ -0,0 +1,45 @@ +package io.realm.kotlin.mongodb.sync + +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.syncSession + +/** + * A _Device Sync_ manager responsible for controlling all sync sessions across all realms + * associated with a given [App] instance. For session functionality associated with a single + * realm, see [syncSession]. + * + * @see App.sync + * @see io.realm.kotlin.mongodb.syncSession + */ +public interface Sync { + + /** + * Returns whether or not any sync sessions are still active. + */ + public val hasSyncSessions: Boolean + + /** + * Realm will automatically detect when a device gets connectivity after being offline and + * resume syncing. However, as some of these checks are performed using incremental backoff, + * this will in some cases not happen immediately. + * + * In those cases it can be beneficial to call this method manually, which will force all + * sessions to attempt to reconnect immediately and reset any timers they are using for + * incremental backoff. + * + * Note, Realm has an internal default socket read timeout of 2 minutes. Calling this method + * within those two minutes will not trigger a reconnect. + */ + public fun reconnect() + + /** + * Calling this method will block until all sync sessions for a given [App] has terminated. + * + * Closing a Realm will terminate the sync session, but it is not synchronous as Realms + * communicate with their sync session using an asynchronous communication channel. This + * has the effect that trying to delete a Realm right after closing it will sometimes throw + * an [IllegalStateException]. Using this method can be a way to ensure it is safe to delete + * the file. + */ + public fun waitForSessionsToTerminate() +} diff --git a/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..4b8841194f --- /dev/null +++ b/packages/library-sync/src/jvmMain/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,6 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // Do nothing on JVM. + // There isn't a great way to detect network connectivity on this platform. +} diff --git a/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt new file mode 100644 index 0000000000..75cb4b9d68 --- /dev/null +++ b/packages/library-sync/src/nativeDarwin/kotlin/io/realm/kotlin/mongodb/internal/NetworkStateObserver.kt @@ -0,0 +1,8 @@ +package io.realm.kotlin.mongodb.internal + +internal actual fun registerSystemNetworkObserver() { + // This is handled automatically by Realm Core which will also call `Sync.reconnect()` + // automatically. So on iOS/macOS we do not do anything. + // See https://github.com/realm/realm-core/blob/a678c36a85cf299f745f68f8b5ceff364d714181/src/realm/object-store/sync/impl/sync_client.hpp#L82C3-L82C3 + // for further details. +} diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt index f203b8c712..b34dbf1794 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/TestApp.kt @@ -21,10 +21,12 @@ package io.realm.kotlin.test.mongodb import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.SynchronizableObject import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import io.realm.kotlin.log.LogLevel +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.log.RealmLogger import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration @@ -50,6 +52,15 @@ val TEST_APP_CLUSTER_NAME = SyncServerConfig.clusterName val TEST_SERVER_BASE_URL = SyncServerConfig.url const val DEFAULT_PASSWORD = "password1234" +// Expose a try-with-resource pattern for Test Apps +inline fun App.use(action: (App) -> Unit) { + try { + action(this) + } finally { + this.close() + } +} + /** * This class merges the classes [App] and [AppAdmin] making it easier to create an App that can be * used for testing. @@ -62,6 +73,8 @@ open class TestApp private constructor( pairAdminApp: Pair ) : App by pairAdminApp.first, AppAdmin by pairAdminApp.second { + var mutex = SynchronizableObject() + var isClosed: Boolean = false val app: App = pairAdminApp.first /** @@ -75,10 +88,13 @@ open class TestApp private constructor( **/ @Suppress("LongParameterList") constructor( + testId: String?, appName: String = TEST_APP_PARTITION, - dispatcher: CoroutineDispatcher = singleThreadDispatcher("test-app-dispatcher"), + dispatcher: CoroutineDispatcher = singleThreadDispatcher("$testId-dispatcher"), logLevel: LogLevel? = LogLevel.WARN, - builder: (AppConfiguration.Builder) -> AppConfiguration.Builder = { it }, + builder: (AppConfiguration.Builder) -> AppConfiguration.Builder = { + it.syncRootDirectory(PlatformUtils.createTempDir("$appName-$testId")) + }, debug: Boolean = false, customLogger: RealmLogger? = null, networkTransport: NetworkTransport? = null, @@ -109,39 +125,47 @@ open class TestApp private constructor( } override fun close() { - // This is needed to "properly reset" all sessions across tests since deleting users - // directly using the REST API doesn't do the trick - runBlocking { - while (currentUser != null) { - (currentUser as User).logOut() + mutex.withLock { + if (isClosed) { + return } - deleteAllUsers() - } - if (dispatcher is CloseableCoroutineDispatcher) { - dispatcher.close() - } - app.close() + app.sync.waitForSessionsToTerminate() + + // This is needed to "properly reset" all sessions across tests since deleting users + // directly using the REST API doesn't do the trick + runBlocking { + try { + while (currentUser != null) { + (currentUser as User).logOut() + } + deleteAllUsers() + } catch (ex: Exception) { + // Some tests might render the server inaccessible, preventing us from + // deleting users. Assume those tests know what they are doing and + // ignore errors here. + RealmLog.warn("Server side users could not be deleted: $ex") + } + } + + if (dispatcher is CloseableCoroutineDispatcher) { + dispatcher.close() + } + app.close() - // Close network client resources - closeClient() + // Close network client resources + closeClient() - // Make sure to clear cached apps before deleting files - RealmInterop.realm_clear_cached_apps() + // Make sure to clear cached apps before deleting files + RealmInterop.realm_clear_cached_apps() - // Delete metadata Realm files - PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm") + // Delete metadata Realm files + PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm") + isClosed = true + } } companion object { - // Expose a try-with-resource pattern for Apps - inline fun TestApp.use(action: (TestApp) -> Unit) { - try { - action(this) - } finally { - this.close() - } - } @Suppress("LongParameterList") fun build( diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt index bfb5f3d4e9..626d505c0f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ApiKeyAuthTests.kt @@ -49,7 +49,7 @@ class ApiKeyAuthTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) user = app.createUserAndLogin() provider = user.apiKeyAuth } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt index cf95af7372..ee6c4f41b4 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt @@ -29,9 +29,9 @@ import io.realm.kotlin.mongodb.exceptions.ServiceException import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp -import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.receiveOrFail @@ -161,12 +161,11 @@ class AppConfigurationTests { @Test fun syncRootDirectory_appendDirectoryToPath() = runBlocking { val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" - val app = TestApp(builder = { + TestApp("syncRootDirectory_appendDirectoryToPath", builder = { it.syncRootDirectory(expectedRoot) - }) - val (email, password) = TestHelper.randomEmail() to "password1234" - val user = app.createUserAndLogIn(email, password) - try { + }).use { app -> + val (email, password) = TestHelper.randomEmail() to "password1234" + val user = app.createUserAndLogIn(email, password) assertEquals(expectedRoot, app.configuration.syncRootDirectory) // When creating the full path for a synced Realm, we will always append `/mongodb-realm` to // the configured `AppConfiguration.syncRootDir` @@ -175,8 +174,6 @@ class AppConfigurationTests { "${PATH_SEPARATOR}myCustomDir${PATH_SEPARATOR}mongodb-realm${PATH_SEPARATOR}${user.app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}s_$partitionValue.realm" val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf()).build() assertTrue(config.path.endsWith(suffix), "Failed: ${config.path} vs. $suffix") - } finally { - app.asTestApp.close() } } @@ -381,21 +378,16 @@ class AppConfigurationTests { // // Check that custom headers and auth header renames are correctly used for HTTP requests. @Test - fun customHeadersTest() { - var app: App? = null - try { - runBlocking { - app = TestApp( - builder = { builder -> - builder.customRequestHeaders { - put(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) - }.authorizationHeaderName(AUTH_HEADER_NAME) - } - ) - doCustomHeaderTest(app!!) + fun customHeadersTest() = runBlocking { + TestApp( + "customHeadersTest", + builder = { builder -> + builder.customRequestHeaders { + put(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) + }.authorizationHeaderName(AUTH_HEADER_NAME) } - } finally { - assertFailsWith { app?.close() } + ).use { app -> + doCustomHeaderTest(app) } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 05a379157a..6079e3edea 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -39,6 +39,7 @@ import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail import io.realm.kotlin.test.util.receiveOrFail @@ -65,7 +66,7 @@ class AppTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest @@ -77,9 +78,10 @@ class AppTests { @Test fun defaultApp() { - val defaultApp = App.create("foo") - assertEquals("foo", defaultApp.configuration.appId) - assertEquals(AppConfiguration.DEFAULT_BASE_URL, defaultApp.configuration.baseUrl) + App.create("foo").use { defaultApp -> + assertEquals("foo", defaultApp.configuration.appId) + assertEquals(AppConfiguration.DEFAULT_BASE_URL, defaultApp.configuration.baseUrl) + } } @Test @@ -372,18 +374,17 @@ class AppTests { fun encryptedMetadataRealm() { // Create new test app with a random encryption key val key = TestHelper.getRandomKey() - val app = TestApp( + TestApp( + "encryptedMetadataRealm", appName = TEST_APP_FLEX, builder = { it .encryptionKey(key) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -403,8 +404,6 @@ class AppTests { // Should be possible to open the encrypted metadata realm file with the encryption key Realm.open(config).close() - } finally { - app.close() } } @@ -412,18 +411,17 @@ class AppTests { fun encryptedMetadataRealm_openWithWrongKeyThrows() { // Create new test app with a random encryption key val correctKey = TestHelper.getRandomKey() - val app = TestApp( + TestApp( + "encryptedMetadataRealm_openWithWrongKeyThrows", appName = TEST_APP_FLEX, builder = { it .encryptionKey(correctKey) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -445,26 +443,23 @@ class AppTests { assertFailsWithMessage("Failed to open Realm file at path") { Realm.open(config) } - } finally { - app.close() } } @Test fun encryptedMetadataRealm_openWithoutKeyThrows() { // Create new test app with a random encryption key - val app = TestApp( + TestApp( + "encryptedMetadataRealm_openWithoutKeyThrows", appName = TEST_APP_FLEX, builder = { it .encryptionKey(TestHelper.getRandomKey()) .syncRootDirectory("${appFilesDirectory()}/foo") } - ) - - try { + ).use { app -> // Create Realm in order to create the sync metadata Realm - val user = app.createUserAndLogin() + val user = app.asTestApp.createUserAndLogin() val syncConfig = SyncConfiguration .Builder(user, setOf(ParentPk::class, ChildPk::class)) .build() @@ -483,31 +478,6 @@ class AppTests { assertFailsWithMessage("Failed to open Realm file at path") { Realm.open(config) } - } finally { - app.close() } } - -// -// // Check that it is possible to have two Java instances of an App class, but they will -// // share the underlying App state. -// @Test -// fun multipleInstancesSameApp() { -// // Create a second copy of the test app -// val app2 = TestApp() -// try { -// // User handling are shared between each app -// val user = app.login(Credentials.anonymous()); -// assertEquals(user, app2.currentUser()) -// assertEquals(user, app.allUsers().values.first()) -// assertEquals(user, app2.allUsers().values.first()) -// -// user.logOut(); -// -// assertNull(app.currentUser()) -// assertNull(app2.currentUser()) -// } finally { -// app2.close() -// } -// } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt index 9e7ad99269..7d2f05821f 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt @@ -97,7 +97,7 @@ class AsymmetricSyncTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt index c9ed5c60d6..561caf2ae9 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/CredentialsTests.kt @@ -60,7 +60,7 @@ class CredentialsTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt index 8ecc85a944..40ee4d0ac1 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/EmailPasswordAuthTests.kt @@ -32,7 +32,7 @@ class EmailPasswordAuthWithAutoConfirmTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) } @AfterTest @@ -243,7 +243,7 @@ class EmailPasswordAuthWithEmailConfirmTests { @BeforeTest fun setup() { - app = TestApp(appName = syncServerAppName("em-cnfrm"), initialSetup = { app: BaasApp, service: Service -> + app = TestApp(this::class.simpleName, appName = syncServerAppName("em-cnfrm"), initialSetup = { app: BaasApp, service: Service -> addEmailProvider(app, autoConfirm = false) }) } @@ -281,7 +281,7 @@ class EmailPasswordAuthWithCustomFunctionTests { @BeforeTest fun setup() { - app = TestApp(appName = syncServerAppName("em-cstm"), initialSetup = { app: BaasApp, service: Service -> + app = TestApp(this::class.simpleName, appName = syncServerAppName("em-cstm"), initialSetup = { app: BaasApp, service: Service -> addEmailProvider(app, autoConfirm = false, runConfirmationFunction = true) }) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt index b6ee0f9a07..1cd8d25aff 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncConfigurationTests.kt @@ -46,7 +46,7 @@ class FlexibleSyncConfigurationTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt index 1922c55e7a..7ad9ff466a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FlexibleSyncIntegrationTests.kt @@ -64,7 +64,7 @@ class FlexibleSyncIntegrationTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX, logLevel = LogLevel.ALL) val (email, password) = TestHelper.randomEmail() to "password1234" runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt index 41699ad677..1fd31781cf 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FunctionsTests.kt @@ -196,6 +196,7 @@ class FunctionsTests { @BeforeTest fun setup() { app = TestApp( + FunctionsTests::class.simpleName, syncServerAppName("funcs"), ejson = EJson( serializersModule = SerializersModule { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt index 592058d5b7..b075ce09a7 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/HttpLogObfuscatorTests.kt @@ -119,6 +119,7 @@ class HttpLogObfuscatorTests { private fun initApp(): TestApp { return TestApp( + this::class.simpleName, appName = syncServerAppName("obfsctr"), logLevel = LogLevel.DEBUG, customLogger = ObfuscatorLoggerInspector(channel), @@ -144,6 +145,7 @@ class HttpLogObfuscatorTests { fun nullObfuscator() = runBlocking { val logger = CustomLogCollector("NULL-OBFUSCATOR", LogLevel.DEBUG) app = TestApp( + "nullObfuscator", appName = syncServerAppName("null-obf"), logLevel = LogLevel.DEBUG, builder = { it.httpLogObfuscator(null) }, diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt index 2e615de299..cf9bc8ef06 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt @@ -57,7 +57,7 @@ class MutableSubscriptionSetTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt index 6c709badd9..3b0b3dcda8 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/ProgressListenerTests.kt @@ -31,6 +31,7 @@ import io.realm.kotlin.test.mongodb.TEST_APP_PARTITION import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.use import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -69,7 +70,7 @@ class ProgressListenerTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_PARTITION) + app = TestApp(this::class.simpleName, appName = TEST_APP_PARTITION) partitionValue = ObjectId().toString() } @@ -239,35 +240,37 @@ class ProgressListenerTests { @Test fun throwsOnFlexibleSync() = runBlocking { - val app = TestApp(TEST_APP_FLEX) - val user = app.createUserAndLogIn() - val configuration: SyncConfiguration = SyncConfiguration.create(user, schema) - Realm.open(configuration).use { realm -> - assertFailsWithMessage( - "Progress listeners are not supported for Flexible Sync" - ) { - realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.CURRENT_CHANGES) + TestApp("throwsOnFlexibleSync", TEST_APP_FLEX).use { + val user = app.createUserAndLogIn() + val configuration: SyncConfiguration = SyncConfiguration.create(user, schema) + Realm.open(configuration).use { realm -> + assertFailsWithMessage( + "Progress listeners are not supported for Flexible Sync" + ) { + realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.CURRENT_CHANGES) + } } } } @Test fun completesOnClose() = runBlocking { - val app = TestApp(TEST_APP_PARTITION) - val user = app.createUserAndLogIn() - val realm = Realm.open(createSyncConfig(user)) - try { - val flow = realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.INDEFINITELY) - val job = async { - withTimeout(10.seconds) { - flow.collect { } + TestApp("completesOnClose", TEST_APP_PARTITION).use { app -> + val user = app.createUserAndLogIn() + val realm = Realm.open(createSyncConfig(user)) + try { + val flow = realm.syncSession.progressAsFlow(Direction.DOWNLOAD, ProgressMode.INDEFINITELY) + val job = async { + withTimeout(10.seconds) { + flow.collect { } + } } - } - realm.close() - job.await() - } finally { - if (!realm.isClosed()) { realm.close() + job.await() + } finally { + if (!realm.isClosed()) { + realm.close() + } } } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt index 596633f9a5..5dec23d4ef 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt @@ -60,7 +60,7 @@ class SubscriptionExtensionsTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt index bc3c8ecf77..af18bf174e 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt @@ -30,6 +30,7 @@ import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TEST_APP_PARTITION import io.realm.kotlin.test.mongodb.TestApp import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.use import kotlin.test.AfterTest @@ -56,7 +57,7 @@ class SubscriptionSetTests { @BeforeTest fun setup() { - app = TestApp(appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -89,18 +90,19 @@ class SubscriptionSetTests { @Test fun subscriptions_failOnNonFlexibleSyncRealms() { - val app = TestApp(appName = TEST_APP_PARTITION) - val (email, password) = TestHelper.randomEmail() to "password1234" - val user = runBlocking { - app.createUserAndLogIn(email, password) - } - val config = SyncConfiguration.create( - user, - TestHelper.randomPartitionValue(), - setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) - ) - Realm.open(config).use { partionBasedRealm -> - assertFailsWith { partionBasedRealm.subscriptions } + TestApp(this::class.simpleName, appName = TEST_APP_PARTITION).use { testApp -> + val (email, password) = TestHelper.randomEmail() to "password1234" + val user = runBlocking { + testApp.createUserAndLogIn(email, password) + } + val config = SyncConfiguration.create( + user, + TestHelper.randomPartitionValue(), + setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + ) + Realm.open(config).use { partionBasedRealm -> + assertFailsWith { partionBasedRealm.subscriptions } + } } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt index 7996094440..1eade76896 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt @@ -55,7 +55,7 @@ class SubscriptionTests { @BeforeTest fun setup() { - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index d1e362e6f0..5e9b6bb58d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -113,6 +113,7 @@ class SyncClientResetIntegrationTests { ) -> Unit ) { val app = TestApp( + this::class.simpleName, appName = appName, logLevel = LogLevel.INFO, customLogger = ClientResetLoggerInspector(logChannel), diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt new file mode 100644 index 0000000000..1a79d7349b --- /dev/null +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientTests.kt @@ -0,0 +1,96 @@ +package io.realm.kotlin.test.mongodb.common + +import io.realm.kotlin.Realm +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.asTestApp +import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.util.TestHelper +import io.realm.kotlin.test.util.use +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for [io.realm.kotlin.mongodb.sync.Sync] that is accessed through + * [io.realm.kotlin.mongodb.App.sync]. + */ +class SyncClientTests { + + private lateinit var user: User + private lateinit var app: App + + @BeforeTest + fun setup() { + app = TestApp(this::class.simpleName) + val (email, password) = TestHelper.randomEmail() to "password1234" + user = runBlocking { + app.createUserAndLogIn(email, password) + } + } + + @AfterTest + fun tearDown() { + if (this::app.isInitialized) { + app.asTestApp.close() + } + } + + @Test + fun sync() { + assertNotNull(app.sync) + } + + // There is no way to test reconnect automatically, so just verify that code path does not crash. + @Test + fun reconnect_noRealms() { + app.sync.reconnect() + } + + // There is no way to test reconnect automatically, so just verify that code path does not crash. + @Test + fun reconnect() { + val config = SyncConfiguration.create(user, schema = setOf()) + Realm.open(config).use { + app.sync.reconnect() + } + } + + @Test + fun hasSyncSessions_noRealms() { + assertFalse(app.sync.hasSyncSessions) + } + + @Test + fun hasSyncSessions() { + val config = SyncConfiguration.create(user, schema = setOf()) + Realm.open(config).use { + assertTrue(app.sync.hasSyncSessions) + } + } + + @Test + fun waitForSessionsToTerminate_noRealms() { + app.sync.waitForSessionsToTerminate() + } + + @Test + fun waitForSessionsToTerminate() { + val config1 = SyncConfiguration.Builder(user, schema = setOf()).build() + val config2 = SyncConfiguration.Builder(user, schema = setOf()).name("other.realm").build() + + Realm.open(config1).use { + assertTrue(app.sync.hasSyncSessions) + Realm.open(config2).use { /* do nothing */ } + assertTrue(app.sync.hasSyncSessions) + } + app.sync.waitForSessionsToTerminate() + assertFalse(app.sync.hasSyncSessions) + } +} diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt index 7a10d27624..ffbb352aab 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncConfigTests.kt @@ -87,7 +87,7 @@ class SyncConfigTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest @@ -1230,7 +1230,7 @@ class SyncConfigTests { fun logLevelDoesNotGetOverwrittenByConfig() { app.asTestApp.close() // Prevent AppConfiguration to set a log level - app = TestApp(logLevel = null) + app = TestApp("logLevelDoesNotGetOverwrittenByConfig", logLevel = null) val expectedLogLevel = LogLevel.ALL diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt index 24a9d76c9d..aac6fec160 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncSessionTests.kt @@ -75,7 +75,7 @@ class SyncSessionTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = TestHelper.randomEmail() to "password1234" user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index 8ddaa64cb7..038d06c91d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -56,6 +56,7 @@ import io.realm.kotlin.test.mongodb.asTestApp import io.realm.kotlin.test.mongodb.common.utils.CustomLogCollector import io.realm.kotlin.test.mongodb.common.utils.assertFailsWithMessage import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.TestHelper.randomEmail @@ -110,7 +111,7 @@ class SyncedRealmTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = randomEmail() to "password1234" val user = runBlocking { @@ -861,36 +862,37 @@ class SyncedRealmTests { @Test fun writeCopyTo_localToFlexibleSync_throws() = runBlocking { - val flexApp = TestApp( + TestApp( + this::class.simpleName, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val (email1, password1) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val localConfig = createWriteCopyLocalConfig("local.realm") - val flexSyncConfig = createFlexibleSyncConfig( - user = user1, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) - ) - Realm.open(localConfig).use { localRealm -> - localRealm.writeBlocking { - copyToRealm( - SyncObjectWithAllTypes().apply { - stringField = "local object" - } + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val flexSyncConfig = createFlexibleSyncConfig( + user = user1, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) - } - assertFailsWith { - localRealm.writeCopyTo(flexSyncConfig) + ) + Realm.open(localConfig).use { localRealm -> + localRealm.writeBlocking { + copyToRealm( + SyncObjectWithAllTypes().apply { + stringField = "local object" + } + ) + } + assertFailsWith { + localRealm.writeCopyTo(flexSyncConfig) + } } } - flexApp.close() } @Test @@ -945,42 +947,43 @@ class SyncedRealmTests { @Test fun writeCopyTo_flexibleSyncToLocal() = runBlocking { - val flexApp = TestApp( + TestApp( + "writeCopyTo_flexibleSyncToLocal", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val (email1, password1) = randomEmail() to "password1234" - val user = flexApp.createUserAndLogIn(email1, password1) - val localConfig = createWriteCopyLocalConfig("local.realm") - val syncConfig = createSyncConfig( - user = user, - name = "sync.realm", - partitionValue = partitionValue, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) - ) - Realm.open(syncConfig).use { flexSyncRealm: Realm -> - flexSyncRealm.writeBlocking { - copyToRealm( - FlexParentObject().apply { - name = "local object" - } + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val syncConfig = createSyncConfig( + user = user, + name = "sync.realm", + partitionValue = partitionValue, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) + ) + Realm.open(syncConfig).use { flexSyncRealm: Realm -> + flexSyncRealm.writeBlocking { + copyToRealm( + FlexParentObject().apply { + name = "local object" + } + ) + } + // Copy to local Realm + flexSyncRealm.writeCopyTo(localConfig) + } + // Open Local Realm and check that data can read. + Realm.open(localConfig).use { localRealm: Realm -> + assertEquals(1, localRealm.query().count().find()) + assertEquals("local object", localRealm.query().first().find()!!.name) } - // Copy to local Realm - flexSyncRealm.writeCopyTo(localConfig) - } - // Open Local Realm and check that data can read. - Realm.open(localConfig).use { localRealm: Realm -> - assertEquals(1, localRealm.query().count().find()) - assertEquals("local object", localRealm.query().first().find()!!.name) } - flexApp.close() } @Test @@ -1073,87 +1076,88 @@ class SyncedRealmTests { @Test fun writeCopyTo_flexibleSyncToFlexibleSync() = runBlocking { - val flexApp = TestApp( + TestApp( + "writeCopyTo_flexibleSyncToFlexibleSync", logLevel = io.realm.kotlin.log.LogLevel.ALL, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val (email2, password2) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val user2 = flexApp.createUserAndLogIn(email2, password2) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe(name = "parentSubscription") - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user2, - name = "sync2.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val (email2, password2) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val user2 = flexApp.createUserAndLogIn(email2, password2) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class + ), + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe(name = "parentSubscription") + } ) - ) - - Realm.open(syncConfig1).use { flexRealm1: Realm -> - // It is not possible to use `writeCopyTo` if data is written to the Realm before - // the SubscriptionSet is `COMPLETE`. Work around the issue for now. - flexRealm1.subscriptions.waitForSynchronization(30.seconds) - flexRealm1.write { - copyToRealm( - FlexParentObject(section).apply { - name = "User1Object" - } + val syncConfig2 = createFlexibleSyncConfig( + user = user2, + name = "sync2.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) - } - flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) - // Copy to another flex RealmRealm - flexRealm1.writeCopyTo(syncConfig2) - assertTrue(fileExists(syncConfig2.path)) - - // Open the copied Realm and verify we can read and write data - Realm.open(syncConfig2).use { flexRealm2: Realm -> - // Subscriptions are copied - assertEquals(1, flexRealm2.subscriptions.size) - assertEquals("parentSubscription", flexRealm2.subscriptions.first().name) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm2.subscriptions.state) - - // As is data - assertEquals(1, flexRealm2.query().count().find()) - assertEquals("User1Object", flexRealm2.query().first().find()!!.name) - - flexRealm2.subscriptions.waitForSynchronization(30.seconds) - flexRealm2.write { + ) + + Realm.open(syncConfig1).use { flexRealm1: Realm -> + // It is not possible to use `writeCopyTo` if data is written to the Realm before + // the SubscriptionSet is `COMPLETE`. Work around the issue for now. + flexRealm1.subscriptions.waitForSynchronization(30.seconds) + flexRealm1.write { copyToRealm( FlexParentObject(section).apply { - name = "User2Object" + name = "User1Object" } ) } - flexRealm2.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(2, flexRealm2.query().count().find()) + flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) + // Copy to another flex RealmRealm + flexRealm1.writeCopyTo(syncConfig2) + assertTrue(fileExists(syncConfig2.path)) + + // Open the copied Realm and verify we can read and write data + Realm.open(syncConfig2).use { flexRealm2: Realm -> + // Subscriptions are copied + assertEquals(1, flexRealm2.subscriptions.size) + assertEquals("parentSubscription", flexRealm2.subscriptions.first().name) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm2.subscriptions.state) + + // As is data + assertEquals(1, flexRealm2.query().count().find()) + assertEquals("User1Object", flexRealm2.query().first().find()!!.name) + + flexRealm2.subscriptions.waitForSynchronization(30.seconds) + flexRealm2.write { + copyToRealm( + FlexParentObject(section).apply { + name = "User2Object" + } + ) + } + flexRealm2.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(2, flexRealm2.query().count().find()) + } } } - flexApp.close() } @Test @@ -1188,69 +1192,70 @@ class SyncedRealmTests { // works well enough. Also, even if it doesn't surface the bug, it will not the fail the test. @Test fun accessSessionAfterRemoteChange() = runBlocking { - val flexApp = TestApp( + TestApp( + "accessSessionAfterRemoteChange", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val (email2, password2) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val user2 = flexApp.createUserAndLogIn(email2, password2) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user2, - name = "sync2.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - val realm1 = Realm.open(syncConfig1) + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val (email2, password2) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val user2 = flexApp.createUserAndLogIn(email2, password2) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + val syncConfig2 = createFlexibleSyncConfig( + user = user2, + name = "sync2.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + val realm1 = Realm.open(syncConfig1) - Realm.open(syncConfig2).use { realm2 -> - realm2.write { - copyToRealm(FlexParentObject(section)) + Realm.open(syncConfig2).use { realm2 -> + realm2.write { + copyToRealm(FlexParentObject(section)) + } + realm2.syncSession.uploadAllLocalChanges() } - realm2.syncSession.uploadAllLocalChanges() - } - - // Reading the object means we received it from the other Realm - withTimeout(30.seconds) { - val obj: FlexParentObject = realm1.query("section = $0", section).asFlow() - .map { it.list } - .filter { it.isNotEmpty() } - .first().first() - assertEquals(section, obj.section) - // 1. Local write to work around https://github.com/realm/realm-kotlin/issues/1070 - realm1.write { } - - // 2. Trigger GC. This will GC the RealmReference JVM object, making the native reference - // eligible for closing. - PlatformUtils.triggerGC() - - // 3. On the next update of Realm, we run through the weak list of all previous - // RealmReferences and close all native pointers with their JVM object GC'ed. - // This should now include the object created in step 1. - realm1.write { } - } + // Reading the object means we received it from the other Realm + withTimeout(30.seconds) { + val obj: FlexParentObject = realm1.query("section = $0", section).asFlow() + .map { it.list } + .filter { it.isNotEmpty() } + .first().first() + assertEquals(section, obj.section) + + // 1. Local write to work around https://github.com/realm/realm-kotlin/issues/1070 + realm1.write { } + + // 2. Trigger GC. This will GC the RealmReference JVM object, making the native reference + // eligible for closing. + PlatformUtils.triggerGC() + + // 3. On the next update of Realm, we run through the weak list of all previous + // RealmReferences and close all native pointers with their JVM object GC'ed. + // This should now include the object created in step 1. + realm1.write { } + } - // 4. With the original native dbPointer now being closed, accessing the syncSession for - // the first time should still work. - try { - realm1.syncSession.pause() - assertEquals(SyncSession.State.PAUSED, realm1.syncSession.state) - } finally { - realm1.close() - flexApp.close() + // 4. With the original native dbPointer now being closed, accessing the syncSession for + // the first time should still work. + try { + realm1.syncSession.pause() + assertEquals(SyncSession.State.PAUSED, realm1.syncSession.state) + } finally { + realm1.close() + } } } @@ -1258,7 +1263,8 @@ class SyncedRealmTests { fun customLoggersReceiveSyncLogs() = runBlocking { val customLogger = CustomLogCollector("CUSTOM", LogLevel.ALL) val section = Random.nextInt() - val flexApp = TestApp( + TestApp( + "customLoggersReceiveSyncLogs", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) @@ -1266,30 +1272,30 @@ class SyncedRealmTests { it.appName("MyCustomApp") it.appVersion("1.0.0") } - ) - val (email, password) = randomEmail() to "password1234" - val user = flexApp.createUserAndLogIn(email, password) - val syncConfig = createFlexibleSyncConfig( - user = user, - name = "flex.realm", - initialSubscriptions = { realm: Realm -> - realm.query("section = $0", section).subscribe() - } - ) - Realm.open(syncConfig).use { flexSyncRealm: Realm -> - flexSyncRealm.writeBlocking { - copyToRealm( - FlexParentObject().apply { - name = "local object" - } - ) + ).use { flexApp -> + val (email, password) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email, password) + val syncConfig = createFlexibleSyncConfig( + user = user, + name = "flex.realm", + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe() + } + ) + Realm.open(syncConfig).use { flexSyncRealm: Realm -> + flexSyncRealm.writeBlocking { + copyToRealm( + FlexParentObject().apply { + name = "local object" + } + ) + } + flexSyncRealm.syncSession.uploadAllLocalChanges() } - flexSyncRealm.syncSession.uploadAllLocalChanges() + assertTrue(customLogger.logs.isNotEmpty()) + assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") + assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") } - assertTrue(customLogger.logs.isNotEmpty()) - assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") - assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") - flexApp.close() } // This test verifies that the user facing Realm instance is actually advanced on an on-needed @@ -1416,76 +1422,78 @@ class SyncedRealmTests { // - test-sync/src/iosTest/resources/asset-fs.realm // - test-sync/src/macosTest/resources/asset-fs.realm fun createInitialRealmFx() = runBlocking { - val flexApp = TestApp( + TestApp( + "createInitialRealmFx", logLevel = LogLevel.ALL, appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - val section = Random.nextInt() - val (email1, password1) = randomEmail() to "password1234" - val user1 = flexApp.createUserAndLogIn(email1, password1) - val syncConfig1 = createFlexibleSyncConfig( - user = user1, - name = "sync1.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), - initialSubscriptions = { realm: Realm -> - realm.query() - .subscribe(name = "parentSubscription") - } - ) - val syncConfig2 = createFlexibleSyncConfig( - user = user1, - name = "asset-fs.realm", - errorHandler = { _, error -> - fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class + ).use { flexApp -> + val section = Random.nextInt() + val (email1, password1) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val syncConfig1 = createFlexibleSyncConfig( + user = user1, + name = "sync1.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class + ), + initialSubscriptions = { realm: Realm -> + realm.query() + .subscribe(name = "parentSubscription") + } ) - ) - - Realm.open(syncConfig1).use { flexRealm1: Realm -> - // It is not possible to use `writeCopyTo` if data is written to the Realm before - // the SubscriptionSet is `COMPLETE`. Work around the issue for now. - flexRealm1.subscriptions.waitForSynchronization(30.seconds) - flexRealm1.write { - copyToRealm( - FlexParentObject(section).apply { - name = "User1Object" - } + val syncConfig2 = createFlexibleSyncConfig( + user = user1, + name = "asset-fs.realm", + errorHandler = { _, error -> + fail(error.toString()) + }, + schema = setOf( + FlexParentObject::class, + FlexChildObject::class, + FlexEmbeddedObject::class ) + ) + + Realm.open(syncConfig1).use { flexRealm1: Realm -> + // It is not possible to use `writeCopyTo` if data is written to the Realm before + // the SubscriptionSet is `COMPLETE`. Work around the issue for now. + flexRealm1.subscriptions.waitForSynchronization(30.seconds) + flexRealm1.write { + copyToRealm( + FlexParentObject(section).apply { + name = "User1Object" + } + ) + } + flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) + assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) + // Copy to another flex RealmRealm + flexRealm1.writeCopyTo(syncConfig2) + assertTrue(fileExists(syncConfig2.path)) + // Debug this test, breakpoint here and grab the bundled realm from the location + println("Flexible sync bundled realm is in ${syncConfig2.path}") } - flexRealm1.syncSession.uploadAllLocalChanges(30.seconds) - assertEquals(SubscriptionSetState.COMPLETE, flexRealm1.subscriptions.state) - // Copy to another flex RealmRealm - flexRealm1.writeCopyTo(syncConfig2) - assertTrue(fileExists(syncConfig2.path)) - // Debug this test, breakpoint here and grab the bundled realm from the location - println("Flexible sync bundled realm is in ${syncConfig2.path}") } } // Sanity check that we can in fact open a flexible sync realm file as initial file @Test fun initialRealm_flexibleSync() = runBlocking { - val flexApp = TestApp( + TestApp( + "initialRealm_flexibleSync", appName = io.realm.kotlin.test.mongodb.TEST_APP_FLEX, builder = { it.syncRootDirectory(PlatformUtils.createTempDir("flx-sync-")) } - ) - try { + ).use { flexApp -> val (email1, password1) = randomEmail() to "password1234" val user1 = flexApp.createUserAndLogIn(email1, password1) val syncConfig1 = createFlexibleSyncConfig( @@ -1510,8 +1518,6 @@ class SyncedRealmTests { assertEquals(1, flexRealm1.subscriptions.size) assertNotNull(flexRealm1.subscriptions.findByName("parentSubscription")) } - } finally { - flexApp.close() } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt index 4833848d4a..929fe66582 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserProfileTests.kt @@ -101,6 +101,7 @@ class UserProfileTests { @BeforeTest fun setUp() { app = TestApp( + this::class.simpleName, networkTransport = object : NetworkTransport { override val authorizationHeaderName: String? get() = "" diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt index 60437fd98f..eb622cf8c0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/UserTests.kt @@ -62,7 +62,7 @@ class UserTests { @BeforeTest fun setUp() { - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt index c06a910d40..5d66dd280a 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/nonlatin/NonLatinTests.kt @@ -34,7 +34,7 @@ class NonLatinTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = TestHelper.randomEmail() to "password1234" user = runBlocking { app.createUserAndLogIn(email, password) diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt index 10c59b8e2b..8c0d0d7f94 100644 --- a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/RealmTests.kt @@ -22,12 +22,11 @@ import io.realm.kotlin.entities.sync.ParentPk import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds class RealmTests { @@ -39,18 +38,18 @@ class RealmTests { // effort in detecting the cases we do know about. @Test fun cleanupAllRealmThreadsOnClose() = runBlocking { - val app = TestApp() + val app = TestApp("cleanupAllRealmThreadsOnClose") val user = app.login(Credentials.anonymous()) val configuration = SyncConfiguration.create(user, TestHelper.randomPartitionValue(), setOf(ParentPk::class, ChildPk::class)) Realm.open(configuration).close() app.close() - // Wait max 10 seconds for threads to settle - var activeThreads = 0 + // Wait max 30 seconds for threads to settle + var activeThreads: List = emptyList() var fullyClosed = false - var count = 10 + var count = 5 while (!fullyClosed && count > 0) { - delay(1.seconds) + PlatformUtils.triggerGC() // Ensure we only have daemon threads after closing Realms and Apps activeThreads = Thread.getAllStackTraces().keys .filter { !it.isDaemon } @@ -62,26 +61,25 @@ class RealmTests { // Test thread it.name.startsWith("Test worker") } - .size - if (activeThreads == 0) { + if (activeThreads.isEmpty()) { fullyClosed = true } else { count -= 1 } } - assertEquals(0, activeThreads, "Active threads where found: ${threadTrace()}") + assertEquals(0, activeThreads.size, "Active threads where found ($activeThreads.size): ${threadTrace(activeThreads)}") } - private fun threadTrace(): String { + private fun threadTrace(threads: List? = null): String { val sb = StringBuilder() sb.appendLine("--------------------------------") - val stack = Thread.getAllStackTraces() - stack.keys + val stack: List = threads ?: Thread.getAllStackTraces().keys.toList() + stack .sortedBy { it.name } .forEach { t: Thread -> sb.appendLine("${t.name} - Is Daemon ${t.isDaemon} - Is Alive ${t.isAlive}") } - sb.appendLine("All threads: ${stack.keys.size}") + sb.appendLine("All threads: ${stack.size}") sb.appendLine("Active threads: ${Thread.activeCount()}") return sb.toString() }