diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0f679753..c67227d993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * 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. + * Kotlin 1.8.20 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. diff --git a/Jenkinsfile b/Jenkinsfile index af83a48421..f92001139b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -84,8 +84,11 @@ pipeline { ANDROID_NDK="${NDK_HOME}" ANDROID_NDK_HOME="${NDK_HOME}" REALM_DISABLE_ANALYTICS=true + REALM_PRINT_ANALYTICS=true + REALM_FAIL_ON_ANALYTICS_ERRORS=false JAVA_8='/Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home' JAVA_11='/Library/Java/JavaVirtualMachines/jdk-11.0.12.jdk/Contents/Home' + JAVA_17='/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home' JAVA_HOME="${JAVA_11}" } stages { @@ -246,8 +249,14 @@ pipeline { stage('Gradle Plugin Integration Tests') { when { expression { runTests } } steps { - testAndCollect("integration-tests/gradle-plugin-test", "integrationTest") - testAndCollect("integration-tests/gradle-plugin-test", "-Pkotlin.experimental.tryK2=true integrationTest") + testAndCollect("integration-tests/gradle/current", "integrationTest") + testAndCollect("integration-tests/gradle/current", "-Pkotlin.experimental.tryK2=true integrationTest") + testAndCollect("integration-tests/gradle/gradle6-test", "integrationTest") + testAndCollect("integration-tests/gradle/gradle71-test", "integrationTest") + testAndCollect("integration-tests/gradle/gradle75-test", "integrationTest") + withEnv(["JAVA_HOME=${JAVA_17}"]) { + testAndCollect("integration-tests/gradle/gradle8-test", "integrationTest") + } } } stage('Tests Android Sample App') { diff --git a/build.gradle.kts b/build.gradle.kts index b4a208ab5f..5637bdb1d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,7 +48,7 @@ fun readAndCacheVersion(): String { return version } val currentVersion = readAndCacheVersion() -val subprojects = listOf("packages", "integration-tests/gradle-plugin-test", "examples/kmm-sample", "benchmarks") +val subprojects = listOf("packages", "examples/kmm-sample", "benchmarks") fun taskName(subdir: String): String { return subdir.split("/", "-").map { it.capitalize() }.joinToString(separator = "") } diff --git a/examples/min-android-sample/build.gradle.kts b/examples/min-android-sample/build.gradle.kts index 4e6818b4d8..5185370ae5 100644 --- a/examples/min-android-sample/build.gradle.kts +++ b/examples/min-android-sample/build.gradle.kts @@ -15,7 +15,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:4.1.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") classpath("io.realm.kotlin:gradle-plugin:${rootProject.extra["realmVersion"]}") } } diff --git a/examples/min-android-sample/gradle.properties b/examples/min-android-sample/gradle.properties index 4214b38bd1..f39955e794 100644 --- a/examples/min-android-sample/gradle.properties +++ b/examples/min-android-sample/gradle.properties @@ -13,4 +13,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true + +org.gradle.configuration-cache=true diff --git a/integration-tests/README.md b/integration-tests/README.md index 81ca3d80c6..6c332e0f83 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -2,8 +2,11 @@ This folder holds the various integration test projects. -- `gradle-plugin-test` - Smoke test project that verifies that our top level Gradle plugin can be +- `gradle/` - Various smoke test project that verifies that our top level Gradle plugin can be applied on a both single and a multi platform modules. It is currently testing: - `single-platform` - Android single module project - `multi-platform` - Kotlin Multiplatform project with JVM and Native targets running on the host platform. + There are various project with specific Gradle versions that has been proven troublesome with + regards to collecting analytics data and a `current` project that will use the versions used to + build the SDK. diff --git a/integration-tests/gradle-plugin-test/buildSrc b/integration-tests/gradle-plugin-test/buildSrc deleted file mode 120000 index da68abaf69..0000000000 --- a/integration-tests/gradle-plugin-test/buildSrc +++ /dev/null @@ -1 +0,0 @@ -../../buildSrc \ No newline at end of file diff --git a/integration-tests/gradle-plugin-test/build.gradle.kts b/integration-tests/gradle/current/build.gradle.kts similarity index 100% rename from integration-tests/gradle-plugin-test/build.gradle.kts rename to integration-tests/gradle/current/build.gradle.kts diff --git a/integration-tests/gradle/current/buildSrc b/integration-tests/gradle/current/buildSrc new file mode 120000 index 0000000000..201ecd5387 --- /dev/null +++ b/integration-tests/gradle/current/buildSrc @@ -0,0 +1 @@ +../../../buildSrc \ No newline at end of file diff --git a/integration-tests/gradle-plugin-test/gradle.properties b/integration-tests/gradle/current/gradle.properties similarity index 95% rename from integration-tests/gradle-plugin-test/gradle.properties rename to integration-tests/gradle/current/gradle.properties index 33756b44c3..d6568c183f 100644 --- a/integration-tests/gradle-plugin-test/gradle.properties +++ b/integration-tests/gradle/current/gradle.properties @@ -28,3 +28,4 @@ kotlin.mpp.stability.nowarn=true kotlin.native.binary.memoryModel=experimental kotlin.native.binary.freezing=disabled +org.gradle.configuration-cache=true diff --git a/integration-tests/gradle-plugin-test/gradle/wrapper/gradle-wrapper.jar b/integration-tests/gradle/current/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from integration-tests/gradle-plugin-test/gradle/wrapper/gradle-wrapper.jar rename to integration-tests/gradle/current/gradle/wrapper/gradle-wrapper.jar diff --git a/integration-tests/gradle-plugin-test/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/current/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from integration-tests/gradle-plugin-test/gradle/wrapper/gradle-wrapper.properties rename to integration-tests/gradle/current/gradle/wrapper/gradle-wrapper.properties diff --git a/integration-tests/gradle-plugin-test/gradlew b/integration-tests/gradle/current/gradlew similarity index 100% rename from integration-tests/gradle-plugin-test/gradlew rename to integration-tests/gradle/current/gradlew diff --git a/integration-tests/gradle-plugin-test/gradlew.bat b/integration-tests/gradle/current/gradlew.bat similarity index 100% rename from integration-tests/gradle-plugin-test/gradlew.bat rename to integration-tests/gradle/current/gradlew.bat diff --git a/integration-tests/gradle-plugin-test/multi-platform/build.gradle.kts b/integration-tests/gradle/current/multi-platform/build.gradle.kts similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/build.gradle.kts rename to integration-tests/gradle/current/multi-platform/build.gradle.kts diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt b/integration-tests/gradle/current/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt rename to integration-tests/gradle/current/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt b/integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt rename to integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt b/integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt rename to integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt rename to integration-tests/gradle/current/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/current/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt rename to integration-tests/gradle/current/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt diff --git a/integration-tests/gradle-plugin-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/current/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt similarity index 100% rename from integration-tests/gradle-plugin-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt rename to integration-tests/gradle/current/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt diff --git a/integration-tests/gradle-plugin-test/settings.gradle.kts b/integration-tests/gradle/current/settings.gradle.kts similarity index 84% rename from integration-tests/gradle-plugin-test/settings.gradle.kts rename to integration-tests/gradle/current/settings.gradle.kts index 9b3ef65c46..afb12d6717 100644 --- a/integration-tests/gradle-plugin-test/settings.gradle.kts +++ b/integration-tests/gradle/current/settings.gradle.kts @@ -22,14 +22,14 @@ pluginManagement { gradlePluginPortal() google() mavenCentral() - maven("file://${rootDir.absolutePath}/../../packages/build/m2-buildrepo") + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") } } dependencyResolutionManagement { repositories { google() mavenCentral() - maven("file://${rootDir.absolutePath}/../../packages/build/m2-buildrepo") + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") } } diff --git a/integration-tests/gradle-plugin-test/single-platform/build.gradle.kts b/integration-tests/gradle/current/single-platform/build.gradle.kts similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/build.gradle.kts rename to integration-tests/gradle/current/single-platform/build.gradle.kts diff --git a/integration-tests/gradle-plugin-test/single-platform/consumer-rules.pro b/integration-tests/gradle/current/single-platform/consumer-rules.pro similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/consumer-rules.pro rename to integration-tests/gradle/current/single-platform/consumer-rules.pro diff --git a/integration-tests/gradle-plugin-test/single-platform/proguard-rules.pro b/integration-tests/gradle/current/single-platform/proguard-rules.pro similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/proguard-rules.pro rename to integration-tests/gradle/current/single-platform/proguard-rules.pro diff --git a/integration-tests/gradle-plugin-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt b/integration-tests/gradle/current/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt rename to integration-tests/gradle/current/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt diff --git a/integration-tests/gradle-plugin-test/single-platform/src/main/AndroidManifest.xml b/integration-tests/gradle/current/single-platform/src/main/AndroidManifest.xml similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/src/main/AndroidManifest.xml rename to integration-tests/gradle/current/single-platform/src/main/AndroidManifest.xml diff --git a/integration-tests/gradle-plugin-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java b/integration-tests/gradle/current/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java rename to integration-tests/gradle/current/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java diff --git a/integration-tests/gradle-plugin-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt b/integration-tests/gradle/current/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt similarity index 100% rename from integration-tests/gradle-plugin-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt rename to integration-tests/gradle/current/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt diff --git a/integration-tests/gradle/gradle6-test/build.gradle.kts b/integration-tests/gradle/gradle6-test/build.gradle.kts new file mode 100644 index 0000000000..99c8f1e0a2 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// Explicitly adding the plugin to the classpath as it makes it easier to control the version +// centrally (don't need version in the 'plugins' block). Further, snapshots are not published with +// marker interface so would need to be added to the classpath manually anyway. +buildscript { + extra["realmVersion"] = file("${rootProject.rootDir.absolutePath}/../../../buildSrc/src/main/kotlin/Config.kt") + .readLines() + .first { it.contains("const val version") } + .let { + it.substringAfter("\"").substringBefore("\"") + } + + repositories { + maven(url = "file://${rootProject.rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + gradlePluginPortal() + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:4.2.2") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("io.realm.kotlin:gradle-plugin:${rootProject.extra["realmVersion"]}") + } +} +group = "io.realm.test" +version = rootProject.extra["realmVersion"] + +// Attempt to make an easy entry point for verifying all modules. Maybe we could do a better split +// when migrating to GHA. +tasks.register("integrationTest") { + dependsOn(":single-platform:connectedDebugAndroidTest") + dependsOn(":multi-platform:cleanAllTests") + dependsOn(":multi-platform:jvmTest") + dependsOn(":multi-platform:nativeTest") +} diff --git a/integration-tests/gradle/gradle6-test/gradle.properties b/integration-tests/gradle/gradle6-test/gradle.properties new file mode 100644 index 0000000000..d6568c183f --- /dev/null +++ b/integration-tests/gradle/gradle6-test/gradle.properties @@ -0,0 +1,31 @@ +# +# 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. +# + +kotlin.code.style=official + +org.gradle.jvmargs=-Xmx4096M + +android.useAndroidX=true +android.enableJetifier=true + +kotlin.mpp.stability.nowarn=true + +# Enable new memory model as default +kotlin.native.binary.memoryModel=experimental +kotlin.native.binary.freezing=disabled + +org.gradle.configuration-cache=true diff --git a/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.jar b/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..8cf6eb5ad2 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-tests/gradle/gradle6-test/gradlew b/integration-tests/gradle/gradle6-test/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-tests/gradle/gradle6-test/gradlew.bat b/integration-tests/gradle/gradle6-test/gradlew.bat new file mode 100644 index 0000000000..53a6b238d4 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-tests/gradle/gradle6-test/multi-platform/build.gradle.kts b/integration-tests/gradle/gradle6-test/multi-platform/build.gradle.kts new file mode 100644 index 0000000000..e7be178e61 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +plugins { + kotlin("multiplatform") + id("io.realm.kotlin") +} + +kotlin { + jvm() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" -> { + val hostArch = System.getProperty("os.arch") + when (hostArch) { + "aarch64" -> macosArm64("native") + "x86_64" -> macosX64("native") + else -> throw GradleException("Unrecognized architecture: $hostArch") + } + } + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt new file mode 100644 index 0000000000..93c7e6f35a --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.multiplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt new file mode 100644 index 0000000000..1e81a97e78 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.multiplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.multiplatform.model.TestClass +import io.realm.test.multiplatform.util.platform.PlatformUtils +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir("integration_test") + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @AfterTest + fun teadDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + // DELETE + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt new file mode 100644 index 0000000000..210e403226 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt @@ -0,0 +1,27 @@ +/* + * 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.test.multiplatform.util + +object Utils { + fun createRandomString(length: Int): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..d4cd211930 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,25 @@ +/* + * 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.test.multiplatform.util.platform + +import io.realm.test.multiplatform.util.Utils + +expect object PlatformUtils { + fun createTempDir(prefix: String = Utils.createRandomString(16)): String + fun deleteTempDir(path: String) +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..087c037b70 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,32 @@ +/* + * 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.test.multiplatform.util.platform + +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + return Files.createTempDirectory("$prefix-jvm_tests").absolutePathString() + } + + actual fun deleteTempDir(path: String) { + File(path).deleteRecursively() + } +} diff --git a/integration-tests/gradle/gradle6-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle6-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..9b5b5b4fca --- /dev/null +++ b/integration-tests/gradle/gradle6-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package io.realm.test.multiplatform.util.platform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + // X is a special char which will be replace by mkdtemp template + val mask = prefix.replace('X', 'Z', ignoreCase = true) + val path = "${platform.Foundation.NSTemporaryDirectory()}$mask-native_tests" + platform.posix.mkdtemp(path.cstr) + return path + } + + actual fun deleteTempDir(path: String) { + platform.Foundation.NSFileManager.defaultManager.removeItemAtURL( + platform.Foundation.NSURL(fileURLWithPath = path), + null + ) + } +} diff --git a/integration-tests/gradle/gradle6-test/settings.gradle.kts b/integration-tests/gradle/gradle6-test/settings.gradle.kts new file mode 100644 index 0000000000..4159ca0d1d --- /dev/null +++ b/integration-tests/gradle/gradle6-test/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +rootProject.name = "gradle6-plugin-test" + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} + +include(":single-platform") +include(":multi-platform") diff --git a/integration-tests/gradle/gradle6-test/single-platform/build.gradle.kts b/integration-tests/gradle/gradle6-test/single-platform/build.gradle.kts new file mode 100644 index 0000000000..76261f5cba --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("io.realm.kotlin") +} + +android { + compileSdk = 33 + + defaultConfig { + minSdk = 16 + targetSdk = 33 + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("androidx.multidex:multidex:2.0.1") + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") +} diff --git a/integration-tests/gradle/gradle6-test/single-platform/consumer-rules.pro b/integration-tests/gradle/gradle6-test/single-platform/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration-tests/gradle/gradle6-test/single-platform/proguard-rules.pro b/integration-tests/gradle/gradle6-test/single-platform/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/integration-tests/gradle/gradle6-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt b/integration-tests/gradle/gradle6-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt new file mode 100644 index 0000000000..524504cfa0 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.singleplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.singleplatform.model.TestClass +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @Before + fun setup() { + tmpDir = Files.createTempDirectory("android_tests").absolutePathString() + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @After + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + File(tmpDir).deleteRecursively() + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle6-test/single-platform/src/main/AndroidManifest.xml b/integration-tests/gradle/gradle6-test/single-platform/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..171018716c --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/integration-tests/gradle/gradle6-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java b/integration-tests/gradle/gradle6-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java new file mode 100644 index 0000000000..1254a34e34 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java @@ -0,0 +1,7 @@ +package io.realm.test.singleplatform.model; + +// This class is only added to verify that we can access the Realm Model classes from plain old +// Java source. See issue for further details https://github.com/realm/realm-kotlin/issues/1256 +public class Pojo { + TestClass o = new TestClass(); +} diff --git a/integration-tests/gradle/gradle6-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt b/integration-tests/gradle/gradle6-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt new file mode 100644 index 0000000000..5adfd22c38 --- /dev/null +++ b/integration-tests/gradle/gradle6-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.singleplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle71-test/build.gradle.kts b/integration-tests/gradle/gradle71-test/build.gradle.kts new file mode 100644 index 0000000000..b3c36fdf42 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// Explicitly adding the plugin to the classpath as it makes it easier to control the version +// centrally (don't need version in the 'plugins' block). Further, snapshots are not published with +// marker interface so would need to be added to the classpath manually anyway. +buildscript { + extra["realmVersion"] = file("${rootProject.rootDir.absolutePath}/../../../buildSrc/src/main/kotlin/Config.kt") + .readLines() + .first { it.contains("const val version") } + .let { + it.substringAfter("\"").substringBefore("\"") + } + + repositories { + maven(url = "file://${rootProject.rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + gradlePluginPortal() + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:7.0.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("io.realm.kotlin:gradle-plugin:${rootProject.extra["realmVersion"]}") + } +} +group = "io.realm.test" +version = rootProject.extra["realmVersion"] + +// Attempt to make an easy entry point for verifying all modules. Maybe we could do a better split +// when migrating to GHA. +tasks.register("integrationTest") { + dependsOn(":single-platform:connectedDebugAndroidTest") + dependsOn(":multi-platform:cleanAllTests") + dependsOn(":multi-platform:jvmTest") + dependsOn(":multi-platform:nativeTest") +} diff --git a/integration-tests/gradle/gradle71-test/gradle.properties b/integration-tests/gradle/gradle71-test/gradle.properties new file mode 100644 index 0000000000..d6568c183f --- /dev/null +++ b/integration-tests/gradle/gradle71-test/gradle.properties @@ -0,0 +1,31 @@ +# +# 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. +# + +kotlin.code.style=official + +org.gradle.jvmargs=-Xmx4096M + +android.useAndroidX=true +android.enableJetifier=true + +kotlin.mpp.stability.nowarn=true + +# Enable new memory model as default +kotlin.native.binary.memoryModel=experimental +kotlin.native.binary.freezing=disabled + +org.gradle.configuration-cache=true diff --git a/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.jar b/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1acc777d74 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-tests/gradle/gradle71-test/gradlew b/integration-tests/gradle/gradle71-test/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-tests/gradle/gradle71-test/gradlew.bat b/integration-tests/gradle/gradle71-test/gradlew.bat new file mode 100644 index 0000000000..53a6b238d4 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-tests/gradle/gradle71-test/multi-platform/build.gradle.kts b/integration-tests/gradle/gradle71-test/multi-platform/build.gradle.kts new file mode 100644 index 0000000000..e7be178e61 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +plugins { + kotlin("multiplatform") + id("io.realm.kotlin") +} + +kotlin { + jvm() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" -> { + val hostArch = System.getProperty("os.arch") + when (hostArch) { + "aarch64" -> macosArm64("native") + "x86_64" -> macosX64("native") + else -> throw GradleException("Unrecognized architecture: $hostArch") + } + } + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt new file mode 100644 index 0000000000..93c7e6f35a --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.multiplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt new file mode 100644 index 0000000000..1e81a97e78 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.multiplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.multiplatform.model.TestClass +import io.realm.test.multiplatform.util.platform.PlatformUtils +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir("integration_test") + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @AfterTest + fun teadDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + // DELETE + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt new file mode 100644 index 0000000000..210e403226 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt @@ -0,0 +1,27 @@ +/* + * 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.test.multiplatform.util + +object Utils { + fun createRandomString(length: Int): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..d4cd211930 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,25 @@ +/* + * 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.test.multiplatform.util.platform + +import io.realm.test.multiplatform.util.Utils + +expect object PlatformUtils { + fun createTempDir(prefix: String = Utils.createRandomString(16)): String + fun deleteTempDir(path: String) +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..087c037b70 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,32 @@ +/* + * 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.test.multiplatform.util.platform + +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + return Files.createTempDirectory("$prefix-jvm_tests").absolutePathString() + } + + actual fun deleteTempDir(path: String) { + File(path).deleteRecursively() + } +} diff --git a/integration-tests/gradle/gradle71-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle71-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..9b5b5b4fca --- /dev/null +++ b/integration-tests/gradle/gradle71-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package io.realm.test.multiplatform.util.platform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + // X is a special char which will be replace by mkdtemp template + val mask = prefix.replace('X', 'Z', ignoreCase = true) + val path = "${platform.Foundation.NSTemporaryDirectory()}$mask-native_tests" + platform.posix.mkdtemp(path.cstr) + return path + } + + actual fun deleteTempDir(path: String) { + platform.Foundation.NSFileManager.defaultManager.removeItemAtURL( + platform.Foundation.NSURL(fileURLWithPath = path), + null + ) + } +} diff --git a/integration-tests/gradle/gradle71-test/settings.gradle.kts b/integration-tests/gradle/gradle71-test/settings.gradle.kts new file mode 100644 index 0000000000..27a751f0ea --- /dev/null +++ b/integration-tests/gradle/gradle71-test/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +rootProject.name = "gradle71-plugin-test" + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} + +include(":single-platform") +include(":multi-platform") diff --git a/integration-tests/gradle/gradle71-test/single-platform/build.gradle.kts b/integration-tests/gradle/gradle71-test/single-platform/build.gradle.kts new file mode 100644 index 0000000000..76261f5cba --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("io.realm.kotlin") +} + +android { + compileSdk = 33 + + defaultConfig { + minSdk = 16 + targetSdk = 33 + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("androidx.multidex:multidex:2.0.1") + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") +} diff --git a/integration-tests/gradle/gradle71-test/single-platform/consumer-rules.pro b/integration-tests/gradle/gradle71-test/single-platform/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration-tests/gradle/gradle71-test/single-platform/proguard-rules.pro b/integration-tests/gradle/gradle71-test/single-platform/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/integration-tests/gradle/gradle71-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt b/integration-tests/gradle/gradle71-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt new file mode 100644 index 0000000000..524504cfa0 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.singleplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.singleplatform.model.TestClass +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @Before + fun setup() { + tmpDir = Files.createTempDirectory("android_tests").absolutePathString() + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @After + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + File(tmpDir).deleteRecursively() + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle71-test/single-platform/src/main/AndroidManifest.xml b/integration-tests/gradle/gradle71-test/single-platform/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..171018716c --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/integration-tests/gradle/gradle71-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java b/integration-tests/gradle/gradle71-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java new file mode 100644 index 0000000000..1254a34e34 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java @@ -0,0 +1,7 @@ +package io.realm.test.singleplatform.model; + +// This class is only added to verify that we can access the Realm Model classes from plain old +// Java source. See issue for further details https://github.com/realm/realm-kotlin/issues/1256 +public class Pojo { + TestClass o = new TestClass(); +} diff --git a/integration-tests/gradle/gradle71-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt b/integration-tests/gradle/gradle71-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt new file mode 100644 index 0000000000..5adfd22c38 --- /dev/null +++ b/integration-tests/gradle/gradle71-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.singleplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle75-test/build.gradle.kts b/integration-tests/gradle/gradle75-test/build.gradle.kts new file mode 100644 index 0000000000..0baabc2b81 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// Explicitly adding the plugin to the classpath as it makes it easier to control the version +// centrally (don't need version in the 'plugins' block). Further, snapshots are not published with +// marker interface so would need to be added to the classpath manually anyway. +buildscript { + extra["realmVersion"] = file("${rootProject.rootDir.absolutePath}/../../../buildSrc/src/main/kotlin/Config.kt") + .readLines() + .first { it.contains("const val version") } + .let { + it.substringAfter("\"").substringBefore("\"") + } + + repositories { + maven(url = "file://${rootProject.rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + gradlePluginPortal() + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:7.4.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("io.realm.kotlin:gradle-plugin:${rootProject.extra["realmVersion"]}") + } +} +group = "io.realm.test" +version = rootProject.extra["realmVersion"] + +// Attempt to make an easy entry point for verifying all modules. Maybe we could do a better split +// when migrating to GHA. +tasks.register("integrationTest") { + dependsOn(":single-platform:connectedDebugAndroidTest") + dependsOn(":multi-platform:cleanAllTests") + dependsOn(":multi-platform:jvmTest") + dependsOn(":multi-platform:nativeTest") +} diff --git a/integration-tests/gradle/gradle75-test/gradle.properties b/integration-tests/gradle/gradle75-test/gradle.properties new file mode 100644 index 0000000000..d6568c183f --- /dev/null +++ b/integration-tests/gradle/gradle75-test/gradle.properties @@ -0,0 +1,31 @@ +# +# 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. +# + +kotlin.code.style=official + +org.gradle.jvmargs=-Xmx4096M + +android.useAndroidX=true +android.enableJetifier=true + +kotlin.mpp.stability.nowarn=true + +# Enable new memory model as default +kotlin.native.binary.memoryModel=experimental +kotlin.native.binary.freezing=disabled + +org.gradle.configuration-cache=true diff --git a/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.jar b/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..2ec77e51a9 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-tests/gradle/gradle75-test/gradlew b/integration-tests/gradle/gradle75-test/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-tests/gradle/gradle75-test/gradlew.bat b/integration-tests/gradle/gradle75-test/gradlew.bat new file mode 100644 index 0000000000..53a6b238d4 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-tests/gradle/gradle75-test/multi-platform/build.gradle.kts b/integration-tests/gradle/gradle75-test/multi-platform/build.gradle.kts new file mode 100644 index 0000000000..e7be178e61 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +plugins { + kotlin("multiplatform") + id("io.realm.kotlin") +} + +kotlin { + jvm() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" -> { + val hostArch = System.getProperty("os.arch") + when (hostArch) { + "aarch64" -> macosArm64("native") + "x86_64" -> macosX64("native") + else -> throw GradleException("Unrecognized architecture: $hostArch") + } + } + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt new file mode 100644 index 0000000000..93c7e6f35a --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.multiplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt new file mode 100644 index 0000000000..1e81a97e78 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.multiplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.multiplatform.model.TestClass +import io.realm.test.multiplatform.util.platform.PlatformUtils +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir("integration_test") + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @AfterTest + fun teadDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + // DELETE + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt new file mode 100644 index 0000000000..210e403226 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt @@ -0,0 +1,27 @@ +/* + * 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.test.multiplatform.util + +object Utils { + fun createRandomString(length: Int): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..d4cd211930 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,25 @@ +/* + * 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.test.multiplatform.util.platform + +import io.realm.test.multiplatform.util.Utils + +expect object PlatformUtils { + fun createTempDir(prefix: String = Utils.createRandomString(16)): String + fun deleteTempDir(path: String) +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..087c037b70 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,32 @@ +/* + * 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.test.multiplatform.util.platform + +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + return Files.createTempDirectory("$prefix-jvm_tests").absolutePathString() + } + + actual fun deleteTempDir(path: String) { + File(path).deleteRecursively() + } +} diff --git a/integration-tests/gradle/gradle75-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle75-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..9b5b5b4fca --- /dev/null +++ b/integration-tests/gradle/gradle75-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package io.realm.test.multiplatform.util.platform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + // X is a special char which will be replace by mkdtemp template + val mask = prefix.replace('X', 'Z', ignoreCase = true) + val path = "${platform.Foundation.NSTemporaryDirectory()}$mask-native_tests" + platform.posix.mkdtemp(path.cstr) + return path + } + + actual fun deleteTempDir(path: String) { + platform.Foundation.NSFileManager.defaultManager.removeItemAtURL( + platform.Foundation.NSURL(fileURLWithPath = path), + null + ) + } +} diff --git a/integration-tests/gradle/gradle75-test/settings.gradle.kts b/integration-tests/gradle/gradle75-test/settings.gradle.kts new file mode 100644 index 0000000000..cf744acad1 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +rootProject.name = "gradle75-plugin-test" + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} + +include(":single-platform") +include(":multi-platform") diff --git a/integration-tests/gradle/gradle75-test/single-platform/build.gradle.kts b/integration-tests/gradle/gradle75-test/single-platform/build.gradle.kts new file mode 100644 index 0000000000..76261f5cba --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("io.realm.kotlin") +} + +android { + compileSdk = 33 + + defaultConfig { + minSdk = 16 + targetSdk = 33 + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("androidx.multidex:multidex:2.0.1") + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") +} diff --git a/integration-tests/gradle/gradle75-test/single-platform/consumer-rules.pro b/integration-tests/gradle/gradle75-test/single-platform/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration-tests/gradle/gradle75-test/single-platform/proguard-rules.pro b/integration-tests/gradle/gradle75-test/single-platform/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/integration-tests/gradle/gradle75-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt b/integration-tests/gradle/gradle75-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt new file mode 100644 index 0000000000..524504cfa0 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.singleplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.singleplatform.model.TestClass +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @Before + fun setup() { + tmpDir = Files.createTempDirectory("android_tests").absolutePathString() + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @After + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + File(tmpDir).deleteRecursively() + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle75-test/single-platform/src/main/AndroidManifest.xml b/integration-tests/gradle/gradle75-test/single-platform/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..171018716c --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/integration-tests/gradle/gradle75-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java b/integration-tests/gradle/gradle75-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java new file mode 100644 index 0000000000..1254a34e34 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java @@ -0,0 +1,7 @@ +package io.realm.test.singleplatform.model; + +// This class is only added to verify that we can access the Realm Model classes from plain old +// Java source. See issue for further details https://github.com/realm/realm-kotlin/issues/1256 +public class Pojo { + TestClass o = new TestClass(); +} diff --git a/integration-tests/gradle/gradle75-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt b/integration-tests/gradle/gradle75-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt new file mode 100644 index 0000000000..5adfd22c38 --- /dev/null +++ b/integration-tests/gradle/gradle75-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.singleplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle8-test/build.gradle.kts b/integration-tests/gradle/gradle8-test/build.gradle.kts new file mode 100644 index 0000000000..1a81127b6d --- /dev/null +++ b/integration-tests/gradle/gradle8-test/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// Explicitly adding the plugin to the classpath as it makes it easier to control the version +// centrally (don't need version in the 'plugins' block). Further, snapshots are not published with +// marker interface so would need to be added to the classpath manually anyway. +buildscript { + extra["realmVersion"] = file("${rootProject.rootDir.absolutePath}/../../../buildSrc/src/main/kotlin/Config.kt") + .readLines() + .first { it.contains("const val version") } + .let { + it.substringAfter("\"").substringBefore("\"") + } + + repositories { + maven(url = "file://${rootProject.rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + gradlePluginPortal() + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.1.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") + classpath("io.realm.kotlin:gradle-plugin:${rootProject.extra["realmVersion"]}") + } +} +group = "io.realm.test" +version = rootProject.extra["realmVersion"] + +// Attempt to make an easy entry point for verifying all modules. Maybe we could do a better split +// when migrating to GHA. +tasks.register("integrationTest") { + dependsOn(":single-platform:connectedDebugAndroidTest") + dependsOn(":multi-platform:cleanAllTests") + dependsOn(":multi-platform:jvmTest") + dependsOn(":multi-platform:nativeTest") +} diff --git a/integration-tests/gradle/gradle8-test/gradle.properties b/integration-tests/gradle/gradle8-test/gradle.properties new file mode 100644 index 0000000000..c095ae993b --- /dev/null +++ b/integration-tests/gradle/gradle8-test/gradle.properties @@ -0,0 +1,31 @@ +# +# 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. +# + +kotlin.code.style=official + +org.gradle.jvmargs=-Xmx4096M + +android.useAndroidX=true +android.enableJetifier=true + +kotlin.mpp.stability.nowarn=true + +# Enable new memory model as default +#kotlin.native.binary.memoryModel=experimental +#kotlin.native.binary.freezing=disabled + +org.gradle.configuration-cache=true diff --git a/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.jar b/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.properties b/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..27313fbc80 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/integration-tests/gradle/gradle8-test/gradlew b/integration-tests/gradle/gradle8-test/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/integration-tests/gradle/gradle8-test/gradlew.bat b/integration-tests/gradle/gradle8-test/gradlew.bat new file mode 100644 index 0000000000..53a6b238d4 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/integration-tests/gradle/gradle8-test/multi-platform/build.gradle.kts b/integration-tests/gradle/gradle8-test/multi-platform/build.gradle.kts new file mode 100644 index 0000000000..e7be178e61 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/build.gradle.kts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +plugins { + kotlin("multiplatform") + id("io.realm.kotlin") +} + +kotlin { + jvm() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" -> { + val hostArch = System.getProperty("os.arch") + when (hostArch) { + "aarch64" -> macosArm64("native") + "x86_64" -> macosX64("native") + else -> throw GradleException("Unrecognized architecture: $hostArch") + } + } + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt new file mode 100644 index 0000000000..93c7e6f35a --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/commonMain/kotlin/io/realm/test/multiplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.multiplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt new file mode 100644 index 0000000000..1e81a97e78 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.multiplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.multiplatform.model.TestClass +import io.realm.test.multiplatform.util.platform.PlatformUtils +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir("integration_test") + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @AfterTest + fun teadDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + // DELETE + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt new file mode 100644 index 0000000000..210e403226 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/Utils.kt @@ -0,0 +1,27 @@ +/* + * 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.test.multiplatform.util + +object Utils { + fun createRandomString(length: Int): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") + } +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..d4cd211930 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/commonTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,25 @@ +/* + * 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.test.multiplatform.util.platform + +import io.realm.test.multiplatform.util.Utils + +expect object PlatformUtils { + fun createTempDir(prefix: String = Utils.createRandomString(16)): String + fun deleteTempDir(path: String) +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..087c037b70 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/jvmTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,32 @@ +/* + * 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.test.multiplatform.util.platform + +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + return Files.createTempDirectory("$prefix-jvm_tests").absolutePathString() + } + + actual fun deleteTempDir(path: String) { + File(path).deleteRecursively() + } +} diff --git a/integration-tests/gradle/gradle8-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt b/integration-tests/gradle/gradle8-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt new file mode 100644 index 0000000000..2aa654ee9b --- /dev/null +++ b/integration-tests/gradle/gradle8-test/multi-platform/src/nativeTest/kotlin/io/realm/test/multiplatform/util/platform/PlatformUtils.kt @@ -0,0 +1,39 @@ +/* + * 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. + */ +@file:OptIn(ExperimentalForeignApi::class) + +package io.realm.test.multiplatform.util.platform + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.cstr + +actual object PlatformUtils { + actual fun createTempDir(prefix: String): String { + // X is a special char which will be replace by mkdtemp template + val mask = prefix.replace('X', 'Z', ignoreCase = true) + val path = "${platform.Foundation.NSTemporaryDirectory()}$mask-native_tests" + platform.posix.mkdtemp(path.cstr) + return path + } + + actual fun deleteTempDir(path: String) { + platform.Foundation.NSFileManager.defaultManager.removeItemAtURL( + platform.Foundation.NSURL(fileURLWithPath = path), + null + ) + } +} diff --git a/integration-tests/gradle/gradle8-test/settings.gradle.kts b/integration-tests/gradle/gradle8-test/settings.gradle.kts new file mode 100644 index 0000000000..152903ee81 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/settings.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +rootProject.name = "gradle8-plugin-test" + +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("file://${rootDir.absolutePath}/../../../packages/build/m2-buildrepo") + } +} + +include(":single-platform") +include(":multi-platform") diff --git a/integration-tests/gradle/gradle8-test/single-platform/build.gradle.kts b/integration-tests/gradle/gradle8-test/single-platform/build.gradle.kts new file mode 100644 index 0000000000..70fdfc5a71 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/build.gradle.kts @@ -0,0 +1,56 @@ +/* + * 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. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("io.realm.kotlin") +} + +android { + compileSdk = 33 + + namespace = "io.realm.test.singleplatform" + + defaultConfig { + minSdk = 16 + targetSdk = 33 + multiDexEnabled = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + name + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("androidx.multidex:multidex:2.0.1") + implementation("io.realm.kotlin:library-base:${rootProject.extra["realmVersion"]}") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") +} diff --git a/integration-tests/gradle/gradle8-test/single-platform/consumer-rules.pro b/integration-tests/gradle/gradle8-test/single-platform/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration-tests/gradle/gradle8-test/single-platform/proguard-rules.pro b/integration-tests/gradle/gradle8-test/single-platform/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/integration-tests/gradle/gradle8-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt b/integration-tests/gradle/gradle8-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt new file mode 100644 index 0000000000..524504cfa0 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/src/androidTest/kotlin/io/realm/test/singleplatform/CrudTests.kt @@ -0,0 +1,85 @@ +/* + * 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.test.singleplatform + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.query +import io.realm.test.singleplatform.model.TestClass +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files +import kotlin.io.path.absolutePathString + +class CrudTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @Before + fun setup() { + tmpDir = Files.createTempDirectory("android_tests").absolutePathString() + realm = RealmConfiguration.Builder(setOf(TestClass::class)) + .directory(tmpDir) + .build() + .let { Realm.open(it) } + } + + @After + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + File(tmpDir).deleteRecursively() + } + + @Test + fun crud() { + // CREATE + realm.writeBlocking { + copyToRealm( + TestClass().apply { + id = 1 + text = "TEST" + } + ) + } + // READ + val testObject = realm.query("id = 1").find().single() + assertEquals("TEST", testObject.text) + // UPDATE + realm.writeBlocking { + findLatest(testObject)?.apply { + text = "UPDATED" + } + } + val updatedTestObject = realm.query("id = 1").find().single() + assertEquals("UPDATED", updatedTestObject.text) + + realm.writeBlocking { + findLatest(updatedTestObject)?.let { delete(it) } + ?: fail("Couldn't find test object") + } + + assertEquals(0, realm.query("id = 1").find().size) + } +} diff --git a/integration-tests/gradle/gradle8-test/single-platform/src/main/AndroidManifest.xml b/integration-tests/gradle/gradle8-test/single-platform/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..946d44f67b --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/integration-tests/gradle/gradle8-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java b/integration-tests/gradle/gradle8-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java new file mode 100644 index 0000000000..1254a34e34 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/src/main/java/io/realm/test/singleplatform/model/Pojo.java @@ -0,0 +1,7 @@ +package io.realm.test.singleplatform.model; + +// This class is only added to verify that we can access the Realm Model classes from plain old +// Java source. See issue for further details https://github.com/realm/realm-kotlin/issues/1256 +public class Pojo { + TestClass o = new TestClass(); +} diff --git a/integration-tests/gradle/gradle8-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt b/integration-tests/gradle/gradle8-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt new file mode 100644 index 0000000000..5adfd22c38 --- /dev/null +++ b/integration-tests/gradle/gradle8-test/single-platform/src/main/kotlin/io/realm/test/singleplatform/model/TestClass.kt @@ -0,0 +1,28 @@ +/* + * 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.test.singleplatform.model + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class TestClass : RealmObject { + @PrimaryKey + var id: Long = 0 + + var text: String = "INIT" +} diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index 4c4480148f..09ac559bbd 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -92,12 +92,21 @@ sourceSets { java.srcDir(versionDirectory) } } -tasks.create("pluginVersion") { - val outputDir = file(versionDirectory) +// Task to generate gradle plugin runtime constants for SDK and core versions +tasks.create("versionConstants") { + val coreDependenciesFile = layout.projectDirectory.file( + listOf("..", "external", "core", "dependencies.list").joinToString(File.separator) + ) + val outputDir = file(versionDirectory) inputs.property("version", project.version) + inputs.file(coreDependenciesFile) outputs.dir(outputDir) + val versionRegex = "^VERSION=(.*)\$".toRegex() + val coreVersion = providers.fileContents(coreDependenciesFile).asText.get().lineSequence() + .map { dependecy -> versionRegex.find(dependecy)?.groups?.get(1)?.value }.single { it != null } + doLast { val versionFile = file("$outputDir/io/realm/kotlin/gradle/version.kt") versionFile.parentFile.mkdirs() @@ -106,8 +115,10 @@ tasks.create("pluginVersion") { // Generated file. Do not edit! package io.realm.kotlin.gradle internal const val PLUGIN_VERSION = "${project.version}" + internal const val CORE_VERSION = "${coreVersion}" """.trimIndent() ) } } -tasks.getByName("compileKotlin").dependsOn("pluginVersion") + +tasks.getByName("compileKotlin").dependsOn("versionConstants") diff --git a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmCompilerSubplugin.kt b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmCompilerSubplugin.kt index 906e853c75..a6cbbccb58 100644 --- a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmCompilerSubplugin.kt +++ b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/RealmCompilerSubplugin.kt @@ -16,27 +16,170 @@ package io.realm.kotlin.gradle +import com.android.build.gradle.BaseExtension +import io.realm.kotlin.gradle.analytics.AnalyticsErrorCatcher +import io.realm.kotlin.gradle.analytics.AnalyticsService +import io.realm.kotlin.gradle.analytics.AnalyticsService.Companion.UNKNOWN +import io.realm.kotlin.gradle.analytics.AnalyticsService.Companion.unknown +import io.realm.kotlin.gradle.analytics.BuilderId +import io.realm.kotlin.gradle.analytics.ComputerId +import io.realm.kotlin.gradle.analytics.HOST_ARCH_NAME +import io.realm.kotlin.gradle.analytics.HOST_OS_NAME +import io.realm.kotlin.gradle.analytics.ProjectConfiguration +import io.realm.kotlin.gradle.analytics.TargetInfo +import io.realm.kotlin.gradle.analytics.hexStringify +import io.realm.kotlin.gradle.analytics.sha256Hash +import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildServiceSpec +import org.gradle.util.GradleVersion +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact import org.jetbrains.kotlin.gradle.plugin.SubpluginOption +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension +import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME +import org.jetbrains.kotlin.gradle.plugin.kotlinToolingVersion +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation +import org.jetbrains.kotlin.konan.target.Architecture +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.konan.target.KonanTarget +import java.io.File -class RealmCompilerSubplugin : KotlinCompilerPluginSupportPlugin { +internal val gradleVersion: GradleVersion = GradleVersion.current().baseVersion +internal val gradle70: GradleVersion = GradleVersion.version("7.0") +internal val gradle75: GradleVersion = GradleVersion.version("7.5") + +class RealmCompilerSubplugin : KotlinCompilerPluginSupportPlugin, AnalyticsErrorCatcher { + + /** + * Flag indicating whether we should submit analytics data to the remote endpoint + */ + private var submitAnalytics: Boolean = true + /** + * Flag indicating whether we should submit analytics data to the remote endpoint + */ + private var printAnalytics: Boolean = false + + /** + * Flag to control if an exception in analytics collection should be causing the whole build to + * fail or just be logged and submitted as [UNKNOWN]. + */ + override var failOnAnalyticsError: Boolean = false + + private var analyticsServiceProvider: Provider? = null + + private lateinit var anonymizedBundleId: String companion object { - // TODO LATER Consider embedding these from the build.gradle's pluginVersion task just + // TODO LATER Consider embedding these from the build.gradle's versionConstants task just // as with the version. But leave it for now as they should be quite stable. // Modules has to match ${project.group}:${project.name} to make composite build work const val groupId = "io.realm.kotlin" const val artifactId = "plugin-compiler" const val version = PLUGIN_VERSION + const val coreVersion = CORE_VERSION + // The id used for passing compiler options from command line const val compilerPluginId = "io.realm.kotlin" + // Must match io.realm.kotlin.compiler.bundleIdKey const val bundleIdKey = "bundleId" + + // Must match io.realm.kotlin.compiler. + const val featureListPathKey = "featureListPath" } + @Suppress("NestedBlockDepth") + override fun apply(target: Project) { + super.apply(target) + + // We build the anonymized bundle id here and pass it to the compiler plugin to ensure + // that the metrics and sync connection parameters are aligned. + val bundleId = target.rootProject.name + ":" + target.name + anonymizedBundleId = hexStringify(sha256Hash(bundleId.toByteArray())) + + printAnalytics = target.providers.environmentVariable("REALM_PRINT_ANALYTICS").getBoolean() + submitAnalytics = !target.gradle.startParameter.isOffline && !target.providers.environmentVariable("REALM_DISABLE_ANALYTICS").getBoolean() + // We never want to break a user's build if collecting/submitting some data fail, so by + // default we are suppressing errors. This flag can control if errors are suppressed or will + // break the build. This allows us to catch errors during development and on CI builds. + failOnAnalyticsError = target.providers.environmentVariable("REALM_FAIL_ON_ANALYTICS_ERRORS").getBoolean() + + // Only register analytics service provider if we either want to submit or print the info + if (submitAnalytics || printAnalytics) { + analyticsServiceProvider = provider(target) + } + } + + private fun provider(target: Project) = + target.gradle.sharedServices.registerIfAbsent( + "Realm Analytics", + AnalyticsService::class.java + ) { spec: BuildServiceSpec -> + // Identify if project is using sync by inspecting dependencies. + // We cannot use resolved configurations here as this code is called in + // afterEvaluate, and resolving it prevents other plugins from modifying + // them. E.g the KMP plugin will crash if we resolve the configurations + // in `afterEvaluate`. This means we can only see dependencies directly set, + // and not their transitive dependencies. This should be fine as we only + // want to track builds directly using Realm. + var usesSync: Boolean = withDefaultOnError("Uses Sync", false) { + var usesSync = false + outer@ + for (conf in target.configurations) { + for (dependency in conf.dependencies) { + if (dependency.group == "io.realm.kotlin" && dependency.name == "library-sync") { + // In Java we can detect Sync through a Gradle configuration closure. + // In Kotlin, this choice is currently determined by which dependency + // people include + usesSync = true + break@outer + } + } + } + usesSync + } + + // Host identifiers collects information through exec/file operations. failOnError + // option is propagated to the actual tasks to ensure that the don't break build if + // collection fail. + val userId: String = target.providers.of(ComputerId::class.java) { + it.parameters.failOnAnalyticsError.set(failOnAnalyticsError) + }.safeProvider().get() + val builderId: String = target.providers.of(BuilderId::class.java) { + it.parameters.failOnAnalyticsError.set(failOnAnalyticsError) + }.safeProvider().get() + + val languageVersion = + withDefaultOnError("Language version", UNKNOWN) { target.kotlinToolingVersion } + val hostOsType = withDefaultOnError("Host Os Type", UNKNOWN) { HOST_OS_NAME } + val hostOsVersion = withDefaultOnError( + "Host Os Version", + UNKNOWN + ) { target.providers.systemProperty("os.version").safeProvider().get() } + val hostCpuArch = withDefaultOnError("Host CPU Arch", UNKNOWN) { HOST_ARCH_NAME } + + spec.parameters.run { + this.appId.set(anonymizedBundleId) + this.userId.set(userId) + this.builderId.set(builderId) + this.hostOsType.set(hostOsType) + this.hostOsVersion.set(hostOsVersion) + this.hostCpuArch.set(hostCpuArch) + this.usesSync.set(usesSync) + this.languageVersion.set(languageVersion.toString()) + } + } + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { return kotlinCompilation.target.project.plugins.findPlugin(RealmCompilerSubplugin::class.java) != null } @@ -56,10 +199,162 @@ class RealmCompilerSubplugin : KotlinCompilerPluginSupportPlugin { override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider> { val project = kotlinCompilation.target.project - val realmPlugin = project.plugins.getPlugin("io.realm.kotlin") as RealmPlugin - val anonymizedBundleId = realmPlugin.anonymizedBundleId + + // Compiler plugin options + val options = mutableListOf( + SubpluginOption(key = bundleIdKey, anonymizedBundleId), + ) + // Only bother collecting info if the analytics service is registered + analyticsServiceProvider?.let { provider -> + // Enable feature collection in compiler plugin by setting a path for the feature list + // file and pass it to the compiler plugin as a compiler plugin option + val featureListPath = listOf( + project.buildDir.path, + "outputs", + "realm-features", + kotlinCompilation.defaultSourceSet.name + ).joinToString(File.separator) + options.add(SubpluginOption(key = featureListPathKey, featureListPath)) + + // Gather target specific information + val targetInfo: TargetInfo? = gatherTargetInfo(kotlinCompilation) + // If we have something to submit register it for submission after the compilation has + // gathered the feature list information + targetInfo?.let { + kotlinCompilation.compileTaskProvider.get().doLast { + val analyticsService = provider.get() + val json = analyticsService.toJson(targetInfo) + if (printAnalytics) { + analyticsService.print(json) + } + if (submitAnalytics) { + analyticsService.submit(json) + } + } + } + } return project.provider { - listOf(SubpluginOption(key = bundleIdKey, anonymizedBundleId)) + options } } } + +/** + * Wrapper to safely obtain provider for usage in configuration phases to support configuration + * cache across Gradle versions. + */ +private fun Provider.safeProvider(): Provider = this.let { + when { + gradleVersion < gradle70 -> { + @Suppress("DEPRECATION") + it.forUseAtConfigurationTime() + } + else -> it + } +} + +@Suppress("ComplexMethod", "NestedBlockDepth") +private fun gatherTargetInfo(kotlinCompilation: KotlinCompilation<*>): TargetInfo? { + val project = kotlinCompilation.target.project + return when (kotlinCompilation) { + // We don't send metrics for common targets but still collect features as the + // target specific features are a union of common and target specific features + is KotlinCommonCompilation, + is KotlinSharedNativeCompilation -> + null + + is KotlinJvmAndroidCompilation -> { + val androidExtension = + project.extensions.findByName("android") as BaseExtension? + val defaultConfig = androidExtension?.defaultConfig + val minSDK = defaultConfig?.minSdkVersion?.apiString + val targetSDK = defaultConfig?.targetSdkVersion?.apiString + val targetCpuArch: String = + defaultConfig?.ndk?.abiFilters?.singleOrNull()?.let { androidArch(it) } + ?: "Universal" + TargetInfo("Android", targetCpuArch, targetSDK, minSDK) + } + + is KotlinJvmCompilation -> { + val jvmTarget = kotlinCompilation.kotlinOptions.jvmTarget + TargetInfo("JVM", "Universal", jvmTarget, jvmTarget) + } + + is KotlinNativeCompilation -> { + // We currently only support Darwin targets, so assume that we can pull minSdk + // from the given deploymentTarget. Non-CocoaPod Xcode project have this in its + // pdxproj-file as OS_DEPLOYMENT_TARGET, but assuming that most people use + // CocoaPods as it is the default. Reevaluate if we see too many missing values. + val kotlinExtension: KotlinMultiplatformExtension = + project.extensions.getByType(KotlinMultiplatformExtension::class.java) + val cocoapodsExtension = + (kotlinExtension as ExtensionAware).extensions.findByName( + COCOAPODS_EXTENSION_NAME + ) as CocoapodsExtension? + val minSdk = cocoapodsExtension?.let { cocoapods -> + when (kotlinCompilation.konanTarget.family) { + Family.OSX -> cocoapods.osx.deploymentTarget + Family.IOS -> cocoapods.ios.deploymentTarget + Family.TVOS -> cocoapods.tvos.deploymentTarget + Family.WATCHOS -> cocoapods.watchos.deploymentTarget + Family.LINUX, + Family.MINGW, + Family.ANDROID, + Family.WASM, + Family.ZEPHYR -> null // Not supported yet + } + } + TargetInfo( + nativeTarget(kotlinCompilation.konanTarget), + nativeArch(kotlinCompilation.konanTarget), + null, + minSdk + ) + } + // Not supported yet so don't try to gather target information + is KotlinJsCompilation, + is KotlinWithJavaCompilation<*, *> -> null + + else -> { + null + } + } +} + +// Helper method to ensure that we align target type string for native builds +fun nativeTarget(target: KonanTarget) = when (target.family) { + Family.OSX -> "macOS" + Family.IOS -> "iOS" + Family.TVOS -> "tvOS" + Family.WATCHOS -> "watchOS" + Family.LINUX -> "Linux" + Family.MINGW -> "MinGW" + Family.ANDROID -> "Android(native)" + Family.WASM -> "Wasm" + Family.ZEPHYR -> "Zephyr" + else -> unknown(target.family.name) +} + +// Helper method to ensure that we align architecture strings for Kotlin native builds +fun nativeArch(target: KonanTarget) = when (target.architecture) { + Architecture.X64 -> io.realm.kotlin.gradle.analytics.Architecture.X64.serializedName + Architecture.X86 -> io.realm.kotlin.gradle.analytics.Architecture.X86.serializedName + Architecture.ARM64 -> io.realm.kotlin.gradle.analytics.Architecture.ARM64.serializedName + Architecture.ARM32 -> io.realm.kotlin.gradle.analytics.Architecture.ARM.serializedName + Architecture.MIPS32 -> "Mips" + Architecture.MIPSEL32 -> "MipsEL32" + Architecture.WASM32 -> "Wasm" + else -> unknown(target.architecture.name) +} + +// Helper method to ensure that we align architecture strings for Android platforms +fun androidArch(target: String): String = when (target) { + "armeabi-v7a" -> io.realm.kotlin.gradle.analytics.Architecture.ARM.serializedName + "arm64-v8a" -> io.realm.kotlin.gradle.analytics.Architecture.ARM64.serializedName + "x86" -> io.realm.kotlin.gradle.analytics.Architecture.X86.serializedName + "x86_64" -> io.realm.kotlin.gradle.analytics.Architecture.X64.serializedName + else -> unknown(target) +} + +fun Provider.getBoolean(): Boolean = + this.safeProvider().getOrElse("false").equals("true", ignoreCase = true) 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 500538896c..22e138ee1e 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 @@ -16,44 +16,20 @@ package io.realm.kotlin.gradle -import io.realm.kotlin.gradle.analytics.AnalyticsService -import io.realm.kotlin.gradle.analytics.hexStringify -import io.realm.kotlin.gradle.analytics.sha256Hash import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.DependencySubstitutions 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 javax.inject.Inject @Suppress("unused") open class RealmPlugin : Plugin { private val logger: Logger = Logging.getLogger("realm-plugin") - internal lateinit var anonymizedBundleId: String - - @Inject - public open fun getBuildEventsRegistry(): BuildEventsListenerRegistry { TODO("Should have been replaced by Gradle.") } - override fun apply(project: Project) { project.pluginManager.apply(RealmCompilerSubplugin::class.java) - - // We build the anonymized bundle id here and pass it to the compiler plugin to ensure - // that the metrics and sync connection parameters are aligned. - val bundleId = project.rootProject.name + ":" + project.name - anonymizedBundleId = hexStringify(sha256Hash(bundleId.toByteArray())) - - // Run analytics as a Build Service to support Gradle Configuration Cache - val serviceProvider: Provider = project.gradle.sharedServices.registerIfAbsent( - "realm-analytics", - AnalyticsService::class.java - ) { /* Do nothing */ } - getBuildEventsRegistry().onTaskCompletion(serviceProvider) - project.configurations.all { conf: Configuration -> // Ensure that android unit tests uses the Realm JVM variant rather than Android. // This is a bit britle. See https://github.com/realm/realm-kotlin/issues/1404 for @@ -71,28 +47,5 @@ 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 { - // 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 - - // Create the analytics during configuration because it needs access to the project - // in order to gather project relevant information in afterEvaluate. Currently - // there doesn't seem a way to get this information during the Execution Phase. - @Suppress("SwallowedException", "TooGenericExceptionCaught") - try { - val analyticsService: AnalyticsService = serviceProvider.get() - analyticsService.collectAnalyticsData(it) - } catch (ex: Exception) { - // Work-around for https://github.com/gradle/gradle/issues/18821 - // Since this only happens in multi-module projects, this should be fine as - // the build will still be registered by the first module that starts the service. - } - } } } diff --git a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/AnalyticsService.kt b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/AnalyticsService.kt index 50ff1d778c..644e5d9e8c 100644 --- a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/AnalyticsService.kt +++ b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/AnalyticsService.kt @@ -15,21 +15,38 @@ */ package io.realm.kotlin.gradle.analytics -import org.gradle.api.Project +import io.realm.kotlin.gradle.RealmCompilerSubplugin +import io.realm.kotlin.gradle.analytics.AnalyticsService.Companion.unknown +import io.realm.kotlin.gradle.gradle75 +import io.realm.kotlin.gradle.gradleVersion import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging +import org.gradle.api.provider.Property +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters -import org.gradle.tooling.events.FinishEvent -import org.gradle.tooling.events.OperationCompletionListener -import org.gradle.tooling.events.task.TaskFailureResult -import org.gradle.tooling.events.task.TaskFinishEvent -import org.gradle.tooling.events.task.TaskOperationResult -import org.gradle.tooling.events.task.TaskSkippedResult -import org.gradle.tooling.events.task.TaskSuccessResult +import org.gradle.process.ExecOperations +import org.jetbrains.kotlin.com.google.gson.GsonBuilder +import org.jetbrains.kotlin.com.google.gson.JsonObject +import org.jetbrains.kotlin.com.google.gson.JsonPrimitive +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.UnsupportedEncodingException +import java.net.HttpURLConnection +import java.net.NetworkInterface +import java.net.URL +import java.nio.charset.Charset +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Scanner +import javax.inject.Inject +import javax.xml.bind.DatatypeConverter +import kotlin.experimental.and /** - * Analytics Build Service responsible for triggering analytics at the correct time. + * Analytics Build Service holding cross-target project specific info and methods for dispatching + * analytics. * Build Services are only marked stable from Gradle 7.4, so additional logging has been added * to this class to catch catches where types are different than expected. We do _NOT_ * want analytics to take down a users build, so exceptions are avoided on purpose. @@ -39,63 +56,400 @@ import org.gradle.tooling.events.task.TaskSuccessResult * * **See:** [Build Services](https://docs.gradle.org/current/userguide/build_services.html) */ -abstract class AnalyticsService : BuildService, OperationCompletionListener { - private val logger: Logger = Logging.getLogger("realm-build-service") - private var analytics: RealmAnalytics? = null +// Asynchronously submits build information to Realm when the gradle compile task run +// +// To be clear: this does *not* run when your app is in production or on +// your end-user's devices; it will only run when you build your app from source. +// +// Why are we doing this? Because it helps us build a better product for you. +// None of the data personally identifies you, your employer or your app, but it +// *will* help us understand what Realm version you use, what host OS you use, +// etc. Having this info will help with prioritizing our time, adding new +// features and deprecating old features. Collecting an anonymized bundle & +// anonymized MAC is the only way for us to count actual usage of the other +// metrics accurately. If we don't have a way to deduplicate the info reported, +// it will be useless, as a single developer building their app on Windows ten +// times would report 10 times more than a single developer that only builds +// once from Mac OS X, making the data all but useless. No one likes sharing +// data unless it's necessary, we get it, and we've debated adding this for a +// long long time. Since Realm is a free product without an email signup, we +// feel this is a necessary step so we can collect relevant data to build a +// better product for you. +// +// Currently the following information is reported: +// - What version of Realm is being used +// - What host you are running on +// - What targets you are building for +// - An anonymized MAC address and bundle ID to aggregate the other information on. +// +// The collected information will be printed as info messages to the Gradle logger named +// [realm-analytics] if settings the system environment variable +// REALM_PRINT_ANALYTICS=true +// Collection and submission of data can be fully disabled by setting the system environment variable +// REALM_DISABLE_ANALYTICS=true +private const val TOKEN = "ce0fac19508f6c8f20066d345d360fd0" +private const val EVENT_NAME = "Run" +private const val URL_PREFIX = "https://data.mongodb-api.com/app/realmsdkmetrics-zmhtm/endpoint/metric_webhook/metric?data=" + +// Container for the project specific details, thus equal across all platforms. +interface ProjectConfiguration : BuildServiceParameters { + val appId: Property + val userId: Property + val builderId: Property + val hostOsType: Property + val hostOsVersion: Property + val hostCpuArch: Property + val usesSync: Property + val languageVersion: Property +} + +// Container object for target specific details that varies across compilation targets. +data class TargetInfo( + val targetOsType: String, + val targetCpuArch: String, + val targetOSVersion: String?, + val targetOSMinVersion: String?, +) + +abstract class AnalyticsService : BuildService { + + private val projectInfo = JsonObject() + + init { + val parameters = parameters + projectInfo.add("event", JsonPrimitive(EVENT_NAME)) + projectInfo.add( + "properties", + JsonObject().apply { + add("token", JsonPrimitive(TOKEN)) + add("distinct_id", JsonPrimitive(parameters.userId.get())) + add("builder_id", JsonPrimitive(parameters.builderId.get())) + add("Anonymized Bundle ID", JsonPrimitive(parameters.appId.get())) + add("Binding", JsonPrimitive("kotlin")) + add("Language", JsonPrimitive("kotlin")) + add("Host OS Type", JsonPrimitive(parameters.hostOsType.get())) + add("Host OS Version", JsonPrimitive(parameters.hostOsVersion.get())) + add("Host CPU Arch", JsonPrimitive(parameters.hostCpuArch.get())) + add("Realm Version", JsonPrimitive(RealmCompilerSubplugin.version)) + add("Core Version", JsonPrimitive(RealmCompilerSubplugin.coreVersion)) + add( + "Sync Enabled", + JsonPrimitive(if (parameters.usesSync.get()) "true" else "false") + ) + add("Language Version", JsonPrimitive(parameters.languageVersion.get())) + } + ) + } + + internal fun toJson( + targetInfo: TargetInfo? = null + ): String { + val targetSpecificJson = projectInfo.deepCopy() + val properties = targetSpecificJson.getAsJsonObject("properties") + targetInfo?.targetCpuArch?.let { properties.add("Target OS Arch", JsonPrimitive(it)) } + targetInfo?.targetOsType?.let { properties.add("Target OS Type", JsonPrimitive(it)) } + targetInfo?.targetOSMinVersion?.let { properties.add("Target OS Minimum Version", JsonPrimitive(it)) } + targetInfo?.targetOSVersion?.let { properties.add("Target OS Version", JsonPrimitive(it)) } + return GsonBuilder().create().toJson(targetSpecificJson) + } + + internal fun print(json: String) { + info("[realm-analytics] Payload: $json") + } + + @Suppress("TooGenericExceptionCaught") + internal fun submit(json: String) { + try { + debug("Submitting analytics payload: $json") + Thread { + try { + val url = URL(URL_PREFIX + base64Encode(json)) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connect() + debug("Analytics payload sent") + } catch (e: InterruptedException) { + debug("Submitting analytics was interrupted") + } catch (e: Throwable) { + debug("Error submitting analytics: ${e.message}") + } + }.apply { + isDaemon = true + }.start() + } catch (e: Exception) { + // Analytics failing for any reason should not crash the build + debug("Submitting analytics payload failed: $e") + } + } + + private fun debug(message: String) = LOGGER.debug(message) + private fun info(message: String) = LOGGER.info(message) + + companion object { + internal val LOGGER: Logger = Logging.getLogger("realm-analytics") + internal const val UNKNOWN = "Unknown" + internal fun unknown(message: String? = null) = "$UNKNOWN${message?.let { "($it)" } ?: ""}" + } +} + +interface AnalyticsErrorCatcher { /** - * Only lifecycle event currently available in Build Services. + * Property controlling whether an error happening in the code `block` of [withDefaultOnError] + * should be causing thrown or ignored and reported as the `default` value instead. */ - override fun onFinish(event: FinishEvent?) { - @Suppress("TooGenericExceptionCaught") - try { - if (event == null) { - logger.warn("Null event received. This should never happen.") - return + val failOnAnalyticsError: Boolean + + /** + * Utility method to wrap property collection in a common pattern that either returns a default + * value or rethrows if collection gathering throws depending on the [failOnAnalyticsError] property. + */ + fun withDefaultOnError(name: String, default: T, block: () -> T): T = + when (failOnAnalyticsError) { + true -> block() + false -> try { + block() + } catch (e: Throwable) { + AnalyticsService.LOGGER.debug("Error collecting '$name': ${e.message}"); default } - when (event) { - is TaskFinishEvent -> handleTaskResult(event) - else -> { - logger.warn("Unknown event type: ${event.javaClass.name}") + } +} + +/** + * [HostIdentifier] parameter to control if errors should trigger default values or propagate out + * and fail the build. + */ +interface HostIdentifierParameters : ValueSourceParameters { + val failOnAnalyticsError: Property +} + +/** + * Abstraction of shell execution to support Gradle configuration cache and hide Gradle version + * differentiation, especially https://github.com/gradle/gradle/issues/18213 + */ +interface Executor { + + val execOperations: ExecOperations + fun exec(args: List): String { + return when { + // Differentiate by gradle version as earlier version does not support ExecOperation + // https://github.com/gradle/gradle/issues/18213 + gradleVersion < gradle75 -> { + val runtime = Runtime.getRuntime() + val process = runtime.exec(args.toTypedArray()) + String(process.inputStream.readBytes()) + } + else -> { + val output = ByteArrayOutputStream() + execOperations.exec { + it.commandLine(args) + it.standardOutput = output } + String(output.toByteArray(), Charset.defaultCharset()) } - } catch (ex: Exception) { - logger.warn("Unexpected error: $ex") } } +} - private fun handleTaskResult(taskEvent: TaskFinishEvent) { - when (val result: TaskOperationResult = taskEvent.result) { - is TaskSkippedResult -> { /* Ignore skipped tasks to avoid excessive work during incremental builds */ } - is TaskFailureResult -> { filterResultAndSendAnalytics(taskEvent) } - is TaskSuccessResult -> { filterResultAndSendAnalytics(taskEvent) } - else -> { - logger.warn("Unknown task type: ${result.javaClass.name}") +/** + * Common abstraction of tasks that collects host identifiers through various exec/file operations. + */ +abstract class HostIdentifier : ValueSource, Executor, AnalyticsErrorCatcher { + + @get:Inject + abstract override val execOperations: ExecOperations + override val failOnAnalyticsError: Boolean + get() = parameters.failOnAnalyticsError.get() + + val identifier: String + get() { + return when (HOST_OS) { + Host.WINDOWS -> windowsIdentifier + Host.MACOS -> macOsIdentifier + Host.LINUX -> linuxIdentifier + else -> throw IllegalStateException("Unknown host identifier") + } + } + abstract val linuxIdentifier: String + abstract val macOsIdentifier: String + abstract val windowsIdentifier: String +} + +/** + * Provider of a unique identifier for a computer. The method being used depends on the platform: + * - OS X: Mac address of en0 + * - Windows: BIOS identifier + * - Linux: Machine ID provided by the OS + */ +abstract class ComputerId : HostIdentifier() { + override val linuxIdentifier: String + get() { + var machineId = File("/var/lib/dbus/machine-id") + if (!machineId.exists()) { + machineId = File("/etc/machine-id") } + if (!machineId.exists()) { + throw IllegalStateException("Cannot locate machine identifier in ${machineId.absolutePath}") + } + var scanner: Scanner? = null + return try { + scanner = Scanner(machineId) + val id = scanner.useDelimiter("\\A").next() + id + } finally { + scanner?.close() + } + } + + override val macOsIdentifier: String + get() { + val networkInterface = NetworkInterface.getByName("en0") + val hardwareAddress = networkInterface.hardwareAddress + return String(hardwareAddress, Charset.defaultCharset()) } + + override val windowsIdentifier: String + get() { + val output = exec(listOf("wmic", "csproduct", "get", "UUID")) + val sc = Scanner(output) + var result: String? = null + while (sc.hasNext()) { + val next = sc.next() + if (next.contains("UUID")) { + result = sc.next().trim { it <= ' ' } + break + } + } + return result!! + } + + override fun obtain(): String? = withDefaultOnError("ComputerId", unknown()) { + hexStringify(sha256Hash(identifier.toByteArray())) } +} - private fun filterResultAndSendAnalytics(taskEvent: TaskFinishEvent) { - // We use `compile` tasks as a heuristic for a "build". This will not detect builds - // that fail very early or incremental builds with no code change, but neither will it - // trigger for tasks unrelated to building code. A normal build consists of multiple - // compile tasks, but the RealmAnalytics class tracks this and only send analytics once. - if (taskEvent.descriptor.name.contains("compile", true)) { - analytics?.sendAnalyticsData() +/** + * Provider of a unique builder identifier for a computer. + * + * Successor of [ComputerId] standardized across SDKs. + */ +abstract class BuilderId : HostIdentifier() { + + override val linuxIdentifier: String + get() { + return File("/etc/machine-id").inputStream().readBytes().toString().trim() + } + + override val macOsIdentifier: String + get() { + val output = exec(listOf("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")) + val regEx = ".*\"IOPlatformUUID\"\\s=\\s\"(.+)\"".toRegex() + val find: MatchResult? = regEx.find(output) + return find?.groups?.get(1)?.value!! } + + override val windowsIdentifier: String + get() { + val output = exec(listOf("reg", "QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid")) + // Manually expanded [:alnum:] as ([[:alnum:]-]+) didn't seems to work + // Output from Windows will be something like `MachineGuid REG_SZ 1c197ec7-adbd-4c3a-8386-306c20e0f686` + val regEx = "\\s*MachineGuid\\s*\\w*\\s*([A-Za-z0-9-]+)".toRegex() + val find: MatchResult? = regEx.find(output) + return find?.groups?.get(1)?.value!! + } + + override fun obtain(): String = withDefaultOnError("BuilderID", unknown()) { + val id = identifier + val data = "Realm is great$id" + base64Encode(sha256Hash(data.toByteArray()))!! } +} - /** - * In order to support the Gradle Configuration Cache, this method must be called during - * the Configuration Phase in `afterEvaluate`. It isn't allowed to store a reference to - * the `project` property. - * - * This method is responsible for gathering all the analytics data we are sending. - */ - @Synchronized - fun collectAnalyticsData(project: Project) { - analytics = RealmAnalytics() - analytics!!.gatherAnalyticsDataIfNeeded(project) +/** + * Encode the given string with Base64 + * @param data the string to encode + * @return the encoded string + * @throws UnsupportedEncodingException + */ +@Throws(UnsupportedEncodingException::class) +internal fun base64Encode(data: String): String? { + return base64Encode(data.toByteArray(charset("UTF-8"))) +} + +internal fun base64Encode(data: ByteArray): String? { + return DatatypeConverter.printBase64Binary(data) +} + +/** + * Compute the SHA-256 hash of the given byte array + * @param data the byte array to hash + * @return the hashed byte array + * @throws NoSuchAlgorithmException + */ +@Throws(NoSuchAlgorithmException::class) +internal fun sha256Hash(data: ByteArray?): ByteArray { + val messageDigest = MessageDigest.getInstance("SHA-256") + return messageDigest.digest(data) +} + +/** + * Convert a byte array to its hex-string + * @param data the byte array to convert + * @return the hex-string of the byte array + */ +@Suppress("MagicNumber") +internal fun hexStringify(data: ByteArray): String { + val stringBuilder = java.lang.StringBuilder() + for (singleByte: Byte in data) { + stringBuilder.append(((singleByte and 0xff.toByte()) + 0x100).toString(16).substring(1)) } + return stringBuilder.toString() +} + +enum class Host(val serializedName: String) { + WINDOWS("Windows"), LINUX("Linux"), MACOS("macOs"); } + +/** + * Define which Host OS the build is running on. + */ +val HOST_OS: Host + get() { + val hostOs = System.getProperty("os.name") + return when { + hostOs.contains("windows", ignoreCase = true) -> Host.WINDOWS + hostOs.contains("inux", ignoreCase = true) -> Host.LINUX + hostOs.contains("mac", ignoreCase = true) -> Host.MACOS + else -> throw IllegalArgumentException(hostOs) + } + } + +val HOST_OS_NAME: String + get() = try { + HOST_OS.serializedName + } catch (e: Throwable) { + unknown(System.getProperty("os.name")) + } + +enum class Architecture(val serializedName: String) { + X86("x86"), + X64("x64"), + ARM("Arm"), + ARM64("Arm64"), +} + +/** + * String that represents the architecture of the host the build is running on. + */ +val HOST_ARCH_NAME: String + get() = run { + val hostArch = System.getProperty("os.arch") + when { + hostArch.contains("x86") && hostArch.contains("64") -> Architecture.X64.serializedName + hostArch.contains("x86") -> Architecture.X64.serializedName + hostArch.contains("aarch") && hostArch.contains("64") -> Architecture.ARM64.serializedName + hostArch.contains("aarch") -> Architecture.ARM.serializedName + else -> unknown(hostArch) + } + } diff --git a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/RealmAnalytics.kt b/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/RealmAnalytics.kt deleted file mode 100644 index 33e7f863d2..0000000000 --- a/packages/gradle-plugin/src/main/kotlin/io/realm/kotlin/gradle/analytics/RealmAnalytics.kt +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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.gradle.analytics - -import com.android.build.gradle.BaseExtension -import io.realm.kotlin.gradle.RealmCompilerSubplugin -import org.gradle.api.Project -import org.gradle.api.logging.Logger -import org.gradle.api.logging.Logging -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.io.UnsupportedEncodingException -import java.net.HttpURLConnection -import java.net.NetworkInterface -import java.net.SocketException -import java.net.URL -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.Scanner -import javax.xml.bind.DatatypeConverter -import kotlin.experimental.and - -// Asynchronously submits build information to Realm when the gradle compile task run -// -// To be clear: this does *not* run when your app is in production or on -// your end-user's devices; it will only run when you build your app from source. -// -// Why are we doing this? Because it helps us build a better product for you. -// None of the data personally identifies you, your employer or your app, but it -// *will* help us understand what Realm version you use, what host OS you use, -// etc. Having this info will help with prioritizing our time, adding new -// features and deprecating old features. Collecting an anonymized bundle & -// anonymized MAC is the only way for us to count actual usage of the other -// metrics accurately. If we don't have a way to deduplicate the info reported, -// it will be useless, as a single developer building their app on Windows ten -// times would report 10 times more than a single developer that only builds -// once from Mac OS X, making the data all but useless. No one likes sharing -// data unless it's necessary, we get it, and we've debated adding this for a -// long long time. Since Realm is a free product without an email signup, we -// feel this is a necessary step so we can collect relevant data to build a -// better product for you. -// -// Currently the following information is reported: -// - What version of Realm is being used -// - What OS you are running on -// - An anonymized MAC address and bundle ID to aggregate the other information on. - -private const val TOKEN = "ce0fac19508f6c8f20066d345d360fd0" -private const val EVENT_NAME = "Run" -private const val URL_PREFIX = "https://data.mongodb-api.com/app/realmsdkmetrics-zmhtm/endpoint/metric_webhook/metric?data=" - -internal class RealmAnalytics { - - private val logger: Logger = Logging.getLogger("realm-analytics") - private var jsonData: String? = null - - companion object { - @Volatile - var METRIC_PROCESSED = false // prevent duplicate reports being sent from the same build run - } - - /** - * Collect analytics information. This method must be called in `project.afterEvaluate()`. - */ - public fun gatherAnalyticsDataIfNeeded(project: Project) { - val disableAnalytics: Boolean = project.gradle.startParameter.isOffline || "true".equals(System.getenv()["REALM_DISABLE_ANALYTICS"], ignoreCase = true) - if (!disableAnalytics) { - jsonData = jsonPayload(project) - // Resetting this flag as the Gradle Daemon keep this class and its state - // alive between builds, preventing analytics from being sent across multiple builds - METRIC_PROCESSED = false - } - } - - /** - * Send any previously gathered analytics data. [gatherAnalyticsDataIfNeeded] must be called - * first. - */ - @Synchronized - public fun sendAnalyticsData() { - if (!METRIC_PROCESSED) { - jsonData?.let { - logger.debug("Sending Realm analytics data: \n$jsonData") - sendAnalytics(it, logger) - } - METRIC_PROCESSED = true - } - } - - @Suppress("NestedBlockDepth") - private fun jsonPayload(project: Project): String { - val userId = ComputerIdentifierGenerator.get() - val appId = anonymousAppId(project) - val osType = System.getProperty("os.name") - val osVersion = System.getProperty("os.version") - - val projectAndroidExtension: BaseExtension? = - project.extensions.findByName("android") as BaseExtension? - val minSDK = projectAndroidExtension?.defaultConfig?.minSdkVersion?.apiString - val targetSDK = projectAndroidExtension?.defaultConfig?.targetSdkVersion?.apiString - - // We cannot use resolved configurations here as this code is called in - // afterEvaluate, and resolving it prevents other plugins from modifying - // them. E.g the KMP plugin will crash if we resolve the configurations - // in `afterEvaluate`. This means we can only see dependencies directly set, - // and not their transitive dependencies. This should be fine as we only - // want to track builds directly using Realm. - var usesSync = false - outer@ - for (conf in project.configurations) { - for (dependency in conf.dependencies) { - if (dependency.group == "io.realm.kotlin" && dependency.name == "library-sync") { - // In Java we can detect Sync through a Gradle configuration closure. - // In Kotlin, this choice is currently determined by which dependency - // people include - usesSync = true - break@outer - } - } - } - - // FIXME Improve metrics with details about targets, etc. - // https://github.com/realm/realm-kotlin/issues/127 - return """{ - "event": "$EVENT_NAME", - "properties": { - "token": "$TOKEN", - "distinct_id": "$userId", - "Anonymized MAC Address": "$userId", - "Anonymized Bundle ID": "$appId", - "Binding": "kotlin", - "Language": "kotlin", - "Realm Version": "${RealmCompilerSubplugin.version}", - "Sync Enabled": ${if (usesSync) "true" else "false"}, - "Host OS Type": "$osType", - "Host OS Version": "$osVersion", - "Target OS Minimum Version": "$minSDK", - "Target OS Version": "$targetSDK" - } - }""" - } - - private fun anonymousAppId(project: Project): String { - var projectName = project.rootProject.name - if (projectName.isEmpty()) { - projectName = project.name - } - - var packageName = project.group.toString() - if (packageName.isEmpty()) { - packageName = project.rootProject.group.toString() - } - - return hexStringify(sha256Hash("$packageName.$projectName".toByteArray())) - } - - @Suppress("TooGenericExceptionCaught") - private fun sendAnalytics(json: String, logger: Logger) { - try { - logger.debug("Sending analytics payload\n$json") - Thread( - Runnable { - try { - val response = networkQuery(json) - logger.debug("Analytics sent: $response") - } catch (e: InterruptedException) { - logger.debug("Sending analytics was interrupted.") - } - } - ).apply { - setDaemon(true) - }.start() - } catch (e: Exception) { - // Analytics failing for any reason should not crash the build - logger.debug("Error when sending: $e") - } - } - - private fun networkQuery(jsonPayload: String): Int { - try { - val url = URL(URL_PREFIX + base64Encode(jsonPayload)) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.connect() - return connection.responseCode - } catch (ignored: Throwable) { - return -1 - } - } -} - -/** - * Generate a unique identifier for a computer. The method being used depends on the platform: - * - OS X: Mac address of en0 - * - Windows: BIOS identifier - * - Linux: Machine ID provided by the OS - */ -internal object ComputerIdentifierGenerator { - private const val UNKNOWN = "unknown" - private val OS = System.getProperty("os.name").lowercase() - @Suppress("TooGenericExceptionCaught", "SwallowedException") - fun get(): String { - return try { - when { - isWindows -> { - windowsIdentifier - } - isMac -> { - macOsIdentifier - } - isLinux -> { - linuxMacAddress - } - else -> { - UNKNOWN - } - } - } catch (e: Exception) { - UNKNOWN - } - } - - private val isWindows: Boolean - get() = OS.contains("win") - private val isMac: Boolean - get() = OS.contains("mac") - private val isLinux: Boolean - get() = OS.contains("inux") - - @get:Throws(FileNotFoundException::class, NoSuchAlgorithmException::class) - private val linuxMacAddress: String - get() { - var machineId = File("/var/lib/dbus/machine-id") - if (!machineId.exists()) { - machineId = File("/etc/machine-id") - } - if (!machineId.exists()) { - return UNKNOWN - } - var scanner: Scanner? = null - return try { - scanner = Scanner(machineId) - val id = scanner.useDelimiter("\\A").next() - hexStringify(sha256Hash(id.toByteArray())) - } finally { - scanner?.close() - } - } - - @get:Throws(SocketException::class, NoSuchAlgorithmException::class) - private val macOsIdentifier: String - get() { - val networkInterface = NetworkInterface.getByName("en0") - val hardwareAddress = networkInterface.hardwareAddress - return hexStringify(sha256Hash(hardwareAddress)) - } - - @get:Throws(IOException::class, NoSuchAlgorithmException::class) - private val windowsIdentifier: String - get() { - val runtime = Runtime.getRuntime() - val process = runtime.exec(arrayOf("wmic", "csproduct", "get", "UUID")) - var result: String? = null - val `is` = process.inputStream - val sc = Scanner(process.inputStream) - `is`.use { - while (sc.hasNext()) { - val next = sc.next() - if (next.contains("UUID")) { - result = sc.next().trim { it <= ' ' } - break - } - } - } - return if (result == null) UNKNOWN else hexStringify(sha256Hash(result!!.toByteArray())) - } -} - -/** - * Encode the given string with Base64 - * @param data the string to encode - * @return the encoded string - * @throws UnsupportedEncodingException - */ -@Throws(UnsupportedEncodingException::class) -internal fun base64Encode(data: String): String? { - return DatatypeConverter.printBase64Binary(data.toByteArray(charset("UTF-8"))) -} - -/** - * Compute the SHA-256 hash of the given byte array - * @param data the byte array to hash - * @return the hashed byte array - * @throws NoSuchAlgorithmException - */ -@Throws(NoSuchAlgorithmException::class) -internal fun sha256Hash(data: ByteArray?): ByteArray { - val messageDigest = MessageDigest.getInstance("SHA-256") - return messageDigest.digest(data) -} - -/** - * Convert a byte array to its hex-string - * @param data the byte array to convert - * @return the hex-string of the byte array - */ -@Suppress("MagicNumber") -internal fun hexStringify(data: ByteArray): String { - val stringBuilder = java.lang.StringBuilder() - for (singleByte: Byte in data) { - stringBuilder.append(((singleByte and 0xff.toByte()) + 0x100).toString(16).substring(1)) - } - return stringBuilder.toString() -} diff --git a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmCommandLineProcessor.kt b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmCommandLineProcessor.kt index cdb947458b..926eec3ed4 100644 --- a/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmCommandLineProcessor.kt +++ b/packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmCommandLineProcessor.kt @@ -26,7 +26,9 @@ import org.jetbrains.kotlin.config.CompilerConfigurationKey // Must match io.realm.kotlin.gradle.RealmCompilerSubplugin.bundleId const val BUNDLE_ID_KEY = "bundleId" +const val FEATURE_LIST_PATH_KEY = "featureListPath" val bundleIdConfigurationKey: CompilerConfigurationKey = CompilerConfigurationKey("io.realm.kotlin.bundleId") +val featureListPathConfigurationKey: CompilerConfigurationKey = CompilerConfigurationKey("io.realm.kotlin.featureListPath") @OptIn(ExperimentalCompilerApi::class) @AutoService(CommandLineProcessor::class) @@ -39,7 +41,14 @@ class RealmCommandLineProcessor : CommandLineProcessor { valueDescription = "Anonymized Bundle Id", required = false, allowMultipleOccurrences = false - ) + ), + CliOption( + optionName = "featureListPath", + description = "Feature List Path", + valueDescription = "Feature List Path", + required = false, + allowMultipleOccurrences = false + ), ) override fun processOption( @@ -47,9 +56,11 @@ class RealmCommandLineProcessor : CommandLineProcessor { value: String, configuration: CompilerConfiguration ) { - when { - option.optionName == BUNDLE_ID_KEY -> + when (option.optionName) { + BUNDLE_ID_KEY -> configuration.put(bundleIdConfigurationKey, value) + FEATURE_LIST_PATH_KEY -> + configuration.put(featureListPathConfigurationKey, value) else -> super.processOption(option, value, configuration) } } diff --git a/tools/update_gradle_wrapper.sh b/tools/update_gradle_wrapper.sh index 19b963cba1..efb802055f 100644 --- a/tools/update_gradle_wrapper.sh +++ b/tools/update_gradle_wrapper.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/sh -e # This script updates Gradle Wrappers in this repository. # To run it: @@ -31,7 +31,7 @@ echo read -n1 -r -p "Press any key to continue..." key for i in $(find $(pwd) -type f -name gradlew); do - if [[ $i != *min-android-sample/gradlew ]]; then + if [[ $i != *min-android-sample/gradlew && $i != *integration-tests/gradle/gradle*/gradlew ]]; then cd $(dirname $i) pwd ./gradlew wrapper --gradle-version=$GRADLE_VERSION --distribution-type all