diff --git a/buildSrc/src/main/kotlin/util/Android.kt b/buildSrc/src/main/kotlin/util/Android.kt index aaf134b1..22f2932d 100644 --- a/buildSrc/src/main/kotlin/util/Android.kt +++ b/buildSrc/src/main/kotlin/util/Android.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 INOVA IT d.o.o. + * Copyright 2024 INOVA IT d.o.o. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -20,6 +20,7 @@ import com.android.build.api.dsl.AndroidResources import com.android.build.api.dsl.BuildFeatures import com.android.build.api.dsl.BuildType import com.android.build.api.dsl.DefaultConfig +import com.android.build.api.dsl.Installation import com.android.build.api.dsl.ProductFlavor import com.android.build.gradle.internal.dsl.InternalTestedExtension import com.android.build.gradle.internal.utils.KOTLIN_ANDROID_PLUGIN_ID @@ -35,7 +36,8 @@ fun Project.commonAndroid( BuildType, DefaultConfig, ProductFlavor, - AndroidResources + AndroidResources, + Installation >> ) { (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", block) diff --git a/config/libs.toml b/config/libs.toml index fcc0d23b..81107e32 100644 --- a/config/libs.toml +++ b/config/libs.toml @@ -1,22 +1,23 @@ [versions] -androidGradlePlugin = "8.2.2" -androidGradleCacheFix = "3.0" +androidGradlePlugin = "8.3.1" +androidGradleCacheFix = "3.0.1" androidx-activity = "1.8.2" -androidx-compose-compiler = "1.5.8" -androidx-compose-material3 = "1.1.2" -androidx-compose-runtime = "1.6.0" +# See https://developer.android.com/jetpack/androidx/releases/compose-kotlin +androidx-compose-compiler = "1.5.11" +androidx-compose-material3 = "1.2.1" +androidx-compose-runtime = "1.6.5" androidx-core = "1.12.0" androidx-fragment = "1.6.2" androidx-lifecycle = "2.7.0" androidx-test-espresso = "3.5.1" autoService = "1.1.1" -coil = "2.5.0" -dokka = "1.9.10" +coil = "2.6.0" +dokka = "1.9.20" espresso = "3.5.1" -anvil = "2.5.0-beta01" +anvil = "2.5.0-beta04" desugarJdkLibs = "2.0.4" -dagger = "2.50" -detekt = "1.23.5" +dagger = "2.51.1" +detekt = "1.23.6" dispatch = "1.0.0-beta10" # Do not update this until we update gradle # there is a version conflict with a gradle-supplied grpc, resulting in @@ -24,20 +25,19 @@ dispatch = "1.0.0-beta10" googleCloud-monitoring = "3.10.0" googleCloud-protobufUtil = "3.25.1" okhttp = "4.12.0" -orgJson = "20231013" -kotlin = "1.9.22" -kotlin-coroutines = "1.7.3" -# Do not update kotest until https://github.com/kotest/kotest/issues/3407 fix is released -kotest = "5.8.0" +orgJson = "20240303" +kotlin = "1.9.23" +kotlin-coroutines = "1.8.0" +kotest = "5.8.1" nexusPublish = "0.30.0" junit4 = "4.13.2" -junit5 = "5.10.1" +junit5 = "5.10.2" moshi = "1.15.1" -retrofit = "2.9.0" +retrofit = "2.11.0" sarif4k = "0.5.0" simpleStack = "2.8.0" -tomlj = "1.1.0" -turbine = "1.0.0" +tomlj = "1.1.1" +turbine = "1.1.0" versionsChecker = "0.51.0" [libraries] diff --git a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Utils.kt b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Utils.kt index c9517a36..571218f2 100644 --- a/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Utils.kt +++ b/core/src/commonMain/kotlin/si/inova/kotlinova/core/outcome/Utils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 INOVA IT d.o.o. + * Copyright 2024 INOVA IT d.o.o. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -17,6 +17,7 @@ package si.inova.kotlinova.core.outcome import si.inova.kotlinova.core.exceptions.UnknownCauseException +import kotlin.coroutines.cancellation.CancellationException /** * Execute passed [block], returning its value, or [Outcome.Error] if block throws any exception @@ -24,6 +25,8 @@ import si.inova.kotlinova.core.exceptions.UnknownCauseException inline fun catchIntoOutcome(block: () -> Outcome): Outcome { return try { block() + } catch (e: CancellationException) { + throw e } catch (e: CauseException) { Outcome.Error(e) } catch (e: Exception) { diff --git a/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt index 420b5c85..cf3d1551 100644 --- a/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt +++ b/core/src/jvmCommon/kotlin/si/inova/kotlinova/core/data/Debouncer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 INOVA IT d.o.o. + * Copyright 2024 INOVA IT d.o.o. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -42,12 +42,12 @@ class Debouncer( private var previousJob: Job? = null private var lastStart = 0L - fun executeDebouncing(task: suspend () -> Unit) { + fun executeDebouncing(debouncingTimeMs: Long = this.debouncingTimeMs, task: suspend () -> Unit) { previousJob?.cancel() previousJob = scope.launch(targetContext) { - if ((lastStart != 0L || !triggerFirstImmediately) && - (timeProvider.currentTimeMillis() - lastStart) < debouncingTimeMs + if (!triggerFirstImmediately || (lastStart != 0L && + (timeProvider.currentTimeMillis() - lastStart) < debouncingTimeMs) ) { delay(debouncingTimeMs) } diff --git a/core/src/jvmTest/kotlin/si/inova/kotlinova/core/data/DebouncerTest.kt b/core/src/jvmTest/kotlin/si/inova/kotlinova/core/data/DebouncerTest.kt index e204c0f5..1fd7ff6e 100644 --- a/core/src/jvmTest/kotlin/si/inova/kotlinova/core/data/DebouncerTest.kt +++ b/core/src/jvmTest/kotlin/si/inova/kotlinova/core/data/DebouncerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 INOVA IT d.o.o. + * Copyright 2024 INOVA IT d.o.o. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -28,11 +28,13 @@ internal class DebouncerTest { @Test fun `Wait for task to start when not in immediate mode`() = runTest { + advanceTimeBy(100_000) + val debouncer = Debouncer(backgroundScope, virtualTimeProvider()) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) runCurrent() task.called shouldBe false @@ -42,22 +44,37 @@ internal class DebouncerTest { @Test fun `Start task immediately in immediate mode`() = runTest { + advanceTimeBy(100_000) + + val debouncer = Debouncer(backgroundScope, virtualTimeProvider(), triggerFirstImmediately = true) + + val task = ExecutableTask() + + debouncer.executeDebouncing(task = task) + runCurrent() + task.called shouldBe true + } + + @Test + fun `Start task immediately in immediate mode with time being zero`() = runTest { val debouncer = Debouncer(backgroundScope, virtualTimeProvider(), triggerFirstImmediately = true) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) runCurrent() task.called shouldBe true } @Test fun `Cancel previous task when triggering new one`() = runTest { + advanceTimeBy(100_000) + val debouncer = Debouncer(backgroundScope, virtualTimeProvider()) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) advanceTimeBy(600) debouncer.executeDebouncing { } @@ -67,13 +84,15 @@ internal class DebouncerTest { @Test fun `Do not start new task until debouncing period is over`() = runTest { + advanceTimeBy(100_000) + val debouncer = Debouncer(backgroundScope, virtualTimeProvider()) debouncer.executeDebouncing {} advanceTimeBy(200) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) runCurrent() task.called shouldBe false @@ -83,13 +102,15 @@ internal class DebouncerTest { @Test fun `Do not start new task until debouncing period is over even in immediate mode`() = runTest { + advanceTimeBy(100_000) + val debouncer = Debouncer(backgroundScope, virtualTimeProvider(), triggerFirstImmediately = true) debouncer.executeDebouncing {} advanceTimeBy(200) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) runCurrent() task.called shouldBe false @@ -99,57 +120,45 @@ internal class DebouncerTest { @Test fun `Do not start any task if debouncing period keeps changing`() = runTest { + advanceTimeBy(100_000) + val debouncer = Debouncer(backgroundScope, virtualTimeProvider()) debouncer.executeDebouncing {} advanceTimeBy(600) val task = ExecutableTask() - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) runCurrent() advanceTimeBy(100) - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) advanceTimeBy(100) - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) advanceTimeBy(100) - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) advanceTimeBy(100) - debouncer.executeDebouncing(task) + debouncer.executeDebouncing(task = task) task.called shouldBe false } -// -// @Test -// fun testTwoTasks() { -// val taskA: LocalFunction0 = mock() -// whenever(taskA.invoke()).thenReturn(Unit) -// val taskB: LocalFunction0 = mock() -// whenever(taskB.invoke()).thenReturn(Unit) -// -// val inOrderTest = inOrder(taskA, taskB) -// -// debouncer.executeDebouncing(taskA) -// scheduler.triggerActions() -// inOrderTest.verify(taskA, never()).invoke() -// -// scheduler.advanceTime(400) -// inOrderTest.verify(taskA, never()).invoke() -// -// debouncer.executeDebouncing(taskB) -// scheduler.triggerActions() -// inOrderTest.verify(taskB, never()).invoke() -// -// scheduler.advanceTime(400) -// inOrderTest.verify(taskB, never()).invoke() -// -// scheduler.advanceTime(500) -// inOrderTest.verify(taskB).invoke() -// -// scheduler.advanceTime(600) -// inOrderTest.verifyNoMoreInteractions() -// } + @Test + fun `Wait for task to start when not in immediate mode with custom delay`() = runTest { + advanceTimeBy(100_000) + + val debouncer = Debouncer(backgroundScope, virtualTimeProvider()) + + val task = ExecutableTask() + + debouncer.executeDebouncing(task = task, debouncingTimeMs = 300L) + runCurrent() + task.called shouldBe false + + advanceTimeBy(300) + runCurrent() + task.called shouldBe true + } private class ExecutableTask : suspend () -> Unit { var called = false diff --git a/gradle.properties b/gradle.properties index 27c4210c..fe8fb8c7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # -# Copyright 2023 INOVA IT d.o.o. +# Copyright 2024 INOVA IT d.o.o. # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -13,14 +13,6 @@ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -37,3 +29,5 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 +# We are manually setting dependencies between source sets, so we must disable the default template +kotlin.mpp.applyDefaultHierarchyTemplate=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 13d67f23..fca20898 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -15,8 +15,8 @@ # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip -distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionSha256Sum=194717442575a6f96e1c1befa2c30e9a4fc90f701d7aee33eb879b79e7ff05c0 networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/kotlinova-gradle/src/main/kotlin/si/inova/kotlinova/gradle/detektprecommit/DetectPreCommitHook.kt b/kotlinova-gradle/src/main/kotlin/si/inova/kotlinova/gradle/detektprecommit/DetectPreCommitHook.kt index bba21aa9..83f8aca0 100644 --- a/kotlinova-gradle/src/main/kotlin/si/inova/kotlinova/gradle/detektprecommit/DetectPreCommitHook.kt +++ b/kotlinova-gradle/src/main/kotlin/si/inova/kotlinova/gradle/detektprecommit/DetectPreCommitHook.kt @@ -19,30 +19,42 @@ package si.inova.kotlinova.gradle.detektprecommit import io.gitlab.arturbosch.detekt.Detekt import org.gradle.api.DefaultTask import org.gradle.api.Project +import org.gradle.api.UnknownTaskException import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Provider import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider import si.inova.kotlinova.gradle.KotlinovaExtension import java.io.File import java.io.FileNotFoundException +internal fun Project.createTopLevelStagedFilesListTask(): TaskProvider { + // This violates build isolation but it can improve performance significantly with large projects with many modules + // Waiting for https://github.com/gradle/gradle/issues/25179 for a better solution + + return try { + rootProject.tasks.named("gitPreCommitFileList", GitPreCommitFilesTask::class.java) + } catch (ignored: UnknownTaskException) { + rootProject.tasks.register("gitPreCommitFileList", GitPreCommitFilesTask::class.java) { task -> + val targetFile = File( + project.layout.buildDirectory.asFile.get(), + "intermediates/gitPreCommitFileList/output" + ) + + targetFile.also { + it.parentFile.mkdirs() + task.gitStagedListFile.set(it) + } + task.outputs.upToDateWhen { false } + } + } +} + internal fun Project.registerDetektPreCommitHook(extension: KotlinovaExtension) { afterEvaluate { if (extension.enableDetektPreCommitHook.getOrElse(false)) { - val gitPreCommitFileListTask = - tasks.register("gitPreCommitFileList", GitPreCommitFilesTask::class.java) { task -> - val targetFile = File( - project.layout.buildDirectory.asFile.get(), - "intermediates/gitPreCommitFileList/output" - ) - - targetFile.also { - it.parentFile.mkdirs() - task.gitStagedListFile.set(it) - } - task.outputs.upToDateWhen { false } - } + val gitPreCommitFileListTask = createTopLevelStagedFilesListTask() tasks.withType(Detekt::class.java).configureEach { detektTask -> if (project.hasProperty("precommit")) { diff --git a/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt index 5790faf9..5d3686ed 100644 --- a/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt +++ b/retrofit/src/jvmCommon/kotlin/si/inova/kotlinova/retrofit/callfactory/StaleWhileRevalidateCallAdapterFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 INOVA IT d.o.o. + * Copyright 2024 INOVA IT d.o.o. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, @@ -42,6 +42,7 @@ import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.net.HttpURLConnection import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException /** * Call adapter factory that handles Flow> return types. @@ -174,6 +175,8 @@ class StaleWhileRevalidateCallAdapterFactory( } send(result) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { send(Outcome.Error(e.transformRetrofitException(networkRequest.url.toString()), dataFromCache)) }