From 6a52f39ec0037cf24cbfd0c193a3936adbe610d6 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 17 Jul 2025 14:54:23 +0200 Subject: [PATCH 1/5] Enable build scans --- .github/workflows/test.yml | 2 +- build.gradle.kts | 16 ++++++++++++++++ settings.gradle.kts | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) 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/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") From 38282da4bd123d0f566f010e3046cb8f27719cfc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 17 Jul 2025 14:55:52 +0200 Subject: [PATCH 2/5] Also enable for docs build --- .github/workflows/docs-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From eb5c7d163eff4544f01254d570db734b796ae0a9 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 17 Jul 2025 17:57:58 +0200 Subject: [PATCH 3/5] Consistently cancel turbine --- .../com/powersync/sync/SyncIntegrationTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt index 3e9136cb..d381fe05 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt @@ -411,8 +411,8 @@ abstract class BaseSyncIntegrationTest( db2.disconnect() turbine2.waitFor { !it.connecting } - turbine1.cancel() - turbine2.cancel() + turbine1.cancelAndIgnoreRemainingEvents() + turbine2.cancelAndIgnoreRemainingEvents() } } @@ -433,7 +433,7 @@ abstract class BaseSyncIntegrationTest( database.disconnect() turbine.waitFor { !it.connecting } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -449,7 +449,7 @@ abstract class BaseSyncIntegrationTest( database.connect(connector, 1000L, retryDelayMs = 5000, options = options) turbine.waitFor { it.connecting } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -650,7 +650,7 @@ abstract class BaseSyncIntegrationTest( turbine.waitFor { !it.connected } connector.cachedCredentials shouldBe null - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } @@ -686,7 +686,7 @@ abstract class BaseSyncIntegrationTest( // Should retry, and the second fetchCredentials call will work turbine.waitFor { it.connected } - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } } @@ -740,7 +740,7 @@ class NewSyncIntegrationTest : BaseSyncIntegrationTest(true) { turbine.waitFor { it.connected } fetchCredentialsCount shouldBe 2 - turbine.cancel() + turbine.cancelAndIgnoreRemainingEvents() } } From 6a734abaeea177eb9802944ec6e37781c6c02b72 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 21 Jul 2025 09:59:04 +0200 Subject: [PATCH 4/5] Make checkpoint during upload test more reliable --- .../com/powersync/sync/SyncIntegrationTest.kt | 15 ++++++++- .../com/powersync/testutils/TestUtils.kt | 5 +-- .../kotlin/com/powersync/sync/SyncStream.kt | 3 +- .../com/powersync/PowerSyncTestLogWriter.kt | 31 +++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/sync/SyncIntegrationTest.kt index d381fe05..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 @@ -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) diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt index 4e9c49a7..4f14a97f 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt @@ -9,6 +9,7 @@ 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 +74,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..dc8800d2 --- /dev/null +++ b/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt @@ -0,0 +1,31 @@ +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 { + return severity.ordinal >= loggable.ordinal + } + + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + lock.withLock { + _logs.add(LogEntry(severity, message, tag, throwable)) + } + } +} From 77d4f5f4b49fbc51207009b3ad4c7ac363d19f44 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 21 Jul 2025 10:10:12 +0200 Subject: [PATCH 5/5] Reformat --- .../com/powersync/testutils/TestUtils.kt | 1 - .../com/powersync/PowerSyncTestLogWriter.kt | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt index 4f14a97f..6faea462 100644 --- a/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt +++ b/core/src/commonIntegrationTest/kotlin/com/powersync/testutils/TestUtils.kt @@ -7,7 +7,6 @@ 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 diff --git a/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt b/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt index dc8800d2..10e2e602 100644 --- a/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt +++ b/core/src/commonTest/kotlin/com/powersync/PowerSyncTestLogWriter.kt @@ -12,18 +12,26 @@ import kotlinx.atomicfu.locks.withLock * for concurrent access. */ @OptIn(ExperimentalKermitApi::class) -class PowerSyncTestLogWriter(private val loggable: Severity) : LogWriter() { +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 { - return severity.ordinal >= loggable.ordinal - } + override fun isLoggable( + tag: String, + severity: Severity, + ): Boolean = severity.ordinal >= loggable.ordinal - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + override fun log( + severity: Severity, + message: String, + tag: String, + throwable: Throwable?, + ) { lock.withLock { _logs.add(LogEntry(severity, message, tag, throwable)) }