From 4412237225daafa367d3b01370beda3bcd39ecb8 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Tue, 30 Jan 2024 08:23:15 +0100 Subject: [PATCH] Merge all changes from https://github.com/realm/realm-kotlin/pull/1633 --- CHANGELOG.md | 2 +- Jenkinsfile | 8 +- buildSrc/src/main/kotlin/Config.kt | 2 +- dependencies.list | 6 +- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../interop/sync/ProtocolErrorCode.kt | 4 +- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../interop/sync/ProtocolErrorCode.kt | 4 +- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../interop/sync/ProtocolErrorCode.kt | 4 +- packages/external/core | 2 +- .../io/realm/kotlin/test/mongodb/TestApp.kt | 26 ++++++ .../test/mongodb/common/AsymmetricSchemas.kt | 63 +++++++++++++ .../kotlin/test/mongodb/common/Schema.kt | 9 +- .../kotlin/test/mongodb/util/AppAdmin.kt | 20 ++++ .../test/mongodb/util/AppServicesClient.kt | 38 ++++++++ .../test/mongodb/util/TestAppInitializer.kt | 92 ------------------- .../mongodb/common/AsymmetricSyncTests.kt | 80 +++------------- .../common/FlexibleSyncIntegrationTests.kt | 21 +++-- .../common/MutableSubscriptionSetTests.kt | 3 +- .../mongodb/common/ProgressListenerTests.kt | 5 +- .../common/SubscriptionExtensionsTests.kt | 3 +- .../mongodb/common/SubscriptionSetTests.kt | 13 +-- .../test/mongodb/common/SyncSessionTests.kt | 20 +++- .../test/mongodb/common/SyncedRealmTests.kt | 19 ++-- .../kotlin/test/mongodb/common/utils/Utils.kt | 14 +++ 26 files changed, 254 insertions(+), 207 deletions(-) create mode 100644 packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSchemas.kt rename packages/test-sync/src/{commonTest => commonMain}/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c4a96882..973f09202b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ ### Internal * Update to Ktor 2.3.4. * Updated to CMake 3.27.7 -* Updated to Realm Core 13.25.0, commit 71f94d75e25bfc8913fcd93ae8de550b57577a4a. +* Updated to Realm Core 13.26.0, commit 5533505d18fda93a7a971d58a191db5005583c92. * Adding Sync tests via Github Action. diff --git a/Jenkinsfile b/Jenkinsfile index 2add8ccda0..effc4e006d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,6 +35,8 @@ runTests = true isReleaseBranch = releaseBranches.contains(currentBranch) // Manually wipe the workspace before checking out the code. This happens automatically on release branches. forceWipeWorkspace = false +// Whether or not to use platform networking for tests +usePlatformNetworking = false // References to Docker containers holding the MongoDB Test server and infrastructure for // controlling it. @@ -191,7 +193,7 @@ pipeline { "integrationtest", { forwardAdbPorts() - testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=true -PincludeSdkModules=false connectedAndroidTest") + testAndCollect("packages", "cleanAllTests -PsyncUsePlatformNetworking=${usePlatformNetworking} -PincludeSdkModules=false connectedAndroidTest") } ) } @@ -215,7 +217,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests jvmTest -PsyncUsePlatformNetworking=true -PincludeSdkModules=false ') + testAndCollect("packages", "cleanAllTests jvmTest -PsyncUsePlatformNetworking=${usePlatformNetworking} -PincludeSdkModules=false") } ]) } @@ -235,7 +237,7 @@ pipeline { steps { testWithServer([ { - testAndCollect("packages", 'cleanAllTests :test-sync:connectedAndroidtest -PsyncUsePlatformNetworking=true -PincludeSdkModules=false -PtestBuildType=debugMinified') + testAndCollect("packages", "cleanAllTests :test-sync:connectedAndroidtest -PsyncUsePlatformNetworking=${usePlatformNetworking} -PincludeSdkModules=false -PtestBuildType=debugMinified") } ]) sh 'rm mapping.zip || true' diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index bcb85872dd..a9735d8935 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -131,7 +131,7 @@ object Versions { const val latestKotlin = "1.9.20" // https://kotlinlang.org/docs/eap.html#build-details const val kotlinCompileTesting = "1.5.0" // https://github.com/tschuchortdev/kotlin-compile-testing const val ktlint = "0.45.2" // https://github.com/pinterest/ktlint - const val ktor = "2.3.4" // https://github.com/ktorio/ktor + const val ktor = "2.3.7" // https://github.com/ktorio/ktor const val multidex = "2.0.1" // https://developer.android.com/jetpack/androidx/releases/multidex const val nexusPublishPlugin = "1.1.0" // https://github.com/gradle-nexus/publish-plugin const val okio = "3.2.0" // https://square.github.io/okio/#releases diff --git a/dependencies.list b/dependencies.list index 97dfbc8b5e..e145cf6f87 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,11 +1,11 @@ # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions -MONGODB_REALM_SERVER=2023-12-15 +MONGODB_REALM_SERVER=2024-01-22 # `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version # note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits # for that date within the following repositories: # https://github.com/10gen/baas/ # https://github.com/10gen/baas-ui/ -REALM_BAAS_GIT_HASH=47d9f6170ab1ac2aa64e7b5046e85247f3ac6d30 -REALM_BAAS_UI_GIT_HASH=49157ef4a6af1c1de4dfbad5d7d02543776b25eb +REALM_BAAS_GIT_HASH=97b7445b1634c7a93fbabefc50967b41ce3cc330 +REALM_BAAS_UI_GIT_HASH=f5f5d71e634c2a64d08b2f911e2d7bf91d9cceda diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 6dc9c8cb8c..7fe946d6ac 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -68,6 +68,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_WRONG_SYNC_TYPE, RLM_ERR_SYNC_WRITE_NOT_ALLOWED, RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH, + RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR, RLM_ERR_SYSTEM_ERROR, RLM_ERR_LOGIC, RLM_ERR_NOT_SUPPORTED, diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index a182d610a8..02a99920ea 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -82,7 +82,9 @@ expect enum class SyncSessionErrorCode : CodeDescription { RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE, RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX, RLM_SYNC_ERR_SESSION_BAD_PROGRESS, - RLM_SYNC_ERR_SESSION_REVERT_TO_PBS; + RLM_SYNC_ERR_SESSION_REVERT_TO_PBS, + RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION, + RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED; companion object { internal fun of(nativeValue: Int): SyncSessionErrorCode? diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 19c7a0c29a..cb35340a91 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -65,6 +65,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno_e.RLM_ERR_WRONG_SYNC_TYPE), RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno_e.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH("SyncLocalClockBeforeEpoch", realm_errno_e.RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH), + RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR("SyncSchemaMigrationError", realm_errno_e.RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno_e.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno_e.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno_e.RLM_ERR_NOT_SUPPORTED), diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index c01be29fc1..f1583e5edc 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -87,7 +87,9 @@ actual enum class SyncSessionErrorCode( RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE("CompensatingWrite", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE), RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX("MigrateToFlexibleSync", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX), RLM_SYNC_ERR_SESSION_BAD_PROGRESS("BadProgress", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_BAD_PROGRESS), - RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS); + RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS), + RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION("BadSchemaVersion", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION), + RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED("SchemaVersionChanged", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED); actual companion object { internal actual fun of(nativeValue: Int): SyncSessionErrorCode? = diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 9851808195..0f676e6aa3 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -69,6 +69,7 @@ actual enum class ErrorCode( RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno.RLM_ERR_WRONG_SYNC_TYPE), RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH("SyncLocalClockBeforeEpoch", realm_errno.RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH), + RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR("SyncSchemaMigrationError", realm_errno.RLM_ERR_SYNC_SCHEMA_MIGRATION_ERROR), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno.RLM_ERR_NOT_SUPPORTED), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 805184bc5c..3e6a4c9c91 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -88,7 +88,9 @@ actual enum class SyncSessionErrorCode( RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE("CompensatingWrite", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE), RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX("MigrateToFlexibleSync", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_MIGRATE_TO_FLX), RLM_SYNC_ERR_SESSION_BAD_PROGRESS("BadProgress", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_BAD_PROGRESS), - RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS); + RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS), + RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION("BadSchemaVersion", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_BAD_SCHEMA_VERSION), + RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED("SchemaVersionChanged", realm_sync_errno_session.RLM_SYNC_ERR_SESSION_SCHEMA_VERSION_CHANGED); override val nativeValue: Int = errorCode.value.toInt() diff --git a/packages/external/core b/packages/external/core index 71f94d75e2..5533505d18 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 71f94d75e25bfc8913fcd93ae8de550b57577a4a +Subproject commit 5533505d18fda93a7a971d58a191db5005583c92 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 91b7ff6aa2..101e0aa435 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 @@ -19,6 +19,7 @@ package io.realm.kotlin.test.mongodb +import io.realm.kotlin.Realm import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.SynchronizableObject @@ -32,6 +33,8 @@ import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA import io.realm.kotlin.test.mongodb.util.AppAdmin import io.realm.kotlin.test.mongodb.util.AppAdminImpl import io.realm.kotlin.test.mongodb.util.AppServicesClient @@ -40,6 +43,7 @@ import io.realm.kotlin.test.mongodb.util.Service import io.realm.kotlin.test.mongodb.util.TestAppInitializer.initializeDefault import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestHelper +import io.realm.kotlin.test.util.use import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ExperimentalKBsonSerializerApi @@ -117,6 +121,28 @@ open class TestApp private constructor( ) ) + init { + // For apps with Flexible Sync, we need to bootstrap all the schemas to work around + // https://github.com/realm/realm-core/issues/7297. + // So we create a dummy Realm, upload all the schemas and close the Realm again. + if (app.configuration.appId.startsWith(TEST_APP_FLEX, ignoreCase = false)) { + runBlocking { + val user = app.login(Credentials.anonymous()) + val config = SyncConfiguration.create(user, FLEXIBLE_SYNC_SCHEMA) + try { + Realm.open(config).use { + // Using syncSession.uploadAllLocalChanges() seems to just hang forever. + // This is tracked by the above Core issue. Instead use the Sync Progress + // endpoint to signal when the schemas are ready. + pairAdminApp.second.waitForSyncBootstrap() + } + } finally { + user.delete() + } + } + } + } + fun createUserAndLogin(): User = runBlocking { val (email, password) = TestHelper.randomEmail() to "password1234" emailPasswordAuth.registerUser(email, password).run { diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSchemas.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSchemas.kt new file mode 100644 index 0000000000..236f77d9a6 --- /dev/null +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSchemas.kt @@ -0,0 +1,63 @@ +package io.realm.kotlin.test.mongodb.common + +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.types.AsymmetricRealmObject +import io.realm.kotlin.types.EmbeddedRealmObject +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PersistedName +import io.realm.kotlin.types.annotations.PrimaryKey +import org.mongodb.kbson.BsonObjectId +import org.mongodb.kbson.ObjectId + +class DeviceParent : RealmObject { + @PersistedName("_id") + @PrimaryKey + var id: ObjectId = BsonObjectId() + var device: Device? = null +} + +class Measurement : AsymmetricRealmObject { + @PersistedName("_id") + @PrimaryKey + var id: ObjectId = BsonObjectId() + var type: String = "temperature" + var value: Float = 0.0f + var device: Device? = null + var backups: RealmList = realmListOf() +} + +class BackupDevice() : EmbeddedRealmObject { + constructor(name: String, serialNumber: String) : this() { + this.name = name + this.serialNumber = serialNumber + } + var name: String = "" + var serialNumber: String = "" +} + +class Device() : EmbeddedRealmObject { + constructor(name: String, serialNumber: String) : this() { + this.name = name + this.serialNumber = serialNumber + } + var name: String = "" + var serialNumber: String = "" + var backupDevice: BackupDevice? = null +} + +class AsymmetricA : AsymmetricRealmObject { + @PrimaryKey + var _id: ObjectId = ObjectId() + var child: EmbeddedB? = null +} + +class EmbeddedB : EmbeddedRealmObject { + var child: StandardC? = null +} + +class StandardC : RealmObject { + @PrimaryKey + var _id: ObjectId = ObjectId() + var name: String = "" +} diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt similarity index 88% rename from packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt rename to packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt index e9eb4265ad..20eb18bdc4 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt @@ -29,9 +29,9 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject private val ASYMMETRIC_SCHEMAS = setOf( - AsymmetricSyncTests.AsymmetricA::class, - AsymmetricSyncTests.EmbeddedB::class, - AsymmetricSyncTests.StandardC::class, + AsymmetricA::class, + EmbeddedB::class, + StandardC::class, Measurement::class, ) private val DEFAULT_SCHEMAS = setOf( @@ -52,4 +52,7 @@ private val DEFAULT_SCHEMAS = setOf( ) val PARTITION_BASED_SCHEMA = DEFAULT_SCHEMAS +// Amount of schema classes that should be created on the server. EmbeddedRealmObjects are not +// included in this count +val FLEXIBLE_SYNC_SCHEMA_COUNT = 11 val FLEXIBLE_SYNC_SCHEMA = DEFAULT_SCHEMAS + ASYMMETRIC_SCHEMAS diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt index e15939520e..be2e3873a6 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt @@ -18,7 +18,9 @@ package io.realm.kotlin.test.mongodb.util import io.realm.kotlin.mongodb.sync.SyncMode import io.realm.kotlin.mongodb.sync.SyncSession +import kotlinx.coroutines.delay import kotlinx.serialization.json.JsonObject +import kotlin.time.Duration.Companion.seconds /** * Wrapper around App Services Server Admin functions needed for tests. @@ -101,6 +103,11 @@ interface AppAdmin { */ suspend fun deleteDocuments(database: String, clazz: String, query: String): JsonObject? + /** + * Wait for Sync bootstrap to complete for all model classes. + */ + suspend fun waitForSyncBootstrap() + fun closeClient() } @@ -201,6 +208,19 @@ class AppAdminImpl( app.deleteDocument(database, clazz, query) } + override suspend fun waitForSyncBootstrap() { + baasClient.run { + var counter = 30 + while (!app.initialSyncComplete() && counter > 0) { + delay(1.seconds) + counter-- + } + if (!app.initialSyncComplete()) { + throw IllegalStateException("Test server did not finish bootstrapping sync in time.") + } + } + } + override fun closeClient() { baasClient.closeClient() } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt index 4757b3bb45..58b770d1a5 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt @@ -39,6 +39,7 @@ import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.sync.SyncMode import io.realm.kotlin.test.mongodb.SyncServerConfig import io.realm.kotlin.test.mongodb.TEST_APP_CLUSTER_NAME +import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA_COUNT import io.realm.kotlin.test.mongodb.util.TestAppInitializer.initialize import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -54,6 +55,7 @@ import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -639,6 +641,42 @@ class AppServicesClient( ) } + suspend fun BaasApp.initialSyncComplete(): Boolean { + return withContext(dispatcher) { + try { + httpClient.typedRequest( + Get, + "$url/sync/progress" + ).let { obj: JsonObject -> + val statuses: JsonElement = obj["progress"]!! + when (statuses) { + is JsonObject -> { + if (statuses.keys.isEmpty()) { + // It might take a few seconds to register the Schemas, so treat + // "empty" progress as initial sync not being complete (as we always + // have at least one pre-defined schema). + false + } + val bootstrapComplete: List = statuses.keys.map { schemaClass -> + statuses[schemaClass]!!.jsonObject["complete"]?.jsonPrimitive?.boolean == true + } + bootstrapComplete.all { it } && statuses.size == FLEXIBLE_SYNC_SCHEMA_COUNT + } + else -> false + } + } + } catch (ex: IllegalStateException) { + if (ex.message!!.contains("there are no mongodb/atlas services with provided sync state")) { + // If the network returns this error, it means that Sync is not enabled for this app, + // in that case, just report success. + true + } else { + throw ex + } + } + } + } + private suspend fun BaasApp.getLocalUserPassProviderId(): String = withContext(dispatcher) { httpClient.typedRequest( diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt index 6076c78261..60d9dfb223 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/TestAppInitializer.kt @@ -54,98 +54,6 @@ object TestAppInitializer { } """.trimIndent() ) - - app.addSchema( - """ - { - "metadata": { - "data_source": "BackingDB", - "database": "$databaseName", - "collection": "FlexChildObject" - }, - "schema": { - "properties": { - "_id": { - "bsonType": "objectId" - }, - "name": { - "bsonType": "string" - }, - "realm_id": { - "bsonType": "string" - } - }, - "required": [ - "name", - "_id" - ], - "title": "FlexChildObject" - } - } - """.trimIndent() - ) - - app.addSchema( - """ - { - "metadata": { - "data_source": "BackingDB", - "database": "$databaseName", - "collection": "FlexParentObject" - }, - "relationships": { - "child": { - "ref": "#/relationship/BackingDB/$databaseName/FlexChildObject", - "source_key": "child", - "foreign_key": "_id", - "is_list": false - } - }, - "schema": { - "properties": { - "_id": { - "bsonType": "objectId" - }, - "age": { - "bsonType": "int" - }, - "child": { - "bsonType": "objectId" - }, - "name": { - "bsonType": "string" - }, - "realm_id": { - "bsonType": "string" - }, - "section": { - "bsonType": "int" - }, - "embedded": { - "title": "FlexEmbeddedObject", - "bsonType": "object", - "required": [ - "embeddedName" - ], - "properties": { - "embeddedName": { - "bsonType": "string" - } - } - } - }, - "required": [ - "name", - "section", - "age", - "_id" - ], - "title": "FlexParentObject" - - } - } - """.trimIndent() - ) } @Suppress("LongMethod") 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 d5d5a35661..daa2a917f7 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 @@ -21,9 +21,9 @@ import io.realm.kotlin.Realm import io.realm.kotlin.dynamic.DynamicMutableRealm import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.ext.query -import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.log.LogLevel import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.annotations.ExperimentalAsymmetricSyncApi import io.realm.kotlin.mongodb.ext.insert @@ -33,15 +33,10 @@ import io.realm.kotlin.schema.RealmClassKind import io.realm.kotlin.test.StandaloneDynamicMutableRealm import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.use -import io.realm.kotlin.types.AsymmetricRealmObject -import io.realm.kotlin.types.EmbeddedRealmObject -import io.realm.kotlin.types.RealmList -import io.realm.kotlin.types.RealmObject -import io.realm.kotlin.types.annotations.PersistedName -import io.realm.kotlin.types.annotations.PrimaryKey import kotlinx.atomicfu.atomic import kotlinx.coroutines.delay import org.mongodb.kbson.ObjectId @@ -54,42 +49,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds -class DeviceParent : RealmObject { - @PersistedName("_id") - @PrimaryKey - var id: ObjectId = ObjectId() - var device: Device? = null -} - -class Measurement : AsymmetricRealmObject { - @PersistedName("_id") - @PrimaryKey - var id: ObjectId = ObjectId() - var type: String = "temperature" - var value: Float = 0.0f - var device: Device? = null - var backups: RealmList = realmListOf() -} - -class BackupDevice() : EmbeddedRealmObject { - constructor(name: String, serialNumber: String) : this() { - this.name = name - this.serialNumber = serialNumber - } - var name: String = "" - var serialNumber: String = "" -} - -class Device() : EmbeddedRealmObject { - constructor(name: String, serialNumber: String) : this() { - this.name = name - this.serialNumber = serialNumber - } - var name: String = "" - var serialNumber: String = "" - var backupDevice: BackupDevice? = null -} - @OptIn(ExperimentalAsymmetricSyncApi::class) class AsymmetricSyncTests { @@ -99,7 +58,10 @@ class AsymmetricSyncTests { @BeforeTest fun setup() { - app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX) + app = TestApp(this::class.simpleName, appName = TEST_APP_FLEX, logLevel = LogLevel.ALL, builder = { builder -> + builder.usePlatformNetworking(true) + builder + }) val (email, password) = TestHelper.randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) @@ -107,7 +69,9 @@ class AsymmetricSyncTests { config = SyncConfiguration.Builder( user, schema = FLEXIBLE_SYNC_SCHEMA - ).initialSubscriptions { + ).errorHandler { session, error -> + println(error) + }.initialSubscriptions { it.query().subscribe() }.build() realm = Realm.open(config) @@ -124,7 +88,6 @@ class AsymmetricSyncTests { } } - @Ignore // See https://github.com/realm/realm-kotlin/issues/1629 @Test fun insert() = runBlocking { val initialServerDocuments = app.countDocuments("Measurement") @@ -140,8 +103,7 @@ class AsymmetricSyncTests { } } - realm.syncSession.uploadAllLocalChanges() - + realm.syncSession.uploadAllLocalChangesOrFail() verifyDocuments(clazz = "Measurement", expectedCount = newDocuments, initialCount = initialServerDocuments) } @@ -282,22 +244,6 @@ class AsymmetricSyncTests { } } - class AsymmetricA : AsymmetricRealmObject { - @PrimaryKey - var _id: ObjectId = ObjectId() - var child: EmbeddedB? = null - } - - class EmbeddedB : EmbeddedRealmObject { - var child: StandardC? = null - } - - class StandardC : RealmObject { - @PrimaryKey - var _id: ObjectId = ObjectId() - var name: String = "" - } - // Verify that a schema of Asymmetric -> Embedded -> RealmObject work. @Test fun asymmetricSchema() = runBlocking { @@ -305,6 +251,7 @@ class AsymmetricSyncTests { app.login(Credentials.anonymous()), schema = FLEXIBLE_SYNC_SCHEMA ).build() + val initialServerDocuments = app.countDocuments("AsymmetricA") Realm.open(config).use { it.write { insert( @@ -315,7 +262,8 @@ class AsymmetricSyncTests { } ) } - it.syncSession.uploadAllLocalChanges() + it.syncSession.uploadAllLocalChangesOrFail() + verifyDocuments("AsymmetricA", 1, initialServerDocuments) } } @@ -324,7 +272,7 @@ class AsymmetricSyncTests { // https://youtrack.jetbrains.com/issue/KT-64139/Native-Bug-with-while-loop-coroutine-which-is-started-and-stopped-on-the-same-thread var documents = atomic(0) var found = false - var attempt = 60 + var attempt = 60 // Wait 1 minute // The translator might be slow to incorporate changes into MongoDB, so we retry for a bit // before giving up. while (!found && attempt > 0) { 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 c3b8de8e18..640c777eee 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 @@ -31,6 +31,8 @@ import io.realm.kotlin.mongodb.sync.SyncSession import io.realm.kotlin.mongodb.syncSession import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.common.utils.uploadAllLocalChangesOrFail +import io.realm.kotlin.test.mongodb.common.utils.waitForSynchronizationOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestChannel import io.realm.kotlin.test.util.TestHelper @@ -50,7 +52,6 @@ import kotlin.test.assertTrue import kotlin.test.fail import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.Duration.Companion.seconds /** * Integration smoke tests for Flexible Sync. This is not intended to cover all cases, but just @@ -87,12 +88,12 @@ class FlexibleSyncIntegrationTests { val subs = realm1.subscriptions.update { add(realm1.query("section = $0", randomSection)) } - assertTrue(subs.waitForSynchronization()) + subs.waitForSynchronizationOrFail() realm1.write { copyToRealm(FlexParentObject(randomSection).apply { name = "red" }) copyToRealm(FlexParentObject(randomSection).apply { name = "blue" }) } - realm1.syncSession.uploadAllLocalChanges() + realm1.syncSession.uploadAllLocalChangesOrFail() } // Download data from user 2 @@ -144,7 +145,7 @@ class FlexibleSyncIntegrationTests { .query("(name = 'red' OR name = 'blue')") add(query, "sub") } - assertTrue(realm.subscriptions.waitForSynchronization(4.minutes)) + realm.subscriptions.waitForSynchronizationOrFail() realm.write { copyToRealm(FlexParentObject(randomSection).apply { name = "red" }) copyToRealm(FlexParentObject(randomSection).apply { name = "blue" }) @@ -154,7 +155,7 @@ class FlexibleSyncIntegrationTests { val query = realm.query("section = $0 AND name = 'red'", randomSection) add(query, "sub", updateExisting = true) } - assertTrue(realm.subscriptions.waitForSynchronization(4.minutes)) + realm.subscriptions.waitForSynchronizationOrFail() assertEquals(1, realm.query().count().find()) } } @@ -203,7 +204,7 @@ class FlexibleSyncIntegrationTests { ) } } - assertTrue(realm.syncSession.uploadAllLocalChanges(1.minutes), "Failed to upload writes in time") + realm.syncSession.uploadAllLocalChangesOrFail() } // User 2 opens a Realm twice @@ -243,7 +244,7 @@ class FlexibleSyncIntegrationTests { add(realm1.query("section = $0", randomSection)) add(realm1.query("section = $0", randomSection)) } - assertTrue(subs.waitForSynchronization()) + subs.waitForSynchronizationOrFail() realm1.write { copyToRealm( FlexParentObject(randomSection).apply { @@ -270,7 +271,7 @@ class FlexibleSyncIntegrationTests { } ) } - realm1.syncSession.uploadAllLocalChanges() + realm1.syncSession.uploadAllLocalChangesOrFail() } // Download data from user 2 @@ -322,14 +323,14 @@ class FlexibleSyncIntegrationTests { realm.subscriptions.update { add(realm.query("_id = $0", objectId)) - }.waitForSynchronization(30.seconds) + }.waitForSynchronizationOrFail() assertNotEquals(expectedPrimaryKey, objectId) realm.write { copyToRealm(FlexParentObject().apply { _id = expectedPrimaryKey }) } - realm.syncSession.uploadAllLocalChanges(30.seconds) + realm.syncSession.uploadAllLocalChangesOrFail() } val exception: CompensatingWriteException = channel.receiveOrFail() 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 623a9e9508..248aa3e7f0 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 @@ -29,6 +29,7 @@ import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.mongodb.syncSession import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.toRealmInstant @@ -381,7 +382,7 @@ class MutableSubscriptionSetTests { copyToRealm(FlexParentObject(sectionId)) } } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } } 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 5a9686b061..51437d237c 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 @@ -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.common.utils.assertFailsWithMessage +import io.realm.kotlin.test.mongodb.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestChannel @@ -165,7 +166,7 @@ class ProgressListenerTests { flow.takeWhile { completed -> completed < 3 } .collect { completed -> realm.writeSampleData(TEST_SIZE, idOffset = (completed + 1) * TEST_SIZE) - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } } @@ -228,7 +229,7 @@ class ProgressListenerTests { Realm.open(createSyncConfig(app.createUserAndLogIn())).use { realm -> withTimeout(10.seconds) { // Ensure that all data is already synced - assertTrue { realm.syncSession.uploadAllLocalChanges() } + realm.syncSession.uploadAllLocalChangesOrFail() assertTrue { realm.syncSession.downloadAllServerChanges() } // Ensure that progress listeners are triggered at least one time even though there // is no data 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 83c7333b83..c254d30efd 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 @@ -30,6 +30,7 @@ import io.realm.kotlin.mongodb.syncSession import io.realm.kotlin.query.RealmResults import io.realm.kotlin.test.mongodb.TEST_APP_FLEX import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.use @@ -404,7 +405,7 @@ class SubscriptionExtensionsTests { copyToRealm(FlexParentObject(sectionId)) } } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } } 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 4264d3329a..993957978e 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 @@ -27,6 +27,7 @@ import io.realm.kotlin.mongodb.sync.SyncConfiguration 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.common.utils.waitForSynchronizationOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.util.TestHelper @@ -154,7 +155,7 @@ class SubscriptionSetTests { realm.query().subscribe("test1") } assertEquals(SubscriptionSetState.PENDING, subscriptions.state) - subscriptions.waitForSynchronization() + subscriptions.waitForSynchronizationOrFail() assertEquals(SubscriptionSetState.COMPLETE, subscriptions.state) subscriptions.update { // Flexible Sync queries cannot use limit @@ -194,7 +195,7 @@ class SubscriptionSetTests { subscriptions.update { removeAll() } - subscriptions.waitForSynchronization() + subscriptions.waitForSynchronizationOrFail() assertNull(subscriptions.errorMessage) } @@ -223,7 +224,7 @@ class SubscriptionSetTests { @Test fun waitForSynchronizationInitialSubscriptions() = runBlocking { val subscriptions = realm.subscriptions - assertTrue(subscriptions.waitForSynchronization()) + subscriptions.waitForSynchronizationOrFail() assertEquals(SubscriptionSetState.COMPLETE, subscriptions.state) assertEquals(0, subscriptions.size) } @@ -232,7 +233,7 @@ class SubscriptionSetTests { fun waitForSynchronizationInitialEmptySubscriptionSet() = runBlocking { val subscriptions = realm.subscriptions subscriptions.update { /* Do nothing */ } - assertTrue(subscriptions.waitForSynchronization()) + subscriptions.waitForSynchronizationOrFail() assertEquals(SubscriptionSetState.COMPLETE, subscriptions.state) assertEquals(0, subscriptions.size) } @@ -243,7 +244,7 @@ class SubscriptionSetTests { realm.query().subscribe("test") } assertNotEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) - assertTrue(updatedSubs.waitForSynchronization()) + updatedSubs.waitForSynchronizationOrFail() assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) } @@ -299,7 +300,7 @@ class SubscriptionSetTests { val subs = realm.subscriptions.update { realm.query().subscribe("sub") }.also { - it.waitForSynchronization() + it.waitForSynchronizationOrFail() } realm.close() 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 c232125581..5832f3b170 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 @@ -34,6 +34,7 @@ import io.realm.kotlin.mongodb.syncSession 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.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.TestChannel @@ -43,6 +44,7 @@ import io.realm.kotlin.test.util.trySendOrFail import io.realm.kotlin.test.util.use import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout @@ -282,7 +284,7 @@ class SyncSessionTests { } assertEquals(10, realm1.query().count().find()) assertEquals(0, realm2.query().count().find()) - assertTrue(realm1.syncSession.uploadAllLocalChanges()) + realm1.syncSession.uploadAllLocalChangesOrFail() // Due to the Server Translator, there is a small delay between data // being uploaded and it not being immediately ready for download @@ -336,7 +338,7 @@ class SyncSessionTests { try { assertFailsWithMessage("Operation is not allowed inside a `SyncSession.ErrorHandler`.") { runBlocking { - session.uploadAllLocalChanges() + session.uploadAllLocalChangesOrFail() } } assertFailsWithMessage("Operation is not allowed inside a `SyncSession.ErrorHandler`.") { @@ -361,7 +363,7 @@ class SyncSessionTests { val session = realm.syncSession app.pauseSync() assertFailsWith { - session.uploadAllLocalChanges() + session.uploadAllLocalChangesOrFail() }.also { assertTrue(it.message!!.contains("End of input", ignoreCase = true), it.message) } @@ -441,7 +443,7 @@ class SyncSessionTests { copyToRealm(objWithPK) } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } @@ -487,7 +489,7 @@ class SyncSessionTests { partitionValue = partitionValue ).name("test1.realm").build() Realm.open(config1).use { realm1 -> - realm1.syncSession.uploadAllLocalChanges() + realm1.syncSession.uploadAllLocalChangesOrFail() // Make sure to sync the realm with the server before opening the second instance assertTrue(realm1.syncSession.uploadAllLocalChanges(1.minutes)) } @@ -555,17 +557,25 @@ class SyncSessionTests { @Test fun connectionState_completeOnClose() = runBlocking { + val channel = TestChannel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) val realm = Realm.open(createSyncConfig(user)) try { val flow1 = realm.syncSession.connectionStateAsFlow() val job = async { withTimeout(10.seconds) { + // We are not guarantee that the connectionFlow will trigger, so are forced + // to send the event before. This still leaves a small chance of a race + // condition, but I assume that the jump between coroutines is always slower + // than executing to instructions in sequence. + channel.send(true) flow1.collect { } } } + channel.receiveOrFail() realm.close() job.await() } finally { + channel.close() if (!realm.isClosed()) { realm.close() } 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 ac299ca170..a233dfb87a 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.TestApp 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.common.utils.uploadAllLocalChangesOrFail import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.mongodb.use import io.realm.kotlin.test.platform.PlatformUtils @@ -245,7 +246,7 @@ class SyncedRealmTests { val id = "id-${Random.nextLong()}" copyToRealm(SyncObjectWithAllTypes().apply { _id = id }) } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } @@ -365,7 +366,7 @@ class SyncedRealmTests { // Open another realm with the same entity but change the type of a field in the schema to // trigger a sync error to be caught by the error handler runBlocking { - realm1.syncSession.uploadAllLocalChanges() + realm1.syncSession.uploadAllLocalChangesOrFail() val config2 = SyncConfiguration.Builder( schema = setOf(io.realm.kotlin.entities.sync.bogus.ChildPk::class), user = user, @@ -448,7 +449,7 @@ class SyncedRealmTests { ) } } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } // 2. Sometimes it can take a little while for the data to be available to other users, @@ -496,7 +497,7 @@ class SyncedRealmTests { ) } } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } // 2. Sometimes it can take a little while for the data to be available to other users, @@ -626,7 +627,7 @@ class SyncedRealmTests { realm.write { copyToRealm(masterObject) } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } } createPartitionSyncConfig( @@ -663,7 +664,7 @@ class SyncedRealmTests { // schema = setOf(SyncObjectWithAllTypes::class, ChildPk::class) ).let { config -> Realm.open(config).use { realm -> - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() val schema: RealmSchema = realm.schema() val childPkSchema: RealmClass? = schema["ChildPk"] assertNotNull(childPkSchema) @@ -739,7 +740,7 @@ class SyncedRealmTests { val masterObject = SyncObjectWithAllTypes().apply { _id = "id-${Random.nextLong()}" } Realm.open(config0).use { realm -> realm.writeBlocking { copyToRealm(masterObject) } - realm.syncSession.uploadAllLocalChanges() + realm.syncSession.uploadAllLocalChangesOrFail() } assertEquals(42, counterValue.receiveOrFail(message = "Failed to receive 42")) @@ -1197,7 +1198,7 @@ class SyncedRealmTests { realm2.write { copyToRealm(FlexParentObject(section)) } - realm2.syncSession.uploadAllLocalChanges() + realm2.syncSession.uploadAllLocalChangesOrFail() } // Reading the object means we received it from the other Realm @@ -1263,7 +1264,7 @@ class SyncedRealmTests { } ) } - flexSyncRealm.syncSession.uploadAllLocalChanges() + flexSyncRealm.syncSession.uploadAllLocalChangesOrFail() } assertTrue(customLogger.logs.isNotEmpty()) assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/utils/Utils.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/utils/Utils.kt index 526af13e85..0f250faa59 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/utils/Utils.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/utils/Utils.kt @@ -15,8 +15,12 @@ */ package io.realm.kotlin.test.mongodb.common.utils +import io.realm.kotlin.mongodb.sync.SubscriptionSet +import io.realm.kotlin.mongodb.sync.SyncSession import kotlin.reflect.KClass import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes // NOTE: Copy from :base:commonTest. It is unclear if there is an easy way to share test code like // this between :base and :sync @@ -47,3 +51,13 @@ fun assertFailsWithMessage(exceptionClass: KClass, exceptionM inline fun assertFailsWithMessage(exceptionMessage: String, noinline block: () -> Unit): T = assertFailsWithMessage(T::class, exceptionMessage, block) + +suspend inline fun SubscriptionSet<*>.waitForSynchronizationOrFail() { + val timeout = 2.minutes + assertTrue(this.waitForSynchronization(timeout), "Failed to synchronize subscriptions in time: $timeout") +} + +suspend inline fun SyncSession.uploadAllLocalChangesOrFail() { + val timeout = 5.minutes + assertTrue(this.uploadAllLocalChanges(timeout), "Failed to upload local changes in time: $timeout") +}