diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e58ee35c577df17c0ef1399553b0a2c4504ebb35..4cc239f93dca70175de051edbb1e6c5364011575 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,27 +2,28 @@ compose-multiplatform = "1.7.1" agp = "8.2.2" java = "17" -kotlin = "2.0.21" -ksp = "2.0.21-1.0.28" +kotlin = "2.1.0" +ksp = "2.1.0-1.0.29" -ktor = "3.0.2" -kotlinx-serialization = "1.7.3" +kotlinx-serialization = "1.8.0-RC" +ktor = "3.0.3" cache4k = "0.13.0" statelyConcurrentCollections = "2.1.0" slf4j = "2.0.16" kotlinx-io = "0.6.0" ktorfit = "2.2.0" -coroutines = "1.9.0" +coroutines = "1.10.1" datetime = "0.6.1" sandwich = "2.0.10" arrow = "1.2.4" # Main Koin version - for core, android deps -koin = "4.1.0-Beta1" -koinTest = "4.1.0-Beta1" +koin = "4.0.1" +koinTest = "4.0.1" gradleBuildConfigPlugin = "5.5.1" +akkurate = "0.11.0" # Koin version for Compose multiplatform -koinComposeMultiplatform = "4.1.0-Beta1" +koinComposeMultiplatform = "4.0.1" mosaic = "0.13.0" molecule = "2.0.0" adaptive = "1.0.1" @@ -116,6 +117,7 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fxCoroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } arrow-optics = { module = "io.arrow-kt:arrow-optics", version.ref = "arrow" } arrow-opticsKspPlugin = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" } +akkurate = { module = "dev.nesk.akkurate:akkurate-core", version.ref = "akkurate" } mosaic = { module = "com.jakewharton.mosaic:mosaic-runtime", version.ref = "mosaic" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" } diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts index 47ae2a134891e484b0d4396aad45149db501a0dc..62d36fb7861d2aed3a52e41b86aa2b1853b9b0ef 100644 --- a/temerity/build.gradle.kts +++ b/temerity/build.gradle.kts @@ -85,7 +85,7 @@ kotlin { implementation(libs.koin.core) implementation(libs.ktorfit.lib) implementation(libs.kotlinx.coroutines.core) - implementation(libs.kermit) + api(libs.kermit) // TODO: Re-add cache4k when updated for Kotlin 2.0.21 implementation(libs.statelyConcurrentCollections) implementation(libs.kmpIo) diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt index d3caf43b6b3599adffeae0e23684371df94ff89e..491f006cd23c7ba5e4e3b89a6dc90309ea65ef9b 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt @@ -33,11 +33,11 @@ import kotlin.time.Duration * @property serviceToken Specifies the authorization token to use for API calls * @property optDebugEnabled Specifies whether debug logging should be enabled * - * @property expectSuccess Specifies whether to expect successful responses by default - * @property useWebTimeout Specifies whether to use a timeout other than default for web requests - * @property webTimeout Specifies the timeout [Duration] to use when making web requests - * @property cacheTimeout Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields. - * @property threadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum + * @property optExpectSuccess Specifies whether to expect successful responses by default + * @property optUseWebTimeout Specifies whether to use a timeout other than default for web requests + * @property optWebTimeoutDuration Specifies the timeout [Duration] to use when making web requests + * @property optCacheTimeoutDuration Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields. + * @property optThreadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum */ @TemDsl public class TemClientConfig { @@ -46,37 +46,37 @@ public class TemClientConfig { public var serviceToken: String? = null public var optLoggingEnabled: Boolean = true public var optDebugEnabled: Boolean = false - public var supportKtxNotebook: Boolean = false + public var optSupportKtxNotebook: Boolean = false - public var expectSuccess: Boolean = true - public var useWebTimeout: Boolean = false + public var optExpectSuccess: Boolean = true + public var optUseWebTimeout: Boolean = false @Suppress("MemberVisibilityCanBePrivate") - public var webTimeout: Duration? = null + public var optWebTimeoutDuration: Duration? = null @Suppress("MemberVisibilityCanBePrivate") // Default cache timeout is set to 15 minutes if this is left unspecified; @see [edu.ucsc.its.temerity.core.Temerity] init block - public var cacheTimeout: Duration? = null + public var optCacheTimeoutDuration: Duration? = null - public var threadCount: Int? = null + public var optThreadCount: Int? = null /** * Configures the HttpClient with the provided block. * @param () -> HttpClient The block to configure the HttpClient. */ - internal var httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null - internal var httpClientBuilder: (() -> HttpClient)? = null - internal var httpClientLoggingBlock: (LoggingConfig.() -> Unit)? = null + internal var optHttpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null + internal var optHttpClientBuilder: (() -> HttpClient)? = null + internal var optHttpClientLoggingBlock: (LoggingConfig.() -> Unit)? = null public fun logging(block: LoggingConfig.() -> Unit) { - httpClientLoggingBlock = block + optHttpClientLoggingBlock = block } /** * Set custom HttpClient configuration for the default HttpClient. */ public fun httpClient(block: HttpClientConfig<*>.() -> Unit) { - httpClientConfigBlock = block + optHttpClientConfigBlock = block } /** @@ -87,13 +87,12 @@ public class TemClientConfig { engineFactory: HttpClientEngineFactory<T>, block: HttpClientConfig<T>.() -> Unit = {}, ) { - httpClientBuilder = { + optHttpClientBuilder = { HttpClient(engineFactory, block) } } public companion object { - internal fun withToken(serviceToken: String): TemClientConfig = TemClientConfig().apply { this.serviceToken = serviceToken } diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index 44a02ddbe90957cb7ec6027bdab48c11b81c56bd..ab73b7c140a8d20f60ad72aac32e60db4ae0759e 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -74,7 +74,6 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime import kotlinx.datetime.minus import kotlinx.serialization.SerializationException -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.koin.core.Koin import org.koin.core.KoinApplication @@ -185,6 +184,7 @@ public class Temerity internal constructor( internal val koinContext = createKoinApp(config) override fun getKoin(): Koin = koinContext.koin + private var libraryLogger: KermitLogger private var platformApi: PlatformApi private var cachedUserRoleList: ConcurrentMutableMap<Int, String> private var libraryCoroutineDispatcher: CoroutineDispatcher @@ -203,7 +203,7 @@ public class Temerity internal constructor( } libraryCoroutineDispatcher = get<CoroutineDispatcher>(named("libraryCoroutineDispatcher")) { - parametersOf(availableThreads(config.threadCount), "Temerity Library Dispatcher") + parametersOf(availableThreads(config.optThreadCount), "Temerity Library Dispatcher") } libraryCoroutineScope = get<CoroutineScope>(named("libraryCoroutineScope")) { parametersOf(libraryCoroutineDispatcher) @@ -213,6 +213,10 @@ public class Temerity internal constructor( webRequestDispatcher = get<CoroutineDispatcher>(named("childDispatcher")) { parametersOf(libraryCoroutineDispatcher, 2, "TemerityWebRequestCoroutineDispatcher") } fileProcessDispatcher = get<CoroutineDispatcher>(named("childDispatcher")) { parametersOf(libraryCoroutineDispatcher, 1, "TemerityFileProcessCoroutineDispatcher") } + libraryLogger = get<KermitLogger>(named("libraryLogger")) { + parametersOf(BuildConfig.PACKAGE_NAME, config) + } + platformApi = get<PlatformApi>(named("ktorfitApi")) { parametersOf(config, webRequestDispatcher) } @@ -620,7 +624,7 @@ internal fun buildHttpClient( } if (config.optDebugEnabled) { - when (config.httpClientLoggingBlock) { + when (config.optHttpClientLoggingBlock) { null -> { install(Logging) { this.logger = object : KtorLogger { @@ -635,18 +639,18 @@ internal fun buildHttpClient( } } else -> { - config.httpClientLoggingBlock?.let { + config.optHttpClientLoggingBlock?.let { Logging(it) } } } } - expectSuccess = config.expectSuccess + expectSuccess = config.optExpectSuccess - if (config.useWebTimeout) { + if (config.optUseWebTimeout) { val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(MILLISECONDS) - val configuredWebTimeout = config.webTimeout?.toLong(MILLISECONDS) + val configuredWebTimeout = config.optWebTimeoutDuration?.toLong(MILLISECONDS) install(HttpTimeout) { connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout requestTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout @@ -654,9 +658,9 @@ internal fun buildHttpClient( } } - config.httpClientConfigBlock?.invoke(this) + config.optHttpClientConfigBlock?.invoke(this) } - return config.httpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig) + return config.optHttpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig) } /** diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt index 76891c922dce04a93a95ce3ef1b2a17e629a85f4..d6fbe81f556afa013b7acc10362d538977c9b6a3 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt @@ -22,8 +22,8 @@ import de.jensklingenberg.ktorfit.ktorfit import edu.ucsc.its.temerity.TemClientConfig import edu.ucsc.its.temerity.api.PlatformApi import edu.ucsc.its.temerity.api.createPlatformApi +import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger import edu.ucsc.its.temerity.core.buildHttpClient -import edu.ucsc.its.temerity.core.createCommonLogger import edu.ucsc.its.temerity.extensions.coroutines.createLibraryScope import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine @@ -41,8 +41,8 @@ internal object LibModule { * It includes the platform module and provides factories for [HttpClient] and [PlatformApi]. */ internal fun libModule() = module { - single<KermitLogger>(named("libraryLogger")) { (tag: String?, config: TemClientConfig?) -> - createCommonLogger(tag = tag, config = config) + single<KermitLogger>(named("libraryLogger")) { (tag: String, config: TemClientConfig) -> + createLogger(tag, config) } single<CoroutineDispatcher>(named("libraryCoroutineDispatcher")) { (threadCount: Int, dispatcherName: String) -> Dispatchers.IO.limitedParallelism(threadCount, dispatcherName) diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt index be3c3b3850f6f14c1ea5ed6342c44ee1b90cb981..19a62a3a5572b022f7f77feddacdf38692e2b5a7 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt @@ -20,7 +20,9 @@ package edu.ucsc.its.temerity.test import edu.ucsc.its.temerity.core.Temerity import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.runBlocking import org.koin.test.KoinTest +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes class DevUtilityTests : @@ -41,17 +43,21 @@ class DevUtilityTests : configureDevEnvironment(dotenv) } } - timeout = 5.minutes.inWholeMilliseconds - test("Delete all Canvas Test Student Users") { - val testStudentList = testTemerity.getUsers().filter { - it.firstName.startsWith("test", ignoreCase = true) && - it.lastName.startsWith("student", ignoreCase = true) && - it.emailAddress.isEmpty() - } - testStudentList.forEach { - testTemerity.deleteUser(it.userId) + test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) { + runBlocking { + var deletedUsers = 0 + val testStudentList = testTemerity.getUsers().filter { + it.firstName.startsWith("test", ignoreCase = true) && + it.lastName.startsWith("student", ignoreCase = true) && + it.emailAddress.isEmpty() + } + testStudentList.forEach { + testTemerity.deleteUser(it.userId) + deletedUsers++ + } + kermit.d("Deleted $deletedUsers test student users.") } } } diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt index af9e10d6fe64095548c9e886d25a856d2a858e42..6e43ac6f5bd8bb39733c6c4f378454d9876b6c16 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt @@ -18,16 +18,24 @@ package edu.ucsc.its.temerity.test import edu.ucsc.its.temerity.core.Temerity +import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger +import io.kotest.common.ExperimentalKotest import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.runBlocking import org.koin.test.KoinTest +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes class ProdUtilityTests : FunSpec(), KoinTest { + private val kermit = createLogger("ProdUtilityTests") + init { coroutineDebugProbes = true + @OptIn(ExperimentalKotest::class) + blockingTest = true val dotenv = dotenvVaultJvm() @@ -41,14 +49,19 @@ class ProdUtilityTests : timeout = 5.minutes.inWholeMilliseconds - test("Delete all Canvas Test Student Users") { - val testStudentList = testTemerity.getUsers().filter { - it.firstName.startsWith("test", ignoreCase = true) && - it.lastName.startsWith("student", ignoreCase = true) && - it.emailAddress.isEmpty() - } - testStudentList.forEach { - testTemerity.deleteUser(it.userId) + test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) { + runBlocking { + var deletedUsers = 0 + val testStudentList = testTemerity.getUsers().filter { + it.firstName.startsWith("test", ignoreCase = true) && + it.lastName.startsWith("student", ignoreCase = true) && + it.emailAddress.isEmpty() + } + testStudentList.forEach { + testTemerity.deleteUser(it.userId) + deletedUsers++ + } + kermit.d("Deleted $deletedUsers test student users.") } } }