diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5d3adfab..cc76998202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,48 @@ -## 1.11.0-SNAPSHOT (YYYY-MM-DD) +## 1.12.0-SNAPSHOT (YYYY-MM-DD) ### Breaking Changes * None. ### Enhancements -* 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)) -* [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)) +* Realm will no longer set the JVM bytecode to 1.8 when applying the Realm plugin. (Issue [#1513](https://github.com/realm/realm-kotlin/issues/1513)) +* The Realm Gradle Plugin no longer has a dependency on KAPT. (Issue [#1513](https://github.com/realm/realm-kotlin/issues/1513)) ### Fixed +* `Realm.close()` is now idempotent. +* Fix error in `RealmAny.equals` that would sometimes return `true` when comparing RealmAnys wrapping same type but different values. (Issue [#1523](https://github.com/realm/realm-kotlin/pull/1523)) +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) +* [Sync] Manual client reset on Windows would not trigger correctly when run inside `onManualResetFallback`. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) +* [Sync] `ClientResetRequiredException.executeClientReset()` now returns a boolean indicating if the manual reset fully succeded or not. (Issue [#1515](https://github.com/realm/realm-kotlin/pull/1515)) +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for +GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) +* [Sync] If calling a function on App Services that resulted in a redirect, it would only redirect for GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* None. + + +## 1.11.1 (2023-09-07) + +### Enhancements * None. +### Fixed +* Opening a Realm would crash with `No built-in scheduler implementation for this platform` on Linux (JVM) and Windows. (Issue [#1502](https://github.com/realm/realm-kotlin/issues/1502), since 1.11.0) + ### Compatibility * File format: Generates Realms with file format v23. * Realm Studio 13.0.0 or above is required to open Realms created by this version. @@ -28,6 +61,62 @@ * None. +## 1.11.0 (2023-09-01) + +### Breaking Changes +* `BaseRealmObject.equals()` has changed from being identity-based only (===) to instead return `true` if two objects come from the same Realm version. This e.g means that reading the same object property twice will now be identical. Note, two Realm objects, even with identical values will not be considered equal if they belong to different versions. + +``` +val childA: Child = realm.query().first().find()!! +val childB: Child = realm.query().first().find()!! + +// This behavior is the same both before 1.11.0 and before +childA === childB // false + +// This will return true in 1.11.0 and onwards. Before it will return false +childA == childB + +realm.writeBlocking { /* Do a write */ } +val childC = realm.query().first().find()!! + +// This will return false because childA belong to version 1, while childC belong to version 2. +// Override equals/hashCode if value semantics are wanted. +childA == childC +``` + +### Enhancements +* Fulltext queries now support prefix search by using the * operator, like `description TEXT 'alex*'`. (Core issue [#6860](https://github.com/realm/realm-core/issues/6860)) +* Realm model classes now generate custom `toString`, `equals` and `hashCode` implementations. This makes it possible to compare by object reference across multiple collections. Note that two objects at different versions will not be considered equal, even +if the content is the same. Custom implementations of these methods will be respected if they are present. (Issue [#1097](https://github.com/realm/realm-kotlin/issues/1097)) +* 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 +* Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440)) +* Compiler error when using Kotlin 1.9.0 and backlinks. (Issue [#1469](https://github.com/realm/realm-kotlin/issues/1469)) +* Leaking `JVMScheduler` instances. In certain circumstances, it could lead to a JNI crash. (Issue [#1463](https://github.com/realm/realm-kotlin/pull/1463)) +* [Sync] Changing a subscriptions query type or query itself will now trigger the `WaitForSync.FIRST_TIME` behaviour, rather than only checking changes to the name. (Issues [#1466](https://github.com/realm/realm-kotlin/issues/1466)) + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* Updated to Realm Core 13.20.0, commit c258e2681bca5fb33bbd23c112493817b43bfa86. + + ## 1.10.2 (2023-07-21) ### Breaking Changes diff --git a/README.md b/README.md index 52d728cbc7..70a5f5dc47 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ Next: head to the full KMM [example](https://github.com/realm/realm-kotlin-sampl If you want to test recent bugfixes or features that have not been packaged in an official release yet, you can use a **-SNAPSHOT** release of the current development version of Realm via Gradle, available on [Maven Central](https://oss.sonatype.org/content/repositories/snapshots/io/realm/kotlin/) ## Groovy -``` +```Gradle // Global build.gradle buildscript { repositories { @@ -280,7 +280,7 @@ apply plugin: "io.realm.kotlin" ``` ## Kotlin -``` +```Kotlin // Global build.gradle buildscript { diff --git a/benchmarks/androidApp/build.gradle.kts b/benchmarks/androidApp/build.gradle.kts index 68c7b0d5e6..9cb9b3ae58 100644 --- a/benchmarks/androidApp/build.gradle.kts +++ b/benchmarks/androidApp/build.gradle.kts @@ -8,12 +8,12 @@ android { compileSdk = Versions.Android.compileSdkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = Versions.kotlinJvmTarget } defaultConfig { diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index 2bebc22f69..1722d6672d 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -32,7 +32,7 @@ allprojects { } tasks.withType { - kotlinOptions.jvmTarget = Versions.jvmTarget + kotlinOptions.jvmTarget = Versions.kotlinJvmTarget } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e5bd2ee582..ee2e79fee3 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -29,6 +29,11 @@ gradlePlugin { } } +java { + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion +} + repositories { google() gradlePluginPortal() diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 783db71ece..3402f0e318 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import org.gradle.api.JavaVersion /** * Enum describing operating systems we can build on. @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.11.0-SNAPSHOT" + const val version = "1.12.0-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" @@ -124,20 +124,22 @@ object Versions { const val jmh = "1.34" // https://github.com/openjdk/jmh const val jmhPlugin = "0.6.6" // https://github.com/melix/jmh-gradle-plugin const val junit = "4.13.2" // https://mvnrepository.com/artifact/junit/junit - const val jvmTarget = "1.8" + const val kbson = "0.3.0" // https://github.com/mongodb/kbson // When updating the Kotlin version, also remember to update /examples/min-android-sample/build.gradle.kts const val kotlin = "1.8.21" // https://github.com/JetBrains/kotlin and https://kotlinlang.org/docs/releases.html#release-details - const val latestKotlin = "1.9.0-Beta" // https://kotlinlang.org/docs/eap.html#build-details + const val kotlinJvmTarget = "1.8" // Which JVM bytecode version is kotlin compiled to. + const val latestKotlin = "1.9.20-Beta" // 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.1.2" // 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 const val relinker = "1.4.5" // https://github.com/KeepSafe/ReLinker const val serialization = "1.4.0" // https://kotlinlang.org/docs/releases.html#release-details const val shadowJar = "6.1.0" // https://mvnrepository.com/artifact/com.github.johnrengelman.shadow/com.github.johnrengelman.shadow.gradle.plugin?repo=gradle-plugins - const val multidex = "2.0.1" // https://developer.android.com/jetpack/androidx/releases/multidex - const val kbson = "0.3.0" // https://github.com/mongodb/kbson + val sourceCompatibilityVersion = JavaVersion.VERSION_1_8 // Language level of any Java source code. + val targetCompatibilityVersion = JavaVersion.VERSION_1_8 // Version of generated JVM bytecode from Java files. } // Could be actual Dependency objects diff --git a/buildSrc/src/main/kotlin/realm-lint.gradle.kts b/buildSrc/src/main/kotlin/realm-lint.gradle.kts index 7875142afd..73714d8f5e 100644 --- a/buildSrc/src/main/kotlin/realm-lint.gradle.kts +++ b/buildSrc/src/main/kotlin/realm-lint.gradle.kts @@ -57,6 +57,7 @@ allprojects { description = "Check Kotlin code style." classpath = ktlint + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED") mainClass.set("com.pinterest.ktlint.Main") args = listOf( "src/**/*.kt", @@ -74,6 +75,7 @@ allprojects { description = "Fix Kotlin code style deviations." classpath = ktlint + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED") mainClass.set("com.pinterest.ktlint.Main") args = listOf( "-F", diff --git a/dependencies.list b/dependencies.list index eb618c86ba..ba8ab7b56a 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-05-15 +MONGODB_REALM_SERVER=2023-09-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=92fc646871b507c73e48cc05ebec54fc9a134ae7 -REALM_BAAS_UI_GIT_HASH=67ad3606ec1137640ea84dcfe7a1a1b29aa7508f +REALM_BAAS_GIT_HASH=0b7562d0401d72c909369030dc29332542614ba3 +REALM_BAAS_UI_GIT_HASH=24baee4eb0e9736969a00a7bfac849565bca17f4 diff --git a/examples/kmm-sample/androidApp/build.gradle.kts b/examples/kmm-sample/androidApp/build.gradle.kts index dd5415befb..4bffec36c9 100644 --- a/examples/kmm-sample/androidApp/build.gradle.kts +++ b/examples/kmm-sample/androidApp/build.gradle.kts @@ -64,7 +64,7 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } } diff --git a/examples/kmm-sample/compose-desktop/build.gradle.kts b/examples/kmm-sample/compose-desktop/build.gradle.kts index 5735815ed3..9cc8cfd1e5 100644 --- a/examples/kmm-sample/compose-desktop/build.gradle.kts +++ b/examples/kmm-sample/compose-desktop/build.gradle.kts @@ -18,7 +18,7 @@ dependencies { } tasks.withType { - kotlinOptions.jvmTarget = Versions.jvmTarget + kotlinOptions.jvmTarget = Versions.kotlinJvmTarget } application { diff --git a/examples/kmm-sample/gradle.properties b/examples/kmm-sample/gradle.properties index eff7ec4f56..abfa6de207 100644 --- a/examples/kmm-sample/gradle.properties +++ b/examples/kmm-sample/gradle.properties @@ -24,3 +24,4 @@ kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false kotlin.mpp.stability.nowarn=true +kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true diff --git a/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt b/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt index 982dc8b5e2..8231e877b9 100644 --- a/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt +++ b/examples/kmm-sample/shared/src/commonMain/kotlin/io/realm/example/kmmsample/AllTypes.kt @@ -17,6 +17,7 @@ package io.realm.example.kmmsample +import io.realm.kotlin.ext.backlinks import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.ObjectId import io.realm.kotlin.types.RealmInstant @@ -83,4 +84,7 @@ class AllTypes : RealmObject { var objectIdRealmList: RealmList = realmListOf(ObjectId.create()) var objectIdRealmListNullable: RealmList = realmListOf(null) var objectRealmList: RealmList = realmListOf() + + // Special types + val parent by backlinks(AllTypes::objectField) } diff --git a/examples/realm-java-compatibility/app/build.gradle b/examples/realm-java-compatibility/app/build.gradle index ba941ea60f..da72b4e451 100644 --- a/examples/realm-java-compatibility/app/build.gradle +++ b/examples/realm-java-compatibility/app/build.gradle @@ -43,11 +43,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility Versions.sourceCompatibilityVersion + targetCompatibility Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = Versions.kotlinJvmTarget } } diff --git a/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts b/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts index f2336d03cd..a94fea7d14 100644 --- a/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts +++ b/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts @@ -40,11 +40,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = Versions.kotlinJvmTarget } } diff --git a/packages/build.gradle.kts b/packages/build.gradle.kts index c593f82c55..296bf17b24 100644 --- a/packages/build.gradle.kts +++ b/packages/build.gradle.kts @@ -27,8 +27,9 @@ allprojects { version = Realm.version group = Realm.group + // Define JVM bytecode target for all Kotlin targets tasks.withType { - kotlinOptions.jvmTarget = "${Versions.jvmTarget}" + kotlinOptions.jvmTarget = "${Versions.kotlinJvmTarget}" } } diff --git a/packages/cinterop/build.gradle.kts b/packages/cinterop/build.gradle.kts index 4bf66b813f..8cbeb83f8e 100644 --- a/packages/cinterop/build.gradle.kts +++ b/packages/cinterop/build.gradle.kts @@ -113,11 +113,7 @@ val nativeLibraryIncludesIosSimulatorArm64Release = includeBinaries(releaseLibs.map { "$absoluteCorePath/build-simulator-arm64/lib/$it" }) kotlin { - jvm { - compilations.all { - kotlinOptions.jvmTarget = Versions.jvmTarget - } - } + jvm() android("android") { publishLibraryVariants("release") } @@ -344,12 +340,10 @@ android { path = project.file("src/jvm/CMakeLists.txt") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle + compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } } diff --git a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt index 4f9490dbe9..1effc3cd32 100644 --- a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt +++ b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test.sync +import io.realm.kotlin.internal.interop.CategoryFlags import io.realm.kotlin.internal.interop.ErrorCategory import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.realm_auth_provider_e @@ -23,23 +24,21 @@ import io.realm.kotlin.internal.interop.realm_errno_e import io.realm.kotlin.internal.interop.realm_error_category_e import io.realm.kotlin.internal.interop.realm_sync_client_metadata_mode_e import io.realm.kotlin.internal.interop.realm_sync_connection_state_e -import io.realm.kotlin.internal.interop.realm_sync_errno_client_e import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e -import io.realm.kotlin.internal.interop.realm_sync_error_category_e import io.realm.kotlin.internal.interop.realm_sync_session_resync_mode_e import io.realm.kotlin.internal.interop.realm_sync_session_state_e import io.realm.kotlin.internal.interop.realm_user_state_e +import io.realm.kotlin.internal.interop.realm_web_socket_errno_e import io.realm.kotlin.internal.interop.sync.AuthProvider import io.realm.kotlin.internal.interop.sync.CoreConnectionState import io.realm.kotlin.internal.interop.sync.CoreSyncSessionState import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolConnectionErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolSessionErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory +import io.realm.kotlin.internal.interop.sync.SyncConnectionErrorCode +import io.realm.kotlin.internal.interop.sync.SyncSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode +import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import org.junit.Test import kotlin.reflect.KClass import kotlin.test.BeforeTest @@ -59,10 +58,11 @@ class SyncEnumTests { } @Test - fun appErrorCategory() { + fun errorCategory() { checkEnum(realm_error_category_e::class) { nativeValue -> ErrorCategory.of(nativeValue) } + assertEquals(ErrorCategory.values().size, CategoryFlags.CATEGORY_ORDER.size) } @Test @@ -94,30 +94,23 @@ class SyncEnumTests { } @Test - fun protocolClientErrorCode() { - checkEnum(realm_sync_errno_client_e::class) { nativeValue -> - ProtocolClientErrorCode.of(nativeValue) - } - } - - @Test - fun protocolConnectionErrorCode() { + fun syncConnectionErrorCode() { checkEnum(realm_sync_errno_connection_e::class) { nativeValue -> - ProtocolConnectionErrorCode.of(nativeValue) + SyncConnectionErrorCode.of(nativeValue) } } @Test - fun protocolSessionErrorCode() { + fun syncSessionErrorCode() { checkEnum(realm_sync_errno_session_e::class) { nativeValue -> - ProtocolSessionErrorCode.of(nativeValue) + SyncSessionErrorCode.of(nativeValue) } } @Test - fun syncErrorCodeCategory() { - checkEnum(realm_sync_error_category_e::class) { nativeValue -> - SyncErrorCodeCategory.of(nativeValue) + fun websocketErrorCode() { + checkEnum(realm_web_socket_errno_e::class) { nativeValue -> + WebsocketErrorCode.of(nativeValue) } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt index b8534d0b67..4d262f206d 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/Callback.kt @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.CoreSubscriptionSetState import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode // TODO Could be replace by lambda. See realm_app_config_new networkTransportFactory for example. interface Callback { @@ -39,7 +38,7 @@ fun interface SyncErrorCallback { // Interface exposed towards `library-sync` interface SyncSessionTransferCompletionCallback { - fun invoke(error: SyncErrorCode?) + fun invoke(error: CoreError?) } interface LogCallback { diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt new file mode 100644 index 0000000000..3d7334fcd7 --- /dev/null +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreError.kt @@ -0,0 +1,66 @@ +package io.realm.kotlin.internal.interop + +/** + * Wrapper for C-API `realm_error_t`. + * See https://github.com/realm/realm-core/blob/master/src/realm.h#L231 + */ +class CoreError( + categoriesNativeValue: Int, + val errorCodeNativeValue: Int, + messageNativeValue: String?, +) { + val categories: CategoryFlags = CategoryFlags((categoriesNativeValue)) + val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue) + val message = messageNativeValue + + operator fun contains(category: ErrorCategory): Boolean = category in categories +} + +data class CategoryFlags(val categoryFlags: Int) { + + companion object { + /** + * See error code mapping to categories here: + * https://github.com/realm/realm-core/blob/master/src/realm/error_codes.cpp#L29 + * + * In most cases, only 1 category is assigned, but some errors have multiple. So instead of + * overwhelming the user with many categories, we only select the most important to show + * in the error message. "important" is of course tricky to define, but generally + * we consider vague categories like [ErrorCategory.RLM_ERR_CAT_RUNTIME] as less important + * than more specific ones like [ErrorCategory.RLM_ERR_CAT_JSON_ERROR]. + * + * In the current implementation, categories between index 0 and 7 are considered equal + * and the order is somewhat arbitrary. No error codes has multiple of these categories + * associated either. + */ + val CATEGORY_ORDER: List = listOf( + ErrorCategory.RLM_ERR_CAT_CUSTOM_ERROR, + ErrorCategory.RLM_ERR_CAT_WEBSOCKET_ERROR, + ErrorCategory.RLM_ERR_CAT_SYNC_ERROR, + ErrorCategory.RLM_ERR_CAT_SERVICE_ERROR, + ErrorCategory.RLM_ERR_CAT_JSON_ERROR, + ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR, + ErrorCategory.RLM_ERR_CAT_SYSTEM_ERROR, + ErrorCategory.RLM_ERR_CAT_FILE_ACCESS, + ErrorCategory.RLM_ERR_CAT_HTTP_ERROR, + ErrorCategory.RLM_ERR_CAT_INVALID_ARG, + ErrorCategory.RLM_ERR_CAT_APP_ERROR, + ErrorCategory.RLM_ERR_CAT_LOGIC, + ErrorCategory.RLM_ERR_CAT_RUNTIME, + ) + } + + /** + * Returns a description of the most important category defined in [categoryFlags]. + * If no known categories are found, the integer values for all the categories is returned + * as debugging information. + */ + val description: String = CATEGORY_ORDER.firstOrNull { category -> + this.contains(category) + }?.description ?: "$categoryFlags" + + /** + * Check whether a given [ErrorCategory] is included in the [categoryFlags]. + */ + operator fun contains(category: ErrorCategory): Boolean = (categoryFlags and category.nativeValue) != 0 +} diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt index fddf363cdf..0a60a8aa7b 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/CoreErrorConverter.kt @@ -31,22 +31,21 @@ object CoreErrorConverter { path: String?, userError: Throwable? ): Throwable { - val categories: CategoryFlag = CategoryFlag(categoriesNativeValue) + val categories: CategoryFlags = CategoryFlags(categoriesNativeValue) val errorCode: ErrorCode? = ErrorCode.of(errorCodeNativeValue) val message: String = "[$errorCode]: $messageNativeValue" return userError ?: when { ErrorCode.RLM_ERR_INDEX_OUT_OF_BOUNDS == errorCode -> IndexOutOfBoundsException(message) - ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories -> + ErrorCategory.RLM_ERR_CAT_INVALID_ARG in categories && ErrorCategory.RLM_ERR_CAT_SYNC_ERROR !in categories -> { + // Some sync errors flagged as both logical and illegal. In our case, we consider those + // IllegalState, so discard them them here and let them fall through to the bottom case IllegalArgumentException(message) + } ErrorCategory.RLM_ERR_CAT_LOGIC in categories || ErrorCategory.RLM_ERR_CAT_RUNTIME in categories -> IllegalStateException(message) else -> Error(message) // This can happen when propagating user level exceptions. } } - - data class CategoryFlag(val categoryCode: Int) { - operator fun contains(other: ErrorCategory): Boolean = categoryCode and other.nativeValue != 0 - } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index fb1716377d..53053fc1da 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -32,7 +32,8 @@ expect enum class ErrorCategory : CodeDescription { RLM_ERR_CAT_SERVICE_ERROR, RLM_ERR_CAT_HTTP_ERROR, RLM_ERR_CAT_CUSTOM_ERROR, - RLM_ERR_CAT_WEBSOCKET_ERROR; + RLM_ERR_CAT_WEBSOCKET_ERROR, + RLM_ERR_CAT_SYNC_ERROR; companion object { internal fun of(nativeValue: Int): ErrorCategory? 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 13138a5605..31db69c4c8 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 @@ -50,6 +50,23 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_SCHEMA_VERSION_MISMATCH, RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE, RLM_ERR_OPERATION_ABORTED, + RLM_ERR_AUTO_CLIENT_RESET_FAILED, + RLM_ERR_BAD_SYNC_PARTITION_VALUE, + RLM_ERR_CONNECTION_CLOSED, + RLM_ERR_INVALID_SUBSCRIPTION_QUERY, + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED, + RLM_ERR_SYNC_COMPENSATING_WRITE, + RLM_ERR_SYNC_CONNECT_FAILED, + RLM_ERR_SYNC_CONNECT_TIMEOUT, + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE, + RLM_ERR_SYNC_PERMISSION_DENIED, + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED, + RLM_ERR_SYNC_USER_MISMATCH, + RLM_ERR_TLS_HANDSHAKE_FAILED, + RLM_ERR_WRONG_SYNC_TYPE, + RLM_ERR_SYNC_WRITE_NOT_ALLOWED, RLM_ERR_SYSTEM_ERROR, RLM_ERR_LOGIC, RLM_ERR_NOT_SUPPORTED, @@ -162,9 +179,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_MAINTENANCE_IN_PROGRESS, RLM_ERR_USERPASS_TOKEN_INVALID, RLM_ERR_INVALID_SERVER_RESPONSE, - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR, - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR, - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR, + REALM_ERR_APP_SERVER_ERROR, RLM_ERR_CALLBACK, RLM_ERR_UNKNOWN; diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt index b73757ac10..4ab0a46515 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/NativePointer.kt @@ -43,3 +43,14 @@ public interface NativePointer { */ public fun isReleased(): Boolean } + +/** + * Deletes the underlying pointer after executing the lambda block. + */ +fun NativePointer.use( + block: (NativePointer) -> R, +): R = try { + block(this) +} finally { + release() +} 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 f553d15266..1cd6c153c9 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 @@ -27,8 +27,6 @@ import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.coroutines.CoroutineDispatcher @@ -70,6 +68,7 @@ interface RealmQueryT : CapiT interface RealmCallbackTokenT : CapiT interface RealmNotificationTokenT : CapiT interface RealmChangesT : CapiT +interface RealmSchedulerT : CapiT // Public type aliases binding to internal verbose type safe type definitions. This should allow us // to easily change implementation details later on. @@ -88,6 +87,7 @@ typealias RealmQueryPointer = NativePointer typealias RealmCallbackTokenPointer = NativePointer typealias RealmNotificationTokenPointer = NativePointer typealias RealmChangesPointer = NativePointer +typealias RealmSchedulerPointer = NativePointer // Sync types // Pure marker interfaces corresponding to the C-API realm_x_t struct types @@ -127,8 +127,6 @@ typealias RealmMutableSubscriptionSetPointer = NativePointer + fun realm_open(config: RealmConfigurationPointer, scheduler: RealmSchedulerPointer): Pair // Opening a Realm asynchronously. Only supported for synchronized realms. fun realm_open_synchronized(config: RealmConfigurationPointer): RealmAsyncOpenTaskPointer @@ -623,8 +620,7 @@ expect object RealmInterop { fun realm_sync_session_resume(syncSession: RealmSyncSessionPointer) fun realm_sync_session_handle_error_for_testing( syncSession: RealmSyncSessionPointer, - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + error: ErrorCode, errorMessage: String, isFatal: Boolean ) @@ -715,6 +711,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/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 741e419ec0..b7984a1d84 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 @@ -18,53 +18,11 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -/** - * Wrapper for C-API `realm_sync_errno_client`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3214 - */ -expect enum class ProtocolClientErrorCode : CodeDescription { - RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED, - RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE, - RLM_SYNC_ERR_CLIENT_BAD_SYNTAX, - RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED, - RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT, - RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER, - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT, - RLM_SYNC_ERR_CLIENT_BAD_PROGRESS, - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX, - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE, - RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT, - RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION, - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET, - RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT, - RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE, - RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION, - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION, - RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED, - RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT, - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT, - RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT, - RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT, - RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP, - RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER, - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER, - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER, - RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH, - RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE, - RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE, - RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED, - RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE; - - companion object { - internal fun of(nativeValue: Int): ProtocolClientErrorCode? - } -} - /** * Wrapper for C-API `realm_sync_errno_connection`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L2942 + * See https://github.com/realm/realm-core/blob/master/src/realm/error_codes.h#L209 */ -expect enum class ProtocolConnectionErrorCode : CodeDescription { +expect enum class SyncConnectionErrorCode : CodeDescription { RLM_SYNC_ERR_CONNECTION_CONNECTION_CLOSED, RLM_SYNC_ERR_CONNECTION_OTHER_ERROR, RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE, @@ -82,15 +40,15 @@ expect enum class ProtocolConnectionErrorCode : CodeDescription { RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS; companion object { - internal fun of(nativeValue: Int): ProtocolConnectionErrorCode? + internal fun of(nativeValue: Int): SyncConnectionErrorCode? } } /** * Wrapper for C-API `realm_sync_errno_session`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L2960 + * See https://github.com/realm/realm-core/blob/master/src/realm/error_codes.h#L228 */ -expect enum class ProtocolSessionErrorCode : CodeDescription { +expect enum class SyncSessionErrorCode : CodeDescription { RLM_SYNC_ERR_SESSION_SESSION_CLOSED, RLM_SYNC_ERR_SESSION_OTHER_SESSION_ERROR, RLM_SYNC_ERR_SESSION_TOKEN_EXPIRED, @@ -127,6 +85,42 @@ expect enum class ProtocolSessionErrorCode : CodeDescription { RLM_SYNC_ERR_SESSION_REVERT_TO_PBS; companion object { - internal fun of(nativeValue: Int): ProtocolSessionErrorCode? + internal fun of(nativeValue: Int): SyncSessionErrorCode? + } +} + +/** + * Wrapper for C-API `realm_web_socket_errno`. + * See https://github.com/realm/realm-core/blob/master/src/realm/error_codes.h#L266 + */ +expect enum class WebsocketErrorCode : CodeDescription { + RLM_ERR_WEBSOCKET_OK, + RLM_ERR_WEBSOCKET_GOINGAWAY, + RLM_ERR_WEBSOCKET_PROTOCOLERROR, + RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA, + RLM_ERR_WEBSOCKET_RESERVED, + RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED, + RLM_ERR_WEBSOCKET_ABNORMALCLOSURE, + RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA, + RLM_ERR_WEBSOCKET_POLICYVIOLATION, + RLM_ERR_WEBSOCKET_MESSAGETOOBIG, + RLM_ERR_WEBSOCKET_INAVALIDEXTENSION, + RLM_ERR_WEBSOCKET_INTERNALSERVERERROR, + RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED, + RLM_ERR_WEBSOCKET_UNAUTHORIZED, + RLM_ERR_WEBSOCKET_FORBIDDEN, + RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY, + RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD, + RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW, + RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH, + RLM_ERR_WEBSOCKET_RESOLVE_FAILED, + RLM_ERR_WEBSOCKET_CONNECTION_FAILED, + RLM_ERR_WEBSOCKET_READ_ERROR, + RLM_ERR_WEBSOCKET_WRITE_ERROR, + RLM_ERR_WEBSOCKET_RETRY_ERROR, + RLM_ERR_WEBSOCKET_FATAL_ERROR; + + companion object { + internal fun of(nativeValue: Int): WebsocketErrorCode? } } diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt index 6cecfdd5f2..dfe69d2ea3 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt @@ -16,13 +16,14 @@ package io.realm.kotlin.internal.interop.sync +import io.realm.kotlin.internal.interop.CoreError + /** * Wrapper for C-API `realm_sync_error`. * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3321 */ data class SyncError constructor( - val errorCode: SyncErrorCode, - val detailedMessage: String?, + val errorCode: CoreError, val originalFilePath: String?, val recoveryFilePath: String?, val isFatal: Boolean, @@ -34,10 +35,9 @@ data class SyncError constructor( // where we receive an error code rather than a full SyncErrorCode, wrapping the code // simplifies the error handling logic. constructor( - errorCode: SyncErrorCode + error: CoreError ) : this( - errorCode = errorCode, - detailedMessage = null, + errorCode = error, originalFilePath = null, recoveryFilePath = null, isFatal = false, @@ -48,10 +48,9 @@ data class SyncError constructor( // Constructor used by JNI so we avoid creating too many objects on the JNI side. constructor( - category: Int, + categoryFlags: Int, value: Int, message: String, - detailedMessage: String?, originalFilePath: String?, recoveryFilePath: String?, isFatal: Boolean, @@ -59,8 +58,7 @@ data class SyncError constructor( isClientResetRequested: Boolean, compensatingWrites: Array ) : this( - SyncErrorCode.newInstance(category, value, message), - detailedMessage, + CoreError(categoryFlags, value, message), originalFilePath, recoveryFilePath, isFatal, diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt deleted file mode 100644 index 93a9b78052..0000000000 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCode.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2022 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.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.UnknownCodeDescription - -/** - * Wrapper for C-API `realm_sync_error_code`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3306 - */ -data class SyncErrorCode internal constructor( - val category: CodeDescription, - val code: CodeDescription, - val message: String -) { - companion object { - fun newInstance( - categoryCode: Int, - errorCode: Int, - message: String - ): SyncErrorCode { - val category = SyncErrorCodeCategory.of(categoryCode) ?: UnknownCodeDescription(categoryCode) - - val code: CodeDescription = when (category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT -> ProtocolClientErrorCode.of(errorCode) - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION -> ProtocolConnectionErrorCode.of(errorCode) - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SESSION -> ProtocolSessionErrorCode.of(errorCode) - // SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM -> // no mapping available - // SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN -> // no mapping available - else -> null - } ?: UnknownCodeDescription(errorCode) - - return SyncErrorCode( - category, - code, - message - ) - } - } -} diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 049e3fbd1b..0000000000 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022 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.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription - -/** - * Wrapper for C-API `realm_sync_error_category`. - * See https://github.com/realm/realm-core/blob/master/src/realm.h#L3198 - */ -expect enum class SyncErrorCodeCategory : CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT, - RLM_SYNC_ERROR_CATEGORY_CONNECTION, - RLM_SYNC_ERROR_CATEGORY_SESSION, - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET, - RLM_SYNC_ERROR_CATEGORY_SYSTEM, - RLM_SYNC_ERROR_CATEGORY_UNKNOWN; - - companion object { - internal fun of(nativeValue: Int): SyncErrorCodeCategory? - } -} diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index 423f657111..5319d0bc90 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -31,7 +31,8 @@ actual enum class ErrorCategory( RLM_ERR_CAT_SERVICE_ERROR("Service", realm_error_category_e.RLM_ERR_CAT_SERVICE_ERROR), RLM_ERR_CAT_HTTP_ERROR("Http", realm_error_category_e.RLM_ERR_CAT_HTTP_ERROR), RLM_ERR_CAT_CUSTOM_ERROR("Custom", realm_error_category_e.RLM_ERR_CAT_CUSTOM_ERROR), - RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category_e.RLM_ERR_CAT_WEBSOCKET_ERROR); + RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category_e.RLM_ERR_CAT_WEBSOCKET_ERROR), + RLM_ERR_CAT_SYNC_ERROR("Sync", realm_error_category_e.RLM_ERR_CAT_SYNC_ERROR); actual companion object { internal actual fun of(nativeValue: Int): ErrorCategory? = 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 935354e9cb..28bdbb8cb0 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 @@ -47,6 +47,23 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_SCHEMA_VERSION_MISMATCH("SchemaVersionMismatch", realm_errno_e.RLM_ERR_SCHEMA_VERSION_MISMATCH), RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE("NoSubscriptionForWrite", realm_errno_e.RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE), RLM_ERR_OPERATION_ABORTED("OperationAborted", realm_errno_e.RLM_ERR_OPERATION_ABORTED), + RLM_ERR_AUTO_CLIENT_RESET_FAILED("AutoClientResetFailed", realm_errno_e.RLM_ERR_AUTO_CLIENT_RESET_FAILED), + RLM_ERR_BAD_SYNC_PARTITION_VALUE("BadSyncPartitionValue", realm_errno_e.RLM_ERR_BAD_SYNC_PARTITION_VALUE), + RLM_ERR_CONNECTION_CLOSED("ConnectionClosed", realm_errno_e.RLM_ERR_CONNECTION_CLOSED), + RLM_ERR_INVALID_SUBSCRIPTION_QUERY("InvalidSubscriptionQuery", realm_errno_e.RLM_ERR_INVALID_SUBSCRIPTION_QUERY), + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno_e.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), + RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno_e.RLM_ERR_SYNC_COMPENSATING_WRITE), + RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno_e.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_CONNECT_TIMEOUT("SyncConnectTimeout", realm_errno_e.RLM_ERR_SYNC_CONNECT_TIMEOUT), + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno_e.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), + RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno_e.RLM_ERR_SYNC_PERMISSION_DENIED), + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno_e.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED("SyncProtocolNegotiationFailed", realm_errno_e.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED), + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED("SyncServerPermissionsChanged", realm_errno_e.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED), + RLM_ERR_SYNC_USER_MISMATCH("SyncUserMismatch", realm_errno_e.RLM_ERR_SYNC_USER_MISMATCH), + RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno_e.RLM_ERR_TLS_HANDSHAKE_FAILED), + 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_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), @@ -118,7 +135,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MONGODB_ERROR("MongodbError", realm_errno_e.RLM_ERR_MONGODB_ERROR), RLM_ERR_ARGUMENTS_NOT_ALLOWED("ArgumentsNotAllowed", realm_errno_e.RLM_ERR_ARGUMENTS_NOT_ALLOWED), RLM_ERR_FUNCTION_EXECUTION_ERROR("FunctionExecutionError", realm_errno_e.RLM_ERR_FUNCTION_EXECUTION_ERROR), - RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno_e.RLM_ERR_NO_MATCHING_RULE), + RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno_e.RLM_ERR_NO_MATCHING_RULE_FOUND), RLM_ERR_INTERNAL_SERVER_ERROR("InternalServerError", realm_errno_e.RLM_ERR_INTERNAL_SERVER_ERROR), RLM_ERR_AUTH_PROVIDER_NOT_FOUND("AuthProviderNotFound", realm_errno_e.RLM_ERR_AUTH_PROVIDER_NOT_FOUND), RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS("AuthProviderAlreadyExists", realm_errno_e.RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS), @@ -159,9 +176,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno_e.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno_e.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno_e.RLM_ERR_INVALID_SERVER_RESPONSE), - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno_e.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), + REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno_e.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno_e.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno_e.RLM_ERR_UNKNOWN); 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 3ef0c015d4..c2b2a07d8f 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 @@ -27,13 +27,10 @@ import io.realm.kotlin.internal.interop.sync.JVMSyncSessionTransferCompletionCal import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch import org.mongodb.kbson.ObjectId @@ -172,6 +169,10 @@ actual object RealmInterop { realmc.realm_config_set_migration_function(config.cptr(), callback) } + actual fun realm_config_set_automatic_backlink_handling(config: RealmConfigurationPointer, enabled: Boolean) { + realmc.realm_config_set_automatic_backlink_handling(config.cptr(), enabled) + } + actual fun realm_config_set_data_initialization_function(config: RealmConfigurationPointer, callback: DataInitializationCallback) { realmc.realm_config_set_data_initialization_function(config.cptr(), callback) } @@ -180,7 +181,16 @@ actual object RealmInterop { realmc.realm_config_set_in_memory(config.cptr(), inMemory) } - actual fun realm_open(config: RealmConfigurationPointer, dispatcher: CoroutineDispatcher?): Pair { + actual fun realm_create_scheduler(): RealmSchedulerPointer = + LongPointerWrapper(realmc.realm_create_generic_scheduler()) + + actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer = + LongPointerWrapper(realmc.realm_create_scheduler(JVMScheduler(dispatcher))) + + actual fun realm_open( + config: RealmConfigurationPointer, + scheduler: RealmSchedulerPointer, + ): Pair { // Configure callback to track if the file was created as part of opening var fileCreated = false val callback = DataInitializationCallback { @@ -188,14 +198,9 @@ actual object RealmInterop { } realm_config_set_data_initialization_function(config, callback) - // create a custom Scheduler for JVM if a Coroutine Dispatcher is provided other wise - // pass null to use the generic one - val realmPtr = LongPointerWrapper( - realmc.open_realm_with_scheduler( - (config as LongPointerWrapper).ptr, - if (dispatcher != null) JVMScheduler(dispatcher) else null - ) - ) + realmc.realm_config_set_scheduler(config.cptr(), scheduler.cptr()) + val realmPtr = LongPointerWrapper(realmc.realm_open(config.cptr())) + // Ensure that we can read version information, etc. realm_begin_read(realmPtr) return Pair(realmPtr, fileCreated) @@ -1336,15 +1341,13 @@ actual object RealmInterop { actual fun realm_sync_session_handle_error_for_testing( syncSession: RealmSyncSessionPointer, - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + error: ErrorCode, errorMessage: String, isFatal: Boolean ) { realmc.realm_sync_session_handle_error_for_testing( syncSession.cptr(), - errorCode.nativeValue, - category.nativeValue, + error.nativeValue, errorMessage, isFatal ) @@ -1392,12 +1395,6 @@ actual object RealmInterop { baseUrl?.let { realmc.realm_app_config_set_base_url(config, it) } // Sync Connection Parameters - connectionParams.localAppName?.let { appName -> - realmc.realm_app_config_set_local_app_name(config, appName) - } - connectionParams.localAppVersion?.let { appVersion -> - realmc.realm_app_config_set_local_app_name(config, appVersion) - } realmc.realm_app_config_set_sdk(config, connectionParams.sdkName) realmc.realm_app_config_set_sdk_version(config, connectionParams.sdkVersion) realmc.realm_app_config_set_platform_version(config, connectionParams.platformVersion) @@ -1564,6 +1561,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 @@ -2093,13 +2101,8 @@ private class JVMScheduler(dispatcher: CoroutineDispatcher) { val scope: CoroutineScope = CoroutineScope(dispatcher) fun notifyCore(schedulerPointer: Long) { - val function: suspend CoroutineScope.() -> Unit = { + scope.launch { realmc.invoke_core_notify_callback(schedulerPointer) } - scope.launch( - scope.coroutineContext, - CoroutineStart.DEFAULT, - function - ) } } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt index d06b775d09..8c3cf2c981 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/JVMSyncSessionTransferCompletionCallback.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.internal.interop.sync +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.SyncSessionTransferCompletionCallback // Interface used internally as a bridge between Kotlin (JVM) and JNI. @@ -28,7 +29,7 @@ internal class JVMSyncSessionTransferCompletionCallback( fun onSuccess() { callback.invoke(null) } - fun onError(category: Int, value: Int, message: String) { - callback.invoke(SyncErrorCode.newInstance(category, value, message)) + fun onError(categoryFlags: Int, value: Int, message: String) { + callback.invoke(CoreError(categoryFlags, value, message)) } } 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 bfc9f20ec5..8ba8028fa8 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 @@ -17,55 +17,11 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.realm_sync_errno_client_e import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e +import io.realm.kotlin.internal.interop.realm_web_socket_errno_e -actual enum class ProtocolClientErrorCode( - override val description: String, - override val nativeValue: Int -) : CodeDescription { - RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED("ConnectionClosed", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED), - RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE), - RLM_SYNC_ERR_CLIENT_BAD_SYNTAX("BadSyntax", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SYNTAX), - RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER("BadMessageOrder", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT("BadClientFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_PROGRESS("BadProgress", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_PROGRESS), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX("BadChangesetHeaderSyntax", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE("BadChangesetSize", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE), - RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT("BadOriginFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION("BadServerVersion", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET("BadChangeset", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET), - RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT("BadRequestIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE("BadErrorCode", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE), - RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION("BadCompression", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION("BadClientVersion", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION), - RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED("SslServerCertRejected", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED), - RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT("PongTimeout", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT("BadClientFileIdentSalt", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT), - RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT("BadFileIdent", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT("ConnectTimeout", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP("BadTimestamp", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP), - RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER("BadProtocolFromServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER("ClientTooOldForServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER("ClientTooNewForServer", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH("ProtocolMismatch", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH), - RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE("BadStateMessage", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE), - RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE("MissingProtocolFeature", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE), - RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED("HttpTunnelFailed", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED), - RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE("AutoClientResetFailure", realm_sync_errno_client_e.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE); - - actual companion object { - internal actual fun of(nativeValue: Int): ProtocolClientErrorCode? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} - -actual enum class ProtocolConnectionErrorCode( +actual enum class SyncConnectionErrorCode( override val description: String, override val nativeValue: Int ) : CodeDescription { @@ -74,7 +30,7 @@ actual enum class ProtocolConnectionErrorCode( RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE), RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX("BadSyntax", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX), RLM_SYNC_ERR_CONNECTION_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION("WrongProtocolVersion", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION), + RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION("WrongBadSyncPartitionValueProtocolVersion", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION), RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT), RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT("ReuseOfSessionIdent", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT), RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION("BoundInOtherSession", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION), @@ -86,14 +42,14 @@ actual enum class ProtocolConnectionErrorCode( RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS("SwitchToPbs", realm_sync_errno_connection_e.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS); actual companion object { - internal actual fun of(nativeValue: Int): ProtocolConnectionErrorCode? = + internal actual fun of(nativeValue: Int): SyncConnectionErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } } } -actual enum class ProtocolSessionErrorCode( +actual enum class SyncSessionErrorCode( override val description: String, override val nativeValue: Int ) : CodeDescription { @@ -133,7 +89,47 @@ actual enum class ProtocolSessionErrorCode( RLM_SYNC_ERR_SESSION_REVERT_TO_PBS("RevertToPartitionBasedSync", realm_sync_errno_session_e.RLM_SYNC_ERR_SESSION_REVERT_TO_PBS); actual companion object { - internal actual fun of(nativeValue: Int): ProtocolSessionErrorCode? = + internal actual fun of(nativeValue: Int): SyncSessionErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + +actual enum class WebsocketErrorCode( + override val description: String, + override val nativeValue: Int +) : CodeDescription { + RLM_ERR_WEBSOCKET_OK("Ok", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_OK), + RLM_ERR_WEBSOCKET_GOINGAWAY("GoingAway", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_GOINGAWAY), + RLM_ERR_WEBSOCKET_PROTOCOLERROR("ProtocolError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_PROTOCOLERROR), + RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA("UnsupportedData", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA), + RLM_ERR_WEBSOCKET_RESERVED("Reserved", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RESERVED), + RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED("NoStatusReceived", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED), + RLM_ERR_WEBSOCKET_ABNORMALCLOSURE("AbnormalClosure", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_ABNORMALCLOSURE), + RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA("InvalidPayloadData", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA), + RLM_ERR_WEBSOCKET_POLICYVIOLATION("PolicyViolation", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_POLICYVIOLATION), + RLM_ERR_WEBSOCKET_MESSAGETOOBIG("MessageToBig", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_MESSAGETOOBIG), + RLM_ERR_WEBSOCKET_INAVALIDEXTENSION("InvalidExtension", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INAVALIDEXTENSION), + RLM_ERR_WEBSOCKET_INTERNALSERVERERROR("InternalServerError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_INTERNALSERVERERROR), + RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED("TlsHandshakeFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED), + + RLM_ERR_WEBSOCKET_UNAUTHORIZED("Unauthorized", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_UNAUTHORIZED), + RLM_ERR_WEBSOCKET_FORBIDDEN("Forbidden", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_FORBIDDEN), + RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY("MovedPermanently", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY), + RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD("ClientTooOld", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD), + RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW("ClientTooNew", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW), + RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH("ProtocolMismatch", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH), + + RLM_ERR_WEBSOCKET_RESOLVE_FAILED("ResolveFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RESOLVE_FAILED), + RLM_ERR_WEBSOCKET_CONNECTION_FAILED("ConnectionFailed", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_CONNECTION_FAILED), + RLM_ERR_WEBSOCKET_READ_ERROR("ReadError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_READ_ERROR), + RLM_ERR_WEBSOCKET_WRITE_ERROR("WriteError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_WRITE_ERROR), + RLM_ERR_WEBSOCKET_RETRY_ERROR("RetryError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_RETRY_ERROR), + RLM_ERR_WEBSOCKET_FATAL_ERROR("FatalError", realm_web_socket_errno_e.RLM_ERR_WEBSOCKET_FATAL_ERROR); + + actual companion object { + internal actual fun of(nativeValue: Int): WebsocketErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 6c66bfbd38..0000000000 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2022 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.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import io.realm.kotlin.internal.interop.realm_sync_error_category_e - -actual enum class SyncErrorCodeCategory(override val description: String, override val nativeValue: Int) : - CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT("Client", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_CLIENT), - RLM_SYNC_ERROR_CATEGORY_CONNECTION("Connection", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_CONNECTION), - RLM_SYNC_ERROR_CATEGORY_SESSION("Session", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_SESSION), - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET("Websocket", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_WEBSOCKET), - RLM_SYNC_ERROR_CATEGORY_SYSTEM("System", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_SYSTEM), - RLM_SYNC_ERROR_CATEGORY_UNKNOWN("Unknown", realm_sync_error_category_e.RLM_SYNC_ERROR_CATEGORY_UNKNOWN); - - actual companion object { - internal actual fun of(nativeValue: Int): SyncErrorCodeCategory? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index d5f34c6379..0c92edfbee 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -11,7 +11,7 @@ headerFilter = realm.h realm/error_codes.h // libraryPaths.macos_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ // libraryPaths.ios_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ linkerOpts = -lcompression -lz -framework Foundation -framework CoreFoundation -framework Security -strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session +strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno --- #include #include diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt index a792b233d7..ab2a7bfad5 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCategory.kt @@ -35,7 +35,8 @@ actual enum class ErrorCategory( RLM_ERR_CAT_SERVICE_ERROR("Service", realm_error_category.RLM_ERR_CAT_SERVICE_ERROR.value.toInt()), RLM_ERR_CAT_HTTP_ERROR("Http", realm_error_category.RLM_ERR_CAT_HTTP_ERROR.value.toInt()), RLM_ERR_CAT_CUSTOM_ERROR("Custom", realm_error_category.RLM_ERR_CAT_CUSTOM_ERROR.value.toInt()), - RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category.RLM_ERR_CAT_WEBSOCKET_ERROR.value.toInt()); + RLM_ERR_CAT_WEBSOCKET_ERROR("Websocket", realm_error_category.RLM_ERR_CAT_WEBSOCKET_ERROR.value.toInt()), + RLM_ERR_CAT_SYNC_ERROR("Sync", realm_error_category.RLM_ERR_CAT_SYNC_ERROR.value.toInt()); actual companion object { 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 70987acd03..aadd8db464 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 @@ -20,7 +20,7 @@ import realm_wrapper.realm_errno actual enum class ErrorCode( override val description: String, - error: realm_errno + private val nativeError: realm_errno ) : CodeDescription { RLM_ERR_NONE("None", realm_errno.RLM_ERR_NONE), RLM_ERR_RUNTIME("Runtime", realm_errno.RLM_ERR_RUNTIME), @@ -51,6 +51,23 @@ actual enum class ErrorCode( RLM_ERR_SCHEMA_VERSION_MISMATCH("SchemaVersionMismatch", realm_errno.RLM_ERR_SCHEMA_VERSION_MISMATCH), RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE("NoSubscriptionForWrite", realm_errno.RLM_ERR_NO_SUBSCRIPTION_FOR_WRITE), RLM_ERR_OPERATION_ABORTED("OperationAborted", realm_errno.RLM_ERR_OPERATION_ABORTED), + RLM_ERR_AUTO_CLIENT_RESET_FAILED("AutoClientResetFailed", realm_errno.RLM_ERR_AUTO_CLIENT_RESET_FAILED), + RLM_ERR_BAD_SYNC_PARTITION_VALUE("BadSyncPartitionValue", realm_errno.RLM_ERR_BAD_SYNC_PARTITION_VALUE), + RLM_ERR_CONNECTION_CLOSED("ConnectionClosed", realm_errno.RLM_ERR_CONNECTION_CLOSED), + RLM_ERR_INVALID_SUBSCRIPTION_QUERY("InvalidSubscriptionQuery", realm_errno.RLM_ERR_INVALID_SUBSCRIPTION_QUERY), + RLM_ERR_SYNC_CLIENT_RESET_REQUIRED("SyncClientResetRequired", realm_errno.RLM_ERR_SYNC_CLIENT_RESET_REQUIRED), + RLM_ERR_SYNC_COMPENSATING_WRITE("CompensatingWrite", realm_errno.RLM_ERR_SYNC_COMPENSATING_WRITE), + RLM_ERR_SYNC_CONNECT_FAILED("SyncConnectFailed", realm_errno.RLM_ERR_SYNC_CONNECT_FAILED), + RLM_ERR_SYNC_CONNECT_TIMEOUT("SyncConnectTimeout", realm_errno.RLM_ERR_SYNC_CONNECT_TIMEOUT), + RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE("SyncInvalidSchemaChange", realm_errno.RLM_ERR_SYNC_INVALID_SCHEMA_CHANGE), + RLM_ERR_SYNC_PERMISSION_DENIED("SyncPermissionDenied", realm_errno.RLM_ERR_SYNC_PERMISSION_DENIED), + RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED("SyncProtocolInvariantFailed", realm_errno.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED), + RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED("SyncProtocolNegotiationFailed", realm_errno.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED), + RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED("SyncServerPermissionsChanged", realm_errno.RLM_ERR_SYNC_SERVER_PERMISSIONS_CHANGED), + RLM_ERR_SYNC_USER_MISMATCH("SyncUserMismatch", realm_errno.RLM_ERR_SYNC_USER_MISMATCH), + RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno.RLM_ERR_TLS_HANDSHAKE_FAILED), + 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_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), @@ -122,7 +139,7 @@ actual enum class ErrorCode( RLM_ERR_MONGODB_ERROR("MongodbError", realm_errno.RLM_ERR_MONGODB_ERROR), RLM_ERR_ARGUMENTS_NOT_ALLOWED("ArgumentsNotAllowed", realm_errno.RLM_ERR_ARGUMENTS_NOT_ALLOWED), RLM_ERR_FUNCTION_EXECUTION_ERROR("FunctionExecutionError", realm_errno.RLM_ERR_FUNCTION_EXECUTION_ERROR), - RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno.RLM_ERR_NO_MATCHING_RULE), + RLM_ERR_NO_MATCHING_RULE("NoMatchingRule", realm_errno.RLM_ERR_NO_MATCHING_RULE_FOUND), RLM_ERR_INTERNAL_SERVER_ERROR("InternalServerError", realm_errno.RLM_ERR_INTERNAL_SERVER_ERROR), RLM_ERR_AUTH_PROVIDER_NOT_FOUND("AuthProviderNotFound", realm_errno.RLM_ERR_AUTH_PROVIDER_NOT_FOUND), RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS("AuthProviderAlreadyExists", realm_errno.RLM_ERR_AUTH_PROVIDER_ALREADY_EXISTS), @@ -163,13 +180,13 @@ actual enum class ErrorCode( RLM_ERR_MAINTENANCE_IN_PROGRESS("MaintenanceInProgress", realm_errno.RLM_ERR_MAINTENANCE_IN_PROGRESS), RLM_ERR_USERPASS_TOKEN_INVALID("UserpassTokenInvalid", realm_errno.RLM_ERR_USERPASS_TOKEN_INVALID), RLM_ERR_INVALID_SERVER_RESPONSE("InvalidServerResponse", realm_errno.RLM_ERR_INVALID_SERVER_RESPONSE), - RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR("ResolveFailedError", realm_errno.RLM_ERR_WEBSOCKET_RESOLVE_FAILED_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR("ConnectionClosedClientError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_CLIENT_ERROR), - RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR("ConnectionClosedServerError", realm_errno.RLM_ERR_WEBSOCKET_CONNECTION_CLOSED_SERVER_ERROR), + REALM_ERR_APP_SERVER_ERROR("AppServerError", realm_errno.RLM_ERR_APP_SERVER_ERROR), RLM_ERR_CALLBACK("Callback", realm_errno.RLM_ERR_CALLBACK), RLM_ERR_UNKNOWN("Unknown", realm_errno.RLM_ERR_UNKNOWN); - override val nativeValue: Int = error.value.toInt() + override val nativeValue: Int = nativeError.value.toInt() + + val asNativeEnum: realm_errno = nativeError actual companion object { actual fun of(nativeValue: Int): ErrorCode? = 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 f569916772..46facf20d2 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 @@ -19,7 +19,6 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH -import io.realm.kotlin.internal.interop.RealmInterop.safeKString import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.AuthProvider @@ -31,11 +30,8 @@ import io.realm.kotlin.internal.interop.sync.CoreUserState import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.NetworkTransport import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.Response import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.interop.sync.SyncUserIdentity import kotlinx.atomicfu.AtomicBoolean @@ -80,7 +76,6 @@ import kotlinx.cinterop.usePinned import kotlinx.cinterop.value import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.ObjectId @@ -121,7 +116,6 @@ import realm_wrapper.realm_scheduler_t import realm_wrapper.realm_set_t import realm_wrapper.realm_string_t import realm_wrapper.realm_sync_client_metadata_mode -import realm_wrapper.realm_sync_error_code_t import realm_wrapper.realm_sync_session_resync_mode import realm_wrapper.realm_sync_session_state_e import realm_wrapper.realm_sync_session_stop_policy_e @@ -433,6 +427,15 @@ actual object RealmInterop { ) } + actual fun realm_config_set_automatic_backlink_handling( + config: RealmConfigurationPointer, + enabled: Boolean + ) { + realm_wrapper.realm_config_set_automatic_backlink_handling( + config.cptr(), + enabled, + ) + } actual fun realm_config_set_migration_function( config: RealmConfigurationPointer, callback: MigrationCallback @@ -494,11 +497,10 @@ actual object RealmInterop { ) } - actual fun realm_open(config: RealmConfigurationPointer, dispatcher: CoroutineDispatcher?): Pair { + actual fun realm_open(config: RealmConfigurationPointer, scheduler: RealmSchedulerPointer): Pair { val fileCreated = atomic(false) val callback = DataInitializationCallback { fileCreated.value = true - true } realm_wrapper.realm_config_set_data_initialization_function( config.cptr(), @@ -516,21 +518,75 @@ actual object RealmInterop { // val dispatcher = runBlocking { coroutineContext[CoroutineDispatcher.Key] } // but requires opting in for @ExperimentalStdlibApi, and have really gotten it to play // for default cases. - if (dispatcher != null) { - val scheduler = checkedPointerResult(createSingleThreadDispatcherScheduler(dispatcher)) - realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler) - } else { - // If there is no notification dispatcher use the default scheduler. - // Re-verify if this is actually needed when notification scheduler is fully in place. - val scheduler = checkedPointerResult(realm_wrapper.realm_scheduler_make_default()) - realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler) - } + realm_wrapper.realm_config_set_scheduler(config.cptr(), scheduler.cptr()) + val realmPtr = CPointerWrapper(realm_wrapper.realm_open(config.cptr())) // Ensure that we can read version information, etc. realm_begin_read(realmPtr) return Pair(realmPtr, fileCreated.value) } + actual fun realm_create_scheduler(): RealmSchedulerPointer { + // If there is no notification dispatcher use the default scheduler. + // Re-verify if this is actually needed when notification scheduler is fully in place. + val scheduler = checkedPointerResult(realm_wrapper.realm_scheduler_make_default()) + return CPointerWrapper(scheduler) + } + + actual fun realm_create_scheduler(dispatcher: CoroutineDispatcher): RealmSchedulerPointer { + printlntid("createSingleThreadDispatcherScheduler") + val scheduler = SingleThreadDispatcherScheduler(tid(), dispatcher) + + val capi_scheduler: CPointer = checkedPointerResult( + realm_wrapper.realm_scheduler_new( + // userdata: kotlinx.cinterop.CValuesRef<*>?, + scheduler.ref, + + // free: realm_wrapper.realm_free_userdata_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, + staticCFunction { userdata -> + printlntid("free") + userdata?.asStableRef()?.dispose() + }, + + // notify: realm_wrapper.realm_scheduler_notify_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, + staticCFunction { userdata -> + // Must be thread safe + val scheduler = + userdata!!.asStableRef().get() + printlntid("$scheduler notify") + try { + scheduler.notify() + } catch (e: Exception) { + // Should never happen, but is included for development to get some indicators + // on errors instead of silent crashes. + e.printStackTrace() + } + }, + + // is_on_thread: realm_wrapper.realm_scheduler_is_on_thread_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { userdata -> + // Must be thread safe + val scheduler = + userdata!!.asStableRef().get() + printlntid("is_on_thread[$scheduler] ${scheduler.threadId} " + tid()) + scheduler.threadId == tid() + }, + + // is_same_as: realm_wrapper.realm_scheduler_is_same_as_func_t? /* = kotlinx.cinterop.CPointer? */, kotlinx.cinterop.COpaquePointer? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { userdata, other -> + userdata == other + }, + + // can_deliver_notifications: realm_wrapper.realm_scheduler_can_deliver_notifications_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, + staticCFunction { _ -> true }, + ) + ) ?: error("Couldn't create scheduler") + + scheduler.set_scheduler(capi_scheduler) + + return CPointerWrapper(capi_scheduler) + } + actual fun realm_open_synchronized(config: RealmConfigurationPointer): RealmAsyncOpenTaskPointer { return CPointerWrapper(realm_wrapper.realm_open_synchronized(config.cptr())) } @@ -553,7 +609,7 @@ actual object RealmInterop { ) err.usercode_error?.let { disposeUserData(it) } } else { - realm_wrapper.realm_release(realm) + realm_release(realm) } safeUserData(userData).invoke(exception) } @@ -2382,10 +2438,10 @@ actual object RealmInterop { syncConfig.cptr(), staticCFunction { userData, syncSession, error -> val syncError: SyncError = error.useContents { - val code = SyncErrorCode.newInstance( - error_code.category.value.toInt(), - error_code.value, - error_code.message.safeKString() + val code = CoreError( + this.status.categories.toInt(), + this.status.error.value.toInt(), + this.status.message.safeKString() ) val userInfoMap = (0 until user_info_length.toInt()) @@ -2412,7 +2468,6 @@ actual object RealmInterop { SyncError( errorCode = code, - detailedMessage = detailed_message.safeKString(), originalFilePath = userInfoMap[c_original_file_path_key.safeKString()], recoveryFilePath = userInfoMap[c_recovery_file_path_key.safeKString()], isFatal = is_fatal, @@ -2489,6 +2544,8 @@ actual object RealmInterop { } catch (e: Throwable) { println(e.message) false + } finally { + realm_wrapper.realm_close(afterRealmPtr) } }, StableRef.create(afterHandler).asCPointer(), @@ -2518,7 +2575,7 @@ actual object RealmInterop { ) { realm_wrapper.realm_sync_session_wait_for_download_completion( syncSession.cptr(), - staticCFunction?, Unit> { userData, error -> + staticCFunction?, Unit> { userData, error -> handleCompletionCallback(userData, error) }, StableRef.create(callback).asCPointer(), @@ -2534,7 +2591,7 @@ actual object RealmInterop { ) { realm_wrapper.realm_sync_session_wait_for_upload_completion( syncSession.cptr(), - staticCFunction?, Unit> { userData, error -> + staticCFunction?, Unit> { userData, error -> handleCompletionCallback(userData, error) }, StableRef.create(callback).asCPointer(), @@ -2565,15 +2622,13 @@ actual object RealmInterop { actual fun realm_sync_session_handle_error_for_testing( syncSession: RealmSyncSessionPointer, - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + error: ErrorCode, errorMessage: String, isFatal: Boolean ) { realm_wrapper.realm_sync_session_handle_error_for_testing( syncSession.cptr(), - errorCode.nativeValue.toInt(), - category.nativeValue, + error.asNativeEnum, errorMessage, isFatal ) @@ -2627,14 +2682,14 @@ actual object RealmInterop { private fun handleCompletionCallback( userData: CPointer?, - error: CPointer? + error: CPointer? ) { val completionCallback = safeUserData(userData) if (error != null) { - val category = error.pointed.category.value.toInt() - val value: Int = error.pointed.value + val category = error.pointed.categories.toInt() + val value: Int = error.pointed.error.value.toInt() val message = error.pointed.message.safeKString() - completionCallback.invoke(SyncErrorCode.newInstance(category, value, message)) + completionCallback.invoke(CoreError(category, value, message)) } else { completionCallback.invoke(null) } @@ -2663,12 +2718,6 @@ actual object RealmInterop { baseUrl?.let { realm_wrapper.realm_app_config_set_base_url(appConfig, it) } // Sync Connection Parameters - connectionParams.localAppName?.let { appName -> - realm_wrapper.realm_app_config_set_local_app_name(appConfig, appName) - } - connectionParams.localAppVersion?.let { appVersion -> - realm_wrapper.realm_app_config_set_local_app_name(appConfig, appVersion) - } realm_wrapper.realm_app_config_set_sdk(appConfig, connectionParams.sdkName) realm_wrapper.realm_app_config_set_sdk_version(appConfig, connectionParams.sdkVersion) realm_wrapper.realm_app_config_set_platform_version(appConfig, connectionParams.platformVersion) @@ -2953,6 +3002,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()) } @@ -3241,61 +3301,6 @@ actual object RealmInterop { ?: throw NullPointerException(identifier?.let { "'$identifier' shouldn't be null." }) } - private fun createSingleThreadDispatcherScheduler( - dispatcher: CoroutineDispatcher - ): CPointer { - printlntid("createSingleThreadDispatcherScheduler") - val scheduler = SingleThreadDispatcherScheduler(tid(), dispatcher) - - val capi_scheduler: CPointer = checkedPointerResult( - realm_wrapper.realm_scheduler_new( - // userdata: kotlinx.cinterop.CValuesRef<*>?, - scheduler.ref, - - // free: realm_wrapper.realm_free_userdata_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, - staticCFunction { userdata -> - printlntid("free") - userdata?.asStableRef()?.dispose() - }, - - // notify: realm_wrapper.realm_scheduler_notify_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Unit>>? */, - staticCFunction { userdata -> - // Must be thread safe - val scheduler = - userdata!!.asStableRef().get() - printlntid("$scheduler notify") - try { - scheduler.notify() - } catch (e: Exception) { - // Should never happen, but is included for development to get some indicators - // on errors instead of silent crashes. - e.printStackTrace() - } - }, - - // is_on_thread: realm_wrapper.realm_scheduler_is_on_thread_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata -> - // Must be thread safe - val scheduler = - userdata!!.asStableRef().get() - printlntid("is_on_thread[$scheduler] ${scheduler.threadId} " + tid()) - scheduler.threadId == tid() - }, - - // is_same_as: realm_wrapper.realm_scheduler_is_same_as_func_t? /* = kotlinx.cinterop.CPointer? */, kotlinx.cinterop.COpaquePointer? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata, other -> - userdata == other - }, - - // can_deliver_notifications: realm_wrapper.realm_scheduler_can_deliver_notifications_func_t? /* = kotlinx.cinterop.CPointer? */) -> kotlin.Boolean>>? */, - staticCFunction { userdata -> true }, - ) - ) ?: error("Couldn't create scheduler") - scheduler.set_scheduler(capi_scheduler) - scheduler - return capi_scheduler - } - private fun handleAppCallback( userData: COpaquePointer?, error: CPointer?, @@ -3401,7 +3406,7 @@ actual object RealmInterop { } override fun notify() { - val function: suspend CoroutineScope.() -> Unit = { + scope.launch { try { printlntid("on dispatcher") realm_wrapper.realm_scheduler_perform_work(scheduler) @@ -3411,11 +3416,6 @@ actual object RealmInterop { e.printStackTrace() } } - scope.launch( - scope.coroutineContext, - CoroutineStart.DEFAULT, - function - ) } } } 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 6504d9b0ea..20817a0ae5 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 @@ -16,57 +16,11 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription -import realm_wrapper.realm_sync_errno_client import realm_wrapper.realm_sync_errno_connection import realm_wrapper.realm_sync_errno_session +import realm_wrapper.realm_web_socket_errno -actual enum class ProtocolClientErrorCode( - override val description: String, - errorCode: realm_sync_errno_client -) : CodeDescription { - RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED("ConnectionClosed", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CONNECTION_CLOSED), - RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE("UnknownMessage", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_UNKNOWN_MESSAGE), - RLM_SYNC_ERR_CLIENT_BAD_SYNTAX("BadSyntax", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SYNTAX), - RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED("LimitsExceeded", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_LIMITS_EXCEEDED), - RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT("BadSessionIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SESSION_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER("BadMessageOrder", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_MESSAGE_ORDER), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT("BadClientFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_PROGRESS("BadProgress", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_PROGRESS), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX("BadChangesetHeaderSyntax", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_HEADER_SYNTAX), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE("BadChangesetSize", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET_SIZE), - RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT("BadOriginFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_ORIGIN_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION("BadServerVersion", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_SERVER_VERSION), - RLM_SYNC_ERR_CLIENT_BAD_CHANGESET("BadChangeset", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CHANGESET), - RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT("BadRequestIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_REQUEST_IDENT), - RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE("BadErrorCode", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_ERROR_CODE), - RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION("BadCompression", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_COMPRESSION), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION("BadClientVersion", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_VERSION), - RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED("SslServerCertRejected", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_SSL_SERVER_CERT_REJECTED), - RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT("PongTimeout", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_PONG_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT("BadClientFileIdentSalt", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_CLIENT_FILE_IDENT_SALT), - RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT("BadFileIdent", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_FILE_IDENT), - RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT("ConnectTimeout", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CONNECT_TIMEOUT), - RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP("BadTimestamp", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_TIMESTAMP), - RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER("BadProtocolFromServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_PROTOCOL_FROM_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER("ClientTooOldForServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_OLD_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER("ClientTooNewForServer", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_CLIENT_TOO_NEW_FOR_SERVER), - RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH("ProtocolMismatch", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_PROTOCOL_MISMATCH), - RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE("BadStateMessage", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_BAD_STATE_MESSAGE), - RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE("MissingProtocolFeature", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_MISSING_PROTOCOL_FEATURE), - RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED("HttpTunnelFailed", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_HTTP_TUNNEL_FAILED), - RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE("AutoClientResetFailure", realm_sync_errno_client.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE); - - override val nativeValue: Int = errorCode.value.toInt() - - actual companion object { - internal actual fun of(nativeValue: Int): ProtocolClientErrorCode? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} - -actual enum class ProtocolConnectionErrorCode( +actual enum class SyncConnectionErrorCode( override val description: String, errorCode: realm_sync_errno_connection ) : CodeDescription { @@ -89,14 +43,14 @@ actual enum class ProtocolConnectionErrorCode( override val nativeValue: Int = errorCode.value.toInt() actual companion object { - internal actual fun of(nativeValue: Int): ProtocolConnectionErrorCode? = + internal actual fun of(nativeValue: Int): SyncConnectionErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } } } -actual enum class ProtocolSessionErrorCode( +actual enum class SyncSessionErrorCode( override val description: String, errorCode: realm_sync_errno_session ) : CodeDescription { @@ -138,7 +92,47 @@ actual enum class ProtocolSessionErrorCode( override val nativeValue: Int = errorCode.value.toInt() actual companion object { - internal actual fun of(nativeValue: Int): ProtocolSessionErrorCode? = + internal actual fun of(nativeValue: Int): SyncSessionErrorCode? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} + +actual enum class WebsocketErrorCode( + override val description: String, + errorCode: realm_web_socket_errno, +) : CodeDescription { + RLM_ERR_WEBSOCKET_OK("Ok", realm_web_socket_errno.RLM_ERR_WEBSOCKET_OK), + RLM_ERR_WEBSOCKET_GOINGAWAY("GoingAway", realm_web_socket_errno.RLM_ERR_WEBSOCKET_GOINGAWAY), + RLM_ERR_WEBSOCKET_PROTOCOLERROR("ProtocolError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_PROTOCOLERROR), + RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA("UnsupportedData", realm_web_socket_errno.RLM_ERR_WEBSOCKET_UNSUPPORTEDDATA), + RLM_ERR_WEBSOCKET_RESERVED("Reserved", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RESERVED), + RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED("NoStatusReceived", realm_web_socket_errno.RLM_ERR_WEBSOCKET_NOSTATUSRECEIVED), + RLM_ERR_WEBSOCKET_ABNORMALCLOSURE("AbnormalClosure", realm_web_socket_errno.RLM_ERR_WEBSOCKET_ABNORMALCLOSURE), + RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA("InvalidPayloadData", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INVALIDPAYLOADDATA), + RLM_ERR_WEBSOCKET_POLICYVIOLATION("PolicyViolation", realm_web_socket_errno.RLM_ERR_WEBSOCKET_POLICYVIOLATION), + RLM_ERR_WEBSOCKET_MESSAGETOOBIG("MessageToBig", realm_web_socket_errno.RLM_ERR_WEBSOCKET_MESSAGETOOBIG), + RLM_ERR_WEBSOCKET_INAVALIDEXTENSION("InvalidExtension", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INAVALIDEXTENSION), + RLM_ERR_WEBSOCKET_INTERNALSERVERERROR("InternalServerError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_INTERNALSERVERERROR), + RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED("TlsHandshakeFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_TLSHANDSHAKEFAILED), + RLM_ERR_WEBSOCKET_UNAUTHORIZED("Unauthorized", realm_web_socket_errno.RLM_ERR_WEBSOCKET_UNAUTHORIZED), + RLM_ERR_WEBSOCKET_FORBIDDEN("Forbidden", realm_web_socket_errno.RLM_ERR_WEBSOCKET_FORBIDDEN), + RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY("MovedPermanently", realm_web_socket_errno.RLM_ERR_WEBSOCKET_MOVEDPERMANENTLY), + RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD("ClientTooOld", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CLIENT_TOO_OLD), + RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW("ClientTooNew", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CLIENT_TOO_NEW), + RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH("ProtocolMismatch", realm_web_socket_errno.RLM_ERR_WEBSOCKET_PROTOCOL_MISMATCH), + RLM_ERR_WEBSOCKET_RESOLVE_FAILED("ResolveFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RESOLVE_FAILED), + RLM_ERR_WEBSOCKET_CONNECTION_FAILED("ConnectionFailed", realm_web_socket_errno.RLM_ERR_WEBSOCKET_CONNECTION_FAILED), + RLM_ERR_WEBSOCKET_READ_ERROR("ReadError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_READ_ERROR), + RLM_ERR_WEBSOCKET_WRITE_ERROR("WriteError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_WRITE_ERROR), + RLM_ERR_WEBSOCKET_RETRY_ERROR("RetryError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_RETRY_ERROR), + RLM_ERR_WEBSOCKET_FATAL_ERROR("FatalError", realm_web_socket_errno.RLM_ERR_WEBSOCKET_FATAL_ERROR); + + override val nativeValue: Int = errorCode.value.toInt() + + actual companion object { + internal actual fun of(nativeValue: Int): WebsocketErrorCode? = values().firstOrNull { value -> value.nativeValue == nativeValue } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt deleted file mode 100644 index 80254655ff..0000000000 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/SyncErrorCodeCategory.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022 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.internal.interop.sync - -import io.realm.kotlin.internal.interop.CodeDescription -import realm_wrapper.realm_sync_error_category - -actual enum class SyncErrorCodeCategory( - override val description: String, - override val nativeValue: Int -) : CodeDescription { - RLM_SYNC_ERROR_CATEGORY_CLIENT("Client", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_CLIENT.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_CONNECTION("Connection", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_CONNECTION.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_SESSION("Session", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_SESSION.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_WEBSOCKET("Websocket", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_WEBSOCKET.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_SYSTEM("System", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_SYSTEM.value.toInt()), - RLM_SYNC_ERROR_CATEGORY_UNKNOWN("Unknown", realm_sync_error_category.RLM_SYNC_ERROR_CATEGORY_UNKNOWN.value.toInt()); - - actual companion object { - - internal actual fun of(nativeValue: Int): SyncErrorCodeCategory? = - values().firstOrNull { value -> - value.nativeValue == nativeValue - } - } -} diff --git a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt index 110d6c47c4..01fd6fec06 100644 --- a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt +++ b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt @@ -28,6 +28,7 @@ import io.realm.kotlin.internal.interop.SchemaMode import io.realm.kotlin.internal.interop.SchemaValidationMode import io.realm.kotlin.internal.interop.set import io.realm.kotlin.internal.interop.toKotlinString +import io.realm.kotlin.internal.interop.use import kotlinx.cinterop.BooleanVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.CPointerVarOf @@ -178,9 +179,12 @@ class CinteropTest { SchemaMode.RLM_SCHEMA_MODE_AUTOMATIC ) RealmInterop.realm_config_set_schema_version(nativeConfig, 1) - - val (realm, fileCreated) = RealmInterop.realm_open(nativeConfig) - assertEquals(1L, RealmInterop.realm_get_num_classes(realm)) + RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (realm, fileCreated) = RealmInterop.realm_open(nativeConfig, scheduler) + assertEquals(1L, RealmInterop.realm_get_num_classes(realm)) + RealmInterop.realm_close(realm) + } } } diff --git a/packages/external/core b/packages/external/core index f1e962cd44..c258e2681b 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit f1e962cd447f8b69f8f7cf46a188b1c6246923c5 +Subproject commit c258e2681bca5fb33bbd23c112493817b43bfa86 diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index da97942a8e..4c4480148f 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -17,7 +17,6 @@ import kotlin.text.toBoolean plugins { kotlin("jvm") - kotlin("kapt") `java-gradle-plugin` id("com.gradle.plugin-publish") version Versions.gradlePluginPublishPlugin id("realm-publisher") @@ -82,8 +81,8 @@ publishing { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Make version information available at runtime diff --git a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt index 00a2225112..500538896c 100644 --- a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt +++ b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmPlugin.kt @@ -27,10 +27,6 @@ import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import org.gradle.build.event.BuildEventsListenerRegistry -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import javax.inject.Inject @Suppress("unused") @@ -79,24 +75,11 @@ open class RealmPlugin : Plugin { // Stand alone Android projects have not initialized kotlin plugin when applying this, so // postpone dependency injection till after evaluation. project.afterEvaluate { - val kotlin: Any? = project.extensions.findByName("kotlin") // TODO AUTO-SETUP To ease configuration we could/should inject dependencies to our // library, but await better insight into when/what to inject and supply appropriate // opt-out options through our own extension? // Dependencies should probably be added by source set and not by target, as // kotlin.sourceSets.getByName("commonMain").dependencies (or "main" for Android), but - when (kotlin) { - is KotlinSingleTargetExtension<*> -> { - updateKotlinOption(kotlin.target) - } - is KotlinMultiplatformExtension -> { - kotlin.targets.all { target -> updateKotlinOption(target) } - } - else -> { - // TODO AUTO-SETUP Should we report errors? Probably an oversighted case - // TODO("Cannot 'realm-kotlin' library dependency to ${if (kotlin != null) kotlin::class.qualifiedName else "null"}") - } - } // Create the analytics during configuration because it needs access to the project // in order to gather project relevant information in afterEvaluate. Currently @@ -112,16 +95,4 @@ open class RealmPlugin : Plugin { } } } - - private fun updateKotlinOption(target: KotlinTarget) { - target.compilations.all { compilation -> - // Setup correct compiler options - // FIXME AUTO-SETUP Are these to dangerous to apply under the hood? - when (val options = compilation.kotlinOptions) { - is KotlinJvmOptions -> { - options.jvmTarget = "1.8" - } - } - } - } } diff --git a/packages/jni-swig-stub/build.gradle.kts b/packages/jni-swig-stub/build.gradle.kts index 2cd951be67..7662f9c2e7 100644 --- a/packages/jni-swig-stub/build.gradle.kts +++ b/packages/jni-swig-stub/build.gradle.kts @@ -32,11 +32,6 @@ java { } } -configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - tasks.create("realmWrapperJvm") { doLast { // If task is actually triggered (not up to date) then we should clean up the old stuff @@ -70,8 +65,8 @@ realmPublish { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } publishing { diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 9bf2bedd97..02880c1df5 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -301,7 +301,7 @@ return $jnicall; realm_flx_sync_mutable_subscription_set_t*, realm_flx_sync_subscription_desc_t*, realm_set_t*, realm_async_open_task_t*, realm_dictionary_t*, realm_sync_session_connection_state_notification_token_t*, - realm_dictionary_changes_t* }; + realm_dictionary_changes_t*, realm_scheduler_t* }; // For all functions returning a pointer or bool, check for null/false and throw an error if // realm_get_last_error returns true. diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index cba045bcd0..ffd4988f26 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -317,8 +317,8 @@ void invoke_core_notify_callback(int64_t scheduler) { realm_scheduler_perform_work(reinterpret_cast(scheduler)); } -realm_t *open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler) { - auto config = reinterpret_cast(config_ptr); +realm_scheduler_t* +realm_create_scheduler(jobject dispatchScheduler) { if (dispatchScheduler) { auto jvmScheduler = new CustomJVMScheduler(dispatchScheduler); auto scheduler = realm_scheduler_new( @@ -330,13 +330,9 @@ realm_t *open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler [](void *userdata) { return static_cast(userdata)->can_invoke(); } ); jvmScheduler->set_scheduler(scheduler); - realm_config_set_scheduler(config, scheduler); - } else { - // TODO refactor to use public C-API https://github.com/realm/realm-kotlin/issues/496 - auto scheduler = new realm_scheduler_t{realm::util::Scheduler::make_generic()}; - realm_config_set_scheduler(config, scheduler); + return scheduler; } - return realm_open(config); + throw std::runtime_error("Null dispatchScheduler"); } jobject convert_to_jvm_app_error(JNIEnv* env, const realm_app_error_t* error) { @@ -718,12 +714,11 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) static JavaMethod sync_error_constructor(jenv, JavaClassGlobalDef::sync_error(), "", - "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;)V"); + "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;)V"); - jint category = static_cast(error.error_code.category); - jint value = error.error_code.value; - jstring msg = to_jstring(jenv, error.error_code.message); - jstring detailed_msg = to_jstring(jenv, error.detailed_message); + jint category = static_cast(error.status.categories); + jint value = static_cast(error.status.error); + jstring msg = to_jstring(jenv, error.status.message); jstring joriginal_file_path = nullptr; jstring jrecovery_file_path = nullptr; jboolean is_fatal = error.is_fatal; @@ -797,7 +792,6 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) category, value, msg, - detailed_msg, joriginal_file_path, jrecovery_file_path, is_fatal, @@ -831,7 +825,7 @@ void sync_set_error_handler(realm_sync_config_t* sync_config, jobject error_hand }); } -void transfer_completion_callback(void* userdata, realm_sync_error_code_t* error) { +void transfer_completion_callback(void* userdata, realm_error_t* error) { auto env = get_env(true); static JavaMethod java_success_callback_method(env, JavaClassGlobalDef::sync_session_transfer_completion_callback(), @@ -842,8 +836,8 @@ void transfer_completion_callback(void* userdata, realm_sync_error_code_t* error "onError", "(IILjava/lang/String;)V"); if (error) { - jint category = static_cast(error->category); - jint value = error->value; + jint category = static_cast(error->categories); + jint value = error->error; jstring msg = to_jstring(env, error->message); env->CallVoidMethod(static_cast(userdata), java_error_callback_method, category, value, msg); } else { @@ -916,7 +910,7 @@ after_client_reset(void* userdata, realm_t* before_realm, realm_t* after_realm_ptr = realm_from_thread_safe_reference(after_realm, &scheduler); auto after_pointer = wrap_pointer(env, reinterpret_cast(after_realm_ptr), false); env->CallVoidMethod(static_cast(userdata), java_after_callback_function, before_pointer, after_pointer, did_recover); - + realm_close(after_realm_ptr); if (env->ExceptionCheck()) { std::string exception_message = get_exception_message(env); std::string message_template = "An error has occurred in the 'onAfter' callback: "; @@ -1058,3 +1052,8 @@ realm_sync_thread_error(realm_userdata_t userdata, const char* error) { env->CallVoidMethod(static_cast(userdata), java_callback_method, to_jstring(env, msg)); jni_check_exception(env); } + +realm_scheduler_t* +realm_create_generic_scheduler() { + return new realm_scheduler_t { realm::util::Scheduler::make_dummy() }; +} diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 0854631d72..210202a6c9 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -47,8 +47,8 @@ realm_network_transport_new(jobject network_transport); void set_log_callback(jint log_level, jobject log_callback); -realm_t* -open_realm_with_scheduler(int64_t config_ptr, jobject dispatchScheduler); +realm_scheduler_t* +realm_create_scheduler(jobject dispatchScheduler); bool realm_should_compact_callback(void* userdata, uint64_t total_bytes, uint64_t used_bytes); @@ -72,7 +72,7 @@ void complete_http_request(void* request_context, jobject j_response); void -transfer_completion_callback(void* userdata, realm_sync_error_code_t* error); +transfer_completion_callback(void* userdata, realm_error_t* error); void realm_subscriptionset_changed_callback(void* userdata, @@ -134,4 +134,7 @@ realm_sync_thread_destroyed(realm_userdata_t userdata); void realm_sync_thread_error(realm_userdata_t userdata, const char* error); +realm_scheduler_t* +realm_create_generic_scheduler(); + #endif //TEST_REALM_API_HELPERS_H diff --git a/packages/library-base/build.gradle.kts b/packages/library-base/build.gradle.kts index 6c9d007386..d5ca3b5ffc 100644 --- a/packages/library-base/build.gradle.kts +++ b/packages/library-base/build.gradle.kts @@ -161,12 +161,9 @@ android { consumerProguardFiles("proguard-rules-consumer-common.pro") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Skip BuildConfig generation as it overlaps with io.realm.kotlin.BuildConfig from realm-java buildFeatures { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt index 5ac3626386..ca26a0f362 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Realm.kt @@ -19,6 +19,7 @@ import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.RealmImpl import io.realm.kotlin.internal.interop.Constants import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.isWindows import io.realm.kotlin.notifications.RealmChange @@ -118,10 +119,19 @@ public interface Realm : TypedRealm { } if (!fileExists(configuration.path)) return false val config = (configuration as InternalConfiguration) - val (dbPointer, _) = RealmInterop.realm_open(config.createNativeConfiguration()) - return RealmInterop.realm_compact(dbPointer).also { - RealmInterop.realm_close(dbPointer) - } + + return RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (dbPointer, _) = RealmInterop.realm_open( + config = config.createNativeConfiguration(), + scheduler = scheduler + ) + try { + RealmInterop.realm_compact(dbPointer) + } finally { + RealmInterop.realm_close(dbPointer) + } + } } } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt index 67a854413a..4b46f62364 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.util.CoroutineDispatcherFactory import io.realm.kotlin.log.RealmLog import io.realm.kotlin.log.RealmLogger +import io.realm.kotlin.migration.AutomaticSchemaMigration import io.realm.kotlin.migration.RealmMigration import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.KClass @@ -55,6 +56,7 @@ public interface RealmConfiguration : Configuration { private var directory: String = appFilesDirectory() private var deleteRealmIfMigrationNeeded: Boolean = false private var migration: RealmMigration? = null + private var automaticEmbeddedObjectConstraintsResolution = false /** * Sets the path to the directory that contains the realm file. If the directory does not @@ -109,6 +111,29 @@ public interface RealmConfiguration : Configuration { public fun migration(migration: RealmMigration): Builder = apply { this.migration = migration } + /** + * Sets the migration to handle schema updates with automatic migration of data. + * + * @param migration the [AutomaticSchemaMigration] instance to handle schema and data + * migration in the event of a schema update. + * @param resolveEmbeddedObjectConstraints a flag to indicate whether realm should resolve + * embedded object constraints after migration. If this is `true` then all embedded objects + * without a parent will be deleted and every embedded object with multiple references to it + * will be duplicated so that every referencing object will hold its own copy of the + * embedded object. + * + * @see RealmMigration + * @see AutomaticSchemaMigration + */ + public fun migration( + migration: AutomaticSchemaMigration, + resolveEmbeddedObjectConstraints: Boolean = false + ): Builder = + apply { + this.migration = migration + this.automaticEmbeddedObjectConstraintsResolution = resolveEmbeddedObjectConstraints + } + override fun name(name: String): Builder = apply { checkName(name) this.name = name @@ -163,6 +188,7 @@ public interface RealmConfiguration : Configuration { deleteRealmIfMigrationNeeded, compactOnLaunchCallback, migration, + automaticEmbeddedObjectConstraintsResolution, initialDataCallback, inMemory, initialRealmFileConfiguration, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 3826d03dae..3463f478c0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.RealmConfigurationPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SchemaMode +import io.realm.kotlin.internal.interop.use import io.realm.kotlin.internal.platform.appFilesDirectory import io.realm.kotlin.internal.platform.prepareRealmFilePath import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow @@ -47,7 +48,7 @@ import kotlin.reflect.KClass // TODO Public due to being accessed from `library-sync` @Suppress("LongParameterList") -public open class ConfigurationImpl constructor( +public open class ConfigurationImpl( directory: String, name: String, schema: Set>, @@ -60,6 +61,7 @@ public open class ConfigurationImpl constructor( private val userEncryptionKey: ByteArray?, compactOnLaunchCallback: CompactOnLaunchCallback?, private val userMigration: RealmMigration?, + automaticBacklinkHandling: Boolean, initialDataCallback: InitialDataCallback?, override val isFlexibleSyncConfiguration: Boolean, inMemory: Boolean, @@ -107,11 +109,14 @@ public open class ConfigurationImpl constructor( override suspend fun openRealm(realm: RealmImpl): Pair { val configPtr = realm.configuration.createNativeConfiguration() - val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr) - val liveRealmReference = LiveRealmReference(realm, dbPointer) - val frozenReference = liveRealmReference.snapshot(realm) - liveRealmReference.close() - return frozenReference to fileCreated + return RealmInterop.realm_create_scheduler() + .use { scheduler -> + val (dbPointer, fileCreated) = RealmInterop.realm_open(configPtr, scheduler) + val liveRealmReference = LiveRealmReference(realm, dbPointer) + val frozenReference = liveRealmReference.snapshot(realm) + liveRealmReference.close() + frozenReference to fileCreated + } } override suspend fun initializeRealmData(realm: RealmImpl, realmFileCreated: Boolean) { @@ -218,6 +223,7 @@ public open class ConfigurationImpl constructor( migrationCallback?.let { RealmInterop.realm_config_set_migration_function(nativeConfig, it) } + RealmInterop.realm_config_set_automatic_backlink_handling(nativeConfig, automaticBacklinkHandling) userEncryptionKey?.let { key: ByteArray -> RealmInterop.realm_config_set_encryption_key(nativeConfig, key) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt index 4267be41c1..7100685660 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/Converters.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.annotations.ExperimentalGeoSpatialApi import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject @@ -36,6 +37,9 @@ import io.realm.kotlin.types.RealmAny import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmUUID +import io.realm.kotlin.types.geo.GeoBox +import io.realm.kotlin.types.geo.GeoCircle +import io.realm.kotlin.types.geo.GeoPolygon import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 import kotlin.native.concurrent.SharedImmutable @@ -352,6 +356,7 @@ internal object RealmValueArgumentConverter { } ?: nullTransport() } + @OptIn(ExperimentalGeoSpatialApi::class) fun MemTrackingAllocator.convertQueryArg(value: Any?): RealmQueryArgument = when (value) { is Collection<*> -> { @@ -374,6 +379,13 @@ internal object RealmValueArgumentConverter { } ) } + is GeoBox, + is GeoCircle, + is GeoPolygon -> { + // Hack support for geospatial arguments until we have propert C-API support. + // See https://github.com/realm/realm-core/pull/6934 + RealmQuerySingleArgument(kAnyToRealmValue(value.toString())) + } else -> { RealmQuerySingleArgument(kAnyToRealmValue(value)) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt index 1118589781..f0c7b7fd97 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt @@ -22,9 +22,9 @@ import io.realm.kotlin.internal.interop.RealmSchemaPointer import io.realm.kotlin.internal.interop.SynchronizableObject import io.realm.kotlin.internal.platform.WeakReference import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.internal.util.LiveRealmContext import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext /** @@ -37,13 +37,13 @@ import kotlinx.coroutines.withContext * * @param owner The owner of the snapshot references of this realm. * @param configuration The configuration of the realm. - * @param dispatcher The single thread dispatcher backing the realm scheduler of this realm. The + * @param scheduler The single thread dispatcher backing the realm scheduler of this realm. The * realm itself must only be access on the same thread. */ internal abstract class LiveRealm( val owner: RealmImpl, configuration: InternalConfiguration, - val dispatcher: CoroutineDispatcher + private val scheduler: LiveRealmContext, ) : BaseRealmImpl(configuration) { private val realmChangeRegistration: NotificationToken @@ -52,7 +52,10 @@ internal abstract class LiveRealm( internal val versionTracker = VersionTracker(this, owner.log) override val realmReference: LiveRealmReference by lazy { - val (dbPointer, _) = RealmInterop.realm_open(configuration.createNativeConfiguration(), dispatcher) + val (dbPointer, _) = RealmInterop.realm_open( + configuration.createNativeConfiguration(), + scheduler.scheduler + ) LiveRealmReference(this, dbPointer) } @@ -168,7 +171,7 @@ internal abstract class LiveRealm( * Dump the current snapshot and tracked versions for debugging purpose. */ internal fun versions(): VersionData = runBlocking { - withContext(dispatcher) { + withContext(scheduler.dispatcher) { snapshotLock.withLock { val active = if (!_closeSnapshotWhenAdvancing) { versionTracker.versions() + _snapshot.value.version() diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt index 0a2791fab8..356eb6397e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmAnyImpl.kt @@ -127,27 +127,12 @@ internal class RealmAnyImpl constructor( if (other.type != this.type) return false if (clazz == ByteArray::class) { if (other.internalValue !is ByteArray) return false - if (!other.internalValue.contentEquals(this.internalValue as ByteArray)) return false - } else if (internalValue is BsonObjectId) { - if (other.clazz != BsonObjectId::class) return false - if (other.internalValue != this.internalValue) return false + return other.internalValue.contentEquals(this.internalValue as ByteArray) } else if (internalValue is RealmObject) { if (other.clazz != this.clazz) return false - if (other.internalValue !== this.internalValue) return false - } else if (internalValue is Number) { // Numerics are the same as long as their value is the same - when (other.internalValue) { - is Char -> if (other.internalValue.code.toLong() != internalValue.toLong()) return false - is Number -> if (other.internalValue.toLong() != this.internalValue.toLong()) return false - else -> return false - } - } else if (internalValue is Char) { // We are comparing chars - when (other.internalValue) { - is Char -> if (other.internalValue.code.toLong() != internalValue.toLong()) return false - is Number -> if (other.internalValue.toLong() != this.internalValue.toLong()) return false - else -> return false - } + return other.internalValue == this.internalValue } - return true + return internalValue == other.internalValue } override fun hashCode(): Int { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt index bbbf8f2304..8b0e620e2d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt @@ -30,7 +30,7 @@ import kotlin.reflect.KClass public const val REALM_FILE_EXTENSION: String = ".realm" @Suppress("LongParameterList") -internal class RealmConfigurationImpl constructor( +internal class RealmConfigurationImpl( directory: String, name: String, schema: Set>, @@ -43,6 +43,7 @@ internal class RealmConfigurationImpl constructor( override val deleteRealmIfMigrationNeeded: Boolean, compactOnLaunchCallback: CompactOnLaunchCallback?, migration: RealmMigration?, + automaticBacklinkHandling: Boolean, initialDataCallback: InitialDataCallback?, inMemory: Boolean, override val initialRealmFileConfiguration: InitialRealmFileConfiguration?, @@ -63,6 +64,7 @@ internal class RealmConfigurationImpl constructor( encryptionKey, compactOnLaunchCallback, migration, + automaticBacklinkHandling, initialDataCallback, false, inMemory, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 4ddd236a02..90a0e2b730 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -27,8 +27,9 @@ import io.realm.kotlin.internal.platform.copyAssetFile import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.schema.RealmSchemaImpl -import io.realm.kotlin.internal.util.DispatcherHolder +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.internal.util.Validation.sdkError +import io.realm.kotlin.internal.util.createLiveRealmContext import io.realm.kotlin.internal.util.terminateWhen import io.realm.kotlin.notifications.RealmChange import io.realm.kotlin.notifications.internal.InitialRealmImpl @@ -45,31 +46,34 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.reflect.KClass // TODO API-PUBLIC Document platform specific internals (RealmInitializer, etc.) // TODO Public due to being accessed from `SyncedRealmContext` public class RealmImpl private constructor( - configuration: InternalConfiguration + configuration: InternalConfiguration, ) : BaseRealmImpl(configuration), Realm, InternalTypedRealm, Flowable> { - private val realmPointerMutex = Mutex() + public val notificationScheduler: LiveRealmContext = + configuration.notificationDispatcherFactory.createLiveRealmContext() - public val notificationDispatcherHolder: DispatcherHolder = - configuration.notificationDispatcherFactory.create() - public val writeDispatcherHolder: DispatcherHolder = - configuration.writeDispatcherFactory.create() + public val writeScheduler: LiveRealmContext = + configuration.writeDispatcherFactory.createLiveRealmContext() internal val realmScope = - CoroutineScope(SupervisorJob() + notificationDispatcherHolder.dispatcher) + CoroutineScope(SupervisorJob() + notificationScheduler.dispatcher) private val notifierFlow: MutableSharedFlow> = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val notifier = - SuspendableNotifier(this, notificationDispatcherHolder.dispatcher) - private val writer = - SuspendableWriter(this, writeDispatcherHolder.dispatcher) + + private val notifier = SuspendableNotifier( + owner = this, + scheduler = notificationScheduler, + ) + private val writer = SuspendableWriter( + owner = this, + scheduler = writeScheduler, + ) // Internal flow to ease monitoring of realm state for closing active flows then the realm is // closed. @@ -78,6 +82,7 @@ public class RealmImpl private constructor( private var _realmReference: AtomicRef = atomic(null) private val realmReferenceLock = SynchronizableObject() + private val isClosed = atomic(false) /** * The current Realm reference that points to the underlying frozen C++ SharedRealm. @@ -96,7 +101,8 @@ public class RealmImpl private constructor( // Injection point for synchronized Realms. This property should only be used to hold state // required by synchronized realms. See `SyncedRealmContext` for more details. - public var syncContext: AtomicRef = atomic(null) + @OptIn(ExperimentalStdlibApi::class) + public var syncContext: AtomicRef = atomic(null) init { @Suppress("TooGenericExceptionCaught") @@ -254,26 +260,39 @@ public class RealmImpl private constructor( return VersionInfo(mainVersions, notifier.versions(), writer.versions()) } + override fun isClosed(): Boolean { + // We cannot rely on `realmReference()` here. If something happens during open, this might + // not be available and will throw, so we need to track closed state separately. + return isClosed.value + } + override fun close() { // TODO Reconsider this constraint. We have the primitives to check is we are on the // writer thread and just close the realm in writer.close() writer.checkInTransaction("Cannot close the Realm while inside a transaction block") - runBlocking { - realmPointerMutex.withLock { + realmReferenceLock.withLock { + if (isClosed()) { + return + } + isClosed.value = true + runBlocking { writer.close() realmScope.cancel() notifier.close() versionTracker.close() + @OptIn(ExperimentalStdlibApi::class) + syncContext.value?.close() // The local realmReference is pointing to a realm reference managed by either the // version tracker, writer or notifier, so it is already closed super.close() } + if (!realmStateFlow.tryEmit(State.CLOSED)) { + log.warn("Cannot signal internal close") + } + + notificationScheduler.close() + writeScheduler.close() } - if (!realmStateFlow.tryEmit(State.CLOSED)) { - log.warn("Cannot signal internal close") - } - notificationDispatcherHolder.close() - writeDispatcherHolder.close() } internal companion object { 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/RealmObjectHelper.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt index c34965306b..8351ee4be2 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt @@ -17,9 +17,12 @@ package io.realm.kotlin.internal import io.realm.kotlin.UpdatePolicy +import io.realm.kotlin.VersionId import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealmObject import io.realm.kotlin.ext.asRealmObject +import io.realm.kotlin.ext.isManaged +import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.toRealmDictionary import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.ext.toRealmSet @@ -27,6 +30,7 @@ import io.realm.kotlin.internal.dynamic.DynamicUnmanagedRealmObject import io.realm.kotlin.internal.interop.ClassKey import io.realm.kotlin.internal.interop.CollectionType import io.realm.kotlin.internal.interop.MemAllocator +import io.realm.kotlin.internal.interop.ObjectKey import io.realm.kotlin.internal.interop.PropertyKey import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop @@ -39,6 +43,7 @@ import io.realm.kotlin.internal.interop.RealmValue import io.realm.kotlin.internal.interop.Timestamp import io.realm.kotlin.internal.interop.getterScope import io.realm.kotlin.internal.interop.inputScope +import io.realm.kotlin.internal.platform.identityHashCode import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow import io.realm.kotlin.internal.schema.ClassMetadata import io.realm.kotlin.internal.schema.PropertyMetadata @@ -1145,6 +1150,66 @@ internal object RealmObjectHelper { } } + @Suppress("unused") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmToString(obj: BaseRealmObject): String { + // This code assumes no race conditions + val schemaName = obj::class.realmObjectCompanionOrNull()?.io_realm_kotlin_className + val fqName = obj::class.qualifiedName + return obj.realmObjectReference?.let { + if (obj.isValid()) { + val id: RealmObjectIdentifier = obj.getIdentifier() + val objKey = id.objectKey.key + val version = id.versionId.version + "$fqName{state=VALID, schemaName=$schemaName, objKey=$objKey, version=$version, realm=${it.owner.owner.configuration.name}}" + } else { + val state = if (it.owner.isClosed()) { + "CLOSED" + } else { + "INVALID" + } + "$fqName{state=$state, schemaName=$schemaName, realm=${it.owner.owner.configuration.name}, hashCode=${obj.hashCode()}}" + } + } ?: "$fqName{state=UNMANAGED, schemaName=$schemaName, hashCode=${obj.hashCode()}}" + } + + @Suppress("unused", "ReturnCount") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmEquals(obj: BaseRealmObject, other: Any?): Boolean { + if (obj === other) return true + if (other == null || obj::class != other::class) return false + + other as BaseRealmObject + + if (other.isManaged()) { + if (obj.isValid() != other.isValid()) return false + return obj.getIdentifierOrNull() == other.getIdentifierOrNull() + } else { + // If one of the objects are unmanaged, they are only equal if identical, which + // should have been caught at the top of this function. + return false + } + } + + @Suppress("unused", "MagicNumber") // Called from generated code + // Inlining this functions somehow break the IntelliJ debugger, unclear why? + internal fun realmHashCode(obj: BaseRealmObject): Int { + // This code assumes no race conditions + return obj.realmObjectReference?.let { + val isValid: Boolean = obj.isValid() + val identifier: RealmObjectIdentifier = if (it.isClosed()) { + RealmObjectIdentifier(ClassKey(-1), ObjectKey(-1), VersionId(0), "") + } else { + obj.getIdentifier() + } + val realmPath: String = it.owner.owner.configuration.path + var hashCode = isValid.hashCode() + hashCode = 31 * hashCode + identifier.hashCode() + hashCode = 31 * hashCode + realmPath.hashCode() + hashCode + } ?: identityHashCode(obj) + } + private fun checkPropertyType( obj: RealmObjectReference, propertyName: String, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt index d29843ceaf..06c796f828 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt @@ -142,9 +142,15 @@ internal fun BaseRealmObject.getIdentifier(): RealmObjectIdentifier { val classKey: ClassKey = metadata.classKey val objKey: ObjectKey = RealmInterop.realm_object_get_key(objectPointer) val version: VersionId = version() - return Triple(classKey, objKey, version) + val path: String = owner.owner.configuration.path + return RealmObjectIdentifier(classKey, objKey, version, path) } ?: throw IllegalStateException("Identifier can only be calculated for managed objects.") - ULong +} + +public fun BaseRealmObject.getIdentifierOrNull(): RealmObjectIdentifier? { + return runIfManaged { + getIdentifier() + } } /** diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt index 9ba86380a5..702dd178fa 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt @@ -48,10 +48,15 @@ import kotlin.reflect.KProperty1 // `equals` method, which in general just is the memory address of the object. internal typealias UnmanagedToManagedObjectCache = MutableMap // Map -// For managed realm objects we use `` as a unique identifier +// For managed realm objects we use `` as a unique identifier // We are using a hash on the Kotlin side so we can use a HashMap for O(1) lookup rather than // having to do O(n) filter with a JNI call for `realm_equals` for each element. -internal typealias RealmObjectIdentifier = Triple +public data class RealmObjectIdentifier( + val classKey: ClassKey, + val objectKey: ObjectKey, + val versionId: VersionId, + val path: String +) internal typealias ManagedToUnmanagedObjectCache = MutableMap /** diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt index f26708e883..bcee667dea 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -6,6 +6,7 @@ import io.realm.kotlin.internal.interop.RealmChangesPointer import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.schema.RealmSchemaImpl +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.internal.util.Validation.sdkError import io.realm.kotlin.notifications.internal.Cancellable import io.realm.kotlin.notifications.internal.Cancellable.Companion.NO_OP_NOTIFICATION_TOKEN @@ -35,7 +36,7 @@ import kotlinx.coroutines.withContext */ internal class SuspendableNotifier( private val owner: RealmImpl, - private val dispatcher: CoroutineDispatcher + private val scheduler: LiveRealmContext, ) : LiveRealmHolder() { // Flow used to emit events when the version of the live realm is updated // Adding extra buffer capacity as we are otherwise never able to emit anything @@ -45,9 +46,15 @@ internal class SuspendableNotifier( extraBufferCapacity = 1 ) + val dispatcher: CoroutineDispatcher = scheduler.dispatcher + // Could just be anonymous class, but easiest way to get BaseRealmImpl.toString to display the // right type with this - private inner class NotifierRealm : LiveRealm(owner, owner.configuration, dispatcher) { + private inner class NotifierRealm : LiveRealm( + owner = owner, + configuration = owner.configuration, + scheduler = scheduler + ) { // This is guaranteed to be triggered before any other notifications for the same // update as we get all callbacks on the same single thread dispatcher override fun onRealmChanged() { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt index f46b3e3dbe..e18f20b11a 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt @@ -24,6 +24,7 @@ import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.threadId import io.realm.kotlin.internal.schema.RealmClassImpl import io.realm.kotlin.internal.schema.RealmSchemaImpl +import io.realm.kotlin.internal.util.LiveRealmContext import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.types.BaseRealmObject import io.realm.kotlin.types.TypedRealmObject @@ -43,13 +44,26 @@ import kotlin.reflect.KClass * it's thread. * * @param owner The Realm instance needed for emitting updates. - * @param dispatcher The dispatcher on which to execute all the writers operations on. + * @param scheduler The scheduler on which to execute all the writers operations on. */ -internal class SuspendableWriter(private val owner: RealmImpl, val dispatcher: CoroutineDispatcher) : +internal class SuspendableWriter( + private val owner: RealmImpl, + private val scheduler: LiveRealmContext, +) : LiveRealmHolder() { private val tid: ULong - internal inner class WriterRealm : LiveRealm(owner, owner.configuration, dispatcher), InternalMutableRealm, InternalTypedRealm, WriteTransactionManager { + val dispatcher: CoroutineDispatcher = scheduler.dispatcher + + internal inner class WriterRealm : + LiveRealm( + owner = owner, + configuration = owner.configuration, + scheduler = scheduler + ), + InternalMutableRealm, + InternalTypedRealm, + WriteTransactionManager { override val realmReference: LiveRealmReference get() = super.realmReference 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 7091f986d1..010b1fa9a5 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 @@ -59,6 +59,13 @@ public expect val DEVICE_MODEL: String */ public expect val PATH_SEPARATOR: String +/** + * Construct a path from individual components + */ +public fun pathOf(vararg pathParts: String): String { + return pathParts.joinToString(PATH_SEPARATOR) +} + /** * Returns the root directory of the platform's App data. */ @@ -131,7 +138,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. @@ -145,3 +152,8 @@ public expect fun returnType(field: KMutableProperty1 * Returns whether or not we are running on Windows */ public expect fun isWindows(): Boolean + +/** + * Returns the identity hashcode for a given object. + */ +internal expect fun identityHashCode(obj: Any?): Int diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt index a7cddce0c3..370af4d31f 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/util/CoroutineDispatcherFactory.kt @@ -16,12 +16,15 @@ package io.realm.kotlin.internal.util +import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmSchedulerPointer import io.realm.kotlin.internal.platform.multiThreadDispatcher +import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlin.jvm.JvmInline +import kotlinx.coroutines.withContext /** * Factory wrapper for passing around dispatchers without needing to create them. This makes it @@ -35,6 +38,7 @@ public fun interface CoroutineDispatcherFactory { * Let Realm create and control the dispatcher. Managed dispatchers will be closed * when their owner Realm/App is closed as well. */ + @OptIn(ExperimentalCoroutinesApi::class) public fun managed(name: String, threads: Int = 1): CoroutineDispatcherFactory { return CoroutineDispatcherFactory { ManagedDispatcherHolder( @@ -88,17 +92,38 @@ public sealed interface DispatcherHolder { public fun close() } -@JvmInline @OptIn(ExperimentalCoroutinesApi::class) -private value class ManagedDispatcherHolder( - override val dispatcher: CloseableCoroutineDispatcher +private class ManagedDispatcherHolder( + override val dispatcher: CloseableCoroutineDispatcher, ) : DispatcherHolder { override fun close(): Unit = dispatcher.close() } -@JvmInline -private value class UnmanagedDispatcherHolder( +private class UnmanagedDispatcherHolder( override val dispatcher: CoroutineDispatcher ) : DispatcherHolder { override fun close(): Unit = Unit } + +/** + * Object that creates a Realm scheduler based and a coroutine dispatcher, and binds their resource + * lifecycle. + */ +public class LiveRealmContext( + private val dispatcherHolder: DispatcherHolder, +) : DispatcherHolder by dispatcherHolder { + + public val scheduler: RealmSchedulerPointer = runBlocking { + withContext(dispatcherHolder.dispatcher) { + RealmInterop.realm_create_scheduler(dispatcher) + } + } + + override fun close() { + scheduler.release() + dispatcherHolder.close() + } +} + +internal fun CoroutineDispatcherFactory.createLiveRealmContext(): LiveRealmContext = + LiveRealmContext(create()) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt index 59f0a1bd39..8352bf3e1d 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/FullText.kt @@ -16,10 +16,11 @@ package io.realm.kotlin.types.annotations * * The full-text index currently support this set of features: * - * - Only token or word search, e.g. `bio TEXT 'computer dancing'` will find all objects that + * - Token or word search, e.g. `bio TEXT 'computer dancing'` will find all objects that * contains the words `computer` and `dancing` in their `bio` property. * - Tokens are diacritics- and case-insensitive, e.g.`bio TEXT 'cafe dancing'` and * `bio TEXT 'café DANCING'` will return the same set of matches. + * - Token prefix search can be done using `*`, like `bio TEXT comp*`. * - Ignoring results with certain tokens are done using `-`, e.g. `bio TEXT 'computer -dancing'` * will find all objects that contain `computer` but not `dancing`. * - Tokens are defined by a simple tokenizer that uses the following rules: @@ -29,7 +30,7 @@ package io.realm.kotlin.types.annotations * * Note the following constraints before using full-text search: * - * - Token prefix or suffix search like `bio TEXT 'comp* *cing'` is not supported. + * - Token suffix search like `bio TEXT '*cing'` is not supported. * - Only ASCII and Latin-1 alphanumerical chars are included in the index (most western languages). * - Only boolean match is supported, i.e. "found" or "not found". It is not possible to sort * results by "relevance" . diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt index dde9e57514..5e3d353b45 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoBox.kt @@ -28,7 +28,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoBox * val bottomLeft = GeoPoint.create(latitude = 5.0, longitude = 5.0) * val topRight = GeoPoint.create(latitude = 10.0, longitude = 10.0) * val searchArea = GeoBox.create(bottomLeft, topRight) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` */ @ExperimentalGeoSpatialApi diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt index ce43fc39fd..df104a6e96 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoCircle.kt @@ -26,7 +26,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoCircle * ``` * val newYork = GeoPoint.create(latitude = 40.730610, longitude = -73.935242) * val searchArea = GeoCircle.create(center = newYork, radius = Distance.fromMiles(2.0)) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` */ @ExperimentalGeoSpatialApi @@ -47,6 +47,7 @@ public interface GeoCircle { * ``` * val circle = GeoCircle.create(center = GeoPoint.create(0.0, 0.0), radius = Distance.fromKilometers(10.0)) * val results = realm.query("location GEOWITHIN $circle").find() + * ``` */ public override fun toString(): String diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt index 774f9c0f8d..275e17be09 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPoint.kt @@ -88,7 +88,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoPoint * * val newYork = GeoPoint.create(latitude = 40.730610, longitude = -73.935242) * val searchArea = GeoCircle.create(center = newYork, radius = Distance.fromMiles(2.0)) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * ``` * * A proper persistable GeoPoint class will be implemented in an upcoming release. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt index 5409aee9cc..e4047d89ae 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/geo/GeoPolygon.kt @@ -60,7 +60,7 @@ import io.realm.kotlin.internal.geo.UnmanagedGeoPolygon * GeoPoint.create(4.0, 1.0) * )) * ) - * val restaurants = realm.query("location GEOWITHIN $searchArea").find() + * val restaurants = realm.query("location GEOWITHIN $0", searchArea).find() * * ``` */ @ExperimentalGeoSpatialApi @@ -88,7 +88,8 @@ public interface GeoPolygon { * GeoPoint.create(0.0, 0.0) * ) * val searchArea = GeoPolygon.create(outerRing) - * val results = realm.query("location GEOWITHIN searchArea").find() + * val results = realm.query("location GEOWITHIN $searchArea").find() + * ``` */ override fun toString(): String 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 8814ab803e..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) } @@ -124,3 +124,5 @@ private fun preparePath(directoryPath: String) { } public actual fun isWindows(): Boolean = OS_NAME.contains("windows", ignoreCase = true) + +internal actual fun identityHashCode(obj: Any?): Int = System.identityHashCode(obj) 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 b71e553187..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 @@ -29,6 +29,7 @@ import platform.Foundation.dataWithContentsOfFile import platform.Foundation.timeIntervalSince1970 import platform.posix.memcpy import platform.posix.pthread_threadid_np +import kotlin.native.identityHashCode import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KType @@ -67,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 @@ -192,3 +193,5 @@ private fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length. } public actual fun isWindows(): Boolean = false + +internal actual fun identityHashCode(obj: Any?): Int = obj.identityHashCode() diff --git a/packages/library-sync/build.gradle.kts b/packages/library-sync/build.gradle.kts index 13a714ad9a..d2f3afe90e 100644 --- a/packages/library-sync/build.gradle.kts +++ b/packages/library-sync/build.gradle.kts @@ -157,12 +157,9 @@ android { consumerProguardFiles("proguard-rules-consumer-common.pro") } } - // To avoid - // Failed to transform kotlinx-coroutines-core-jvm-1.5.0-native-mt.jar ... - // The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Skip BuildConfig generation as it overlaps with io.realm.kotlin.BuildConfig from realm-java buildFeatures { 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/exceptions/ClientResetRequiredException.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt index 697100dc81..26b2daa2e4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt @@ -56,9 +56,10 @@ public class ClientResetRequiredException constructor( * associated to the session in which this error is generated **must be closed**. Not doing so * might result in unexpected file system errors. * + * @return `true` if the Client Reset succeeded, `false` if not. * @throws IllegalStateException if not all instances have been closed. */ - public fun executeClientReset() { - RealmInterop.realm_sync_immediately_run_file_actions(appPointer, originalFilePath) + public fun executeClientReset(): Boolean { + return RealmInterop.realm_sync_immediately_run_file_actions(appPointer, originalFilePath) } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt index e12dafdbfd..425ae1ca7f 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt @@ -60,7 +60,9 @@ import kotlin.time.Duration * depend on which [mode] was used. * @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before * a query result could be returned. - * @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not + * accept the set of queries. The exact reason is found in the exception message. */ @ExperimentalFlexibleSyncApi public suspend fun RealmQuery.subscribe( diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt index 53804d2dd6..d2c3aeb16e 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt @@ -62,7 +62,9 @@ import kotlin.time.Duration * depend on which [mode] was used. * @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before * a query result could be returned. - * @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync. + * @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not + * accept the set of queries. The exact reason is found in the exception message. */ @ExperimentalFlexibleSyncApi public suspend fun RealmResults.subscribe( diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 98f9edb155..cb8fa12c7a 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -73,7 +73,7 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) val appDispatcher = appNetworkDispatcherFactory.create() val networkTransport = networkTransportFactory(appDispatcher) val appConfigPointer: RealmAppConfigurationPointer = - initializeRealmAppConfig(appName, appVersion, bundleId, networkTransport) + initializeRealmAppConfig(bundleId, networkTransport) var applicationInfo: String? = null // Define user agent strings sent when making the WebSocket connection to Device Sync if (appName != null || appVersion == null) { @@ -121,8 +121,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) // Only freeze anything after all properties are setup as this triggers freezing the actual // AppConfigurationImpl instance itself private fun initializeRealmAppConfig( - localAppName: String?, - localAppVersion: String?, bundleId: String, networkTransport: NetworkTransport ): RealmAppConfigurationPointer { @@ -133,8 +131,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) connectionParams = SyncConnectionParams( sdkVersion = SDK_VERSION, bundleId = bundleId, - localAppName = localAppName, - localAppVersion = localAppVersion, platformVersion = OS_VERSION, device = DEVICE_MANUFACTURER, deviceVersion = DEVICE_MODEL, 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/HttpClientCache.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt index d918aa4d6a..208769c6f9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/HttpClientCache.kt @@ -2,6 +2,7 @@ package io.realm.kotlin.mongodb.internal import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.HttpRedirect import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger @@ -34,7 +35,11 @@ internal fun createClient(timeoutMs: Long, customLogger: Logger?): HttpClient { } } - followRedirects = true + // We should allow redirects for all types, not just GET and HEAD + // See https://github.com/ktorio/ktor/issues/1793 + install(HttpRedirect) { + checkHttpMethod = false + } } } 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/RealmQueryExtImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt index 867acd213a..3798454fac 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +@file:Suppress("invisible_reference", "invisible_member") package io.realm.kotlin.mongodb.internal import io.realm.kotlin.Realm import io.realm.kotlin.internal.RealmImpl import io.realm.kotlin.internal.getRealm +import io.realm.kotlin.internal.query.ObjectQuery +import io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException import io.realm.kotlin.mongodb.subscriptions import io.realm.kotlin.mongodb.sync.Subscription +import io.realm.kotlin.mongodb.sync.SubscriptionSet import io.realm.kotlin.mongodb.sync.SyncConfiguration import io.realm.kotlin.mongodb.sync.WaitForSync import io.realm.kotlin.mongodb.syncSession @@ -32,7 +35,6 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlin.time.Duration -@Suppress("invisible_reference", "invisible_member") internal suspend fun createSubscriptionFromQuery( query: RealmQuery, name: String?, @@ -41,7 +43,7 @@ internal suspend fun createSubscriptionFromQuery( timeout: Duration ): RealmResults { - if (query !is io.realm.kotlin.internal.query.ObjectQuery) { + if (query !is ObjectQuery) { throw IllegalStateException("Only queries on objects are supported. This was: ${query::class}") } if (query.realmReference.owner !is RealmImpl) { @@ -53,8 +55,7 @@ internal suspend fun createSubscriptionFromQuery( return withTimeout(timeout) { withContext(appDispatcher) { - val existingSubscription: Subscription? = - if (name != null) subscriptions.findByName(name) else subscriptions.findByQuery(query) + val existingSubscription: Subscription? = findExistingQueryInSubscriptions(name, query, subscriptions) if (existingSubscription == null || updateExisting) { subscriptions.update { add(query, name, updateExisting) @@ -66,9 +67,33 @@ internal suspend fun createSubscriptionFromQuery( // The subscription should already exist, just make sure we downloaded all // server data before continuing. realm.syncSession.downloadAllServerChanges() + subscriptions.refresh() + subscriptions.errorMessage?.let { errorMessage: String -> + throw BadFlexibleSyncQueryException(errorMessage) + } } // Rerun the query on the latest Realm version. realm.query(query.clazz, query.description()).find() } } } + +// A subscription only matches if name, type and query all matches +private fun findExistingQueryInSubscriptions( + name: String?, + query: ObjectQuery, + subscriptions: SubscriptionSet +): Subscription? { + return if (name != null) { + val sub: Subscription? = subscriptions.findByName(name) + val companion = io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow(query.clazz) + val userTypeName = companion.io_realm_kotlin_className + if (sub?.queryDescription == query.description() && sub.objectType == userTypeName) { + sub + } else { + null + } + } else { + subscriptions.findByQuery(query) + } +} diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt index e5c3d424bb..fb72304c78 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt @@ -1,14 +1,11 @@ package io.realm.kotlin.mongodb.internal import io.realm.kotlin.internal.interop.AppCallback +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.ErrorCategory import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.sync.AppError -import io.realm.kotlin.internal.interop.sync.ProtocolConnectionErrorCode -import io.realm.kotlin.internal.interop.sync.ProtocolSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.mongodb.exceptions.AppException import io.realm.kotlin.mongodb.exceptions.AuthException import io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException @@ -80,64 +77,26 @@ internal fun channelResultCallback( internal fun convertSyncError(syncError: SyncError): SyncException { val errorCode = syncError.errorCode - // FIXME Client Reset errors are just reported as normal Sync Errors for now. - // Will be fixed by https://github.com/realm/realm-kotlin/issues/417 val message = createMessageFromSyncError(errorCode) - return when (errorCode.category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/client_base.hpp#L73 - // For now, it is unclear how to categorize these, so for now, just report as generic - // errors. - SyncException(message) - } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/protocol.hpp#L200 - // Use https://docs.google.com/spreadsheets/d/1SmiRxhFpD1XojqCKC-xAjjV-LKa9azeeWHg-zgr07lE/edit - // as guide for how to categorize Connection type errors. - when (errorCode.code) { - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_UNKNOWN_MESSAGE, // Unknown type of input message - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_SYNTAX, // Bad syntax in input message head - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_WRONG_PROTOCOL_VERSION, // Wrong protocol version (CLIENT) (obsolete) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_SESSION_IDENT, // Bad session identifier in input message - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_REUSE_OF_SESSION_IDENT, // Overlapping reuse of session identifier (BIND) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BOUND_IN_OTHER_SESSION, // Client file bound in other session (IDENT) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_MESSAGE_ORDER, // Bad input message order - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_DECOMPRESSION, // Error in decompression (UPLOAD) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_CHANGESET_HEADER_SYNTAX, // Bad syntax in a changeset header (UPLOAD) - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_BAD_CHANGESET_SIZE -> { // Bad size specified in changeset header (UPLOAD) - UnrecoverableSyncException(message) - } - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_FLX_SYNC, // Connected with wrong wire protocol - should switch to FLX sync - ProtocolConnectionErrorCode.RLM_SYNC_ERR_CONNECTION_SWITCH_TO_PBS -> { // Connected with wrong wire protocol - should switch to PBS - WrongSyncTypeException(message) - } - else -> SyncException(message) - } - } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SESSION -> { - // See https://github.com/realm/realm-core/blob/master/src/realm/sync/protocol.hpp#L217 - // Use https://docs.google.com/spreadsheets/d/1SmiRxhFpD1XojqCKC-xAjjV-LKa9azeeWHg-zgr07lE/edit - // as guide for how to categorize Session type errors. - when (errorCode.code) { - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_BAD_QUERY -> { // Flexible Sync Query was rejected by the server - BadFlexibleSyncQueryException(message) - } - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_PERMISSION_DENIED -> - // Permission denied errors should be unrecoverable according to Core, i.e. the - // client will disconnect sync and transition to the "inactive" state - UnrecoverableSyncException(message) - ProtocolSessionErrorCode.RLM_SYNC_ERR_SESSION_COMPENSATING_WRITE -> - CompensatingWriteException(message, syncError.compensatingWrites) - else -> SyncException(message) - } + return when (errorCode.errorCode) { + ErrorCode.RLM_ERR_WRONG_SYNC_TYPE -> WrongSyncTypeException(message) + + ErrorCode.RLM_ERR_INVALID_SUBSCRIPTION_QUERY -> { + // Flexible Sync Query was rejected by the server + BadFlexibleSyncQueryException(message) } - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN -> { - // It is unclear how to handle system level errors, so even though some of them - // are probably benign, report as top-level errors for now. - SyncException(message) + ErrorCode.RLM_ERR_SYNC_COMPENSATING_WRITE -> CompensatingWriteException(message, syncError.compensatingWrites) + + ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, + ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, + ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> { + // Permission denied errors should be unrecoverable according to Core, i.e. the + // client will disconnect sync and transition to the "inactive" state + UnrecoverableSyncException(message) } else -> { + // An error happened we are not sure how to handle. Just report as a generic + // SyncException. SyncException(message) } } @@ -277,30 +236,27 @@ internal fun convertAppError(appError: AppError): Throwable { } } -internal fun createMessageFromSyncError(error: SyncErrorCode): String { - val categoryDesc = error.category.description ?: error.category.nativeValue.toString() - val errorCodeDesc: String? = error.code.description ?: when (error.category) { - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_SYSTEM, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN, - -> { - // We lack information about these kinds of errors, - // so rather than returning a potentially misleading - // name, just return nothing. - null - } - else -> "Unknown" +internal fun createMessageFromSyncError(error: CoreError): String { + val categoryDesc = error.categories.description + val errorCodeDesc: String? = error.errorCode?.description ?: if (ErrorCategory.RLM_ERR_CAT_SYSTEM_ERROR in error.categories) { + // We lack information about these kinds of errors, + // so rather than returning a potentially misleading + // name, just return nothing. + null + } else { + "Unknown" } // Combine all the parts to form an error format that is human-readable. // An example could be this: `[Connection][WrongProtocolVersion(104)] Wrong protocol version was used: 25` val errorDesc: String = - if (errorCodeDesc == null) error.code.nativeValue.toString() else "$errorCodeDesc(${error.code.nativeValue})" + if (errorCodeDesc == null) error.errorCodeNativeValue.toString() else "$errorCodeDesc(${error.errorCodeNativeValue})" // Make sure that messages are uniformly formatted, so it looks nice if we append the // server log. - val msg = error.message.let { message: String -> + val msg = error.message?.let { message: String -> " $message${if (!message.endsWith(".")) "." else ""}" - } + } ?: "" return "[$categoryDesc][$errorDesc]$msg" } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt index f9f21fc131..d908305357 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt @@ -39,7 +39,8 @@ internal class SubscriptionImpl( override val updatedAt: RealmInstant = RealmInstantImpl(RealmInterop.realm_sync_subscription_updated_at(nativePointer)) override val name: String? = RealmInterop.realm_sync_subscription_name(nativePointer) override val objectType: String = RealmInterop.realm_sync_subscription_object_class_name(nativePointer) - override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer) + // Trim the query to match the output of RealmQuery.description() + override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer).trim() @Suppress("invisible_member") override fun asQuery(type: KClass): RealmQuery { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt index 453e947935..968678cfc9 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionSetImpl.kt @@ -84,19 +84,17 @@ internal class SubscriptionSetImpl( try { val result: Any = withTimeout(timeout) { // TODO Assuming this is always a RealmImpl is probably dangerous. But should be safe until we introduce a public DynamicRealm. - withContext((realm as RealmImpl).notificationDispatcherHolder.dispatcher) { - val callback = object : SubscriptionSetCallback { - override fun onChange(state: CoreSubscriptionSetState) { - when (state) { - CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_COMPLETE -> { - channel.trySend(true) - } - CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_ERROR -> { - channel.trySend(false) - } - else -> { - // Ignore all other states, wait for either complete or error. - } + withContext((realm as RealmImpl).notificationScheduler.dispatcher) { + val callback = SubscriptionSetCallback { state -> + when (state) { + CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_COMPLETE -> { + channel.trySend(true) + } + CoreSubscriptionSetState.RLM_SYNC_SUBSCRIPTION_ERROR -> { + channel.trySend(false) + } + else -> { + // Ignore all other states, wait for either complete or error. } } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt index c23ae4c4d1..23d689a20d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncConfigurationImpl.kt @@ -36,6 +36,7 @@ import io.realm.kotlin.internal.interop.SyncErrorCallback import io.realm.kotlin.internal.interop.sync.SyncError import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode import io.realm.kotlin.internal.platform.fileExists +import io.realm.kotlin.log.RealmLog import io.realm.kotlin.mongodb.exceptions.ClientResetRequiredException import io.realm.kotlin.mongodb.exceptions.DownloadingRealmTimeOutException import io.realm.kotlin.mongodb.subscriptions @@ -52,8 +53,11 @@ import io.realm.kotlin.mongodb.sync.SyncSession import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.mongodb.kbson.BsonValue @@ -88,7 +92,7 @@ internal class SyncConfigurationImpl( val taskPointer: AtomicRef = atomic(null) try { val result: Any = withTimeout(initialRemoteData.timeout.inWholeMilliseconds) { - withContext(realm.notificationDispatcherHolder.dispatcher) { + withContext(realm.notificationScheduler.dispatcher) { val callback = AsyncOpenCallback { error: Throwable? -> if (error != null) { channel.trySend(error) @@ -194,10 +198,33 @@ internal class SyncConfigurationImpl( SyncErrorCallback { pointer: RealmSyncSessionPointer, error: SyncError -> val session = SyncSessionImpl(pointer) val syncError = convertSyncError(error) - - // Notify before/after callbacks too if error is client reset if (error.isClientResetRequested) { - initializerHelper.onSyncError(session, frozenAppPointer, error) + // If a Client Reset happened, we only get here if `onManualResetFallback` needs + // to be called. This means there is a high likelihood that users will want to + // call ClientResetRequiredException.executeClientReset() inside the callback. + // + // In order to do that, they will need to close the Realm first. + // + // On POSIX this will work fine, but on Windows this will fail as the + // C++ session still holds a DBPointer preventing the release of the file during + // the callback. + // + // So, in order to prevent errors on Windows, we are running the Kotlin callback + // on a separate worker thread. This will allow Core to finish its callback so + // when we close the Realm from the worker thread, the underlying + // session can also be fully freed. + // + // Given that we do not make any promises regarding which thread the callback + // is running on. This should be fine. + @OptIn(DelicateCoroutinesApi::class) + try { + GlobalScope.launch { + initializerHelper.onSyncError(session, frozenAppPointer, error) + } + } catch (ex: Exception) { + @Suppress("invisible_member") + RealmLog.error("Error thrown and ignored in `onManualResetFallback`: $ex") + } } else { userErrorHandler.onError(session, syncError) } 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/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 8409b7c556..6ce5e305c6 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -19,16 +19,15 @@ package io.realm.kotlin.mongodb.internal import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.NotificationToken import io.realm.kotlin.internal.RealmImpl +import io.realm.kotlin.internal.interop.CoreError +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmSyncSessionPointer import io.realm.kotlin.internal.interop.SyncSessionTransferCompletionCallback import io.realm.kotlin.internal.interop.sync.CoreConnectionState import io.realm.kotlin.internal.interop.sync.CoreSyncSessionState import io.realm.kotlin.internal.interop.sync.ProgressDirection -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.internal.util.Validation import io.realm.kotlin.internal.util.trySendWithBufferOverflowCheck import io.realm.kotlin.mongodb.User @@ -165,15 +164,13 @@ internal open class SyncSessionImpl( /** * Simulates a sync error. Internal visibility only for testing. */ - internal fun simulateError( - errorCode: ProtocolClientErrorCode, - category: SyncErrorCodeCategory, + internal fun simulateSyncError( + error: ErrorCode, message: String = "Simulate Client Reset" ) { RealmInterop.realm_sync_session_handle_error_for_testing( nativePointer, - errorCode, - category, + error, message, true ) @@ -199,9 +196,9 @@ internal open class SyncSessionImpl( val channel = Channel(1) try { val result: Any = withTimeout(timeout) { - withContext(realm.notificationDispatcherHolder.dispatcher) { + withContext(realm.notificationScheduler.dispatcher) { val callback = object : SyncSessionTransferCompletionCallback { - override fun invoke(errorCode: SyncErrorCode?) { + override fun invoke(errorCode: CoreError?) { if (errorCode != null) { // Transform the errorCode into a dummy syncError so we can have a // common path. @@ -250,6 +247,10 @@ internal open class SyncSessionImpl( } } + fun close() { + nativePointer.release() + } + internal companion object { internal fun stateFrom(coreState: CoreSyncSessionState): SyncSession.State { return when (coreState) { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt index 04f914e697..95c505b1f3 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncedRealmContext.kt @@ -32,7 +32,8 @@ import io.realm.kotlin.mongodb.sync.SyncSession * In order to work around the bootstrap problem, all public API entry points that access this * class must do so through the [executeInSyncContext] closure. */ -internal class SyncedRealmContext(realm: T) { +@OptIn(ExperimentalStdlibApi::class) +internal class SyncedRealmContext(realm: T) : AutoCloseable { // TODO For now this can only be a RealmImpl, which is required by the SyncSessionImpl // When we introduce a public DynamicRealm, this can also be a `DynamicRealmImpl` // And we probably need to modify the SyncSessionImpl to take either of these two. @@ -40,18 +41,27 @@ internal class SyncedRealmContext(realm: T) { internal val config: SyncConfiguration = baseRealm.configuration as SyncConfiguration // Note: Session and Subscriptions only need a valid dbPointer when being created, after that, they // have their own lifecycle and can be cached. - internal val session: SyncSession by lazy { + private val sessionDelegate: Lazy = lazy { SyncSessionImpl( baseRealm, RealmInterop.realm_sync_session_get(baseRealm.realmReference.dbPointer) ) } - internal val subscriptions: SubscriptionSet by lazy { + internal val session: SyncSession by sessionDelegate + + private val subscriptionsDelegate: Lazy> = lazy { SubscriptionSetImpl( realm, RealmInterop.realm_sync_get_latest_subscriptionset(baseRealm.realmReference.dbPointer) ) } + internal val subscriptions: SubscriptionSet by subscriptionsDelegate + + override fun close() { + if (sessionDelegate.isInitialized()) { + (session as SyncSessionImpl).close() + } + } } /** @@ -77,6 +87,7 @@ internal fun executeInSyncContext(realm: R, block: (context: } } +@OptIn(ExperimentalStdlibApi::class) private fun initSyncContextIfNeeded(realm: T): SyncedRealmContext { // INVARIANT: `syncContext` is only ever set once, and never to `null`. // This code works around the fact that `Mutex`'s can only be locked inside suspend functions on 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/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index db756d4769..6b1e865ae4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -568,6 +568,7 @@ public interface SyncConfiguration : Configuration { encryptionKey, compactOnLaunchCallback, null, // migration is not relevant for sync, + false, // automatic backlink handling is not relevant for sync initialDataCallback, partitionValue == null, inMemory, 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/plugin-compiler-shaded/build.gradle.kts b/packages/plugin-compiler-shaded/build.gradle.kts index 927f017dba..733801d17b 100644 --- a/packages/plugin-compiler-shaded/build.gradle.kts +++ b/packages/plugin-compiler-shaded/build.gradle.kts @@ -55,8 +55,8 @@ realmPublish { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } publishing { diff --git a/packages/plugin-compiler/build.gradle.kts b/packages/plugin-compiler/build.gradle.kts index 5dfd688326..d68062946b 100644 --- a/packages/plugin-compiler/build.gradle.kts +++ b/packages/plugin-compiler/build.gradle.kts @@ -43,8 +43,7 @@ dependencies { tasks.withType { kotlinOptions { - jvmTarget = "${Versions.jvmTarget}" - freeCompilerArgs = listOf("-Xjvm-default=enable") + freeCompilerArgs = listOf("-Xjvm-default=all-compatibility") } } @@ -69,6 +68,6 @@ publishing { java { withSourcesJar() withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt index 428929dace..ddf3f025f8 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/AccessorModifierIrGeneration.kt @@ -13,28 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.ASYMMETRIC_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.EMBEDDED_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.IGNORE_ANNOTATION -import io.realm.kotlin.compiler.FqNames.KBSON_DECIMAL128 -import io.realm.kotlin.compiler.FqNames.KBSON_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_ANY -import io.realm.kotlin.compiler.FqNames.REALM_BACKLINKS -import io.realm.kotlin.compiler.FqNames.REALM_DICTIONARY -import io.realm.kotlin.compiler.FqNames.REALM_EMBEDDED_BACKLINKS -import io.realm.kotlin.compiler.FqNames.REALM_INSTANT -import io.realm.kotlin.compiler.FqNames.REALM_LIST -import io.realm.kotlin.compiler.FqNames.REALM_MUTABLE_INTEGER -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_HELPER -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_SET -import io.realm.kotlin.compiler.FqNames.REALM_UUID -import io.realm.kotlin.compiler.FqNames.TRANSIENT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.ASYMMETRIC_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.EMBEDDED_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.IGNORE_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.KBSON_DECIMAL128 +import io.realm.kotlin.compiler.ClassIds.KBSON_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_ANY +import io.realm.kotlin.compiler.ClassIds.REALM_BACKLINKS +import io.realm.kotlin.compiler.ClassIds.REALM_DICTIONARY +import io.realm.kotlin.compiler.ClassIds.REALM_EMBEDDED_BACKLINKS +import io.realm.kotlin.compiler.ClassIds.REALM_INSTANT +import io.realm.kotlin.compiler.ClassIds.REALM_LIST +import io.realm.kotlin.compiler.ClassIds.REALM_MUTABLE_INTEGER +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_HELPER +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_SET +import io.realm.kotlin.compiler.ClassIds.REALM_UUID +import io.realm.kotlin.compiler.ClassIds.TRANSIENT_ANNOTATION import io.realm.kotlin.compiler.Names.OBJECT_REFERENCE import io.realm.kotlin.compiler.Names.REALM_ACCESSOR_HELPER_GET_BOOLEAN import io.realm.kotlin.compiler.Names.REALM_ACCESSOR_HELPER_GET_BYTE_ARRAY @@ -59,10 +58,8 @@ import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_LIST import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_OBJECT import io.realm.kotlin.compiler.Names.REALM_OBJECT_HELPER_SET_SET import io.realm.kotlin.compiler.Names.REALM_SYNTHETIC_PROPERTY_PREFIX -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.IrStatement -import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.builders.IrBlockBuilder import org.jetbrains.kotlin.ir.builders.Scope import org.jetbrains.kotlin.ir.builders.irBlockBody @@ -78,6 +75,8 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.descriptors.toIrBasedDescriptor +import org.jetbrains.kotlin.ir.descriptors.toIrBasedKotlinType import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrDeclarationReference import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin @@ -85,7 +84,6 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrSetFieldImpl import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.ir.types.classifierOrFail import org.jetbrains.kotlin.ir.types.impl.IrAbstractSimpleType import org.jetbrains.kotlin.ir.types.isBoolean import org.jetbrains.kotlin.ir.types.isByte @@ -100,16 +98,16 @@ import org.jetbrains.kotlin.ir.types.isShort import org.jetbrains.kotlin.ir.types.isString import org.jetbrains.kotlin.ir.types.isSubtypeOfClass import org.jetbrains.kotlin.ir.types.makeNotNull -import org.jetbrains.kotlin.ir.types.toKotlinType +import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.defaultType -import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.parentAsClass import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.classId -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.StarProjectionImpl import org.jetbrains.kotlin.types.isNullable @@ -120,7 +118,6 @@ import kotlin.collections.set * Modifies the IR tree to transform getter/setter to call the C-Interop layer to retrieve read the managed values from the Realm * It also collect the schema information while processing the class properties. */ -@OptIn(ObsoleteDescriptorBasedAPI::class) class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(REALM_OBJECT_HELPER) @@ -192,25 +189,25 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { // Top level SDK->Core converters private val byteToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.byteToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("byteToLong"))).first().owner private val charToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.charToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("charToLong"))).first().owner private val shortToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.shortToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("shortToLong"))).first().owner private val intToLong: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.intToLong")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("intToLong"))).first().owner // Top level Core->SDK converters private val longToByte: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToByte")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToByte"))).first().owner private val longToChar: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToChar")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToChar"))).first().owner private val longToShort: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToShort")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToShort"))).first().owner private val longToInt: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.longToInt")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("longToInt"))).first().owner private val objectIdToRealmObjectId: IrSimpleFunction = - pluginContext.referenceFunctions(FqName("io.realm.kotlin.internal.objectIdToRealmObjectId")).first().owner + pluginContext.referenceFunctions(CallableId(FqName("io.realm.kotlin.internal"), Name.identifier("objectIdToRealmObjectId"))).first().owner private lateinit var objectReferenceProperty: IrProperty private lateinit var objectReferenceType: IrType @@ -546,7 +543,7 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { if (!(isValidTargetType || isValidGenericType)) { val targetPropertyName = getLinkingObjectPropertyName(declaration.backingField!!) logError( - "Error in backlinks field '${declaration.name}' - target property '$targetPropertyName' does not reference '${sourceType.toKotlinType()}'.", + "Error in backlinks field '${declaration.name}' - target property '$targetPropertyName' does not reference '${sourceType.toIrBasedKotlinType().getKotlinTypeFqNameCompat(true)}'.", declaration.locationOf() ) } @@ -715,7 +712,7 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { name: String, declaration: IrProperty ) { - val type = declaration.symbol.descriptor.type + val type: KotlinType = declaration.symbol.owner.toIrBasedDescriptor().type if (type.arguments[0] is StarProjectionImpl) { logError( "Error in field ${declaration.name} - ${collectionType.description} cannot use a '*' projection.", @@ -960,80 +957,80 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { } private fun IrType.isRealmList(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmListClassId = realmListClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmListClassId: ClassId? = realmListClass.classId return propertyClassId == realmListClassId } private fun IrType.isRealmSet(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmSetClassId = realmSetClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmSetClassId: ClassId? = realmSetClass.classId return propertyClassId == realmSetClassId } private fun IrType.isRealmDictionary(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmDictionaryClassId = realmDictionaryClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmDictionaryClassId: ClassId? = realmDictionaryClass.classId return propertyClassId == realmDictionaryClassId } private fun IrType.isRealmInstant(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmInstantClassId = realmInstantClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmInstantClassId: ClassId? = realmInstantClass.classId return propertyClassId == realmInstantClassId } private fun IrType.isLinkingObject(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmBacklinksClassId: ClassId? = realmBacklinksClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmBacklinksClassId: ClassId? = realmBacklinksClass.classId return propertyClassId == realmBacklinksClassId } private fun IrType.isEmbeddedLinkingObject(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmEmbeddedBacklinksClassId: ClassId? = realmEmbeddedBacklinksClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmEmbeddedBacklinksClassId: ClassId? = realmEmbeddedBacklinksClass.classId return propertyClassId == realmEmbeddedBacklinksClassId } private fun IrType.isDecimal128(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = decimal128Class.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = decimal128Class.classId return propertyClassId == objectIdClassId } private fun IrType.isObjectId(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = objectIdClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = objectIdClass.classId return propertyClassId == objectIdClassId } private fun IrType.isRealmObjectId(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val objectIdClassId = realmObjectIdClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val objectIdClassId: ClassId? = realmObjectIdClass.classId return propertyClassId == objectIdClassId } private fun IrType.hasSameClassId(other: IrType): Boolean { - val classId = this.classifierOrFail.descriptor.classId - val otherClassId = other.classifierOrFail.descriptor.classId - return classId == otherClassId + val propertyClassId: ClassId = this.classIdOrFail() + val otherClassId = other.classIdOrFail() + return propertyClassId == otherClassId } private fun IrType.isRealmUUID(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val realmUUIDClassId = realmUUIDClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val realmUUIDClassId: ClassId? = realmUUIDClass.classId return propertyClassId == realmUUIDClassId } fun IrType.isMutableRealmInteger(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val mutableRealmIntegerClassId = mutableRealmIntegerClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val mutableRealmIntegerClassId: ClassId? = mutableRealmIntegerClass.classId return propertyClassId == mutableRealmIntegerClassId } fun IrType.isRealmAny(): Boolean { - val propertyClassId = this.classifierOrFail.descriptor.classId - val mutableRealmIntegerClassId = realmAnyClass.descriptor.classId + val propertyClassId: ClassId = this.classIdOrFail() + val mutableRealmIntegerClassId: ClassId? = realmAnyClass.classId return propertyClassId == mutableRealmIntegerClassId } @@ -1043,8 +1040,8 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { declaration: IrProperty ): CoreType? { // Check first if the generic is a subclass of RealmObject - val descriptorType = declaration.symbol.descriptor.type - val collectionGenericType = descriptorType.arguments[0].type + val descriptorType: KotlinType = declaration.toIrBasedDescriptor().type + val collectionGenericType: KotlinType = descriptorType.arguments[0].type val supertypes = collectionGenericType.constructor.supertypes val isEmbedded = inheritsFromRealmObject(supertypes, RealmObjectType.EMBEDDED) @@ -1101,10 +1098,11 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { } // Otherwise just return the matching core type present in the declaration - val genericPropertyType = getPropertyTypeFromKotlinType(collectionGenericType) + val genericPropertyType: PropertyType? = getPropertyTypeFromKotlinType(collectionGenericType) return if (genericPropertyType == null) { logError( - "Unsupported type for ${collectionType.description}: '$collectionGenericType'", + "Unsupported type for ${collectionType.description}: '${collectionGenericType.getKotlinTypeFqNameCompat(true) + }'", declaration.locationOf() ) null @@ -1160,12 +1158,12 @@ class AccessorModifierIrGeneration(private val pluginContext: IrPluginContext) { supertypes: Collection, objectType: RealmObjectType = RealmObjectType.EITHER ): Boolean = supertypes.any { - val objectFqNames = when (objectType) { + val objectFqNames: Set = when (objectType) { RealmObjectType.OBJECT -> realmObjectInterfaceFqNames RealmObjectType.EMBEDDED -> realmEmbeddedObjectInterfaceFqNames RealmObjectType.EITHER -> realmObjectInterfaceFqNames + realmEmbeddedObjectInterfaceFqNames } - it.constructor.declarationDescriptor?.fqNameSafe in objectFqNames + it.constructor.declarationDescriptor?.classId in objectFqNames } } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt index 27111250af..c4d1a87176 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Identifiers.kt @@ -16,9 +16,9 @@ package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.CLASS_APP_CONFIGURATION import io.realm.kotlin.compiler.FqNames.PACKAGE_MONGODB import io.realm.kotlin.compiler.FqNames.PACKAGE_MONGODB_INTERNAL +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -86,69 +86,79 @@ internal object Names { } internal object FqNames { + val PACKAGE_ANNOTATIONS = FqName("io.realm.kotlin.types.annotations") + val PACKAGE_KBSON = FqName("org.mongodb.kbson") + val PACKAGE_KOTLIN_COLLECTIONS = FqName("kotlin.collections") + val PACKAGE_KOTLIN_REFLECT = FqName("kotlin.reflect") + val PACKAGE_TYPES = FqName("io.realm.kotlin.types") + val PACKAGE_REALM_INTEROP = FqName("io.realm.kotlin.internal.interop") + val PACKAGE_REALM_INTERNAL = FqName("io.realm.kotlin.internal") + val PACKAGE_MONGODB = FqName("io.realm.kotlin.mongodb") + val PACKAGE_MONGODB_INTERNAL = FqName("io.realm.kotlin.mongodb.internal") +} + +object ClassIds { + // TODO we can replace with RealmObject::class.java.canonicalName if we make the runtime_api available as a compile time only dependency for the compiler-plugin val REALM_NATIVE_POINTER = FqName("io.realm.kotlin.internal.interop.NativePointer") - val REALM_OBJECT_INTERNAL_INTERFACE = FqName("io.realm.kotlin.internal.RealmObjectInternal") + val REALM_OBJECT_INTERNAL_INTERFACE = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectInternal")) + + val REALM_MODEL_COMPANION = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectCompanion")) + val REALM_OBJECT_HELPER = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectHelper")) + val REALM_CLASS_IMPL = ClassId(FqName("io.realm.kotlin.internal.schema"), Name.identifier("RealmClassImpl")) + val OBJECT_REFERENCE_CLASS = ClassId(FqNames.PACKAGE_REALM_INTERNAL, Name.identifier("RealmObjectReference")) - val REALM_MODEL_COMPANION = FqName("io.realm.kotlin.internal.RealmObjectCompanion") - val REALM_OBJECT_HELPER = FqName("io.realm.kotlin.internal.RealmObjectHelper") - val REALM_CLASS_IMPL = FqName("io.realm.kotlin.internal.schema.RealmClassImpl") - val OBJECT_REFERENCE_CLASS = FqName("io.realm.kotlin.internal.RealmObjectReference") + val BASE_REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("BaseRealmObject")) + val REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmObject")) + val TYPED_REALM_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("TypedRealmObject")) + val EMBEDDED_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("EmbeddedRealmObject")) + val ASYMMETRIC_OBJECT_INTERFACE = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("AsymmetricRealmObject")) - val BASE_REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.BaseRealmObject") - val REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.RealmObject") - val TYPED_REALM_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.TypedRealmObject") - val EMBEDDED_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.EmbeddedRealmObject") - val ASYMMETRIC_OBJECT_INTERFACE = FqName("io.realm.kotlin.types.AsymmetricRealmObject") + val CLASS_APP_CONFIGURATION = ClassId(FqNames.PACKAGE_MONGODB, Name.identifier("AppConfiguration")) // External visible interface of Realm objects - val KOTLIN_COLLECTIONS_SET = FqName("kotlin.collections.Set") - val KOTLIN_COLLECTIONS_LIST = FqName("kotlin.collections.List") - val KOTLIN_COLLECTIONS_LISTOF = FqName("kotlin.collections.listOf") - val KOTLIN_COLLECTIONS_MAP = FqName("kotlin.collections.Map") - val KOTLIN_COLLECTIONS_MAPOF = FqName("kotlin.collections.mapOf") - val KOTLIN_REFLECT_KMUTABLEPROPERTY1 = FqName("kotlin.reflect.KMutableProperty1") - val KOTLIN_REFLECT_KPROPERTY1 = FqName("kotlin.reflect.KProperty1") - val KOTLIN_PAIR = FqName("kotlin.Pair") + val KOTLIN_COLLECTIONS_SET = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("Set")) + val KOTLIN_COLLECTIONS_LIST = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("List")) + val KOTLIN_COLLECTIONS_LISTOF = CallableId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("listOf")) + val KOTLIN_COLLECTIONS_MAP = ClassId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("Map")) + val KOTLIN_COLLECTIONS_MAPOF = CallableId(FqNames.PACKAGE_KOTLIN_COLLECTIONS, Name.identifier("mapOf")) + val KOTLIN_REFLECT_KMUTABLEPROPERTY1 = ClassId(FqNames.PACKAGE_KOTLIN_REFLECT, Name.identifier("KMutableProperty1")) + val KOTLIN_REFLECT_KPROPERTY1 = ClassId(FqNames.PACKAGE_KOTLIN_REFLECT, Name.identifier("KProperty1")) + val KOTLIN_PAIR = ClassId(FqName("kotlin"), Name.identifier("Pair")) // Schema related types - val CLASS_INFO = FqName("io.realm.kotlin.internal.interop.ClassInfo") - val PROPERTY_INFO = FqName("io.realm.kotlin.internal.interop.PropertyInfo") - val PROPERTY_TYPE = FqName("io.realm.kotlin.internal.interop.PropertyType") - val COLLECTION_TYPE = FqName("io.realm.kotlin.internal.interop.CollectionType") - val PRIMARY_KEY_ANNOTATION = FqName("io.realm.kotlin.types.annotations.PrimaryKey") - val INDEX_ANNOTATION = FqName("io.realm.kotlin.types.annotations.Index") - val FULLTEXT_ANNOTATION = FqName("io.realm.kotlin.types.annotations.FullText") - val IGNORE_ANNOTATION = FqName("io.realm.kotlin.types.annotations.Ignore") - val PERSISTED_NAME_ANNOTATION = FqName("io.realm.kotlin.types.annotations.PersistedName") - val TRANSIENT_ANNOTATION = FqName("kotlin.jvm.Transient") - val MODEL_OBJECT_ANNOTATION = FqName("io.realm.kotlin.internal.platform.ModelObject") - val PROPERTY_INFO_CREATE = FqName("io.realm.kotlin.internal.schema.createPropertyInfo") - val CLASS_KIND_TYPE = FqName("io.realm.kotlin.schema.RealmClassKind") + val CLASS_INFO = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("ClassInfo")) + val PROPERTY_INFO = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("PropertyInfo")) + val PROPERTY_TYPE = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("PropertyType")) + val COLLECTION_TYPE = ClassId(FqNames.PACKAGE_REALM_INTEROP, Name.identifier("CollectionType")) + val PRIMARY_KEY_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("PrimaryKey")) + val INDEX_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("Index")) + val FULLTEXT_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("FullText")) + val IGNORE_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("Ignore")) + val PERSISTED_NAME_ANNOTATION = ClassId(FqNames.PACKAGE_ANNOTATIONS, Name.identifier("PersistedName")) + val TRANSIENT_ANNOTATION = ClassId(FqName("kotlin.jvm"), Name.identifier("Transient")) + val MODEL_OBJECT_ANNOTATION = ClassId(FqName("io.realm.kotlin.internal.platform"), Name.identifier("ModelObject")) + val PROPERTY_INFO_CREATE = CallableId(FqName("io.realm.kotlin.internal.schema"), Name.identifier("createPropertyInfo")) + val CLASS_KIND_TYPE = ClassId(FqName("io.realm.kotlin.schema"), Name.identifier("RealmClassKind")) // Realm data types - val REALM_LIST = FqName("io.realm.kotlin.types.RealmList") - val REALM_SET = FqName("io.realm.kotlin.types.RealmSet") - val REALM_DICTIONARY = FqName("io.realm.kotlin.types.RealmDictionary") - val REALM_INSTANT = FqName("io.realm.kotlin.types.RealmInstant") - val REALM_BACKLINKS = FqName("io.realm.kotlin.types.BacklinksDelegate") - val REALM_EMBEDDED_BACKLINKS = FqName("io.realm.kotlin.types.EmbeddedBacklinksDelegate") - val REALM_OBJECT_ID = FqName("io.realm.kotlin.types.ObjectId") - val KBSON_OBJECT_ID = FqName("org.mongodb.kbson.BsonObjectId") - val KBSON_DECIMAL128 = FqName("org.mongodb.kbson.BsonDecimal128") - val REALM_UUID = FqName("io.realm.kotlin.types.RealmUUID") - val REALM_MUTABLE_INTEGER = FqName("io.realm.kotlin.types.MutableRealmInt") - val REALM_ANY = FqName("io.realm.kotlin.types.RealmAny") - - val PACKAGE_MONGODB = FqName("io.realm.kotlin.mongodb") - val PACKAGE_MONGODB_INTERNAL = FqName("io.realm.kotlin.mongodb.internal") - val CLASS_APP_CONFIGURATION = FqName("io.realm.kotlin.mongodb.AppConfiguration") -} - -object ClassIds { + val REALM_LIST = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmList")) + val REALM_SET = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmSet")) + val REALM_DICTIONARY = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmDictionary")) + val REALM_INSTANT = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmInstant")) + val REALM_BACKLINKS = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("BacklinksDelegate")) + val REALM_EMBEDDED_BACKLINKS = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("EmbeddedBacklinksDelegate")) + val REALM_OBJECT_ID = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("ObjectId")) + val KBSON_OBJECT_ID = ClassId(FqNames.PACKAGE_KBSON, Name.identifier("BsonObjectId")) + val KBSON_DECIMAL128 = ClassId(FqNames.PACKAGE_KBSON, Name.identifier("BsonDecimal128")) + val REALM_UUID = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmUUID")) + val REALM_MUTABLE_INTEGER = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("MutableRealmInt")) + val REALM_ANY = ClassId(FqNames.PACKAGE_TYPES, Name.identifier("RealmAny")) + + // Sync types val APP = ClassId(PACKAGE_MONGODB, Name.identifier("App")) val APP_IMPL = ClassId(PACKAGE_MONGODB_INTERNAL, Name.identifier("AppImpl")) val APP_CONFIGURATION = ClassId(PACKAGE_MONGODB, Name.identifier("AppConfiguration")) val APP_CONFIGURATION_IMPL = ClassId(PACKAGE_MONGODB_INTERNAL, Name.identifier("AppConfigurationImpl")) - val APP_CONFIGURATION_BUILDER = ClassId(CLASS_APP_CONFIGURATION, Name.identifier("Builder")) + val APP_CONFIGURATION_BUILDER = ClassId(FqName("io.realm.kotlin.mongodb.AppConfiguration"), FqName("Builder"), true) } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt index 49d337ae5d..7c5d1f5f13 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/IrUtils.kt @@ -13,25 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.ASYMMETRIC_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.BASE_REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.EMBEDDED_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_LISTOF -import io.realm.kotlin.compiler.FqNames.PERSISTED_NAME_ANNOTATION -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI +import io.realm.kotlin.compiler.ClassIds.ASYMMETRIC_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.BASE_REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.EMBEDDED_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_LISTOF +import io.realm.kotlin.compiler.ClassIds.PERSISTED_NAME_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocationWithRange import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation +import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder import org.jetbrains.kotlin.ir.builders.IrBlockBuilder @@ -49,6 +50,7 @@ import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.builders.irBlockBody import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin @@ -82,13 +84,12 @@ import org.jetbrains.kotlin.ir.symbols.IrValueSymbol import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.impl.IrAbstractSimpleType -import org.jetbrains.kotlin.ir.types.impl.IrErrorClassImpl.superTypes import org.jetbrains.kotlin.ir.types.impl.IrTypeBase import org.jetbrains.kotlin.ir.types.makeNullable -import org.jetbrains.kotlin.ir.types.superTypes import org.jetbrains.kotlin.ir.types.typeWith +import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.companionObject import org.jetbrains.kotlin.ir.util.copyTo import org.jetbrains.kotlin.ir.util.file @@ -97,12 +98,16 @@ import org.jetbrains.kotlin.ir.util.getPropertyGetter import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.isVararg import org.jetbrains.kotlin.ir.util.properties +import org.jetbrains.kotlin.ir.util.render import org.jetbrains.kotlin.ir.util.superTypes import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.SUPER_TYPE_LIST +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.types.KotlinType import java.lang.reflect.Field import java.util.function.Predicate @@ -129,11 +134,13 @@ fun IrPluginContext.blockBody( val ClassDescriptor.isRealmObjectCompanion get() = isCompanionObject && (containingDeclaration as ClassDescriptor).isBaseRealmObject -val realmObjectInterfaceFqNames = setOf(FqNames.REALM_OBJECT_INTERFACE) -val realmEmbeddedObjectInterfaceFqNames = setOf(FqNames.EMBEDDED_OBJECT_INTERFACE) -val realmAsymmetricObjectInterfaceFqNames = setOf(FqNames.ASYMMETRIC_OBJECT_INTERFACE) +val realmObjectInterfaceFqNames = setOf(REALM_OBJECT_INTERFACE) +val realmEmbeddedObjectInterfaceFqNames = setOf(EMBEDDED_OBJECT_INTERFACE) +val realmAsymmetricObjectInterfaceFqNames = setOf(ASYMMETRIC_OBJECT_INTERFACE) val anyRealmObjectInterfacesFqNames = realmObjectInterfaceFqNames + realmEmbeddedObjectInterfaceFqNames + realmAsymmetricObjectInterfaceFqNames +fun IrType.classIdOrFail(): ClassId = getClass()?.classId ?: error("Can't get classId of ${render()}") + inline fun ClassDescriptor.hasInterfacePsi(interfaces: Set): Boolean { // Using PSI to find super types to avoid cyclic reference (see https://github.com/realm/realm-kotlin/issues/339) var hasRealmObjectAsSuperType = false @@ -179,27 +186,37 @@ val ClassDescriptor.isEmbeddedRealmObject: Boolean val ClassDescriptor.isBaseRealmObject: Boolean get() = this.hasInterfacePsi(realmObjectPsiNames + embeddedRealmObjectPsiNames + asymmetricRealmObjectPsiNames) && !this.hasInterfacePsi(realmJavaObjectPsiNames) +// JetBrains already have a method `fun IrAnnotationContainer.hasAnnotation(symbol: IrClassSymbol)` +// It is unclear exactly what the difference is and how to get a ClassSymbol from a ClassId, +// so for now just work around it. +fun IrAnnotationContainer?.hasAnnotation(annotation: ClassId): Boolean { + return this?.hasAnnotation(annotation.asSingleFqName()) ?: false +} + fun IrMutableAnnotationContainer.hasAnnotation(annotation: FqName): Boolean { return annotations.hasAnnotation(annotation) } val IrClass.isBaseRealmObject - get() = superTypes.any { it.classFqName in anyRealmObjectInterfacesFqNames } + get() = superTypes.any { it.classId in anyRealmObjectInterfacesFqNames } val IrClass.isRealmObject - get() = superTypes.any { it.classFqName == BASE_REALM_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == BASE_REALM_OBJECT_INTERFACE } val IrClass.isEmbeddedRealmObject: Boolean - get() = superTypes.any { it.classFqName == EMBEDDED_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == EMBEDDED_OBJECT_INTERFACE } val IrClass.isAsymmetricRealmObject: Boolean - get() = superTypes.any { it.classFqName == ASYMMETRIC_OBJECT_INTERFACE } + get() = superTypes.any { it.classId == ASYMMETRIC_OBJECT_INTERFACE } + +val IrType.classId: ClassId? + get() = this.getClass()?.classId val IrType.isEmbeddedRealmObject: Boolean - get() = superTypes().any { it.classFqName == EMBEDDED_OBJECT_INTERFACE } + get() = superTypes().any { it.classId == EMBEDDED_OBJECT_INTERFACE } val IrType.isAsymmetricRealmObject: Boolean - get() = superTypes().any { it.classFqName == ASYMMETRIC_OBJECT_INTERFACE } + get() = superTypes().any { it.classId == ASYMMETRIC_OBJECT_INTERFACE } internal fun IrFunctionBuilder.at(startOffset: Int, endOffset: Int) = also { this.startOffset = startOffset @@ -227,10 +244,10 @@ internal fun IrClass.lookupProperty(name: Name): IrProperty { } internal fun IrPluginContext.lookupFunctionInClass( - fqName: FqName, + clazz: ClassId, function: String ): IrSimpleFunction { - return lookupClassOrThrow(fqName).functions.first { + return lookupClassOrThrow(clazz).functions.first { it.name == Name.identifier(function) } } @@ -240,16 +257,11 @@ internal fun IrPluginContext.lookupClassOrThrow(name: ClassId): IrClass { ?: fatalError("Cannot find ${name.asString()} on platform $platform.") } -internal fun IrPluginContext.lookupClassOrThrow(name: FqName): IrClass { - return referenceClass(name)?.owner - ?: fatalError("Cannot find ${name.asString()} on platform $platform.") -} - internal fun IrPluginContext.lookupConstructorInClass( - fqName: FqName, + clazz: ClassId, filter: (ctor: IrConstructorSymbol) -> Boolean = { true } ): IrConstructorSymbol { - return referenceConstructors(fqName).first { + return referenceConstructors(clazz).first { filter(it) } } @@ -263,6 +275,28 @@ internal fun IrClass.lookupCompanionDeclaration( ?: fatalError("Cannot find companion method ${name.asString()} on ${this.name}") } +// Copy of `KotlinType.getKotlinTypeFqName` from Kotlin 1.8.21. This method needs to be backported +// as it is not available in Kotlin 1.8.0. +internal fun KotlinType.getKotlinTypeFqNameCompat(printTypeArguments: Boolean): String { + val declaration = requireNotNull(constructor.declarationDescriptor) { + "declarationDescriptor is null for constructor = $constructor with ${constructor.javaClass}" + } + if (declaration is TypeParameterDescriptor) { + return StringUtil.join(declaration.upperBounds, { type -> type.getKotlinTypeFqNameCompat(printTypeArguments) }, "&") + } + + val typeArguments = arguments + val typeArgumentsAsString = if (printTypeArguments && !typeArguments.isEmpty()) { + val joinedTypeArguments = StringUtil.join(typeArguments, { projection -> projection.type.getKotlinTypeFqNameCompat(false) }, ", ") + + "<$joinedTypeArguments>" + } else { + "" + } + + return DescriptorUtils.getFqName(declaration).asString() + typeArgumentsAsString +} + object SchemaCollector { val properties = mutableMapOf>() } @@ -344,7 +378,7 @@ data class SchemaProperty( companion object { fun getPersistedName(declaration: IrProperty): String { @Suppress("UNCHECKED_CAST") - return (declaration.getAnnotation(PERSISTED_NAME_ANNOTATION).getValueArgument(0)!! as IrConstImpl).value + return (declaration.getAnnotation(PERSISTED_NAME_ANNOTATION.asSingleFqName()).getValueArgument(0)!! as IrConstImpl).value } } } @@ -391,12 +425,12 @@ internal fun buildSetOf( elementType: IrType, args: List ): IrExpression { - val setOf = context.referenceFunctions(FqName("kotlin.collections.setOf")) + val setOf = context.referenceFunctions(CallableId(FqName("kotlin.collections"), Name.identifier("setOf"))) .first { val parameters = it.owner.valueParameters parameters.size == 1 && parameters.first().isVararg } - val setIrClass: IrClass = context.lookupClassOrThrow(FqNames.KOTLIN_COLLECTIONS_SET) + val setIrClass: IrClass = context.lookupClassOrThrow(ClassIds.KOTLIN_COLLECTIONS_SET) return buildOf(context, startOffset, endOffset, setOf, setIrClass, elementType, args) } @@ -412,7 +446,7 @@ internal fun buildListOf( val parameters = it.owner.valueParameters parameters.size == 1 && parameters.first().isVararg } - val listIrClass: IrClass = context.lookupClassOrThrow(FqNames.KOTLIN_COLLECTIONS_LIST) + val listIrClass: IrClass = context.lookupClassOrThrow(ClassIds.KOTLIN_COLLECTIONS_LIST) return buildOf(context, startOffset, endOffset, listOf, listIrClass, elementType, args) } @@ -587,11 +621,11 @@ fun getBacklinksTargetPropertyType(declaration: IrProperty): IrType? { fun getLinkingObjectPropertyName(backingField: IrField): String { (backingField.initializer!!.expression as IrCall).let { irCall -> val propertyReference = irCall.getValueArgument(0) as IrPropertyReference - val targetProperty = propertyReference.symbol.owner + val targetProperty: IrProperty = propertyReference.symbol.owner return if (targetProperty.hasAnnotation(PERSISTED_NAME_ANNOTATION)) { SchemaProperty.getPersistedName(targetProperty) } else { - propertyReference.referencedName.identifier + targetProperty.name.identifier } } } @@ -602,7 +636,7 @@ fun getLinkingObjectPropertyName(backingField: IrField): String { fun getSchemaClassName(clazz: IrClass): String { return if (clazz.hasAnnotation(PERSISTED_NAME_ANNOTATION)) { @Suppress("UNCHECKED_CAST") - return (clazz.getAnnotation(PERSISTED_NAME_ANNOTATION).getValueArgument(0)!! as IrConstImpl).value + return (clazz.getAnnotation(PERSISTED_NAME_ANNOTATION.asSingleFqName()).getValueArgument(0)!! as IrConstImpl).value } else { clazz.name.identifier } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt new file mode 100644 index 0000000000..996c947463 --- /dev/null +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelDefaultMethodGeneration.kt @@ -0,0 +1,123 @@ +package io.realm.kotlin.compiler + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irGetObject +import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrProperty +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.name.Name + +/** + * Class responsible for adding Realm specific logic to the default object methods like: + * - toString() + * - hashCode() + * - equals() + * + * WARNING: The current logic in here does not work well with incremental compilation. The reason + * is that we check if these methods are "empty" before filling them out, and during incremental + * compilation they already have content, and since all of these methods are using inlined + * methods they will not pick up changes in the RealmObjectHelper. + * + * This should only impact us as SDK developers though, but it does mean that changes to + * RealmObjectHelper methods will require a clean build to take effect. + */ +class RealmModelDefaultMethodGeneration(private val pluginContext: IrPluginContext) { + + private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(ClassIds.REALM_OBJECT_HELPER) + private val realmToString: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmToString")) + private val realmEquals: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmEquals")) + private val realmHashCode: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmHashCode")) + private lateinit var objectReferenceProperty: IrProperty + private lateinit var objectReferenceType: IrType + + fun addDefaultMethods(irClass: IrClass) { + objectReferenceProperty = irClass.lookupProperty(Names.OBJECT_REFERENCE) + objectReferenceType = objectReferenceProperty.backingField!!.type + + if (syntheticMethodExists(irClass, "toString")) { + addToStringMethodBody(irClass) + } + if (syntheticMethodExists(irClass, "hashCode")) { + addHashCodeMethod(irClass) + } + if (syntheticMethodExists(irClass, "equals")) { + addEqualsMethod(irClass) + } + } + + /** + * Checks if a synthetic method exists in the given class. Methods in super classes + * are ignored, only methods actually declared in the class will return `true`. + * + * These methods are created by an earlier step by the Realm compiler plugin and are + * recognized by not being fake and having an empty body.ß + */ + private fun syntheticMethodExists(irClass: IrClass, methodName: String): Boolean { + return irClass.functions.firstOrNull { + !it.isFakeOverride && it.body == null && it.name == Name.identifier(methodName) + } != null + } + + private fun addEqualsMethod(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "equals" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.booleanType, + symbol = realmEquals.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 2 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + putValueArgument(1, irGet(function.valueParameters[0].type, function.valueParameters[0].symbol)) + } + ) + } + } + + private fun addHashCodeMethod(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "hashCode" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.intType, + symbol = realmHashCode.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 1 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + } + ) + } + } + + private fun addToStringMethodBody(irClass: IrClass) { + val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "toString" } + function.body = pluginContext.blockBody(function.symbol) { + +irReturn( + IrCallImpl( + startOffset = startOffset, + endOffset = endOffset, + type = pluginContext.irBuiltIns.stringType, + symbol = realmToString.symbol, + typeArgumentsCount = 0, + valueArgumentsCount = 1 + ).apply { + dispatchReceiver = irGetObject(realmObjectHelper.symbol) + putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol)) + } + ) + } + } +} diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt index 9529a5124b..7aac109667 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt @@ -16,9 +16,9 @@ package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.MODEL_OBJECT_ANNOTATION -import io.realm.kotlin.compiler.FqNames.REALM_MODEL_COMPANION -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERNAL_INTERFACE +import io.realm.kotlin.compiler.ClassIds.MODEL_OBJECT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.REALM_MODEL_COMPANION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERNAL_INTERFACE import org.jetbrains.kotlin.backend.common.ClassLoweringPass import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext @@ -107,6 +107,10 @@ private class RealmModelLowering(private val pluginContext: IrPluginContext) : C // Modify properties accessor to generate custom getter/setter AccessorModifierIrGeneration(pluginContext).modifyPropertiesAndCollectSchema(irClass) + // Add custom toString, equals and hashCode methods + val methodGenerator = RealmModelDefaultMethodGeneration(pluginContext) + methodGenerator.addDefaultMethods(irClass) + // Add body for synthetic companion methods val companion = irClass.companionObject() ?: fatalError("RealmObject without companion: ${irClass.kotlinFqName}") generator.addCompanionFields(irClass, companion, SchemaCollector.properties[irClass]) diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt index 02b8658fb5..f72760985b 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticCompanionExtension.kt @@ -19,12 +19,9 @@ package io.realm.kotlin.compiler import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_NEW_INSTANCE_METHOD import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_SCHEMA_METHOD import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor -import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor -import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor import org.jetbrains.kotlin.descriptors.annotations.Annotations import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl @@ -33,11 +30,6 @@ import org.jetbrains.kotlin.name.SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension -import org.jetbrains.kotlin.resolve.lazy.LazyClassContext -import org.jetbrains.kotlin.resolve.lazy.declarations.ClassMemberDeclarationProvider -import org.jetbrains.kotlin.resolve.lazy.declarations.PackageMemberDeclarationProvider -import org.jetbrains.kotlin.types.KotlinType -import java.util.ArrayList /** * Triggers generation of companion objects and ensures that the companion object implement the @@ -58,48 +50,6 @@ class RealmModelSyntheticCompanionExtension : SyntheticResolveExtension { } } - override fun addSyntheticSupertypes( - thisDescriptor: ClassDescriptor, - supertypes: MutableList - ) { - } - - override fun generateSyntheticClasses( - thisDescriptor: ClassDescriptor, - name: Name, - ctx: LazyClassContext, - declarationProvider: ClassMemberDeclarationProvider, - result: MutableSet - ) { - } - - override fun generateSyntheticClasses( - thisDescriptor: PackageFragmentDescriptor, - name: Name, - ctx: LazyClassContext, - declarationProvider: PackageMemberDeclarationProvider, - result: MutableSet - ) { - } - - override fun generateSyntheticProperties( - thisDescriptor: ClassDescriptor, - name: Name, - bindingContext: BindingContext, - fromSupertypes: ArrayList, - result: MutableSet - ) { - } - - override fun generateSyntheticSecondaryConstructors( - thisDescriptor: ClassDescriptor, - bindingContext: BindingContext, - result: MutableCollection - ) { - } - - override fun getSyntheticNestedClassNames(thisDescriptor: ClassDescriptor): List = emptyList() - override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List { return when { thisDescriptor.isRealmObjectCompanion -> { diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt new file mode 100644 index 0000000000..5342091838 --- /dev/null +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticMethodsExtension.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2020 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.compiler + +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl +import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns +import org.jetbrains.kotlin.resolve.descriptorUtil.parents +import org.jetbrains.kotlin.resolve.extensions.SyntheticResolveExtension +import org.jetbrains.kotlin.types.SimpleType + +/** + * Triggers generation of synthetic methods on Realm model classes, in particular + * `toString()`, `equals()` and `hashCode()`. + */ +@Suppress("ComplexCondition") +class RealmModelSyntheticMethodsExtension : SyntheticResolveExtension { + + override fun generateSyntheticMethods( + thisDescriptor: ClassDescriptor, + name: Name, + bindingContext: BindingContext, + fromSupertypes: List, + result: MutableCollection + ) { + if (thisDescriptor.isRealmObject && + !thisDescriptor.isCompanionObject && /* Do not override companion object methods */ + !thisDescriptor.isInner && /* Do not override inner class methods */ + !isNestedInRealmModelClass(thisDescriptor) && /* do not override nested class methods */ + result.isEmpty() /* = no method has been declared in the current class */ + ) { + when (name.identifier) { + "toString" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = emptyList(), + returnType = thisDescriptor.builtIns.stringType + ) + ) + } + "equals" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = listOf(Pair("other", thisDescriptor.builtIns.nullableAnyType)), + returnType = thisDescriptor.builtIns.booleanType + ) + ) + } + "hashCode" -> { + result.add( + createMethod( + classDescriptor = thisDescriptor, + methodName = name, + arguments = emptyList(), + returnType = thisDescriptor.builtIns.intType + ) + ) + } + } + } + } + + private fun createMethod( + classDescriptor: ClassDescriptor, + methodName: Name, + arguments: List>, + returnType: SimpleType + ): SimpleFunctionDescriptor { + return SimpleFunctionDescriptorImpl.create( + classDescriptor, + Annotations.EMPTY, + methodName, + CallableMemberDescriptor.Kind.SYNTHESIZED, + classDescriptor.source + ).apply { + initialize( + null, + classDescriptor.thisAsReceiverParameter, + emptyList(), + emptyList(), + arguments.map { (argumentName, argumentType) -> + ValueParameterDescriptorImpl( + containingDeclaration = this, + original = null, + index = 0, + annotations = Annotations.EMPTY, + name = Name.identifier(argumentName), + outType = argumentType, + declaresDefaultValue = false, + isCrossinline = false, + isNoinline = false, + varargElementType = null, + source = this.source + ) + }, + returnType, + Modality.OPEN, + DescriptorVisibilities.PUBLIC + ) + } + } + + private fun isNestedInRealmModelClass(classDescriptor: ClassDescriptor): Boolean { + return classDescriptor.parents.firstOrNull { + return if (it is ClassDescriptor) { + it.isRealmObject + } else { + false + } + } != null + } +} diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt index 868fe782a9..db43326fcf 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelSyntheticPropertiesGeneration.kt @@ -13,32 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(FirIncompatiblePluginAPI::class) package io.realm.kotlin.compiler -import io.realm.kotlin.compiler.FqNames.CLASS_INFO -import io.realm.kotlin.compiler.FqNames.CLASS_KIND_TYPE -import io.realm.kotlin.compiler.FqNames.COLLECTION_TYPE -import io.realm.kotlin.compiler.FqNames.FULLTEXT_ANNOTATION -import io.realm.kotlin.compiler.FqNames.INDEX_ANNOTATION -import io.realm.kotlin.compiler.FqNames.KBSON_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_MAP -import io.realm.kotlin.compiler.FqNames.KOTLIN_COLLECTIONS_MAPOF -import io.realm.kotlin.compiler.FqNames.KOTLIN_PAIR -import io.realm.kotlin.compiler.FqNames.OBJECT_REFERENCE_CLASS -import io.realm.kotlin.compiler.FqNames.PRIMARY_KEY_ANNOTATION -import io.realm.kotlin.compiler.FqNames.PROPERTY_INFO -import io.realm.kotlin.compiler.FqNames.PROPERTY_INFO_CREATE -import io.realm.kotlin.compiler.FqNames.PROPERTY_TYPE -import io.realm.kotlin.compiler.FqNames.REALM_ANY -import io.realm.kotlin.compiler.FqNames.REALM_INSTANT -import io.realm.kotlin.compiler.FqNames.REALM_MODEL_COMPANION -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_ID -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_OBJECT_INTERNAL_INTERFACE -import io.realm.kotlin.compiler.FqNames.REALM_UUID -import io.realm.kotlin.compiler.FqNames.TYPED_REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.CLASS_INFO +import io.realm.kotlin.compiler.ClassIds.CLASS_KIND_TYPE +import io.realm.kotlin.compiler.ClassIds.COLLECTION_TYPE +import io.realm.kotlin.compiler.ClassIds.FULLTEXT_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.INDEX_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.KBSON_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_MAP +import io.realm.kotlin.compiler.ClassIds.KOTLIN_COLLECTIONS_MAPOF +import io.realm.kotlin.compiler.ClassIds.KOTLIN_PAIR +import io.realm.kotlin.compiler.ClassIds.OBJECT_REFERENCE_CLASS +import io.realm.kotlin.compiler.ClassIds.PRIMARY_KEY_ANNOTATION +import io.realm.kotlin.compiler.ClassIds.PROPERTY_INFO +import io.realm.kotlin.compiler.ClassIds.PROPERTY_INFO_CREATE +import io.realm.kotlin.compiler.ClassIds.PROPERTY_TYPE +import io.realm.kotlin.compiler.ClassIds.REALM_ANY +import io.realm.kotlin.compiler.ClassIds.REALM_INSTANT +import io.realm.kotlin.compiler.ClassIds.REALM_MODEL_COMPANION +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_ID +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_OBJECT_INTERNAL_INTERFACE +import io.realm.kotlin.compiler.ClassIds.REALM_UUID +import io.realm.kotlin.compiler.ClassIds.TYPED_REALM_OBJECT_INTERFACE import io.realm.kotlin.compiler.Names.CLASS_INFO_CREATE import io.realm.kotlin.compiler.Names.OBJECT_REFERENCE import io.realm.kotlin.compiler.Names.PROPERTY_COLLECTION_TYPE_DICTIONARY @@ -55,13 +54,11 @@ import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_NEW_INSTANCE_METHOD import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_PRIMARY_KEY_MEMBER import io.realm.kotlin.compiler.Names.REALM_OBJECT_COMPANION_SCHEMA_METHOD import io.realm.kotlin.compiler.Names.SET -import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality -import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.builders.at import org.jetbrains.kotlin.ir.builders.declarations.addGetter @@ -96,7 +93,7 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrPropertyReferenceImpl import org.jetbrains.kotlin.ir.expressions.impl.IrSetFieldImpl import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.classifierOrFail +import org.jetbrains.kotlin.ir.types.classFqName import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.isNullable import org.jetbrains.kotlin.ir.types.makeNullable @@ -156,10 +153,10 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi private val realmAnyType: IrType = pluginContext.lookupClassOrThrow(REALM_ANY).defaultType private val kMutableProperty1Class: IrClass = - pluginContext.lookupClassOrThrow(FqNames.KOTLIN_REFLECT_KMUTABLEPROPERTY1) + pluginContext.lookupClassOrThrow(ClassIds.KOTLIN_REFLECT_KMUTABLEPROPERTY1) private val kProperty1Class: IrClass = - pluginContext.lookupClassOrThrow(FqNames.KOTLIN_REFLECT_KPROPERTY1) + pluginContext.lookupClassOrThrow(ClassIds.KOTLIN_REFLECT_KPROPERTY1) private val mapClass: IrClass = pluginContext.lookupClassOrThrow(KOTLIN_COLLECTIONS_MAP) private val pairClass: IrClass = pluginContext.lookupClassOrThrow(KOTLIN_PAIR) @@ -191,8 +188,8 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi realmObjectMutablePropertyType ) - val realmClassImpl = pluginContext.lookupClassOrThrow(FqNames.REALM_CLASS_IMPL) - private val realmClassCtor = pluginContext.lookupConstructorInClass(FqNames.REALM_CLASS_IMPL) { + val realmClassImpl = pluginContext.lookupClassOrThrow(ClassIds.REALM_CLASS_IMPL) + private val realmClassCtor = pluginContext.lookupConstructorInClass(ClassIds.REALM_CLASS_IMPL) { it.owner.valueParameters.size == 2 } @@ -207,7 +204,7 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi objectIdType, realmObjectIdType, realmUUIDType - ).map { it.classifierOrFail } + ) } private val indexableTypes = with(pluginContext.irBuiltIns) { setOf( @@ -223,12 +220,12 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi realmObjectIdType, realmUUIDType, realmAnyType - ).map { it.classifierOrFail } + ) } private val fullTextIndexableTypes = with(pluginContext.irBuiltIns) { setOf( stringType - ).map { it.classifierOrFail } + ) } /** @@ -424,7 +421,6 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi // Generate body for the synthetic schema method defined inside the Companion instance previously declared via `RealmModelSyntheticCompanionExtension` // TODO OPTIMIZE should be a one time only constructed object - @OptIn(ObsoleteDescriptorBasedAPI::class) @Suppress("LongMethod", "ComplexMethod") fun addSchemaMethodBody(irClass: IrClass) { val companionObject = irClass.companionObject() as? IrClass @@ -545,25 +541,24 @@ class RealmModelSyntheticPropertiesGeneration(private val pluginContext: IrPlugi value.coreGenericTypes?.get(0)?.nullable ?: fatalError("Missing generic type while processing a collection field.") } - val primaryKey = backingField.hasAnnotation(PRIMARY_KEY_ANNOTATION) - if (primaryKey && backingField.type.classifierOrFail !in validPrimaryKeyTypes) { + if (primaryKey && validPrimaryKeyTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Primary key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${validPrimaryKeyTypes.map { it.owner.symbol.descriptor.name }}", + "Primary key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${validPrimaryKeyTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } val isIndexed = backingField.hasAnnotation(INDEX_ANNOTATION) - if (isIndexed && backingField.type.classifierOrFail !in indexableTypes) { + if (isIndexed && indexableTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Indexed key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${indexableTypes.map { it.owner.symbol.descriptor.name }}", + "Indexed key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${indexableTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } val isFullTextIndexed = backingField.hasAnnotation(FULLTEXT_ANNOTATION) - if (isFullTextIndexed && backingField.type.classifierOrFail !in fullTextIndexableTypes) { + if (isFullTextIndexed && fullTextIndexableTypes.find { it.classFqName == backingField.type.classFqName } == null) { logError( - "Full-text key ${property.name} is of type ${backingField.type.classifierOrFail.owner.symbol.descriptor.name} but must be of type ${fullTextIndexableTypes.map { it.owner.symbol.descriptor.name }}", + "Full-text key ${property.name} is of type ${backingField.type.classId?.shortClassName} but must be of type ${fullTextIndexableTypes.map { it.classId?.shortClassName }}", property.locationOf() ) } diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt index e5fadb50a4..ed03b01688 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/Registrar.kt @@ -69,6 +69,13 @@ class Registrar : ComponentRegistrar { LoadingOrder.LAST, project ) + // Trigger generation of Realm specific methods in model classes: + // toString(), equals() and hashCode() + getExtensionPoint(SyntheticResolveExtension.extensionPointName).registerExtension( + RealmModelSyntheticMethodsExtension(), + LoadingOrder.LAST, + project + ) // Adds RealmObjectInternal properties, rewires accessors and adds static companion // properties and methods getExtensionPoint(IrGenerationExtension.extensionPointName).registerExtension( diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt index c6ae6d6455..12c088f671 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/SyncLoweringExtension.kt @@ -23,7 +23,6 @@ import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower import org.jetbrains.kotlin.backend.common.runOnFilePostfix -import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrDeclarationContainer import org.jetbrains.kotlin.ir.declarations.IrFile @@ -130,7 +129,7 @@ private class SyncLowering(private val pluginContext: IrPluginContext, private v val transformer = object : IrElementTransformerVoid() { override fun visitCall(expression: IrCall): IrExpression { - replacements.get(expression.symbol)?.let { (target, dispatchReceiverFunction) -> + replacements[expression.symbol]?.let { (target, dispatchReceiverFunction) -> return IrCallImpl( startOffset = expression.startOffset, endOffset = expression.endOffset, @@ -142,7 +141,8 @@ private class SyncLowering(private val pluginContext: IrPluginContext, private v superQualifierSymbol = null ).apply { dispatchReceiver = dispatchReceiverFunction(expression) - expression.valueArguments.forEachIndexed { index, irExpression -> + val valueArguments = List(expression.valueArgumentsCount) { expression.getValueArgument(it) } + valueArguments.forEachIndexed { index, irExpression -> putValueArgument(index, irExpression,) } putValueArgument( diff --git a/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt b/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt index e43d7ad3ee..b6f0f9bd15 100644 --- a/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt +++ b/packages/plugin-compiler/src/test/kotlin/io/realm/kotlin/compiler/GenerationExtensionTest.kt @@ -402,7 +402,7 @@ class GenerationExtensionTest { componentRegistrars = plugins inheritClassPath = true kotlincArguments = listOf( - "-Xjvm-default=enable", + "-Xjvm-default=all-compatibility", "-Xdump-directory=${inputs.outputDir()}", "-Xphases-to-dump-after=ValidateIrBeforeLowering" ) diff --git a/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir b/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir index 5ef8f93504..3ca90d16e5 100644 --- a/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir +++ b/packages/plugin-compiler/src/test/resources/sample/expected/01_AFTER.ValidateIrBeforeLowering.ir @@ -8508,19 +8508,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.Sample.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.Sample.Companion declared in sample.input.Sample.Companion.' type=sample.input.Sample.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.Sample, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Sample VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.Sample' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.equals' type=sample.input.Sample origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.Sample.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.Sample) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.Sample + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.Sample' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.hashCode' type=sample.input.Sample origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.Sample) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Sample + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.Sample' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Sample declared in sample.input.Sample.toString' type=sample.input.Sample origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -8795,19 +8811,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.Child.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.Child.Companion declared in sample.input.Child.Companion.' type=sample.input.Child.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.Child, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Child VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.Child' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.equals' type=sample.input.Child origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.Child.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.Child) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.Child + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.Child' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.hashCode' type=sample.input.Child origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.Child) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.Child + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.Child' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.Child declared in sample.input.Child.toString' type=sample.input.Child origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -9016,19 +9048,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.EmbeddedParent.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.EmbeddedParent.Companion declared in sample.input.EmbeddedParent.Companion.' type=sample.input.EmbeddedParent.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.equals' type=sample.input.EmbeddedParent origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.EmbeddedParent.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.hashCode' type=sample.input.EmbeddedParent origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.EmbeddedParent) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedParent + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.EmbeddedParent' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedParent declared in sample.input.EmbeddedParent.toString' type=sample.input.EmbeddedParent origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -9235,19 +9283,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in sample.input.EmbeddedChild.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': sample.input.EmbeddedChild.Companion declared in sample.input.EmbeddedChild.Companion.' type=sample.input.EmbeddedChild.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.equals' type=sample.input.EmbeddedChild origin=null + other: GET_VAR 'other: kotlin.Any? declared in sample.input.EmbeddedChild.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.hashCode' type=sample.input.EmbeddedChild origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:sample.input.EmbeddedChild) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.EmbeddedRealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:sample.input.EmbeddedChild + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in sample.input.EmbeddedChild' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': sample.input.EmbeddedChild declared in sample.input.EmbeddedChild.toString' type=sample.input.EmbeddedChild origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY diff --git a/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir b/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir index 3a6151e3a8..72f689feb3 100644 --- a/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir +++ b/packages/plugin-compiler/src/test/resources/schema/expected/01_AFTER.ValidateIrBeforeLowering.ir @@ -118,19 +118,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.A.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.A.Companion declared in schema.input.A.Companion.' type=schema.input.A.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.A, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.A VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.A' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.equals' type=schema.input.A origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.A.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.A) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.A + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.A' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.hashCode' type=schema.input.A origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.A) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.A + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.A' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.A declared in schema.input.A.toString' type=schema.input.A origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -271,19 +287,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.B.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.B.Companion declared in schema.input.B.Companion.' type=schema.input.B.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.B, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.B VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.B' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.equals' type=schema.input.B origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.B.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.B) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.B + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.B' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.hashCode' type=schema.input.B origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.B) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.B + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.B' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.B declared in schema.input.B.toString' type=schema.input.B origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY @@ -424,19 +456,35 @@ MODULE_FRAGMENT name:
RETURN type=kotlin.Nothing from='public final fun (): io.realm.kotlin.schema.RealmClassKind declared in schema.input.C.Companion' GET_FIELD 'FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_classKind type:io.realm.kotlin.schema.RealmClassKind visibility:private' type=io.realm.kotlin.schema.RealmClassKind origin=null receiver: GET_VAR ': schema.input.C.Companion declared in schema.input.C.Companion.' type=schema.input.C.Companion origin=null - FUN FAKE_OVERRIDE name:equals visibility:public modality:OPEN <> ($this:kotlin.Any, other:kotlin.Any?) returnType:kotlin.Boolean [fake_override,operator] + FUN name:equals visibility:public modality:OPEN <> ($this:schema.input.C, other:kotlin.Any?) returnType:kotlin.Boolean [operator] overridden: public open fun equals (other: kotlin.Any?): kotlin.Boolean [fake_override,operator] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.C VALUE_PARAMETER name:other index:0 type:kotlin.Any? - FUN FAKE_OVERRIDE name:hashCode visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.Int [fake_override] + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun equals (other: kotlin.Any?): kotlin.Boolean [operator] declared in schema.input.C' + CALL 'internal final fun realmEquals (obj: io.realm.kotlin.types.BaseRealmObject, other: kotlin.Any?): kotlin.Boolean declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Boolean origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.equals' type=schema.input.C origin=null + other: GET_VAR 'other: kotlin.Any? declared in schema.input.C.equals' type=kotlin.Any? origin=null + FUN name:hashCode visibility:public modality:OPEN <> ($this:schema.input.C) returnType:kotlin.Int overridden: public open fun hashCode (): kotlin.Int [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any - FUN FAKE_OVERRIDE name:toString visibility:public modality:OPEN <> ($this:kotlin.Any) returnType:kotlin.String [fake_override] + $this: VALUE_PARAMETER name: type:schema.input.C + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun hashCode (): kotlin.Int declared in schema.input.C' + CALL 'internal final fun realmHashCode (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.Int declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.Int origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.hashCode' type=schema.input.C origin=null + FUN name:toString visibility:public modality:OPEN <> ($this:schema.input.C) returnType:kotlin.String overridden: public open fun toString (): kotlin.String [fake_override] declared in io.realm.kotlin.types.RealmObject - $this: VALUE_PARAMETER name: type:kotlin.Any + $this: VALUE_PARAMETER name: type:schema.input.C + BLOCK_BODY + RETURN type=kotlin.Nothing from='public open fun toString (): kotlin.String declared in schema.input.C' + CALL 'internal final fun realmToString (obj: io.realm.kotlin.types.BaseRealmObject): kotlin.String declared in io.realm.kotlin.internal.RealmObjectHelper' type=kotlin.String origin=null + $this: GET_OBJECT 'CLASS IR_EXTERNAL_DECLARATION_STUB OBJECT name:RealmObjectHelper modality:FINAL visibility:internal superTypes:[kotlin.Any]' type=io.realm.kotlin.internal.RealmObjectHelper + obj: GET_VAR ': schema.input.C declared in schema.input.C.toString' type=schema.input.C origin=null PROPERTY name:io_realm_kotlin_objectReference visibility:public modality:OPEN [var] FIELD PROPERTY_BACKING_FIELD name:io_realm_kotlin_objectReference type:io.realm.kotlin.internal.RealmObjectReference? visibility:private EXPRESSION_BODY diff --git a/packages/test-base/build.gradle.kts b/packages/test-base/build.gradle.kts index c1caa81308..8976c4f7c8 100644 --- a/packages/test-base/build.gradle.kts +++ b/packages/test-base/build.gradle.kts @@ -103,10 +103,6 @@ kotlin { } } } - - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).all { - kotlinOptions.jvmTarget = Versions.jvmTarget - } } // Android configuration @@ -150,8 +146,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Remove overlapping resources after adding "org.jetbrains.kotlinx:kotlinx-coroutines-test" to diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 2b693a5fa9..cfc7d1f33f 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -21,7 +21,6 @@ import android.os.SystemClock import java.io.File import java.nio.file.Files import java.nio.file.Path -import java.nio.file.attribute.PosixFilePermission import kotlin.io.path.absolutePathString import kotlin.time.Duration @@ -30,14 +29,8 @@ actual object PlatformUtils { actual fun createTempDir(prefix: String, readOnly: Boolean): String { val dir: Path = Files.createTempDirectory("$prefix-android_tests") if (readOnly) { - Files.setPosixFilePermissions( - dir, - setOf( - PosixFilePermission.GROUP_READ, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OWNER_READ - ) - ) + // Use the File API as it works across Windows and POSIX. + dir.toFile().setReadOnly() } return dir.absolutePathString() } diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt index 9f1a4ab718..9e6b939bf7 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/SerializableSample.kt @@ -50,6 +50,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.mongodb.kbson.BsonObjectId import org.mongodb.kbson.Decimal128 +import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 @Suppress("MagicNumber") @@ -65,7 +66,9 @@ class SerializableSample : RealmObject { var floatField: Float = 3.14f var doubleField: Double = 1.19840122 var decimal128Field: Decimal128 = Decimal128("1.8446744073709551618E-6157") - var timestampField: RealmInstant = RealmInstant.from(100, 1000) + // We will loose nano second precision when we round trip these, so framework only works for + // timestamps with 0-nanosecond fraction. + var timestampField: RealmInstant = RealmInstant.from(100, 1000000) var bsonObjectIdField: BsonObjectId = BsonObjectId("507f1f77bcf86cd799439011") var uuidField: RealmUUID = RealmUUID.from("46423f1b-ce3e-4a7e-812f-004cf9c42d76") var binaryField: ByteArray = byteArrayOf(42) @@ -212,7 +215,7 @@ class SerializableSample : RealmObject { ) @Suppress("UNCHECKED_CAST") - val listNullableProperties = mapOf( + val listNullableProperties: Map, KMutableProperty1>> = mapOf( String::class to SerializableSample::nullableStringListField as KMutableProperty1>, Byte::class to SerializableSample::nullableByteListField as KMutableProperty1>, Char::class to SerializableSample::nullableCharListField as KMutableProperty1>, diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt new file mode 100644 index 0000000000..acbe76a089 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationChild.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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.entities.migration.embedded.after + +import io.realm.kotlin.types.EmbeddedRealmObject + +class EmbeddedMigrationChild : EmbeddedRealmObject { + var id: String = "child" +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt new file mode 100644 index 0000000000..0c24e40fc6 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/after/EmbeddedMigrationParent.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 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.entities.migration.embedded.after + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationParent : RealmObject { + var id: String = "parent" + var child: EmbeddedMigrationChild? = null +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt new file mode 100644 index 0000000000..05d2ad15e3 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationChild.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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.entities.migration.embedded.before + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationChild : RealmObject { + var id: String = "child" +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt new file mode 100644 index 0000000000..a75c4af2e7 --- /dev/null +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/entities/migration/embedded/before/EmbeddedMigrationParent.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 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.entities.migration.embedded.before + +import io.realm.kotlin.types.RealmObject + +class EmbeddedMigrationParent : RealmObject { + var id: String = "parent" + var child: EmbeddedMigrationChild? = null +} diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt index a860f9c469..5784b90573 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/StandaloneDynamicMutableRealm.kt @@ -20,6 +20,7 @@ package io.realm.kotlin.test import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.dynamic.DynamicMutableRealmImpl import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmSchedulerPointer /** * Special dynamic mutable realm with methods for managing a write transaction. @@ -29,13 +30,26 @@ import io.realm.kotlin.internal.interop.RealmInterop * on it's own shared realm with the ability to manage the write transaction, which allows us to * test the [DynamicMutableRealm] API outside of a migration. */ -internal class StandaloneDynamicMutableRealm(configuration: InternalConfiguration) : +internal class StandaloneDynamicMutableRealm private constructor( + configuration: InternalConfiguration, + private val scheduler: RealmSchedulerPointer, +) : DynamicMutableRealmImpl( configuration, - RealmInterop.realm_open(configuration.createNativeConfiguration(), null) + try { + RealmInterop.realm_open(configuration.createNativeConfiguration(), scheduler) + } catch (exception: Exception) { + scheduler.release() + throw exception + } ) { + constructor(configuration: InternalConfiguration) : this( + configuration, + RealmInterop.realm_create_scheduler() + ) override fun close() { realmReference.close() + scheduler.release() } } diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt index 3068aea342..5e72a273dd 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/util/Utils.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.withTimeout import kotlinx.datetime.Instant import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.minutes // Platform independent helper methods object Utils { @@ -92,7 +92,7 @@ fun Instant.toRealmInstant(): RealmInstant { } // Variant of `Channel.receiveOrFail()` that will will throw if a timeout is hit. -suspend fun Channel.receiveOrFail(timeout: Duration = 30.seconds): T { +suspend fun Channel.receiveOrFail(timeout: Duration = 1.minutes): T { return withTimeout(timeout) { receive() } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt index 967ed0a5de..fec02fddd5 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/GeoSpatialTests.kt @@ -520,4 +520,75 @@ class GeoSpatialTests { } } } + + // Verify that geo objects can be passed directly as query arguments + // Kotlin will do implicit conversion to strings until native type support is added + @Test + fun asQueryArguments() { + realm.writeBlocking { + copyToRealm( + Restaurant().apply { + location = Location(latitude = 20.0, longitude = 20.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = 5.0, longitude = 5.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = -5.0, longitude = -5.0) + } + ) + copyToRealm( + Restaurant().apply { + location = Location(latitude = 0.0, longitude = 0.0) + } + ) + } + + var sphere = GeoCircle.create( + center = GeoPoint.create(0.0, 0.0), + radius = Distance.fromKilometers(0.0) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", sphere).count().find()) + + var box = GeoBox.create( + bottomLeft = GeoPoint.create(-1.0, -1.0), + topRight = GeoPoint.create(1.0, 1.0) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", box).count().find()) + + val onlyOuterRing = GeoPolygon.create( + outerRing = listOf( + GeoPoint.create(-5.0, -5.0), + GeoPoint.create(5.0, -5.0), + GeoPoint.create(5.0, 5.0), + GeoPoint.create(-5.0, 5.0), + GeoPoint.create(-5.0, -5.0) + ) + ) + assertEquals(2, realm.query("location GEOWITHIN $0", onlyOuterRing).count().find()) + + val polygonWithHole = GeoPolygon.create( + outerRing = listOf( + GeoPoint.create(-5.0, -5.0), + GeoPoint.create(5.0, -5.0), + GeoPoint.create(5.0, 5.0), + GeoPoint.create(-5.0, 5.0), + GeoPoint.create(-5.0, -5.0) + ), + holes = arrayOf( + listOf( + GeoPoint.create(-4.0, -4.0), + GeoPoint.create(4.0, -4.0), + GeoPoint.create(4.0, 4.0), + GeoPoint.create(-4.0, 4.0), + GeoPoint.create(-4.0, -4.0) + ) + ) + ) + assertEquals(1, realm.query("location GEOWITHIN $0", polygonWithHole).count().find()) + } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt index 95bc2f555d..3bff6902d1 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/MigrationTests.kt @@ -21,9 +21,15 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.entities.link.Child import io.realm.kotlin.entities.link.Parent +import io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild +import io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent import io.realm.kotlin.ext.query +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.migration.AutomaticSchemaMigration import io.realm.kotlin.query.find +import io.realm.kotlin.test.common.utils.assertFailsWithMessage import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.use import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -178,6 +184,132 @@ class MigrationTests { } } + @Test + fun migrationThrowsOnViolatingEmbeddedObjectConstraints() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open(initialConfiguration).use { + it.write { + copyToRealm(EmbeddedMigrationChild().apply { id = "orphaned-child" }) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }) + .build() + + assertFailsWithMessage("Cannot convert 'EmbeddedMigrationChild' to embedded: at least one object has no incoming links and would be deleted.") { + Realm.open(migratedConfiguration).use { } + } + } + + @Test + fun automaticBacklinkHandling_deleteOrphanedChildren() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open(initialConfiguration).use { + it.write { + copyToRealm( + EmbeddedMigrationParent().apply { + child = EmbeddedMigrationChild().apply { id = "child-with-parent" } + } + ) + copyToRealm(EmbeddedMigrationChild().apply { id = "orphaned-child" }) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }, resolveEmbeddedObjectConstraints = true) + .build() + + Realm.open(migratedConfiguration).use { + val childWithParent = + it.query() + .find().single() + assertEquals("child-with-parent", childWithParent.id) + } + } + + @Test + fun automaticBacklinkHandling_cloneDuplicateReferences() = runBlocking { + val initialConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.before.EmbeddedMigrationChild::class + ) + ) + .directory(tmpDir) + .build() + + Realm.open( + initialConfiguration + ).use { + it.write { + // Add two parents referencing the same child + val child = copyToRealm(EmbeddedMigrationChild().apply { id = "child-with-parent" }) + copyToRealm( + EmbeddedMigrationParent().apply { + id = "mom1" + this.child = child + } + ) + copyToRealm( + EmbeddedMigrationParent().apply { + id = "mom2" + this.child = child + } + ) + } + } + + val migratedConfiguration = RealmConfiguration.Builder( + schema = setOf( + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationParent::class, + io.realm.kotlin.entities.migration.embedded.after.EmbeddedMigrationChild::class, + ) + ) + .directory(tmpDir) + .schemaVersion(1) + .migration(AutomaticSchemaMigration { }, resolveEmbeddedObjectConstraints = true) + .build() + + Realm.open(migratedConfiguration).use { + assertEquals( + 2, + it.query() + .find().count() + ) + } + } + // TODO add test for adding/remove columns when we have an API to open with an existing Realm. // https://github.com/realm/realm-kotlin/issues/304 } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt index d3239242c2..07dd7069e1 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/PersistedNameTests.kt @@ -386,15 +386,27 @@ class PersistedNameTests { realm.writeBlocking { copyToRealm( Parent().apply { - this.child = Child() + this.child = Child().apply { name = "child1" } + this.children.add( + Child().apply { + name = "first-child" + children.add( + Child().apply { name = "first-grand-child" } + ) + } + ) } ) } assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) + assertEquals(3, realm.query().count().find()) - assertEquals("child", realm.query().first().find()!!.child!!.name) + val parent = realm.query().first().find()!! + assertEquals("child1", parent.child!!.name) + val child2 = parent.children.first() + assertEquals("first-child", child2.name) + assertEquals("first-grand-child", child2.children.first().name) } } @@ -507,9 +519,14 @@ class RealmChild(var id: Int) : RealmObject { class Parent : RealmObject { var name = "parent" var child: Child? = null + + @PersistedName("renamedChildren") + var children: RealmList = realmListOf() } @PersistedName(name = "RenamedChild") class Child : RealmObject { var name = "child" + @PersistedName("renamedChildren") + var children: RealmList = realmListOf() } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt index 23745dbffc..3f7267c8a0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/QueryTests.kt @@ -2361,7 +2361,7 @@ class QueryTests { assertEquals(1, realm.query("fulltextField TEXT 'quick dog'").find().size) // words at different locations assertEquals(0, realm.query("fulltextField TEXT 'brown -fox'").find().size) // exclusion - assertEquals(0, realm.query("fulltextField TEXT 'fo*'").find().size) // token prefix search does not work + assertEquals(2, realm.query("fulltextField TEXT 'fo*'").find().size) // token prefix search is supported. assertEquals(1, realm.query("fulltextField TEXT 'cafe big'").find().size) // case- and diacritics-insensitive assertEquals(1, realm.query("fulltextField TEXT 'rødgrød'").find().size) // Latin-1 supplement assertEquals(0, realm.query("fulltextField TEXT '😊'").find().size) // Searching outside supported chars return nothing diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt index 843fbd29c2..f99e36d2d0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmAnyTests.kt @@ -58,6 +58,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.fail @@ -421,6 +422,74 @@ class RealmAnyTests { } } + @Test + fun equals() { + RealmAny.Type.values().forEach { type -> + when (type) { + RealmAny.Type.INT -> { + assertEquals(RealmAny.create(1), RealmAny.create(Char(1))) + assertEquals(RealmAny.create(1), RealmAny.create(1.toByte())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toShort())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toInt())) + assertEquals(RealmAny.create(1), RealmAny.create(1.toLong())) + assertNotEquals(RealmAny.create(1), RealmAny.create(2)) + } + RealmAny.Type.BOOL -> { + assertEquals(RealmAny.create(true), RealmAny.create(true)) + assertNotEquals(RealmAny.create(true), RealmAny.create(false)) + } + RealmAny.Type.STRING -> { + assertEquals(RealmAny.create("Realm"), RealmAny.create("Realm")) + assertNotEquals(RealmAny.create("Realm"), RealmAny.create("Not Realm")) + } + RealmAny.Type.BINARY -> { + assertEquals( + RealmAny.create(byteArrayOf(1, 2)), RealmAny.create(byteArrayOf(1, 2)) + ) + assertNotEquals( + RealmAny.create(byteArrayOf(1, 2)), RealmAny.create(byteArrayOf(2, 1)) + ) + } + RealmAny.Type.TIMESTAMP -> { + val now = RealmInstant.now() + assertEquals(RealmAny.create(now), RealmAny.create(now)) + assertNotEquals(RealmAny.create(RealmInstant.from(1, 1)), RealmAny.create(now)) + } + RealmAny.Type.FLOAT -> { + assertEquals(RealmAny.create(1.5f), RealmAny.create(1.5f)) + assertNotEquals(RealmAny.create(1.2f), RealmAny.create(1.3f)) + } + RealmAny.Type.DOUBLE -> { + assertEquals(RealmAny.create(1.5), RealmAny.create(1.5)) + assertNotEquals(RealmAny.create(1.2), RealmAny.create(1.3)) + } + RealmAny.Type.DECIMAL128 -> { + assertEquals(RealmAny.create(Decimal128("1E64")), RealmAny.create(Decimal128("1E64"))) + assertNotEquals(RealmAny.create(Decimal128("1E64")), RealmAny.create(Decimal128("-1E64"))) + } + RealmAny.Type.OBJECT_ID -> { + val value = ObjectId() + assertEquals(RealmAny.create(value), RealmAny.create(value)) + assertNotEquals(RealmAny.create(ObjectId()), RealmAny.create(value)) + } + RealmAny.Type.UUID -> { + val value = RealmUUID.random() + assertEquals(RealmAny.create(value), RealmAny.create(value)) + assertNotEquals(RealmAny.create(RealmUUID.random()), RealmAny.create(value)) + } + RealmAny.Type.OBJECT -> { + val realmObject = Sample() + // Same object is equal + assertEquals(RealmAny.create(realmObject), RealmAny.create(realmObject)) + // Different kind of objects are not equal + assertNotEquals(RealmAny.create(RealmAnyContainer()), RealmAny.create(realmObject)) + // Different objects of same type are not equal + assertNotEquals(RealmAny.create(Sample()), RealmAny.create(realmObject)) + } + } + } + } + @Test fun embeddedObject_worksInsideParent() { val embeddedChild = EmbeddedChild("CHILD") diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt index 81d0a96efd..cc9c22f728 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmConfigurationTests.kt @@ -23,6 +23,7 @@ import io.realm.kotlin.entities.Sample import io.realm.kotlin.internal.InternalConfiguration import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.appFilesDirectory +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.util.CoroutineDispatcherFactory import io.realm.kotlin.log.LogLevel @@ -77,7 +78,7 @@ class RealmConfigurationTests { fun with() { val config = RealmConfiguration.create(schema = setOf(Sample::class)) assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), config.path ) assertEquals(Realm.DEFAULT_FILE_NAME, config.name) @@ -95,7 +96,7 @@ class RealmConfigurationTests { fun defaultPath() { val config = RealmConfiguration.create(schema = setOf(Sample::class)) assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), config.path ) @@ -103,7 +104,7 @@ class RealmConfigurationTests { RealmConfiguration.Builder(schema = setOf(Sample::class)) .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", + pathOf(appFilesDirectory(), Realm.DEFAULT_FILE_NAME), configFromBuilderWithDefaultName.path ) @@ -112,7 +113,7 @@ class RealmConfigurationTests { .name("custom.realm") .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}custom.realm", + pathOf(appFilesDirectory(), "custom.realm"), configFromBuilderWithCustomName.path ) @@ -122,7 +123,7 @@ class RealmConfigurationTests { .name("foo.realm") .build() assertEquals( - "${appFilesDirectory()}${PATH_SEPARATOR}my_dir${PATH_SEPARATOR}foo.realm", + pathOf(appFilesDirectory(), "my_dir", "foo.realm"), configFromBuilderWithCurrentDir.path ) } @@ -133,16 +134,16 @@ class RealmConfigurationTests { val config = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) .build() - assertEquals("$tmpDir${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", config.path) + assertEquals(pathOf(tmpDir, Realm.DEFAULT_FILE_NAME), config.path) } @Test fun directory_withSpace() { - val realmDir = tmpDir + "${PATH_SEPARATOR}dir with space" + val realmDir = pathOf(tmpDir, "dir with space") val config = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) .build() - assertEquals("$realmDir${PATH_SEPARATOR}${Realm.DEFAULT_FILE_NAME}", config.path) + assertEquals(pathOf(realmDir, Realm.DEFAULT_FILE_NAME), config.path) // Just verifying that we can open the realm Realm.open(config).use { } } @@ -158,7 +159,7 @@ class RealmConfigurationTests { @Test fun directory_createIntermediateDirs() { - val realmDir = tmpDir + listOf("my", "intermediate", "dir").joinToString(separator = PATH_SEPARATOR, prefix = PATH_SEPARATOR) + val realmDir = pathOf(tmpDir, "my", "intermediate", "dir") val configBuilder = RealmConfiguration.Builder(schema = setOf(Sample::class)) .directory(realmDir) @@ -168,7 +169,7 @@ class RealmConfigurationTests { @Test fun directory_isFileThrows() { - val tmpFile = "$tmpDir${PATH_SEPARATOR}file" + val tmpFile = pathOf(tmpDir, "file") platformFileSystem.write(tmpFile.toPath(), mustCreate = true) { write(ByteArray(0)) } @@ -186,7 +187,7 @@ class RealmConfigurationTests { fun directoryAndNameCombine() { val realmDir = tmpDir val realmName = "my.realm" - val expectedPath = "$realmDir${PATH_SEPARATOR}$realmName" + val expectedPath = pathOf(realmDir, realmName) val config = RealmConfiguration.Builder(setOf(Sample::class)) @@ -236,7 +237,7 @@ class RealmConfigurationTests { .directory(tmpDir) .name(name) .build() - assertEquals("$tmpDir${PATH_SEPARATOR}$name", config.path) + assertEquals(pathOf(tmpDir, name), config.path) // Just verifying that we can open the realm Realm.open(config).use { } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt index 20f7aa8f2e..4e1bcf0b8f 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmListTests.kt @@ -282,6 +282,13 @@ class RealmListTests : EmbeddedObjectCollectionQueryTests { managedTesters[0].clearFailsIfClosed(getCloseableRealm()) } + @Test + fun remove() { + for (tester in managedTesters) { + tester.remove() + } + } + @Test fun removeAt() { for (tester in managedTesters) { @@ -729,6 +736,7 @@ internal interface ListApiTester : ErrorCatcher { fun addAllWithIndexFailsIfClosed(realm: Realm) fun clear() fun clearFailsIfClosed(realm: Realm) + fun remove() fun removeAt() fun removeAtFailsIfClosed(realm: Realm) fun set() @@ -809,7 +817,7 @@ internal class ListTypeSafetyManager( */ internal abstract class ManagedListTester( override val realm: Realm, - private val typeSafetyManager: ListTypeSafetyManager, + protected val typeSafetyManager: ListTypeSafetyManager, override val classifier: KClassifier ) : ListApiTester { @@ -1068,6 +1076,25 @@ internal abstract class ManagedListTester( } } + override fun remove() { + val dataSet = typeSafetyManager.dataSetToLoad + val assertions = { list: RealmList -> + assertTrue(list.isEmpty()) + } + + errorCatcher { + realm.writeBlocking { + val list = typeSafetyManager.createContainerAndGetCollection(this) + assertFalse(list.remove(dataSet[0])) + assertTrue(list.add(dataSet[0])) + assertTrue(list.remove(list.last())) + assertions(list) + } + } + + assertListAndCleanup { list -> assertions(list) } + } + override fun removeAt() { val dataSet = typeSafetyManager.dataSetToLoad val assertions = { list: RealmList -> @@ -1204,7 +1231,7 @@ internal abstract class ManagedListTester( } // Retrieves the list again but this time from Realm to check the getter is called correctly - private fun assertListAndCleanup(assertion: (RealmList) -> Unit) { + protected fun assertListAndCleanup(assertion: (RealmList) -> Unit) { realm.writeBlocking { val container = this.query() .first() @@ -1318,6 +1345,27 @@ internal class ByteArrayListTester( ) : ManagedListTester(realm, typeSafetyManager, ByteArray::class) { override fun assertElementsAreEqual(expected: ByteArray?, actual: ByteArray?) = assertContentEquals(expected, actual) + + // Removing elements using equals/hashcode will fail for byte arrays since they are + // are only equal if identical + override fun remove() { + val dataSet = typeSafetyManager.dataSetToLoad + val assertions = { list: RealmList -> + assertFalse(list.isEmpty()) + } + + errorCatcher { + realm.writeBlocking { + val list = typeSafetyManager.createContainerAndGetCollection(this) + assertFalse(list.remove(dataSet[0])) + assertTrue(list.add(dataSet[0])) + assertFalse(list.remove(list.last())) + assertions(list) + } + } + + assertListAndCleanup { list -> assertions(list) } + } } // ----------------------------------- diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt index db044732a7..09cf50fec8 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmObjectTests.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.test.common import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.VersionId +import io.realm.kotlin.entities.SampleWithPrimaryKey import io.realm.kotlin.entities.link.Child import io.realm.kotlin.entities.link.Parent import io.realm.kotlin.ext.isFrozen @@ -25,6 +26,7 @@ import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.version import io.realm.kotlin.test.common.utils.RealmStateTest import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.types.RealmObject import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -32,8 +34,34 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertTrue +// Model class with toString/equals/hashCode +class CustomMethods : RealmObject { + var name: String = "" + var age: Int = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as CustomMethods + return this.age == 42 && other.age == 42 + } + + override fun hashCode(): Int { + return if (isValid()) { + 42 + } else { + -1 + } + } + + override fun toString(): String { + return "customToString" + } +} + class RealmObjectTests : RealmStateTest { companion object { @@ -48,7 +76,7 @@ class RealmObjectTests : RealmStateTest { @BeforeTest fun setup() { tmpDir = PlatformUtils.createTempDir() - val configuration = RealmConfiguration.Builder(schema = setOf(Parent::class, Child::class)) + val configuration = RealmConfiguration.Builder(schema = setOf(Parent::class, Child::class, SampleWithPrimaryKey::class, CustomMethods::class)) .directory(tmpDir) .build() realm = Realm.open(configuration) @@ -113,6 +141,175 @@ class RealmObjectTests : RealmStateTest { // FIXME } + @Test + fun toString_managed() { + val managedObj = realm.writeBlocking { + copyToRealm(Parent()) + } + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=VALID, schemaName=Parent, objKey=[0-9]*, version=[0-9]*, realm=${realm.configuration.name}\\}") + assertTrue(regex.matches(managedObj.toString()), managedObj.toString()) + } + + @Test + fun toString_managed_cyclicData() { + val p1 = SampleWithPrimaryKey() + p1.stringField = "Parent" + p1.nullableObject = p1 + val managedObj = realm.writeBlocking { copyToRealm(p1) } + val regex = Regex("io.realm.kotlin.entities.SampleWithPrimaryKey\\{state=VALID, schemaName=SampleWithPrimaryKey, objKey=[0-9]*, version=[0-9]*, realm=${realm.configuration.name}\\}") + assertTrue(regex.matches(managedObj.toString()), managedObj.toString()) + } + + @Test + fun toString_managed_invalid() { + realm.writeBlocking { + val managedObject = copyToRealm(Parent()) + delete(managedObject) + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=INVALID, schemaName=Parent, realm=${realm.configuration.name}, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(managedObject.toString()), managedObject.toString()) + cancelWrite() + } + } + + @Test + fun toString_managed_closedRealm() { + val managedObject = realm.writeBlocking { + copyToRealm(Parent()) + } + realm.close() + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=CLOSED, schemaName=Parent, realm=${realm.configuration.name}, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(managedObject.toString()), managedObject.toString()) + } + + @Test + fun toString_customMethod() { + assertEquals("customToString", CustomMethods().toString()) + val managedObj = realm.writeBlocking { copyToRealm(CustomMethods()) } + assertEquals("customToString", managedObj.toString()) + } + + @Test + fun toString_unmanaged() { + val unmanagedObject = Parent() + val regex = Regex("io.realm.kotlin.entities.link.Parent\\{state=UNMANAGED, schemaName=Parent, hashCode=[-0-9]*\\}") + assertTrue(regex.matches(unmanagedObject.toString()), unmanagedObject.toString()) + } + + @Test + fun equals_hashCode_managed() { + realm.writeBlocking { + val p1 = copyToRealm(Parent().apply { this.name = "Jane" }) + val p2 = copyToRealm(Parent()) + val p3 = query(Parent::class, "name = 'Jane'").first().find()!! + assertEquals(p1, p1) + assertEquals(p1, p3) + assertEquals(p1.hashCode(), p1.hashCode()) + assertEquals(p1.hashCode(), p3.hashCode()) + + // Not restrictions are given on hashCode if two objects are not equal + assertNotEquals(p2, p1) + assertNotEquals(p2, p3) + } + } + + @Test + fun equals_hashCode_unmanaged() { + val p1 = Parent() + val p2 = Parent() + assertEquals(p1, p1) + assertEquals(p1.hashCode(), p1.hashCode()) + assertNotEquals(p1, p2) + } + + @Test + fun equals_hashCode_mixed() { + val unmanagedObj = Parent() + val managedObj = realm.writeBlocking { copyToRealm(Parent()) } + assertNotEquals(unmanagedObj, managedObj) + assertNotEquals(managedObj, unmanagedObj) + // When objects are not equal, no guarantees are given on the behavior of hashCode() + // thus nothing can be asserted here. + } + + @Test + fun equals_hashCode_managed_cyclicData() { + realm.writeBlocking { + val p1 = copyToRealm( + SampleWithPrimaryKey().apply { + primaryKey = 1 + stringField = "Jane" + } + ) + p1.nullableObject = p1 + val p2 = copyToRealm( + SampleWithPrimaryKey().apply { + primaryKey = 2 + stringField = "John" + } + ) + val p3 = query(SampleWithPrimaryKey::class, "stringField = 'Jane'").first().find()!! + assertEquals(p1, p1) + assertEquals(p1, p3) + assertEquals(p1.hashCode(), p1.hashCode()) + assertEquals(p1.hashCode(), p3.hashCode()) + + // Not restrictions are given on hashCode if two objects are not equal + assertNotEquals(p2, p1) + assertNotEquals(p2, p3) + } + } + + @Test + fun equals_hashCode_customMethod() { + // Only equals if age = 42 or same instance + val obj1 = CustomMethods() + val obj2 = CustomMethods() + assertEquals(obj1, obj1) + assertEquals(obj1.hashCode(), obj1.hashCode()) + assertEquals(42, obj1.hashCode()) + assertNotEquals(obj1, obj2) + + val obj3 = CustomMethods().apply { age = 42 } + val obj4 = CustomMethods().apply { age = 42 } + assertEquals(obj3, obj3) + assertEquals(obj3.hashCode(), obj4.hashCode()) + assertEquals(42, obj3.hashCode()) + assertEquals(obj3, obj4) + assertEquals(obj3.hashCode(), obj4.hashCode()) + + // Managed objects + realm.writeBlocking { + val obj1 = copyToRealm(CustomMethods()) + val obj2 = copyToRealm(CustomMethods()) + assertEquals(obj1, obj1) + assertEquals(obj1.hashCode(), obj1.hashCode()) + assertEquals(42, obj1.hashCode()) + assertNotEquals(obj1, obj2) + + val obj3 = copyToRealm(CustomMethods().apply { age = 42 }) + val obj4 = copyToRealm(CustomMethods().apply { age = 42 }) + assertEquals(obj3, obj3) + assertEquals(obj3.hashCode(), obj3.hashCode()) + assertEquals(42, obj1.hashCode()) + assertEquals(obj3, obj4) + assertEquals(obj3.hashCode(), obj4.hashCode()) + } + } + + @Test + fun equals_hashCode_managed_invalid() { + realm.writeBlocking { + val p1 = copyToRealm(Parent().apply { this.name = "Jane" }) + val p2 = copyToRealm(Parent()) + delete(p1) + delete(p2) + + assertEquals(p1, p1) + assertEquals(p1.hashCode(), p1.hashCode()) + assertNotEquals(p1, p2) + } + } + override fun isFrozen_throwsIfRealmIsClosed() { realm.close() assertFailsWith { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt index e1e60b4b19..437ff45ebc 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmSetTests.kt @@ -777,26 +777,26 @@ internal abstract class ManagedSetTester( } override fun remove() { - // TODO https://github.com/realm/realm-kotlin/issues/1097 - // Ignore RealmObject: structural equality cannot be assessed for this type when removing - // elements from the set - if (classifier != RealmObject::class) { - val dataSet = typeSafetyManager.dataSetToLoad + val dataSet = typeSafetyManager.dataSetToLoad - errorCatcher { - realm.writeBlocking { - val set = typeSafetyManager.createContainerAndGetCollection(this) - set.add(dataSet[0]) - assertTrue(set.remove(dataSet[0])) - assertTrue(set.isEmpty()) + errorCatcher { + realm.writeBlocking { + val set = typeSafetyManager.createContainerAndGetCollection(this) + val element = if (classifier == RealmObject::class) { + copyToRealm(dataSet[0] as RealmObject) as T + } else { + dataSet[0] } - } - - assertContainerAndCleanup { container -> - val set = typeSafetyManager.getCollection(container) + set.add(element) + assertTrue(set.remove(element)) assertTrue(set.isEmpty()) } } + + assertContainerAndCleanup { container -> + val set = typeSafetyManager.getCollection(container) + assertTrue(set.isEmpty()) + } } override fun removeAll() { diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt index 9ddca51450..a398bc409e 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/RealmTests.kt @@ -25,9 +25,9 @@ import io.realm.kotlin.ext.isManaged import io.realm.kotlin.ext.isValid import io.realm.kotlin.ext.query import io.realm.kotlin.ext.version -import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.isWindows +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.query.find import io.realm.kotlin.test.common.utils.assertFailsWithMessage import io.realm.kotlin.test.platform.PlatformUtils @@ -429,6 +429,14 @@ class RealmTests { } } + @Test + fun close_idempotent() { + realm.close() + assertTrue(realm.isClosed()) + realm.close() + assertTrue(realm.isClosed()) + } + @Test @Suppress("LongMethod") fun deleteRealm() { @@ -524,7 +532,7 @@ class RealmTests { val anotherRealm = Realm.open(configA) // Deleting it without having closed it should fail. - assertFailsWithMessage("Cannot delete files of an open Realm: '$tempDirA${PATH_SEPARATOR}anotherRealm.realm' is still in use") { + assertFailsWithMessage("Cannot delete files of an open Realm: '${pathOf(tempDirA, "anotherRealm.realm")}' is still in use") { Realm.deleteRealm(configA) } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt index 088cf1919e..223f0a9af0 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/SerializationTests.kt @@ -139,6 +139,11 @@ class SerializationTests { RealmInstant::class -> dataSet.map { (it as RealmInstant?)?.restrictToMillisPrecision() as T } + RealmAny::class -> dataSet.map { + if ((it as? RealmAny)?.type == RealmAny.Type.TIMESTAMP) { + RealmAny.create((it.asRealmInstant()!!.restrictToMillisPrecision()))as T + } else { it } + } else -> dataSet } @@ -177,6 +182,12 @@ class SerializationTests { RealmInstant::class -> dataSet.map { entry -> entry.first to (entry.second as RealmInstant?)?.restrictToMillisPrecision() as T } + RealmAny::class -> dataSet.map { entry -> + val (key, value) = entry + if ((value as? RealmAny)?.type == RealmAny.Type.TIMESTAMP) { + key to RealmAny.create((value.asRealmInstant()!!.restrictToMillisPrecision()))as T + } else { entry } + } else -> dataSet } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt index 37d59c81fb..919c21ba0d 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt @@ -81,7 +81,11 @@ class VersionTrackingTests { realm.activeVersions().run { assertEquals(1, all.size) assertEquals(1, allTracked.size) - assertNull(notifier) + // The notifier might or might not had time to run + notifier?.let { + assertEquals(2, it.current.version) + assertEquals(0, it.active.size) + } assertNull(writer) } } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt index ac838b5de6..b43ba6c9a6 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/notifications/SystemNotificationTests.kt @@ -19,7 +19,10 @@ package io.realm.kotlin.test.common.notifications import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample -import io.realm.kotlin.internal.platform.singleThreadDispatcher +import io.realm.kotlin.internal.RealmImpl +import io.realm.kotlin.internal.SuspendableWriter +import io.realm.kotlin.internal.util.CoroutineDispatcherFactory +import io.realm.kotlin.internal.util.createLiveRealmContext import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.Utils import kotlinx.coroutines.runBlocking @@ -57,10 +60,14 @@ class SystemNotificationTests { @Test fun multipleSchedulersOnSameThread() { Utils.printlntid("main") - val baseRealm = Realm.open(configuration) as io.realm.kotlin.internal.RealmImpl - val dispatcher = singleThreadDispatcher("background") - val writer1 = io.realm.kotlin.internal.SuspendableWriter(baseRealm, dispatcher) - val writer2 = io.realm.kotlin.internal.SuspendableWriter(baseRealm, dispatcher) + val baseRealm = Realm.open(configuration) as RealmImpl + + val liveRealmContext = CoroutineDispatcherFactory + .managed("multipleSchedulersOnSameThread") + .createLiveRealmContext() + + val writer1 = SuspendableWriter(baseRealm, liveRealmContext) + val writer2 = SuspendableWriter(baseRealm, liveRealmContext) runBlocking { baseRealm.write { copyToRealm(Sample()) } writer1.write { copyToRealm(Sample()) } diff --git a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt index 84d7d9838c..2375417402 100644 --- a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt +++ b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/util/Compiler.kt @@ -34,6 +34,6 @@ object Compiler { messageOutputStream = System.out componentRegistrars = plugins inheritClassPath = true - kotlincArguments = listOf("-Xjvm-default=enable") + kotlincArguments = listOf("-Xjvm-default=all-compatibility") }.compile() } diff --git a/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt b/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt index 23266602ff..a58fdaf774 100644 --- a/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt +++ b/packages/test-base/src/jvmTest/kotlin/io/realm/kotlin/test/compiler/CollectionTests.kt @@ -114,7 +114,7 @@ abstract class CollectionTests( ) ) assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode) - assertTrue(result.messages.contains("Unsupported type for ${collectionType.description}: 'A'")) + assertTrue(result.messages.contains("Unsupported type for ${collectionType.description}: 'A'"), result.messages) } // ------------------------------------------------ diff --git a/packages/test-sync/build.gradle.kts b/packages/test-sync/build.gradle.kts index c4b9a718a4..d04f38661f 100644 --- a/packages/test-sync/build.gradle.kts +++ b/packages/test-sync/build.gradle.kts @@ -122,7 +122,6 @@ kotlin { } // JVM specific KotlinCompilation tasks tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).all { - kotlinOptions.jvmTarget = Versions.jvmTarget kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } } @@ -161,8 +160,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Versions.sourceCompatibilityVersion + targetCompatibility = Versions.targetCompatibilityVersion } // Remove overlapping resources after adding "org.jetbrains.kotlinx:kotlinx-coroutines-test" to @@ -287,27 +286,33 @@ kotlin { // - 'syncTestUrl` defines the root URL for the App Services server. Default is `http://localhost:9090` // - 'syncTestAppNamePrefix' is added a differentiator for all apps created by tests. This makes // it possible for builds in parallel to run against the same test server. Default is `test-app`. +fun getPropertyValue(propertyName: String): String? { + if (project.hasProperty(propertyName)) { + return project.property(propertyName) as String + } + return System.getenv(propertyName) +} buildkonfig { packageName = "io.realm.kotlin.test.mongodb" objectName = "SyncServerConfig" defaultConfigs { - buildConfigField(Type.STRING, "url", properties["syncTestUrl"]!! as String) - buildConfigField(Type.STRING, "appPrefix", properties["syncTestAppNamePrefix"]!! as String) - if (properties.containsKey("syncTestLoginEmail") && properties.containsKey("syncTestLoginPassword")) { - buildConfigField(Type.STRING, "email", properties["syncTestLoginEmail"]!! as String) - buildConfigField(Type.STRING, "password", properties["syncTestLoginPassword"]!! as String) + buildConfigField(Type.STRING, "url", getPropertyValue("syncTestUrl")) + buildConfigField(Type.STRING, "appPrefix", getPropertyValue("syncTestAppNamePrefix")) + if (project.hasProperty("syncTestLoginEmail") && project.hasProperty("syncTestLoginPassword")) { + buildConfigField(Type.STRING, "email", getPropertyValue("syncTestLoginEmail")) + buildConfigField(Type.STRING, "password", getPropertyValue("syncTestLoginPassword")) } else { buildConfigField(Type.STRING, "email", "") buildConfigField(Type.STRING, "password", "") } - if (properties.containsKey("syncTestLoginPublicApiKey") && properties.containsKey("syncTestLoginPrivateApiKey")) { - buildConfigField(Type.STRING, "publicApiKey", properties["syncTestLoginPublicApiKey"]!! as String) - buildConfigField(Type.STRING, "privateApiKey", properties["syncTestLoginPrivateApiKey"]!! as String) + if (project.hasProperty("syncTestLoginPublicApiKey") && project.hasProperty("syncTestLoginPrivateApiKey")) { + buildConfigField(Type.STRING, "publicApiKey", getPropertyValue("syncTestLoginPublicApiKey")) + buildConfigField(Type.STRING, "privateApiKey", getPropertyValue("syncTestLoginPrivateApiKey")) } else { buildConfigField(Type.STRING, "publicApiKey", "") buildConfigField(Type.STRING, "privateApiKey", "") } - buildConfigField(Type.STRING, "clusterName", properties["syncTestClusterName"] as String? ?: "") + buildConfigField(Type.STRING, "clusterName", getPropertyValue("syncTestClusterName") ?: "") } } 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/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt index cc8a6ee969..da9e02c6e7 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/HttpClient.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.test.mongodb.util import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig +import io.ktor.client.plugins.HttpRedirect import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.Logger @@ -65,7 +66,11 @@ fun defaultClient(name: String, debug: Boolean, block: HttpClientConfig<*>.() -> } } - followRedirects = true + // We should allow redirects for all types, not just GET and HEAD + // See https://github.com/ktorio/ktor/issues/1793 + install(HttpRedirect) { + checkHttpMethod = false + } // TODO connectionPool? this.apply(block) 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..3e73576d1b 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 @@ -17,8 +17,8 @@ package io.realm.kotlin.test.mongodb.common -import io.realm.kotlin.internal.platform.PATH_SEPARATOR import io.realm.kotlin.internal.platform.appFilesDirectory +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel import io.realm.kotlin.log.RealmLog @@ -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 @@ -142,7 +142,7 @@ class AppConfigurationTests { @Test fun syncRootDirectory() { val builder: AppConfiguration.Builder = AppConfiguration.Builder(APP_ID) - val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" + val expectedRoot = pathOf(appFilesDirectory(), "myCustomDir") val config = builder .syncRootDirectory(expectedRoot) .build() @@ -160,23 +160,20 @@ class AppConfigurationTests { // the configured `AppConfiguration.syncRootDir` @Test fun syncRootDirectory_appendDirectoryToPath() = runBlocking { - val expectedRoot = "${appFilesDirectory()}${PATH_SEPARATOR}myCustomDir" - val app = TestApp(builder = { + val expectedRoot = pathOf(appFilesDirectory(), "myCustomDir") + 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` val partitionValue = TestHelper.randomPartitionValue() val suffix = - "${PATH_SEPARATOR}myCustomDir${PATH_SEPARATOR}mongodb-realm${PATH_SEPARATOR}${user.app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}s_$partitionValue.realm" + pathOf("", "myCustomDir", "mongodb-realm", user.app.configuration.appId, user.id, "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..642278458c 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,19 +97,14 @@ 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) } config = SyncConfiguration.Builder( user, - schema = setOf( - Measurement::class, - Device::class, - BackupDevice::class, - DeviceParent::class - ) + schema = FLX_SYNC_SCHEMA ).initialSubscriptions { it.query().subscribe() }.build() @@ -299,11 +294,7 @@ class AsymmetricSyncTests { fun asymmetricSchema() = runBlocking { config = SyncConfiguration.Builder( app.login(Credentials.anonymous()), - schema = setOf( - AsymmetricA::class, - EmbeddedB::class, - StandardC::class, - ) + schema = FLX_SYNC_SCHEMA ).build() Realm.open(config).use { it.write { 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..841def7936 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 @@ -17,6 +17,7 @@ package io.realm.kotlin.test.mongodb.common import io.realm.kotlin.Realm import io.realm.kotlin.internal.platform.PATH_SEPARATOR +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.User @@ -46,7 +47,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) @@ -225,6 +226,6 @@ class FlexibleSyncConfigurationTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, setOf()) .name("custom.realm") .build() - assertTrue(config.path.endsWith("${app.configuration.appId}${PATH_SEPARATOR}${user.id}${PATH_SEPARATOR}custom.realm"), "Path is: ${config.path}") + assertTrue(config.path.endsWith(pathOf(app.configuration.appId, user.id, "custom.realm")), "Path is: ${config.path}") } } 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 661a2538fb..f3093f2e6a 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 @@ -58,12 +58,11 @@ import kotlin.time.Duration.Companion.seconds */ class FlexibleSyncIntegrationTests { - private val defaultSchema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) private lateinit var app: TestApp @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" runBlocking { app.createUserAndLogIn(email, password) @@ -83,7 +82,7 @@ class FlexibleSyncIntegrationTests { // Upload data from user 1 val user1 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm1 -> val subs = realm1.subscriptions.update { add(realm1.query("section = $0", randomSection)) @@ -98,7 +97,7 @@ class FlexibleSyncIntegrationTests { // Download data from user 2 val user2 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> add( realm.query( @@ -119,7 +118,7 @@ class FlexibleSyncIntegrationTests { @Test fun writeFailsIfNoSubscription() = runBlocking { val user = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config = SyncConfiguration.Builder(user, defaultSchema) + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA) .build() Realm.open(config).use { realm -> @@ -137,7 +136,7 @@ class FlexibleSyncIntegrationTests { val randomSection = Random.nextInt() // Generate random section to allow replays of unit tests val user = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config = SyncConfiguration.Builder(user, defaultSchema).build() + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA).build() Realm.open(config).use { realm -> realm.subscriptions.update { val query = realm.query() @@ -145,7 +144,7 @@ class FlexibleSyncIntegrationTests { .query("(name = 'red' OR name = 'blue')") add(query, "sub") } - assertTrue(realm.subscriptions.waitForSynchronization(60.seconds)) + assertTrue(realm.subscriptions.waitForSynchronization(120.seconds)) realm.write { copyToRealm(FlexParentObject(randomSection).apply { name = "red" }) copyToRealm(FlexParentObject(randomSection).apply { name = "blue" }) @@ -155,14 +154,14 @@ class FlexibleSyncIntegrationTests { val query = realm.query("section = $0 AND name = 'red'", randomSection) add(query, "sub", updateExisting = true) } - assertTrue(realm.subscriptions.waitForSynchronization(60.seconds)) + assertTrue(realm.subscriptions.waitForSynchronization(120.seconds)) assertEquals(1, realm.query().count().find()) } } @Test fun initialSubscriptions_timeOut() { - val config = SyncConfiguration.Builder(app.currentUser!!, defaultSchema) + val config = SyncConfiguration.Builder(app.currentUser!!, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> repeat(10) { add(realm.query("section = $0", it)) @@ -185,7 +184,7 @@ class FlexibleSyncIntegrationTests { // Prepare some user data val user1 = app.createUserAndLogin() - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm -> realm.subscriptions.update { add(realm.query("section = $0", randomSection)) @@ -207,7 +206,7 @@ class FlexibleSyncIntegrationTests { // User 2 opens a Realm twice val counter = atomic(0) val user2 = app.createUserAndLogin() - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions(rerunOnOpen = true) { realm -> add( realm.query( @@ -235,7 +234,7 @@ class FlexibleSyncIntegrationTests { // Upload data from user 1 val user1 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config1 = SyncConfiguration.create(user1, defaultSchema) + val config1 = SyncConfiguration.create(user1, FLX_SYNC_SCHEMA) Realm.open(config1).use { realm1 -> val subs = realm1.subscriptions.update { add(realm1.query("section = $0", randomSection)) @@ -273,7 +272,7 @@ class FlexibleSyncIntegrationTests { // Download data from user 2 val user2 = app.createUserAndLogIn(TestHelper.randomEmail(), "123456") - val config2 = SyncConfiguration.Builder(user2, defaultSchema) + val config2 = SyncConfiguration.Builder(user2, FLX_SYNC_SCHEMA) .initialSubscriptions { realm -> add( realm.query( @@ -304,7 +303,7 @@ class FlexibleSyncIntegrationTests { val channel = Channel(1) - val config1 = SyncConfiguration.Builder(user1, defaultSchema) + val config1 = SyncConfiguration.Builder(user1, FLX_SYNC_SCHEMA) .errorHandler { _: SyncSession, syncException: SyncException -> runBlocking { channel.send(syncException as CompensatingWriteException) @@ -332,7 +331,7 @@ class FlexibleSyncIntegrationTests { val exception: CompensatingWriteException = channel.receiveOrFail() - assertEquals("[Session][CompensatingWrite(231)] Client attempted a write that is disallowed by permissions, or modifies an object outside the current query, and the server undid the change.", exception.message) + assertTrue(exception.message!!.startsWith("[Sync][CompensatingWrite(1033)] Client attempted a write that is outside of permissions or query filters; it has been reverted Logs:"), exception.message) assertEquals(1, exception.writes.size) exception.writes[0].run { 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..354bc859ac 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 { @@ -1014,7 +1015,7 @@ class FunctionsTests { runBlocking { anonUser.logOut() } - assertFailsWithMessage("[Service][Unknown(4351)] expected Authorization header with JWT") { + assertFailsWithMessage("[Service][Unknown(4351)] unauthorized") { runBlocking { functions.call(FIRST_ARG_FUNCTION.name, 1, 2, 3) } 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..56ef66dc9a 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,14 +57,14 @@ 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) } config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -102,7 +102,7 @@ class MutableSubscriptionSetTests { assertEquals(SubscriptionSetState.PENDING, updatedSubs.state) val sub: Subscription = updatedSubs.first() assertEquals("test", sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}") assertEquals(sub.updatedAt, sub.createdAt) @@ -122,7 +122,7 @@ class MutableSubscriptionSetTests { assertEquals(SubscriptionSetState.PENDING, updatedSubs.state) val sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}") assertEquals(sub.updatedAt, sub.createdAt) @@ -185,7 +185,7 @@ class MutableSubscriptionSetTests { val sub = subs.first() assertEquals("sub1", sub.name) assertEquals("FlexParentObject", sub.objectType) - assertEquals("name == \"red\" ", sub.queryDescription) + assertEquals("name == \"red\"", sub.queryDescription) assertTrue(sub.createdAt < sub.updatedAt) assertEquals(createdAt, sub.createdAt) } @@ -284,7 +284,7 @@ class MutableSubscriptionSetTests { // Not part of schema realm.subscriptions.update { assertFailsWith { - removeAll(io.realm.kotlin.entities.sync.ParentPk::class) + removeAll(io.realm.kotlin.entities.Sample::class) } } 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..a3310c5494 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 @@ -60,8 +61,6 @@ import kotlin.time.Duration.Companion.seconds private const val TEST_SIZE = 500 private val TIMEOUT = 30.seconds -private val schema = setOf(SyncObjectWithAllTypes::class) - class ProgressListenerTests { private lateinit var app: TestApp @@ -69,7 +68,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 +238,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, FLX_SYNC_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() + } } } } @@ -303,7 +304,7 @@ class ProgressListenerTests { user: User, partitionValue: String = getTestPartitionValue() ): SyncConfiguration { - return SyncConfiguration.Builder(user, partitionValue, schema) + return SyncConfiguration.Builder(user, partitionValue, io.realm.kotlin.test.mongodb.common.PARTITION_SYNC_SCHEMA) .build() } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt new file mode 100644 index 0000000000..13591ef636 --- /dev/null +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/Schema.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 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.test.mongodb.common + +import io.realm.kotlin.entities.sync.ChildPk +import io.realm.kotlin.entities.sync.ObjectIdPk +import io.realm.kotlin.entities.sync.ParentPk +import io.realm.kotlin.entities.sync.SyncObjectWithAllTypes +import io.realm.kotlin.entities.sync.SyncPerson +import io.realm.kotlin.entities.sync.flx.FlexChildObject +import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject +import io.realm.kotlin.entities.sync.flx.FlexParentObject + +private val ASYMMETRIC_CLASSES = setOf( + AsymmetricSyncTests.AsymmetricA::class, + AsymmetricSyncTests.EmbeddedB::class, + AsymmetricSyncTests.StandardC::class, + Measurement::class, +) + +private val DEFAULT_CLASSES = setOf( + BackupDevice::class, + ChildPk::class, + Device::class, + DeviceParent::class, + FlexChildObject::class, + FlexEmbeddedObject::class, + FlexParentObject::class, + ObjectIdPk::class, + ParentPk::class, + SyncObjectWithAllTypes::class, + SyncPerson::class +) + +val FLX_SYNC_SCHEMA = DEFAULT_CLASSES + ASYMMETRIC_CLASSES +val PARTITION_SYNC_SCHEMA = DEFAULT_CLASSES 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..bce3f8697a 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 @@ -17,8 +17,6 @@ package io.realm.kotlin.test.mongodb.common import io.realm.kotlin.Realm -import io.realm.kotlin.entities.sync.flx.FlexChildObject -import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.runBlocking @@ -45,7 +43,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlin.text.Typography.section import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds @@ -60,14 +57,14 @@ 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) } val config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -92,7 +89,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -107,7 +104,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -122,7 +119,7 @@ class SubscriptionExtensionsTests { assertEquals(1, subs.size) val sub: Subscription = subs.first() assertNull(sub.name) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) assertEquals(FlexParentObject::class.simpleName, sub.objectType) } @@ -136,7 +133,7 @@ class SubscriptionExtensionsTests { val user1 = app.createUserAndLogIn(email, password) val config = SyncConfiguration.Builder( user1, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ).initialSubscriptions { realm: Realm -> realm.query("section = $0", section).subscribe() }.build() @@ -168,7 +165,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) var sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -184,7 +181,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) sub = updatedSubs.last() assertEquals("my-name", sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -269,7 +266,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) var sub: Subscription = updatedSubs.first() assertNull(sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -285,7 +282,7 @@ class SubscriptionExtensionsTests { assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state) sub = updatedSubs.last() assertEquals("my-name", sub.name) - assertEquals("section == $section ", sub.queryDescription) + assertEquals("section == $section", sub.queryDescription) assertEquals("FlexParentObject", sub.objectType) // Checking that we don't hit the network the 2nd time around @@ -340,12 +337,12 @@ class SubscriptionExtensionsTests { subQueryResult.subscribe() val subs = realm.subscriptions assertEquals(1, subs.size) - assertEquals("section == 42 and name == \"Jane\" ", subs.first().queryDescription) + assertEquals("section == 42 and name == \"Jane\"", subs.first().queryDescription) subQueryResult.subscribe("my-name") assertEquals(2, subs.size) val lastSub = subs.last() assertEquals("my-name", lastSub.name) - assertEquals("section == 42 and name == \"Jane\" ", lastSub.queryDescription) + assertEquals("section == 42 and name == \"Jane\"", lastSub.queryDescription) } @Test @@ -374,9 +371,27 @@ class SubscriptionExtensionsTests { } } + @Test + fun updatingOnlyQueryWillTriggerFirstTimeBehavior() = runBlocking { + val section = Random.nextInt() + + // 1. Create a named subscription + realm.query("section = $0", section).subscribe("my-name", mode = WaitForSync.FIRST_TIME) + + // 2. Pause the connection in order to go offline + realm.syncSession.pause() + + // 3. Update the query of the named subscription. This should trigger FIRST_TIME behavior again. + // and because we are offline, the subscribe call should throw. + val query = realm.query("section = $0 AND TRUEPREDICATE", section) + assertFailsWith { + query.subscribe("my-name", updateExisting = true, mode = WaitForSync.FIRST_TIME, timeout = 1.seconds) + } + } + private suspend fun uploadServerData(sectionId: Int, noOfObjects: Int) { val user = app.createUserAndLogin() - val config = SyncConfiguration.Builder(user, setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class)) + val config = SyncConfiguration.Builder(user, FLX_SYNC_SCHEMA) .initialSubscriptions { it.query().subscribe() } 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..73487856bb 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,14 +57,14 @@ 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) } val config = SyncConfiguration.Builder( user, - schema = setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -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 } + } } } @@ -132,7 +134,7 @@ class SubscriptionSetTests { val sub: Subscription = subscriptions.findByQuery(query)!! assertNotNull(sub) assertEquals("FlexParentObject", sub.objectType) - assertEquals("TRUEPREDICATE ", sub.queryDescription) + assertEquals("TRUEPREDICATE", sub.queryDescription) } @Test @@ -190,7 +192,10 @@ class SubscriptionSetTests { assertFailsWith { subscriptions.waitForSynchronization() } - assertTrue(subscriptions.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + subscriptions.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + subscriptions.errorMessage + ) subscriptions.update { removeAll() } @@ -256,7 +261,10 @@ class SubscriptionSetTests { updatedSubs.waitForSynchronization() } assertEquals(SubscriptionSetState.ERROR, updatedSubs.state) - assertTrue(updatedSubs.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + updatedSubs.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + updatedSubs.errorMessage + ) } // Test case for https://github.com/realm/realm-core/issues/5504 @@ -270,7 +278,10 @@ class SubscriptionSetTests { } assertEquals(SubscriptionSetState.ERROR, updatedSubs.state) assertEquals("TRUEPREDICATE and TRUEPREDICATE LIMIT(1)", updatedSubs.first().queryDescription) - assertTrue(updatedSubs.errorMessage!!.contains("Client provided query with bad syntax")) + assertTrue( + updatedSubs.errorMessage!!.contains("Invalid query: invalid RQL for table \"FlexParentObject\": syntax error: unexpected Limit, expecting Or or RightParenthesis"), + updatedSubs.errorMessage + ) } @Test 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..1bc7105f75 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,14 +55,14 @@ 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) } val config = SyncConfiguration.Builder( user, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ) .build() realm = Realm.open(config) @@ -90,7 +90,7 @@ class SubscriptionTests { assertEquals("mySub", namedSub.name) assertEquals("ParentPk", namedSub.objectType) - assertEquals("TRUEPREDICATE ", namedSub.queryDescription) + assertEquals("TRUEPREDICATE", namedSub.queryDescription) assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}") assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}") @@ -100,7 +100,7 @@ class SubscriptionTests { }.first() assertNull(anonSub.name) assertEquals("ParentPk", anonSub.objectType) - assertEquals("TRUEPREDICATE ", anonSub.queryDescription) + assertEquals("TRUEPREDICATE", anonSub.queryDescription) assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}") assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}") } @@ -119,7 +119,7 @@ class SubscriptionTests { // Check that properties still work even if subscription is deleted elsewhere assertEquals("mySub", snapshotSub.name) assertEquals("ParentPk", snapshotSub.objectType) - assertEquals("TRUEPREDICATE ", snapshotSub.queryDescription) + assertEquals("TRUEPREDICATE", snapshotSub.queryDescription) assertNotNull(snapshotSub.updatedAt) assertNotNull(snapshotSub.createdAt) Unit 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 6dbadc3571..16b68768c6 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 @@ -21,12 +21,9 @@ import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm import io.realm.kotlin.TypedRealm import io.realm.kotlin.entities.sync.SyncPerson -import io.realm.kotlin.entities.sync.flx.FlexChildObject -import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query -import io.realm.kotlin.internal.interop.sync.ProtocolClientErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.platform.fileExists import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel @@ -114,6 +111,7 @@ class SyncClientResetIntegrationTests { ) -> Unit ) { val app = TestApp( + this::class.simpleName, appName = appName, logLevel = LogLevel.INFO, customLogger = ClientResetLoggerInspector(logChannel), @@ -161,11 +159,7 @@ class SyncClientResetIntegrationTests { configBuilderGenerator = { user -> return@TestEnvironment SyncConfiguration.Builder( user, - setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ) + FLX_SYNC_SCHEMA ).initialSubscriptions { realm -> realm.query( "section = $0 AND name = $1", @@ -220,7 +214,7 @@ class SyncClientResetIntegrationTests { return@TestEnvironment SyncConfiguration.Builder( user, TestHelper.randomPartitionValue(), - schema = setOf(SyncPerson::class) + schema = PARTITION_SYNC_SCHEMA ) }, insertElement = { realm: Realm -> @@ -596,7 +590,7 @@ class SyncClientResetIntegrationTests { // testing the server will send a different message. This just ensures that // we don't accidentally modify or remove the message. assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) @@ -609,10 +603,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) // TODO Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) @@ -624,21 +615,25 @@ class SyncClientResetIntegrationTests { @Test fun discardUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - discardUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + discardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun discardUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - discardUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + discardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun discardUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { + var testRealm: Realm? = null // Channel size is 2 because both onError and onManualResetFallback are called val channel = Channel(2) val config = builder.syncClientResetStrategy(object : DiscardUnsyncedChangesStrategy { @@ -662,12 +657,14 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) @@ -677,17 +674,13 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) + // Twice until the deprecated method is removed assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } @@ -735,7 +728,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -747,7 +740,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -819,7 +812,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -831,7 +824,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -898,9 +891,8 @@ class SyncClientResetIntegrationTests { ) // assertEquals(ClientResetLogEvents.DISCARD_LOCAL_ON_AFTER_RESET, logChannel.receiveOrFail()) - (realm.syncSession as SyncSessionImpl).simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT + (realm.syncSession as SyncSessionImpl).simulateSyncError( + ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED ) // Validate that we receive logs on the error callback val actual = logChannel.receiveOrFail() @@ -948,10 +940,7 @@ class SyncClientResetIntegrationTests { realm.syncSession.downloadAllServerChanges(defaultTimeout) with((realm.syncSession as SyncSessionImpl)) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) val exception = channel.receiveOrFail() val originalFilePath = assertNotNull(exception.originalFilePath) @@ -959,7 +948,7 @@ class SyncClientResetIntegrationTests { assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] Simulate Client Reset.", exception.message ) } @@ -969,19 +958,22 @@ class SyncClientResetIntegrationTests { @Test fun manuallyRecoverUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - manuallyRecoverUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + manuallyRecoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun manuallyRecoverUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - manuallyRecoverUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + manuallyRecoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun manuallyRecoverUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { val channel = Channel(1) @@ -997,24 +989,18 @@ class SyncClientResetIntegrationTests { } ).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) - + runBlocking { + Realm.open(config).use { realm -> + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) val exception = channel.receiveOrFail() - val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) + realm.close() assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - - exception.executeClientReset() + assertTrue(exception.executeClientReset()) assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) } @@ -1121,17 +1107,14 @@ class SyncClientResetIntegrationTests { runBlocking { realm.syncSession.downloadAllServerChanges(defaultTimeout) - (realm.syncSession as SyncSessionImpl).simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) + (realm.syncSession as SyncSessionImpl).simulateSyncError(ErrorCode.RLM_ERR_AUTO_CLIENT_RESET_FAILED) val exception = channel.receiveOrFail() assertNotNull(exception.recoveryFilePath) assertNotNull(exception.originalFilePath) assertFalse(fileExists(exception.recoveryFilePath)) assertTrue(fileExists(exception.originalFilePath)) - assertTrue(exception.message!!.contains("Automatic recovery from client reset failed")) + assertTrue(exception.message!!.contains("Simulate Client Reset")) } } } @@ -1174,7 +1157,7 @@ class SyncClientResetIntegrationTests { ) { // Notify that this callback has been invoked assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) channel.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) @@ -1196,23 +1179,30 @@ class SyncClientResetIntegrationTests { @Test fun recoverUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - recoverUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + recoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun recoverUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - recoverUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + recoverUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } - private fun recoverUnsyncedChanges_executeClientReset(builder: SyncConfiguration.Builder) { + private fun recoverUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, + builder: SyncConfiguration.Builder + ) { + var testRealm: Realm? = null val channel = Channel(2) val config = builder.syncClientResetStrategy(object : RecoverUnsyncedChangesStrategy { override fun onBeforeReset(realm: TypedRealm) { - fail("Should not call onBeforeReset") + @Suppress("TooGenericExceptionThrown") + throw RuntimeException("Trigger onManualResetFallback") } override fun onAfterReset(before: TypedRealm, after: MutableRealm) { @@ -1223,19 +1213,21 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) @@ -1243,17 +1235,12 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } } @@ -1391,25 +1378,30 @@ class SyncClientResetIntegrationTests { @Test fun recoverOrDiscardUnsyncedChanges_executeClientReset_pbs() = runBlocking { - performPbsTest { _, _, _, builder -> - recoverOrDiscardUnsyncedChanges_executeClientReset(builder) + performPbsTest { syncMode, app, user, builder -> + recoverOrDiscardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } @Test fun recoverOrDiscardUnsyncedChanges_executeClientReset_flx() = runBlocking { - performFlxTest { _, _, _, builder -> - recoverOrDiscardUnsyncedChanges_executeClientReset(builder) + performFlxTest { syncMode, app, user, builder -> + recoverOrDiscardUnsyncedChanges_executeClientReset(syncMode, app, user, builder) } } private fun recoverOrDiscardUnsyncedChanges_executeClientReset( + syncMode: SyncMode, + app: TestApp, + user: User, builder: SyncConfiguration.Builder ) { + var testRealm: Realm? = null val channel = Channel(2) val config = builder.syncClientResetStrategy(object : RecoverOrDiscardUnsyncedChangesStrategy { override fun onBeforeReset(realm: TypedRealm) { - fail("Should not call onBeforeReset") + @Suppress("TooGenericExceptionThrown") + throw RuntimeException("Trigger onManualResetFallback") } override fun onAfterRecovery(before: TypedRealm, after: MutableRealm) { @@ -1424,19 +1416,21 @@ class SyncClientResetIntegrationTests { session: SyncSession, exception: ClientResetRequiredException ) { + testRealm!!.close() + val originalFilePath = assertNotNull(exception.originalFilePath) val recoveryFilePath = assertNotNull(exception.recoveryFilePath) assertTrue(fileExists(originalFilePath)) assertFalse(fileExists(recoveryFilePath)) - exception.executeClientReset() + assertTrue(exception.executeClientReset()) // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) assertEquals( - "[Client][AutoClientResetFailure(132)] Automatic recovery from client reset failed.", + "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) @@ -1444,17 +1438,12 @@ class SyncClientResetIntegrationTests { } }).build() - Realm.open(config).use { realm -> - runBlocking { - realm.syncSession.downloadAllServerChanges(defaultTimeout) - - with(realm.syncSession as SyncSessionImpl) { - simulateError( - ProtocolClientErrorCode.RLM_SYNC_ERR_CLIENT_AUTO_CLIENT_RESET_FAILURE, - SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CLIENT - ) - - // TODO Twice until the deprecated method is removed + runBlocking { + Realm.open(config).use { realm -> + testRealm = realm + with(realm.syncSession) { + downloadAllServerChanges(defaultTimeout) + app.triggerClientReset(syncMode, this, user.id) assertEquals(ClientResetEvents.ON_MANUAL_RESET_FALLBACK, channel.receiveOrFail()) } } 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..5cdd58e678 --- /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 = FLX_SYNC_SCHEMA) + Realm.open(config).use { + app.sync.reconnect() + } + } + + @Test + fun hasSyncSessions_noRealms() { + assertFalse(app.sync.hasSyncSessions) + } + + @Test + fun hasSyncSessions() { + val config = SyncConfiguration.create(user, schema = FLX_SYNC_SCHEMA) + Realm.open(config).use { + assertTrue(app.sync.hasSyncSessions) + } + } + + @Test + fun waitForSessionsToTerminate_noRealms() { + app.sync.waitForSessionsToTerminate() + } + + @Test + fun waitForSessionsToTerminate() { + val config1 = SyncConfiguration.Builder(user, schema = FLX_SYNC_SCHEMA).build() + val config2 = SyncConfiguration.Builder(user, schema = FLX_SYNC_SCHEMA).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..77a80463d5 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 @@ -30,6 +30,7 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.createDefaultSystemLogger +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.internal.platform.singleThreadDispatcher import io.realm.kotlin.log.LogLevel @@ -87,7 +88,7 @@ class SyncConfigTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) } @AfterTest @@ -104,7 +105,7 @@ class SyncConfigTests { val logger = createDefaultSystemLogger("TEST", LogLevel.DEBUG) val customLoggers = listOf(logger) val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -123,7 +124,7 @@ class SyncConfigTests { } val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -136,7 +137,7 @@ class SyncConfigTests { fun errorHandler_default() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -149,7 +150,7 @@ class SyncConfigTests { fun compactOnLaunch_default() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -163,7 +164,7 @@ class SyncConfigTests { val user = createTestUser() val callback = CompactOnLaunchCallback { _, _ -> false } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ) @@ -185,7 +186,7 @@ class SyncConfigTests { ) } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ) @@ -248,7 +249,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -276,7 +277,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -308,7 +309,7 @@ class SyncConfigTests { } } val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).syncClientResetStrategy(strategy) @@ -320,7 +321,7 @@ class SyncConfigTests { fun equals_sameObject() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -373,7 +374,7 @@ class SyncConfigTests { fun equals_syncSpecificFields() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -406,7 +407,7 @@ class SyncConfigTests { private fun verifyName(builder: SyncConfiguration.Builder, expectedFileName: String) { val config = builder.build() - val suffix = "/mongodb-realm/${config.user.app.configuration.appId}/${config.user.identity}/$expectedFileName" + val suffix = pathOf("", "mongodb-realm", config.user.app.configuration.appId, config.user.id, expectedFileName) assertTrue(config.path.contains(suffix), "${config.path} failed.") assertEquals(expectedFileName, config.name) } @@ -531,7 +532,7 @@ class SyncConfigTests { fun encryption() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).also { builder -> @@ -758,7 +759,7 @@ class SyncConfigTests { fun getPartitionValue() { val user = createTestUser() val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -1230,7 +1231,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 @@ -1242,7 +1243,7 @@ class SyncConfigTests { } SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).build() @@ -1260,7 +1261,7 @@ class SyncConfigTests { val config: SyncConfiguration = SyncConfiguration.Builder(user, partitionValue, setOf()) .name(fileName) .build() - val suffix = "/mongodb-realm/${user.app.configuration.appId}/${user.identity}/$fileName" + val suffix = pathOf("", "mongodb-realm", user.app.configuration.appId, user.id, fileName) assertTrue(config.path.endsWith(suffix), "${config.path} failed.") assertEquals(fileName, config.name, "${config.name} failed.") } 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..827832ed2f 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) @@ -258,13 +258,13 @@ class SyncSessionTests { val config1 = SyncConfiguration.Builder( user1, partitionValue, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ).name("user1.realm") .build() val config2 = SyncConfiguration.Builder( user2, partitionValue, - schema = setOf(ParentPk::class, ChildPk::class) + schema = FLX_SYNC_SCHEMA ).name("user2.realm") .build() @@ -320,7 +320,7 @@ class SyncSessionTests { val user = app.createUserAndLogIn(email, password) val channel = Channel(1) val config = SyncConfiguration.Builder( - schema = setOf(ParentPk::class, ChildPk::class), + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).errorHandler { session, _ -> @@ -382,7 +382,7 @@ class SyncSessionTests { @Test fun syncingObjectIdFromMongoDB() = runBlocking { val adminApi = app.asTestApp - val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf(ObjectIdPk::class)).build() + val config = SyncConfiguration.Builder(user, partitionValue, schema = PARTITION_SYNC_SCHEMA).build() Realm.open(config).use { realm -> val json: JsonObject = adminApi.insertDocument( ObjectIdPk::class.simpleName!!, @@ -427,7 +427,7 @@ class SyncSessionTests { val config = SyncConfiguration.Builder( user, partitionValue, - schema = setOf(ObjectIdPk::class) + schema = PARTITION_SYNC_SCHEMA ) .build() Realm.open(config).use { realm -> @@ -473,6 +473,7 @@ class SyncSessionTests { } } + @Ignore // TODO Find another way to test with with developer mode v2 @Test fun getConfiguration_inErrorHandlerThrows() = runBlocking { // Open and close a realm with a schema. @@ -480,7 +481,7 @@ class SyncSessionTests { val (email, password) = TestHelper.randomEmail() to "password1234" val user = app.createUserAndLogIn(email, password) val config1 = SyncConfiguration.Builder( - schema = setOf(ChildPk::class), + schema = FLX_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).name("test1.realm").build() 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 38e0fa85b4..e486bd9e4d 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 @@ -29,6 +29,7 @@ import io.realm.kotlin.entities.sync.flx.FlexEmbeddedObject import io.realm.kotlin.entities.sync.flx.FlexParentObject import io.realm.kotlin.ext.query import io.realm.kotlin.internal.platform.fileExists +import io.realm.kotlin.internal.platform.pathOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.log.LogLevel import io.realm.kotlin.mongodb.App @@ -56,12 +57,12 @@ 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 import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.test.util.use -import io.realm.kotlin.types.BaseRealmObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -77,7 +78,6 @@ import okio.Path.Companion.toPath import org.mongodb.kbson.ObjectId import kotlin.random.Random import kotlin.random.nextULong -import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Ignore @@ -110,14 +110,14 @@ class SyncedRealmTests { @BeforeTest fun setup() { partitionValue = TestHelper.randomPartitionValue() - app = TestApp() + app = TestApp(this::class.simpleName) val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) } - syncConfiguration = createSyncConfig( + syncConfiguration = createPartitionSyncConfig( user = user, partitionValue = partitionValue, ) @@ -147,7 +147,7 @@ class SyncedRealmTests { val user = app.createUserAndLogIn(email, password) val partitionValue = Random.nextULong().toString() - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -156,7 +156,7 @@ class SyncedRealmTests { } ) Realm.open(config1).use { realm1 -> - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db2", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -214,11 +214,10 @@ class SyncedRealmTests { val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user1, name = "db1.realm", - partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) + partitionValue = partitionValue ) val realm1 = Realm.open(config1) val c = Channel>(1) @@ -231,11 +230,10 @@ class SyncedRealmTests { assertTrue(event is InitialRealm) // Write remote change - createSyncConfig( + createPartitionSyncConfig( user = user2, name = "db2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> realm.write { @@ -272,10 +270,10 @@ class SyncedRealmTests { val partitionValue = Random.nextLong().toString() // Setup two realms that synchronizes with the backend - val config1 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config1 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") val realm1 = Realm.open(config1) assertNotNull(realm1) - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db2") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db2") val realm2 = Realm.open(config2) assertNotNull(realm2) @@ -294,7 +292,7 @@ class SyncedRealmTests { // writer and notifier) are opened before the schema is synced from the server, but // empirically it has shown not to be the case and cause trouble if opening the second or // third realm with the wrong sync-intended schema mode. - val config3 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db3") + val config3 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db3") val realm3 = Realm.open(config3) assertNotNull(realm3) @@ -386,15 +384,9 @@ class SyncedRealmTests { exception.message.let { errorMessage -> assertNotNull(errorMessage) // Some race on JVM in particular mean that different errors can be reported. - if (errorMessage.contains("[Client]")) { - assertTrue(errorMessage.contains("[BadChangeset(112)]"), errorMessage) - assertTrue(errorMessage.contains("Bad changeset (DOWNLOAD)"), errorMessage) - } else if (errorMessage.contains("[Session]")) { - assertTrue(errorMessage.contains("InvalidSchemaChange(225)"), errorMessage) - assertTrue( - errorMessage.contains("Invalid schema change (UPLOAD)"), - errorMessage - ) + if (errorMessage.contains("[Sync]")) { + assertTrue(errorMessage.contains("[BadChangeset(1015)]"), errorMessage) + assertTrue(errorMessage.contains("Schema mismatch"), errorMessage) } else { fail("Unexpected error message: $errorMessage") } @@ -553,18 +545,19 @@ class SyncedRealmTests { } } - // Currently no good way to delete synced Realms that has been opened. - // See https://github.com/realm/realm-core/issues/5542 + // Currently there isn't a good good way to delete synced Realms that has been opened, but + // `Sync.waitForSessionsToTerminate` can be used in some cases. + // + // See https://github.com/realm/realm-core/issues/5542 for more details @Test @Suppress("LongMethod") - @Ignore fun deleteRealm() { val fileSystem = FileSystem.SYSTEM val user = app.asTestApp.createUserAndLogin() val configuration: SyncConfiguration = SyncConfiguration.create(user, partitionValue, setOf()) val syncDir: Path = - "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/${user.identity}".toPath() + pathOf(app.configuration.syncRootDirectory, "mongodb-realm", app.configuration.appId, user.id).toPath() val bgThreadReadyChannel = Channel(1) val readyToCloseChannel = Channel(1) @@ -595,6 +588,7 @@ class SyncedRealmTests { closedChannel.receiveOrFail() // Delete realm now that it's fully closed. + app.sync.waitForSessionsToTerminate() Realm.deleteRealm(configuration) // Lock file should never be deleted. @@ -622,10 +616,9 @@ class SyncedRealmTests { val id = "id-${Random.nextLong()}" val masterObject = SyncObjectWithAllTypes.createWithSampleData(id) - createSyncConfig( + createPartitionSyncConfig( user = user1, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> realm.write { @@ -634,10 +627,9 @@ class SyncedRealmTests { realm.syncSession.uploadAllLocalChanges() } } - createSyncConfig( + createPartitionSyncConfig( user = user2, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ).let { config -> Realm.open(config).use { realm -> val list: RealmResults = @@ -656,16 +648,17 @@ class SyncedRealmTests { // return the full on-disk schema from ObjectStore, but for typed Realms the user visible schema // should still only return classes and properties that was defined by the user. @Test + @Ignore // TODO Need to adopt this to developer mode fun onlyLocalSchemaIsVisible() = runBlocking { val (email1, password1) = randomEmail() to "password1234" val (email2, password2) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - createSyncConfig( + createPartitionSyncConfig( user = user1, partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class, ChildPk::class) + // schema = setOf(SyncObjectWithAllTypes::class, ChildPk::class) ).let { config -> Realm.open(config).use { realm -> realm.syncSession.uploadAllLocalChanges() @@ -678,10 +671,10 @@ class SyncedRealmTests { assertNotNull(childPkSchema["linkedFrom"]) } } - createSyncConfig( + createPartitionSyncConfig( user = user2, partitionValue = partitionValue, - schema = setOf(io.realm.kotlin.entities.sync.subset.ChildPk::class) + // schema = setOf(io.realm.kotlin.entities.sync.subset.ChildPk::class) ).let { config -> Realm.open(config).use { realm -> // Make sure that server schema changes are integrated @@ -703,27 +696,24 @@ class SyncedRealmTests { @Test fun mutableRealmInt_convergesAcrossClients() = runBlocking { // Updates and initial data upload are carried out using this config - val config0 = createSyncConfig( + val config0 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db1", - schema = setOf(SyncObjectWithAllTypes::class) ) // Config for update 1 - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db2", - schema = setOf(SyncObjectWithAllTypes::class) ) // Config for update 2 - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = app.createUserAndLogIn(randomEmail(), "password1234"), partitionValue = partitionValue, name = "db3", - schema = setOf(SyncObjectWithAllTypes::class) ) val counterValue = Channel(1) @@ -818,17 +808,15 @@ class SyncedRealmTests { val user2 = app.createUserAndLogIn(email2, password2) val localConfig = createWriteCopyLocalConfig("local.realm") val partitionValue = TestHelper.randomPartitionValue() - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(localConfig).use { localRealm -> localRealm.writeBlocking { @@ -867,36 +855,32 @@ 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 + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user1 = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val flexSyncConfig = createFlexibleSyncConfig( + user = user1 ) - ) - Realm.open(localConfig).use { localRealm -> - localRealm.writeBlocking { - copyToRealm( - SyncObjectWithAllTypes().apply { - stringField = "local object" - } - ) - } - assertFailsWith { - localRealm.writeCopyTo(flexSyncConfig) + Realm.open(localConfig).use { localRealm -> + localRealm.writeBlocking { + copyToRealm( + SyncObjectWithAllTypes().apply { + stringField = "local object" + } + ) + } + assertFailsWith { + localRealm.writeCopyTo(flexSyncConfig) + } } } - flexApp.close() } @Test @@ -908,11 +892,10 @@ class SyncedRealmTests { val migratedLocalConfig = createWriteCopyLocalConfig("local.realm", directory = dir, schemaVersion = 1) val partitionValue = TestHelper.randomPartitionValue() - val syncConfig = createSyncConfig( + val syncConfig = createPartitionSyncConfig( user = user, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig).use { syncRealm -> // Write local data @@ -951,42 +934,38 @@ 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 + ).use { flexApp -> + val (email1, password1) = randomEmail() to "password1234" + val user = flexApp.createUserAndLogIn(email1, password1) + val localConfig = createWriteCopyLocalConfig("local.realm") + val syncConfig = createPartitionSyncConfig( + user = user, + name = "sync.realm", + partitionValue = partitionValue, ) - ) - Realm.open(syncConfig).use { flexSyncRealm: Realm -> - flexSyncRealm.writeBlocking { - copyToRealm( - FlexParentObject().apply { - name = "local object" - } - ) + 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 @@ -995,17 +974,15 @@ class SyncedRealmTests { val (email2, password2) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig1).use { syncRealm1 -> syncRealm1.writeBlocking { @@ -1039,17 +1016,15 @@ class SyncedRealmTests { val user1 = app.createUserAndLogIn(email1, password1) val user2 = app.createUserAndLogIn(email2, password2) val partitionValue = TestHelper.randomPartitionValue() - val syncConfig1 = createSyncConfig( + val syncConfig1 = createPartitionSyncConfig( user = user1, name = "sync1.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfig2 = createSyncConfig( + val syncConfig2 = createPartitionSyncConfig( user = user2, name = "sync2.realm", partitionValue = partitionValue, - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfig1).use { syncRealm1 -> // Write local data @@ -1079,104 +1054,93 @@ 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()) + }, + initialSubscriptions = { realm: Realm -> + realm.query("section = $0", section).subscribe(name = "parentSubscription") + } + ) + val syncConfig2 = createFlexibleSyncConfig( + user = user2, + name = "sync2.realm", + errorHandler = { _, error -> + fail(error.toString()) + } ) - ) - 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)) - - // 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 fun writeCopyTo_dataNotUploaded_throws() = runBlocking { val (email1, password1) = randomEmail() to "password1234" val user1 = app.createUserAndLogIn(email1, password1) - val syncConfigA = createSyncConfig( + val syncConfigA = createPartitionSyncConfig( user = user1, name = "a.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) - val syncConfigB = createSyncConfig( + val syncConfigB = createPartitionSyncConfig( user = user1, name = "b.realm", partitionValue = TestHelper.randomPartitionValue(), - schema = setOf(SyncObjectWithAllTypes::class) ) Realm.open(syncConfigA).use { realm -> realm.syncSession.pause() @@ -1194,69 +1158,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() + } } } @@ -1264,7 +1229,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-")) @@ -1272,30 +1238,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 @@ -1309,8 +1275,8 @@ class SyncedRealmTests { partitionValue = TestHelper.randomPartitionValue() - val config1 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db2") + val config1 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db2") Realm.open(config1).use { realm1 -> Realm.open(config2).use { realm2 -> @@ -1353,7 +1319,7 @@ class SyncedRealmTests { copyToRealm(ParentPk().apply { _id = ObjectId().toString() }) } .build() - val config2 = createSyncConfig(user = user, partitionValue = partitionValue, name = "db1") + val config2 = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "db1") Realm.open(config1).use { assertEquals(2, it.query().find().size) it.writeCopyTo(config2) @@ -1369,7 +1335,7 @@ class SyncedRealmTests { val user = runBlocking { app.createUserAndLogIn(email, password) } - val config1 = createSyncConfig( + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -1392,7 +1358,7 @@ class SyncedRealmTests { } } - val config2 = createSyncConfig( + val config2 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { override fun onError(session: SyncSession, error: SyncException) { @@ -1422,76 +1388,68 @@ 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()) + }, + initialSubscriptions = { realm: Realm -> + realm.query() + .subscribe(name = "parentSubscription") + } + ) + val syncConfig2 = createFlexibleSyncConfig( + user = user1, + name = "asset-fs.realm", + errorHandler = { _, error -> + fail(error.toString()) + } ) - ) - 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" - } - ) + 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( @@ -1499,12 +1457,7 @@ class SyncedRealmTests { name = "sync1.realm", errorHandler = { _, error -> fail(error.toString()) - }, - schema = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), + } ) { initialRealmFile("asset-fs.realm") initialData { @@ -1516,8 +1469,6 @@ class SyncedRealmTests { assertEquals(1, flexRealm1.subscriptions.size) assertNotNull(flexRealm1.subscriptions.findByName("parentSubscription")) } - } finally { - flexApp.close() } } @@ -1528,7 +1479,7 @@ class SyncedRealmTests { app.createUserAndLogIn(email, password) } - val local = createSyncConfig(user = user, partitionValue = partitionValue, name = "local") { + val local = createPartitionSyncConfig(user = user, partitionValue = partitionValue, name = "local") { initialRealmFile("asset-local.realm") } assertFalse(fileExists(local.path)) @@ -1895,17 +1846,16 @@ class SyncedRealmTests { // } @Suppress("LongParameterList") - private fun createSyncConfig( + private fun createPartitionSyncConfig( user: User, partitionValue: String, name: String = DEFAULT_NAME, encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, - schema: Set> = setOf(ParentPk::class, ChildPk::class), block: SyncConfiguration.Builder.() -> Unit = {} ): SyncConfiguration = SyncConfiguration.Builder( - schema = schema, + schema = PARTITION_SYNC_SCHEMA, user = user, partitionValue = partitionValue ).name(name).also { builder -> @@ -1922,16 +1872,11 @@ class SyncedRealmTests { encryptionKey: ByteArray? = null, log: LogConfiguration? = null, errorHandler: ErrorHandler? = null, - schema: Set> = setOf( - FlexParentObject::class, - FlexChildObject::class, - FlexEmbeddedObject::class - ), initialSubscriptions: InitialSubscriptionsCallback? = null, block: SyncConfiguration.Builder.() -> Unit = {}, ): SyncConfiguration = SyncConfiguration.Builder( user = user, - schema = schema + schema = FLX_SYNC_SCHEMA ).name(name).also { builder -> if (encryptionKey != null) builder.encryptionKey(encryptionKey) if (errorHandler != null) builder.errorHandler(errorHandler) 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/internal/RealmSyncUtilsTest.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt index 9cf0bd49fd..6c6f5a1fc0 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/RealmSyncUtilsTest.kt @@ -18,67 +18,74 @@ package io.realm.kotlin.test.mongodb.common.internal +import io.realm.kotlin.internal.interop.CoreError import io.realm.kotlin.internal.interop.ErrorCategory +import io.realm.kotlin.internal.interop.ErrorCode import io.realm.kotlin.internal.interop.UnknownCodeDescription import io.realm.kotlin.internal.interop.sync.AppError import io.realm.kotlin.internal.interop.sync.SyncError -import io.realm.kotlin.internal.interop.sync.SyncErrorCode -import io.realm.kotlin.internal.interop.sync.SyncErrorCodeCategory import io.realm.kotlin.mongodb.internal.convertAppError import io.realm.kotlin.mongodb.internal.convertSyncError import kotlin.test.Test import kotlin.test.assertEquals -const val UNMAPPED_CODE: Int = 0 +const val UNMAPPED_CATEGORY_CODE: Int = 0 +const val UNMAPPED_ERROR_CODE: Int = -1 class RealmSyncUtilsTest { + @Test - fun convertSyncErrorCode_unmappedErrorCode_categoryTypeUnknown() { + fun convertSyncErrorCode_unmappedErrorCode2() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_UNKNOWN, - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR.nativeValue, + errorCodeNativeValue = UNMAPPED_ERROR_CODE, + messageNativeValue = "Placeholder message" ) ) ) - assertEquals("[Unknown][$UNMAPPED_CODE] Placeholder message.", syncException.message) + assertEquals( + "[Client][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", + syncException.message + ) } @Test - fun convertSyncErrorCode_unmappedErrorCode2() { + fun convertSyncErrorCode_unmappedErrorCategory() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = SyncErrorCodeCategory.RLM_SYNC_ERROR_CATEGORY_CONNECTION, - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = UNMAPPED_CATEGORY_CODE, + errorCodeNativeValue = UNMAPPED_ERROR_CODE, + messageNativeValue = "Placeholder message" ) ) ) assertEquals( - "[Connection][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", syncException.message ) } + // Core also has a concept of an "unknown" error code. It is being reported the same way + // as a truly unknown code with the description "Unknown()" @Test - fun convertSyncErrorCode_unmappedErrorCategory() { + fun convertSyncErrorCode_unknownNativeErrrorCode() { val syncException = convertSyncError( SyncError( - SyncErrorCode( - category = UnknownCodeDescription(UNMAPPED_CODE), - code = UnknownCodeDescription(UNMAPPED_CODE), - message = "Placeholder message" + CoreError( + categoriesNativeValue = ErrorCategory.RLM_ERR_CAT_CLIENT_ERROR.nativeValue, + errorCodeNativeValue = ErrorCode.RLM_ERR_UNKNOWN.nativeValue, + messageNativeValue = "Placeholder message" ) ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[Client][Unknown(2000000)] Placeholder message.", syncException.message ) } @@ -88,30 +95,30 @@ class RealmSyncUtilsTest { val appException = convertAppError( AppError( categoryFlags = ErrorCategory.RLM_ERR_CAT_CUSTOM_ERROR.nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) - assertEquals("[Custom][Unknown($UNMAPPED_CODE)] Placeholder message.", appException.message) + assertEquals("[Custom][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", appException.message) } @Test fun convertAppError_unmappedErrorCategory() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message.", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message.", appException.message ) } @@ -120,31 +127,31 @@ class RealmSyncUtilsTest { fun convertAppError_unmappedErrorCategoryAndErrorCode_noMessage() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = null, - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = null ) ) - assertEquals("[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)]", appException.message) + assertEquals("[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)]", appException.message) } @Test fun convertAppError_unmappedErrorCategoryAndErrorCode_linkServerLog() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = "Placeholder message", - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = "http://realm.io" ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Placeholder message. Server log entry: http://realm.io", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Placeholder message. Server log entry: http://realm.io", appException.message ) } @@ -153,16 +160,16 @@ class RealmSyncUtilsTest { fun convertAppError_unmappedErrorCategoryAndErrorCode_noMessage_linkServerLog() { val appException = convertAppError( AppError( - categoryFlags = UnknownCodeDescription(UNMAPPED_CODE).nativeValue, - code = UnknownCodeDescription(UNMAPPED_CODE), + categoryFlags = UnknownCodeDescription(UNMAPPED_CATEGORY_CODE).nativeValue, + code = UnknownCodeDescription(UNMAPPED_ERROR_CODE), message = null, - httpStatusCode = UNMAPPED_CODE, + httpStatusCode = UNMAPPED_ERROR_CODE, linkToServerLog = "http://realm.io" ) ) assertEquals( - "[$UNMAPPED_CODE][Unknown($UNMAPPED_CODE)] Server log entry: http://realm.io", + "[$UNMAPPED_CATEGORY_CODE][Unknown($UNMAPPED_ERROR_CODE)] Server log entry: http://realm.io", appException.message ) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt index 6ef9b2f491..4ece5ae87c 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/internal/SyncConnectionParamsTests.kt @@ -29,8 +29,6 @@ internal class SyncConnectionParamsTests { fun allProperties() { val props = SyncConnectionParams( sdkVersion = "sdkVersion", - localAppName = "appName", - localAppVersion = "appVersion", bundleId = "bundleId", platformVersion = "platformVersion", device = "device", @@ -40,8 +38,6 @@ internal class SyncConnectionParamsTests { ) assertEquals("Kotlin", props.sdkName) assertEquals("sdkVersion", props.sdkVersion) - assertEquals("appName", props.localAppName) - assertEquals("appVersion", props.localAppVersion) assertEquals("bundleId", props.bundleId) assertEquals("platformVersion", props.platformVersion) assertEquals("device", props.device) 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..effa1d2db0 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 @@ -8,6 +8,7 @@ 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.common.PARTITION_SYNC_SCHEMA import io.realm.kotlin.test.mongodb.createUserAndLogIn import io.realm.kotlin.test.util.TestHelper import io.realm.kotlin.test.util.receiveOrFail @@ -34,7 +35,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) @@ -54,7 +55,7 @@ class NonLatinTests { @Test fun readNullCharacterFromMongoDB() = runBlocking { val adminApi = app.asTestApp - val config = SyncConfiguration.Builder(user, partitionValue, schema = setOf(ObjectIdPk::class)).build() + val config = SyncConfiguration.Builder(user, partitionValue, schema = PARTITION_SYNC_SCHEMA).build() Realm.open(config).use { realm -> val json: JsonObject = adminApi.insertDocument( ObjectIdPk::class.simpleName!!, 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() }