From 0b701a3e9998fffbebe336712bee8389f8c29138 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 31 Oct 2025 11:21:06 +0600 Subject: [PATCH 1/6] start 0.26.7 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9f83d306a..0a492047111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.26.7 + ## 0.26.6 * `Versions`: diff --git a/gradle.properties b/gradle.properties index ca645dc0da3..2fb41aa903c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,5 +18,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.26.6 -android_code_version=305 +version=0.26.7 +android_code_version=306 From fce47897d5e2056633aac7eb8ce117726b4b69e6 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 31 Oct 2025 19:34:09 +0600 Subject: [PATCH 2/6] add getCurrentLocale and compose translation --- .../language_codes/CurrentIetfLang.kt | 3 +++ .../src/jsMain/kotlin/CurrentIetfLang.js.kt | 6 +++++ .../src/jvmMain/kotlin/CurrentIetfLang.jvm.kt | 6 +++++ .../kotlin/CurrentIetfLang.mingwX64.kt | 27 +++++++++++++++++++ .../kotlin/CurrentIetfLang.native.kt | 12 +++++++++ .../kotlin/CurrentIetfLang.wasmJs.kt | 10 +++++++ resources/compose/build.gradle | 20 ++++++++++++++ .../kotlin/GetCurrentLocale.android.kt | 24 +++++++++++++++++ .../src/commonMain/kotlin/GetCurrentLocale.kt | 7 +++++ .../kotlin/StringComposeTranslation.kt | 10 +++++++ .../src/jsMain/kotlin/GetCurrentLocale.js.kt | 9 +++++++ .../jvmMain/kotlin/GetCurrentLocale.jvm.kt | 10 +++++++ .../kotlin/GetCurrentLocale.native.kt | 9 +++++++ .../kotlin/GetCurrentLocale.wasmJs.kt | 9 +++++++ settings.gradle | 1 + 15 files changed, 163 insertions(+) create mode 100644 language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt create mode 100644 language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt create mode 100644 language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt create mode 100644 language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt create mode 100644 language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt create mode 100644 language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt create mode 100644 resources/compose/build.gradle create mode 100644 resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt create mode 100644 resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt create mode 100644 resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt create mode 100644 resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt create mode 100644 resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt create mode 100644 resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt create mode 100644 resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt diff --git a/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt b/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt new file mode 100644 index 00000000000..33aac6f6a69 --- /dev/null +++ b/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt @@ -0,0 +1,3 @@ +package dev.inmo.micro_utils.language_codes + +expect val currentIetfLang: IetfLang? diff --git a/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt b/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt new file mode 100644 index 00000000000..3fe613c6caf --- /dev/null +++ b/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.language_codes + +import kotlinx.browser.window + +actual val currentIetfLang: IetfLang? + get() = window.navigator.language.unsafeCast() ?.let { IetfLang(it) } \ No newline at end of file diff --git a/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt b/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt new file mode 100644 index 00000000000..442391c4343 --- /dev/null +++ b/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.language_codes + +import java.util.Locale + +actual val currentIetfLang: IetfLang? + get() = Locale.getDefault() ?.toIetfLang() \ No newline at end of file diff --git a/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt b/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt new file mode 100644 index 00000000000..a8da465e759 --- /dev/null +++ b/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt @@ -0,0 +1,27 @@ +package dev.inmo.micro_utils.language_codes + +import dev.inmo.micro_utils.language_codes.IetfLang +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import platform.posix.getenv +import platform.windows.GetUserDefaultLocaleName +import platform.windows.LOCALE_NAME_MAX_LENGTH +import platform.windows.WCHARVar + +@OptIn(ExperimentalForeignApi::class) +actual val currentIetfLang: IetfLang? + get() { + val rawLocale = memScoped { + val buffer = allocArray(LOCALE_NAME_MAX_LENGTH) + val result = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH) + + if (result > 0) { + // Convert WCHAR* to String + buffer.toKString() + } + "en-US" // fallback + } + return IetfLang(rawLocale) + } \ No newline at end of file diff --git a/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt b/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt new file mode 100644 index 00000000000..4a4bbd14553 --- /dev/null +++ b/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt @@ -0,0 +1,12 @@ +package dev.inmo.micro_utils.language_codes + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.getenv + +@OptIn(ExperimentalForeignApi::class) +actual val currentIetfLang: IetfLang? + get() { + val localeStr = getenv("LANG") ?.toKString() ?.replace("_", "-") ?: "en-US" + return IetfLang(localeStr) + } \ No newline at end of file diff --git a/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt b/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt new file mode 100644 index 00000000000..76eac9b9d2a --- /dev/null +++ b/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt @@ -0,0 +1,10 @@ +package dev.inmo.micro_utils.language_codes + +external interface Navigator { + val language: String +} + +external val navigator: Navigator + +actual val currentIetfLang: IetfLang? + get() = IetfLang(navigator.language) \ No newline at end of file diff --git a/resources/compose/build.gradle b/resources/compose/build.gradle new file mode 100644 index 00000000000..b0db99d9ade --- /dev/null +++ b/resources/compose/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" + alias(libs.plugins.jb.compose) + alias(libs.plugins.kt.jb.compose) +} + +apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" + +kotlin { + sourceSets { + commonMain { + dependencies { + api libs.kt.coroutines + api project(":micro_utils.resources") + } + } + } +} diff --git a/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt b/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt new file mode 100644 index 00000000000..7de2304b005 --- /dev/null +++ b/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt @@ -0,0 +1,24 @@ +package dev.inmo.micro_utils.resources.compose + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import dev.inmo.micro_utils.language_codes.IetfLang +import dev.inmo.micro_utils.language_codes.toIetfLang + +@Composable +actual fun getCurrentLocale(): IetfLang? { + val configuration = LocalConfiguration.current + + val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (configuration.locales.isEmpty) { + return null + } + configuration.locales.get(0) + } else { + @Suppress("DEPRECATION") + configuration.locale + } + + return locale.toIetfLang() +} \ No newline at end of file diff --git a/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt b/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt new file mode 100644 index 00000000000..1c6d1109bb1 --- /dev/null +++ b/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt @@ -0,0 +1,7 @@ +package dev.inmo.micro_utils.resources.compose + +import androidx.compose.runtime.Composable +import dev.inmo.micro_utils.language_codes.IetfLang + +@Composable +expect fun getCurrentLocale(): IetfLang? diff --git a/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt b/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt new file mode 100644 index 00000000000..c248767a8bd --- /dev/null +++ b/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt @@ -0,0 +1,10 @@ +package dev.inmo.micro_utils.resources.compose + +import androidx.compose.runtime.Composable +import dev.inmo.micro_utils.strings.StringResource + +@Suppress("unused") +@Composable +fun StringResource.composeTranslation(): String { + return translation(getCurrentLocale()) +} diff --git a/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt b/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt new file mode 100644 index 00000000000..736ea39582b --- /dev/null +++ b/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt @@ -0,0 +1,9 @@ +package dev.inmo.micro_utils.resources.compose + +import dev.inmo.micro_utils.language_codes.IetfLang +import dev.inmo.micro_utils.language_codes.currentIetfLang + +@androidx.compose.runtime.Composable +actual fun getCurrentLocale(): IetfLang? { + return currentIetfLang +} \ No newline at end of file diff --git a/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt b/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt new file mode 100644 index 00000000000..f17a2468f5d --- /dev/null +++ b/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt @@ -0,0 +1,10 @@ +package dev.inmo.micro_utils.resources.compose + +import androidx.compose.ui.text.intl.Locale +import dev.inmo.micro_utils.language_codes.IetfLang +import dev.inmo.micro_utils.language_codes.currentIetfLang + +@androidx.compose.runtime.Composable +actual fun getCurrentLocale(): IetfLang? { + return currentIetfLang +} \ No newline at end of file diff --git a/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt b/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt new file mode 100644 index 00000000000..736ea39582b --- /dev/null +++ b/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt @@ -0,0 +1,9 @@ +package dev.inmo.micro_utils.resources.compose + +import dev.inmo.micro_utils.language_codes.IetfLang +import dev.inmo.micro_utils.language_codes.currentIetfLang + +@androidx.compose.runtime.Composable +actual fun getCurrentLocale(): IetfLang? { + return currentIetfLang +} \ No newline at end of file diff --git a/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt b/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt new file mode 100644 index 00000000000..736ea39582b --- /dev/null +++ b/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt @@ -0,0 +1,9 @@ +package dev.inmo.micro_utils.resources.compose + +import dev.inmo.micro_utils.language_codes.IetfLang +import dev.inmo.micro_utils.language_codes.currentIetfLang + +@androidx.compose.runtime.Composable +actual fun getCurrentLocale(): IetfLang? { + return currentIetfLang +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 99d4168788a..d1f109f6560 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,7 @@ String[] includes = [ ":colors:common", ":resources", + ":resources:compose", ":fsm:common", ":fsm:repos:common", From b152986b4e5aaab68403f75ab69a558be5347efa Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 5 Nov 2025 12:27:24 +0600 Subject: [PATCH 3/6] improve SmartKeyRWLockerTests and fix SmartSemaphore --- .../micro_utils/coroutines/SmartSemaphore.kt | 2 +- .../kotlin/SmartKeyRWLockerTests.kt | 596 ++++++++++++++---- 2 files changed, 466 insertions(+), 132 deletions(-) diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt index c557037a3b5..eb06cafd9c7 100644 --- a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/SmartSemaphore.kt @@ -72,7 +72,7 @@ sealed interface SmartSemaphore { acquiredPermits != checkedPermits } if (shouldContinue) { - waitRelease() + waitRelease(checkedPermits - acquiredPermits) } } while (shouldContinue && currentCoroutineContext().isActive) } catch (e: Throwable) { diff --git a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt index 70038c189b4..a0906812b28 100644 --- a/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt +++ b/coroutines/src/commonTest/kotlin/SmartKeyRWLockerTests.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class SmartKeyRWLockerTests { - @Test - fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite() + private lateinit var locker: SmartKeyRWLocker - assertTrue { locker.isWriteLocked() } - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - } - assertFalse { locker.isWriteLocked(testKey) } - - locker.unlockWrite() - assertFalse { locker.isWriteLocked() } - - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - assertTrue { locker.isWriteLocked(testKey) } - assertTrue { locker.unlockWrite(testKey) } - assertFalse { locker.isWriteLocked(testKey) } + @BeforeTest + fun setup() { + locker = SmartKeyRWLocker() } + + // ==================== Global Read Tests ==================== + @Test - fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" + fun testGlobalReadAllowsMultipleConcurrentReads() = runTest { + val results = mutableListOf() + locker.acquireRead() - assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) + val jobs = List(5) { + launch { + locker.acquireRead() + delay(100.milliseconds) + results.add(true) + locker.releaseRead() } } - assertFalse { locker.isWriteLocked(testKey) } + + jobs.joinAll() + locker.releaseRead() + + assertEquals(5, results.size) + } + + @Test + fun testGlobalReadBlocksGlobalWrite() = runTest { + locker.acquireRead() + + var writeAcquired = false + val writeJob = launch { + locker.lockWrite() + writeAcquired = true + locker.unlockWrite() + } + + delay(200.milliseconds) + assertFalse(writeAcquired, "Write should be blocked by global read") locker.releaseRead() - assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) + writeJob.join() - realWithTimeout(1.seconds) { - locker.lockWrite(testKey) - } - assertTrue { locker.isWriteLocked(testKey) } - assertTrue { locker.unlockWrite(testKey) } - assertFalse { locker.isWriteLocked(testKey) } + assertTrue(writeAcquired, "Write should succeed after read released") } + @Test - fun readLockFailedOnWriteLockKeyTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite(testKey) + fun testGlobalReadBlocksAllKeyWrites() = runTest { + locker.acquireRead() - assertTrue { locker.isWriteLocked(testKey) } + val writeFlags = mutableMapOf() + val keys = listOf("key1", "key2", "key3") - assertFails { - realWithTimeout(1.seconds) { + val jobs = keys.map { key -> + launch { + locker.lockWrite(key) + writeFlags[key] = true + locker.unlockWrite(key) + } + } + + delay(200.milliseconds) + assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active") + + locker.releaseRead() + jobs.joinAll() + + assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released") + } + + // ==================== Global Write Tests ==================== + + @Test + fun testGlobalWriteBlocksAllOperations() = runTest { + locker.lockWrite() + + var globalReadAcquired = false + var keyReadAcquired = false + var keyWriteAcquired = false + + val jobs = listOf( + launch { locker.acquireRead() + globalReadAcquired = true + locker.releaseRead() + }, + launch { + locker.acquireRead("key1") + keyReadAcquired = true + locker.releaseRead("key1") + }, + launch { + locker.lockWrite("key2") + keyWriteAcquired = true + locker.unlockWrite("key2") } - } - assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) + ) - locker.unlockWrite(testKey) - assertFalse { locker.isWriteLocked(testKey) } + delay(200.milliseconds) + assertFalse(globalReadAcquired, "Global read should be blocked") + assertFalse(keyReadAcquired, "Key read should be blocked") + assertFalse(keyWriteAcquired, "Key write should be blocked") - realWithTimeout(1.seconds) { - locker.acquireRead() - } - assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) - assertTrue { locker.releaseRead() } - assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits) + locker.unlockWrite() + jobs.joinAll() + + assertTrue(globalReadAcquired) + assertTrue(keyReadAcquired) + assertTrue(keyWriteAcquired) } + @Test - fun writeLockFailedOnWriteLockKeyTest() = runTest { - val locker = SmartKeyRWLocker() - val testKey = "test" - locker.lockWrite(testKey) + fun testGlobalWriteIsExclusive() = runTest { + locker.lockWrite() - assertTrue { locker.isWriteLocked(testKey) } - - assertFails { - realWithTimeout(1.seconds) { - locker.lockWrite() - } - } - assertFalse(locker.isWriteLocked()) - - locker.unlockWrite(testKey) - assertFalse { locker.isWriteLocked(testKey) } - - realWithTimeout(1.seconds) { + var secondWriteAcquired = false + val job = launch { locker.lockWrite() + secondWriteAcquired = true + locker.unlockWrite() } - assertTrue(locker.isWriteLocked()) - assertTrue { locker.unlockWrite() } - assertFalse(locker.isWriteLocked()) + + delay(200.milliseconds) + assertFalse(secondWriteAcquired, "Second global write should be blocked") + + locker.unlockWrite() + job.join() + + assertTrue(secondWriteAcquired) } + + // ==================== Key Read Tests ==================== + @Test - fun readsBlockingGlobalWrite() = runTest { - val locker = SmartKeyRWLocker() + fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest { + val key = "testKey" + val results = mutableListOf() - val testKeys = (0 until 100).map { "test$it" } + locker.acquireRead(key) - for (i in testKeys.indices) { - val it = testKeys[i] - locker.acquireRead(it) - val previous = testKeys.take(i) - val next = testKeys.drop(i + 1) - - previous.forEach { - assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 } - } - next.forEach { - assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null } + val jobs = List(5) { + launch { + locker.acquireRead(key) + delay(50.milliseconds) + results.add(true) + locker.releaseRead(key) } } - for (i in testKeys.indices) { - val it = testKeys[i] - assertFails { - realWithTimeout(13.milliseconds) { locker.lockWrite() } + jobs.joinAll() + locker.releaseRead(key) + + assertEquals(5, results.size) + } + + @Test + fun testKeyReadAllowsReadsForDifferentKeys() = runTest { + val results = mutableMapOf() + + locker.acquireRead("key1") + + val jobs = listOf("key2", "key3", "key4").map { key -> + launch { + locker.acquireRead(key) + delay(50.milliseconds) + results[key] = true + locker.releaseRead(key) } - val readPermitsBeforeLock = locker.readSemaphore().freePermits - realWithTimeout(1.seconds) { locker.acquireRead() } + } + + jobs.joinAll() + locker.releaseRead("key1") + + assertEquals(3, results.size) + } + + @Test + fun testKeyReadBlocksWriteForSameKey() = runTest { + val key = "testKey" + locker.acquireRead(key) + + var writeAcquired = false + val job = launch { + locker.lockWrite(key) + writeAcquired = true + locker.unlockWrite(key) + } + + delay(200.milliseconds) + assertFalse(writeAcquired, "Write for same key should be blocked") + + locker.releaseRead(key) + job.join() + + assertTrue(writeAcquired) + } + + @Test + fun testKeyReadBlocksGlobalWrite() = runTest { + locker.acquireRead("key1") + + var globalWriteAcquired = false + val job = launch { + locker.lockWrite() + globalWriteAcquired = true + locker.unlockWrite() + } + + delay(200.milliseconds) + assertFalse(globalWriteAcquired, "Global write should be blocked by key read") + + locker.releaseRead("key1") + job.join() + + assertTrue(globalWriteAcquired) + } + + @Test + fun testKeyReadAllowsWriteForDifferentKey() = runTest { + locker.acquireRead("key1") + + var writeAcquired = false + val job = launch { + locker.lockWrite("key2") + writeAcquired = true + locker.unlockWrite("key2") + } + + job.join() + assertTrue(writeAcquired, "Write for different key should succeed") + + locker.releaseRead("key1") + } + + // ==================== Key Write Tests ==================== + + @Test + fun testKeyWriteBlocksReadForSameKey() = runTest { + val key = "testKey" + locker.lockWrite(key) + + var readAcquired = false + val job = launch { + locker.acquireRead(key) + readAcquired = true + locker.releaseRead(key) + } + + delay(200.milliseconds) + assertFalse(readAcquired, "Read for same key should be blocked") + + locker.unlockWrite(key) + job.join() + + assertTrue(readAcquired) + } + + @Test + fun testKeyWriteBlocksGlobalRead() = runTest { + locker.lockWrite("key1") + + var globalReadAcquired = false + val job = launch { + locker.acquireRead() + globalReadAcquired = true locker.releaseRead() - assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) - - locker.releaseRead(it) } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } - realWithTimeout(1.seconds) { locker.lockWrite() } - assertFails { - realWithTimeout(13.milliseconds) { locker.acquireRead() } - } - assertTrue { locker.unlockWrite() } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + delay(200.milliseconds) + assertFalse(globalReadAcquired, "Global read should be blocked by key write") + + locker.unlockWrite("key1") + job.join() + + assertTrue(globalReadAcquired) } + @Test - fun writesBlockingGlobalWrite() = runTest { - val locker = SmartKeyRWLocker() + fun testKeyWriteIsExclusiveForSameKey() = runTest { + val key = "testKey" + locker.lockWrite(key) - val testKeys = (0 until 100).map { "test$it" } + var secondWriteAcquired = false + val job = launch { + locker.lockWrite(key) + secondWriteAcquired = true + locker.unlockWrite(key) + } - for (i in testKeys.indices) { - val it = testKeys[i] - locker.lockWrite(it) - val previous = testKeys.take(i) - val next = testKeys.drop(i + 1) + delay(200.milliseconds) + assertFalse(secondWriteAcquired, "Second write for same key should be blocked") - previous.forEach { - assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } + locker.unlockWrite(key) + job.join() + + assertTrue(secondWriteAcquired) + } + + @Test + fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest { + locker.lockWrite("key1") + + val results = mutableMapOf() + + val jobs = listOf( + launch { + locker.acquireRead("key2") + results["read-key2"] = true + locker.releaseRead("key2") + }, + launch { + locker.lockWrite("key3") + results["write-key3"] = true + locker.unlockWrite("key3") } - next.forEach { - assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true } + ) + + jobs.joinAll() + assertEquals(2, results.size, "Operations on different keys should succeed") + + locker.unlockWrite("key1") + } + + // ==================== Complex Scenarios ==================== + + @Test + fun testMultipleReadersThenWriter() = runTest { + val key = "testKey" + val readCount = 5 + val readers = mutableListOf() + + repeat(readCount) { + readers.add(launch { + locker.acquireRead(key) + delay(100.milliseconds) + locker.releaseRead(key) + }) + } + + delay(50.milliseconds) // Let readers acquire + + var writerExecuted = false + val writer = launch { + locker.lockWrite(key) + writerExecuted = true + locker.unlockWrite(key) + } + + delay(50.milliseconds) + assertFalse(writerExecuted, "Writer should wait for all readers") + + readers.joinAll() + writer.join() + + assertTrue(writerExecuted, "Writer should execute after all readers done") + } + + @Test + fun testWriterThenMultipleReaders() = runTest { + val key = "testKey" + + locker.lockWrite(key) + + val readerFlags = mutableListOf() + val readers = List(5) { + launch { + locker.acquireRead(key) + readerFlags.add(true) + locker.releaseRead(key) } } - for (i in testKeys.indices) { - val it = testKeys[i] - assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } } + delay(200.milliseconds) + assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer") - val readPermitsBeforeLock = locker.readSemaphore().freePermits - assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } - assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits) + locker.unlockWrite(key) + readers.joinAll() - locker.unlockWrite(it) + assertEquals(5, readerFlags.size, "All readers should succeed after writer") + } + + @Test + fun testCascadingLocksWithDifferentKeys() = runTest { + val executed = mutableMapOf() + + launch { + locker.lockWrite("key1") + executed["write-key1-start"] = true + delay(100.milliseconds) + locker.unlockWrite("key1") + executed["write-key1-end"] = true } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } - realWithTimeout(1.seconds) { locker.lockWrite() } - assertFails { - realWithTimeout(13.milliseconds) { locker.acquireRead() } + delay(50.milliseconds) + + launch { + locker.acquireRead("key2") + executed["read-key2"] = true + delay(100.milliseconds) + locker.releaseRead("key2") } - assertTrue { locker.unlockWrite() } - assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } + + delay(200.milliseconds) + + assertTrue(executed["write-key1-start"] == true) + assertTrue(executed["read-key2"] == true) + assertTrue(executed["write-key1-end"] == true) + } + + @Test + fun testReleaseWithoutAcquireReturnsFalse() = runTest { + assertFalse(locker.releaseRead(), "Release without acquire should return false") + assertFalse(locker.releaseRead("key1"), "Release without acquire should return false") + } + + @Test + fun testUnlockWithoutLockReturnsFalse() = runTest { + assertFalse(locker.unlockWrite(), "Unlock without lock should return false") + assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false") + } + + @Test + fun testProperReleaseReturnsTrue() = runTest { + locker.acquireRead() + assertTrue(locker.releaseRead(), "Release after acquire should return true") + + locker.acquireRead("key1") + assertTrue(locker.releaseRead("key1"), "Release after acquire should return true") + } + + @Test + fun testProperUnlockReturnsTrue() = runTest { + locker.lockWrite() + assertTrue(locker.unlockWrite(), "Unlock after lock should return true") + + locker.lockWrite("key1") + assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true") + } + + // ==================== Stress Tests ==================== + + @Test + fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) { + val operations = 100 + val keys = listOf("key1", "key2", "key3", "key4", "key5") + val jobs = mutableListOf() + + repeat(operations) { i -> + val key = keys[i % keys.size] + + when (i % 4) { + 0 -> jobs.add(launch { + locker.acquireRead(key) + delay(10.milliseconds) + locker.releaseRead(key) + }) + 1 -> jobs.add(launch { + locker.lockWrite(key) + delay(10.milliseconds) + locker.unlockWrite(key) + }) + 2 -> jobs.add(launch { + locker.acquireRead() + delay(10.milliseconds) + locker.releaseRead() + }) + 3 -> jobs.add(launch { + locker.lockWrite() + delay(10.milliseconds) + locker.unlockWrite() + }) + } + } + + jobs.joinAll() + // If we reach here without deadlock or exceptions, test passes + } + + @Test + fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) { + val key = "testKey" + var writerExecuted = false + + // Start continuous readers + val readers = List(10) { + launch { + repeat(5) { + locker.acquireRead(key) + delay(50.milliseconds) + locker.releaseRead(key) + delay(10.milliseconds) + } + } + } + + delay(100.milliseconds) + + // Try to acquire write lock + val writer = launch { + locker.lockWrite(key) + writerExecuted = true + locker.unlockWrite(key) + } + + readers.joinAll() + writer.join() + + assertTrue(writerExecuted, "Writer should eventually execute") } } From cb56bf9793950b6c7f49d861f23fa4e9308c3c70 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 5 Nov 2025 12:29:33 +0600 Subject: [PATCH 4/6] Revert "add getCurrentLocale and compose translation" This reverts commit fce47897d5e2056633aac7eb8ce117726b4b69e6. --- .../language_codes/CurrentIetfLang.kt | 3 --- .../src/jsMain/kotlin/CurrentIetfLang.js.kt | 6 ----- .../src/jvmMain/kotlin/CurrentIetfLang.jvm.kt | 6 ----- .../kotlin/CurrentIetfLang.mingwX64.kt | 27 ------------------- .../kotlin/CurrentIetfLang.native.kt | 12 --------- .../kotlin/CurrentIetfLang.wasmJs.kt | 10 ------- resources/compose/build.gradle | 20 -------------- .../kotlin/GetCurrentLocale.android.kt | 24 ----------------- .../src/commonMain/kotlin/GetCurrentLocale.kt | 7 ----- .../kotlin/StringComposeTranslation.kt | 10 ------- .../src/jsMain/kotlin/GetCurrentLocale.js.kt | 9 ------- .../jvmMain/kotlin/GetCurrentLocale.jvm.kt | 10 ------- .../kotlin/GetCurrentLocale.native.kt | 9 ------- .../kotlin/GetCurrentLocale.wasmJs.kt | 9 ------- settings.gradle | 1 - 15 files changed, 163 deletions(-) delete mode 100644 language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt delete mode 100644 language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt delete mode 100644 language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt delete mode 100644 language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt delete mode 100644 language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt delete mode 100644 language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt delete mode 100644 resources/compose/build.gradle delete mode 100644 resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt delete mode 100644 resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt delete mode 100644 resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt delete mode 100644 resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt delete mode 100644 resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt delete mode 100644 resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt delete mode 100644 resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt diff --git a/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt b/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt deleted file mode 100644 index 33aac6f6a69..00000000000 --- a/language_codes/src/commonMain/kotlin/dev/inmo/micro_utils/language_codes/CurrentIetfLang.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -expect val currentIetfLang: IetfLang? diff --git a/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt b/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt deleted file mode 100644 index 3fe613c6caf..00000000000 --- a/language_codes/src/jsMain/kotlin/CurrentIetfLang.js.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -import kotlinx.browser.window - -actual val currentIetfLang: IetfLang? - get() = window.navigator.language.unsafeCast() ?.let { IetfLang(it) } \ No newline at end of file diff --git a/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt b/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt deleted file mode 100644 index 442391c4343..00000000000 --- a/language_codes/src/jvmMain/kotlin/CurrentIetfLang.jvm.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -import java.util.Locale - -actual val currentIetfLang: IetfLang? - get() = Locale.getDefault() ?.toIetfLang() \ No newline at end of file diff --git a/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt b/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt deleted file mode 100644 index a8da465e759..00000000000 --- a/language_codes/src/mingwX64Main/kotlin/CurrentIetfLang.mingwX64.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -import dev.inmo.micro_utils.language_codes.IetfLang -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.allocArray -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.toKString -import platform.posix.getenv -import platform.windows.GetUserDefaultLocaleName -import platform.windows.LOCALE_NAME_MAX_LENGTH -import platform.windows.WCHARVar - -@OptIn(ExperimentalForeignApi::class) -actual val currentIetfLang: IetfLang? - get() { - val rawLocale = memScoped { - val buffer = allocArray(LOCALE_NAME_MAX_LENGTH) - val result = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH) - - if (result > 0) { - // Convert WCHAR* to String - buffer.toKString() - } - "en-US" // fallback - } - return IetfLang(rawLocale) - } \ No newline at end of file diff --git a/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt b/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt deleted file mode 100644 index 4a4bbd14553..00000000000 --- a/language_codes/src/nativeMain/kotlin/CurrentIetfLang.native.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.toKString -import platform.posix.getenv - -@OptIn(ExperimentalForeignApi::class) -actual val currentIetfLang: IetfLang? - get() { - val localeStr = getenv("LANG") ?.toKString() ?.replace("_", "-") ?: "en-US" - return IetfLang(localeStr) - } \ No newline at end of file diff --git a/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt b/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt deleted file mode 100644 index 76eac9b9d2a..00000000000 --- a/language_codes/src/wasmJsMain/kotlin/CurrentIetfLang.wasmJs.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.inmo.micro_utils.language_codes - -external interface Navigator { - val language: String -} - -external val navigator: Navigator - -actual val currentIetfLang: IetfLang? - get() = IetfLang(navigator.language) \ No newline at end of file diff --git a/resources/compose/build.gradle b/resources/compose/build.gradle deleted file mode 100644 index b0db99d9ade..00000000000 --- a/resources/compose/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" - id "com.android.library" - alias(libs.plugins.jb.compose) - alias(libs.plugins.kt.jb.compose) -} - -apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" - -kotlin { - sourceSets { - commonMain { - dependencies { - api libs.kt.coroutines - api project(":micro_utils.resources") - } - } - } -} diff --git a/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt b/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt deleted file mode 100644 index 7de2304b005..00000000000 --- a/resources/compose/src/androidMain/kotlin/GetCurrentLocale.android.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import android.os.Build -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalConfiguration -import dev.inmo.micro_utils.language_codes.IetfLang -import dev.inmo.micro_utils.language_codes.toIetfLang - -@Composable -actual fun getCurrentLocale(): IetfLang? { - val configuration = LocalConfiguration.current - - val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (configuration.locales.isEmpty) { - return null - } - configuration.locales.get(0) - } else { - @Suppress("DEPRECATION") - configuration.locale - } - - return locale.toIetfLang() -} \ No newline at end of file diff --git a/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt b/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt deleted file mode 100644 index 1c6d1109bb1..00000000000 --- a/resources/compose/src/commonMain/kotlin/GetCurrentLocale.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import androidx.compose.runtime.Composable -import dev.inmo.micro_utils.language_codes.IetfLang - -@Composable -expect fun getCurrentLocale(): IetfLang? diff --git a/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt b/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt deleted file mode 100644 index c248767a8bd..00000000000 --- a/resources/compose/src/commonMain/kotlin/StringComposeTranslation.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import androidx.compose.runtime.Composable -import dev.inmo.micro_utils.strings.StringResource - -@Suppress("unused") -@Composable -fun StringResource.composeTranslation(): String { - return translation(getCurrentLocale()) -} diff --git a/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt b/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt deleted file mode 100644 index 736ea39582b..00000000000 --- a/resources/compose/src/jsMain/kotlin/GetCurrentLocale.js.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import dev.inmo.micro_utils.language_codes.IetfLang -import dev.inmo.micro_utils.language_codes.currentIetfLang - -@androidx.compose.runtime.Composable -actual fun getCurrentLocale(): IetfLang? { - return currentIetfLang -} \ No newline at end of file diff --git a/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt b/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt deleted file mode 100644 index f17a2468f5d..00000000000 --- a/resources/compose/src/jvmMain/kotlin/GetCurrentLocale.jvm.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import androidx.compose.ui.text.intl.Locale -import dev.inmo.micro_utils.language_codes.IetfLang -import dev.inmo.micro_utils.language_codes.currentIetfLang - -@androidx.compose.runtime.Composable -actual fun getCurrentLocale(): IetfLang? { - return currentIetfLang -} \ No newline at end of file diff --git a/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt b/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt deleted file mode 100644 index 736ea39582b..00000000000 --- a/resources/compose/src/nativeMain/kotlin/GetCurrentLocale.native.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import dev.inmo.micro_utils.language_codes.IetfLang -import dev.inmo.micro_utils.language_codes.currentIetfLang - -@androidx.compose.runtime.Composable -actual fun getCurrentLocale(): IetfLang? { - return currentIetfLang -} \ No newline at end of file diff --git a/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt b/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt deleted file mode 100644 index 736ea39582b..00000000000 --- a/resources/compose/src/wasmJsMain/kotlin/GetCurrentLocale.wasmJs.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.micro_utils.resources.compose - -import dev.inmo.micro_utils.language_codes.IetfLang -import dev.inmo.micro_utils.language_codes.currentIetfLang - -@androidx.compose.runtime.Composable -actual fun getCurrentLocale(): IetfLang? { - return currentIetfLang -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d1f109f6560..99d4168788a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,7 +50,6 @@ String[] includes = [ ":colors:common", ":resources", - ":resources:compose", ":fsm:common", ":fsm:repos:common", From 078aedfb6813fbb14c44a44f3d2b91051e06c47d Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 5 Nov 2025 13:42:27 +0600 Subject: [PATCH 5/6] update dependencies --- android/pickers/build.gradle | 1 + gradle.properties | 4 +-- gradle/libs.versions.toml | 15 +++++---- koin/generator/src/main/kotlin/Processor.kt | 32 ++++++++++++++++--- .../generator/src/main/kotlin/Processor.kt | 15 +++++++-- .../main/kotlin/NoSuchElementWorkaround.kt | 12 +++++++ .../generator/src/main/kotlin/Processor.kt | 17 ++++++++-- .../generator/src/main/kotlin/Processor.kt | 5 ++- 8 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt diff --git a/android/pickers/build.gradle b/android/pickers/build.gradle index 17c6ce042b4..faacb584ba8 100644 --- a/android/pickers/build.gradle +++ b/android/pickers/build.gradle @@ -13,6 +13,7 @@ kotlin { androidMain { dependencies { api project(":micro_utils.android.smalltextfield") + api libs.jb.compose.icons } } } diff --git a/gradle.properties b/gradle.properties index 2fb41aa903c..ad67b9865ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,8 +8,8 @@ android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g -# https://github.com/google/ksp/issues/2491 -ksp.useKSP2=false +## https://github.com/google/ksp/issues/2491 +#ksp.useKSP2=false # JS NPM diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41e75db3ba2..cfe1d0ac472 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kt = "2.2.20" +kt = "2.2.21" kt-serialization = "1.9.0" kt-coroutines = "1.10.2" @@ -8,11 +8,13 @@ kotlinx-browser = "0.5.0" kslog = "1.5.1" -jb-compose = "1.8.2" +jb-compose = "1.9.2" +jb-compose-material3 = "1.9.0" +jb-compose-icons = "1.7.8" jb-exposed = "0.61.0" jb-dokka = "2.1.0" -# 3.50.3.0 contains bug https://github.com/InsanusMokrassar/MicroUtils/actions/runs/18138301958/job/51629588088 +# 3.51.0.0 contains bug, checking with ./gradlew :micro_utils.repos.exposed:jvmTest sqlite = "3.50.1.0" korlibs = "5.4.0" @@ -26,11 +28,11 @@ koin = "4.1.1" okio = "3.16.2" -ksp = "2.2.20-2.0.3" +ksp = "2.3.1" kotlin-poet = "2.2.0" versions = "0.52.0" -nmcp = "1.1.0" +nmcp = "1.2.0" android-gradle = "8.10.+" dexcount = "4.0.0" @@ -89,7 +91,8 @@ jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb- jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" } sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } -jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose" } +jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "jb-compose-material3" } +jb-compose-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "jb-compose-icons" } android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" } android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } diff --git a/koin/generator/src/main/kotlin/Processor.kt b/koin/generator/src/main/kotlin/Processor.kt index 6933cafb563..a0496fd743a 100644 --- a/koin/generator/src/main/kotlin/Processor.kt +++ b/koin/generator/src/main/kotlin/Processor.kt @@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo import dev.inmo.micro_ksp.generator.safeClassName +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import org.koin.core.Koin @@ -240,9 +241,14 @@ class Processor( ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { val type = safeClassName { it.type } val targetType = runCatching { - type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type)) + type.parameterizedBy( + *withNoSuchElementWorkaround(emptyArray()) { + it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type + } + ) }.getOrElse { e -> when (e) { + is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type is KSTypeNotPresentException -> e.ksType.toClassName() } if (e is KSTypesNotPresentException) { @@ -251,14 +257,32 @@ class Processor( throw e } }.copy( - nullable = it.nullable + nullable = withNoSuchElementWorkaround(true) { it.nullable } ) - addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) + addCodeForType( + targetType, + it.name, + withNoSuchElementWorkaround(true) { + it.nullable + }, + withNoSuchElementWorkaround(true) { + it.generateSingle + }, + withNoSuchElementWorkaround(true) { + it.generateFactory + } + ) } ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach { val targetType = TypeVariableName("T", Any::class) - addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) + addCodeForType( + targetType = targetType, + name = it.name, + nullable = withNoSuchElementWorkaround(true) { it.nullable }, + generateSingle = withNoSuchElementWorkaround(true) { it.generateSingle }, + generateFactory = withNoSuchElementWorkaround(true) { it.generateFactory } + ) } }.build().let { File( diff --git a/ksp/classcasts/generator/src/main/kotlin/Processor.kt b/ksp/classcasts/generator/src/main/kotlin/Processor.kt index bd387d233b8..7c3e8ea6ee7 100644 --- a/ksp/classcasts/generator/src/main/kotlin/Processor.kt +++ b/ksp/classcasts/generator/src/main/kotlin/Processor.kt @@ -10,6 +10,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.* import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ksp.toClassName +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded @@ -25,7 +26,11 @@ class Processor( ) { val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first() val (includeRegex: Regex?, excludeRegex: Regex?) = rootAnnotation.let { - it.typesRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) to it.excludeRegex.takeIf { it.isNotEmpty() } ?.let(::Regex) + withNoSuchElementWorkaround("") { + it.typesRegex + }.takeIf { it.isNotEmpty() } ?.let(::Regex) to withNoSuchElementWorkaround("") { + it.excludeRegex + }.takeIf { it.isNotEmpty() } ?.let(::Regex) } val classesSubtypes = mutableMapOf>() @@ -49,7 +54,9 @@ class Processor( when { potentialSubtype === ksClassDeclaration -> {} potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class) -> return@forEach - potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel(rootAnnotation.levelsToInclude.takeIf { it >= 0 }) -> return@forEach + potentialSubtype !is KSClassDeclaration || !potentialSubtype.checkSupertypeLevel( + withNoSuchElementWorkaround(-1) { rootAnnotation.levelsToInclude }.takeIf { it >= 0 } + ) -> return@forEach excludeRegex ?.matches(simpleName) == true -> return@forEach includeRegex ?.matches(simpleName) == false -> {} else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype) @@ -96,7 +103,9 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix + val prefix = withNoSuchElementWorkaround("") { + it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix + } it.writeFile(prefix = prefix, suffix = "ClassCasts") { FileSpec.builder( it.packageName.asString(), diff --git a/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt b/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt new file mode 100644 index 00000000000..a5e19134ddc --- /dev/null +++ b/ksp/generator/src/main/kotlin/NoSuchElementWorkaround.kt @@ -0,0 +1,12 @@ +package dev.inmo.micro_ksp.generator + +inline fun withNoSuchElementWorkaround( + default: T, + block: () -> T +): T = runCatching(block).getOrElse { + if (it is NoSuchElementException) { + default + } else { + throw it + } +} diff --git a/ksp/sealed/generator/src/main/kotlin/Processor.kt b/ksp/sealed/generator/src/main/kotlin/Processor.kt index f11bd40257b..f46ae51775e 100644 --- a/ksp/sealed/generator/src/main/kotlin/Processor.kt +++ b/ksp/sealed/generator/src/main/kotlin/Processor.kt @@ -12,6 +12,7 @@ import com.squareup.kotlinpoet.ksp.toClassName import dev.inmo.micro_ksp.generator.buildSubFileName import dev.inmo.micro_ksp.generator.companion import dev.inmo.micro_ksp.generator.findSubClasses +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround @@ -113,7 +114,7 @@ class Processor( val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation val subClasses = ksClassDeclaration.resolveSubclasses( searchIn = resolver.getAllFiles(), - allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false + allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false ).distinct() val subClassesNames = subClasses.filter { it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0 @@ -164,7 +165,15 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getGenerateSealedWorkaroundAnnotation) ?.prefix ?.takeIf { + val prefix = runCatching { + (it.getGenerateSealedWorkaroundAnnotation) ?.prefix + }.getOrElse { + if (it is NoSuchElementException) { + "" + } else { + throw it + } + } ?.takeIf { it.isNotEmpty() } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { @@ -184,7 +193,9 @@ class Processor( } } (resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf { + val prefix = withNoSuchElementWorkaround("") { + (it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix + } ?.takeIf { it.isNotEmpty() } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") { diff --git a/ksp/variations/generator/src/main/kotlin/Processor.kt b/ksp/variations/generator/src/main/kotlin/Processor.kt index 59d8ad6bc5e..085196f8105 100644 --- a/ksp/variations/generator/src/main/kotlin/Processor.kt +++ b/ksp/variations/generator/src/main/kotlin/Processor.kt @@ -16,6 +16,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName import dev.inmo.micro_ksp.generator.convertToClassName import dev.inmo.micro_ksp.generator.convertToClassNames import dev.inmo.micro_ksp.generator.findSubClasses +import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_utils.ksp.variations.GenerateVariations import dev.inmo.micro_utils.ksp.variations.GenerationVariant @@ -218,7 +219,9 @@ class Processor( @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { (resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance().forEach { - val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf { + val prefix = withNoSuchElementWorkaround("") { + (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix + } ?.takeIf { it.isNotEmpty() } ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "") it.writeFile(prefix = prefix, suffix = "GeneratedVariation") { From 7bcb81400ba031255408e4a477b51ccc3bf2e29c Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 5 Nov 2025 15:14:21 +0600 Subject: [PATCH 6/6] fill changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a492047111..144bfe91565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## 0.26.7 +* `Versions`: + * `Kotlin`: `2.2.20` -> `2.2.21` + * `Compose`: `1.8.2` -> `1.9.2` + * `KSP`: `2.2.20-2.0.3` -> `2.3.1` +* `Coroutines`: + * Fix `SmartSemaphore.waitRelease` to wait for the exact number of permits + * Improve `SmartKeyRWLocker` tests +* `KSP`: + * `Sealed`/`ClassCasts`/`Variations`: + * Add workaround for `NoSuchElementException` to improve processors stability on new `KSP` +* `Koin`: + * `Generator`: + * Handle missing annotation values safely (`NoSuchElementException` workaround) +* `Android`: + * `Pickers`: + * Add dependency `androidx.compose.material:material-icons-extended` + ## 0.26.6 * `Versions`: