diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 40c5ea8b..8299c488 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -31,7 +31,7 @@ jobs: validate-wrappers: true - name: Build Docs run: | - ./gradlew \ + ./gradlew --scan \ --no-configuration-cache \ -PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} \ dokkaGenerate diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 409b938d..4a3e2fa9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: - name: Build and run tests with Gradle run: | - ./gradlew \ + ./gradlew --scan \ ${{ matrix.targets }} shell: bash diff --git a/build.gradle.kts b/build.gradle.kts index b8bff3d1..92d75028 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,6 +75,22 @@ dokka { moduleName.set("PowerSync Kotlin") } +develocity { + val isPowerSyncCI = System.getenv("GITHUB_REPOSITORY") == "powersync-ja/powersync-kotlin" + + buildScan { + // We can't know if everyone running this build has accepted the TOS, but we've accepted + // them for our CI. + if (isPowerSyncCI) { + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } + + // Only upload build scan if the --scan parameter is set + publishing.onlyIf { false } + } +} + // Serve the generated Dokka documentation using a simple HTTP server // File changes are not watched here tasks.register("serveDokka") { diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt index 3e9136cb..84743ac0 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt @@ -32,6 +32,8 @@ import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlin.test.Test @@ -411,8 +413,8 @@ abstract class BaseSyncIntegrationTest( db2.disconnect() turbine2.waitFor { !it.connecting } - turbine1.cancel() - turbine2.cancel() + turbine1.cancelAndIgnoreRemainingEvents() + turbine2.cancelAndIgnoreRemainingEvents() } } @@ -433,7 +435,7 @@ abstract class BaseSyncIntegrationTest( database.disconnect() turbine.waitFor { !it.connecting } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -449,7 +451,7 @@ abstract class BaseSyncIntegrationTest( database.connect(connector, 1000L, retryDelayMs = 5000, options = options) turbine.waitFor { it.connecting } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -482,10 +484,21 @@ abstract class BaseSyncIntegrationTest( turbineScope { val turbine = database.currentStatus.asFlow().testIn(this) syncLines.send(SyncLine.KeepAlive(1234)) - turbine.waitFor { it.connected && !it.uploading } + turbine.waitFor { it.connected } turbine.cancelAndIgnoreRemainingEvents() } + // Wait for the first upload task triggered when connecting to be complete. + withContext(Dispatchers.Default) { + waitFor { + assertNotNull( + logWriter.logs.find { + it.message.contains("crud upload: notify completion") + }, + ) + } + } + database.execute("INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)", listOf("local", "local@example.org")) expectUserRows(1) @@ -650,7 +663,7 @@ abstract class BaseSyncIntegrationTest( turbine.waitFor { !it.connected } connector.cachedCredentials shouldBe null - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -686,7 +699,7 @@ abstract class BaseSyncIntegrationTest( // Should retry, and the second fetchCredentials call will work turbine.waitFor { it.connected } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } } @@ -740,7 +753,7 @@ class NewSyncIntegrationTest : BaseSyncIntegrationTest(true) { turbine.waitFor { it.connected } fetchCredentialsCount shouldBe 2 - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt index 4e9c49a7..6faea462 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt @@ -7,8 +7,8 @@ import co.touchlab.kermit.LogWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import co.touchlab.kermit.TestConfig -import co.touchlab.kermit.TestLogWriter import com.powersync.DatabaseDriverFactory +import com.powersync.PowerSyncTestLogWriter import com.powersync.TestConnector import com.powersync.bucket.WriteCheckpointData import com.powersync.bucket.WriteCheckpointResponse @@ -73,8 +73,8 @@ internal class ActiveDatabaseTest( lateinit var database: PowerSyncDatabaseImpl val logWriter = - TestLogWriter( - loggable = Severity.Debug, + PowerSyncTestLogWriter( + loggable = Severity.Verbose, ) val logger = Logger( diff --git a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt index 0897e51c..826cdd24 100644 --- a/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt +++ b/core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt @@ -36,6 +36,7 @@ import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.readUTF8Line import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable @@ -122,7 +123,7 @@ internal class SyncStream( } fun triggerCrudUploadAsync(): Job = - uploadScope.launch { + uploadScope.launch(CoroutineName("triggerCrudUploadAsync")) { val thisIteration = PendingCrudUpload(CompletableDeferred()) var holdingUploadLock = false diff --git a/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt b/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt new file mode 100644 index 00000000..10e2e602 --- /dev/null +++ b/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt @@ -0,0 +1,39 @@ +package com.powersync + +import co.touchlab.kermit.ExperimentalKermitApi +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.TestLogWriter.LogEntry +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock + +/** + * A version of the `TestLogWriter` from Kermit that uses a mutex around logs instead of throwing + * for concurrent access. +*/ +@OptIn(ExperimentalKermitApi::class) +class PowerSyncTestLogWriter( + private val loggable: Severity, +) : LogWriter() { + private val lock = reentrantLock() + private val _logs = mutableListOf() + + val logs: List + get() = lock.withLock { _logs.toList() } + + override fun isLoggable( + tag: String, + severity: Severity, + ): Boolean = severity.ordinal >= loggable.ordinal + + override fun log( + severity: Severity, + message: String, + tag: String, + throwable: Throwable?, + ) { + lock.withLock { + _logs.add(LogEntry(severity, message, tag, throwable)) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b7472215..5d32ed81 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,10 @@ dependencyResolutionManagement { } } +plugins { + id("com.gradle.develocity") version "4.1" +} + rootProject.name = "powersync-root" include(":core")