Compare commits

..

22 Commits

Author SHA1 Message Date
renovate[bot]
13531286a6 Update dependency org.xerial:sqlite-jdbc to v3.51.0.0 2025-11-05 09:48:25 +00:00
0170b92272 Merge pull request #622 from InsanusMokrassar/0.26.7
0.26.7
2025-11-05 15:41:10 +06:00
7bcb81400b fill changelog 2025-11-05 15:14:21 +06:00
078aedfb68 update dependencies 2025-11-05 13:42:27 +06:00
cb56bf9793 Revert "add getCurrentLocale and compose translation"
This reverts commit fce47897d5.
2025-11-05 12:29:33 +06:00
b152986b4e improve SmartKeyRWLockerTests and fix SmartSemaphore 2025-11-05 12:27:24 +06:00
fce47897d5 add getCurrentLocale and compose translation 2025-10-31 19:34:09 +06:00
0b701a3e99 start 0.26.7 2025-10-31 11:21:06 +06:00
9dad353957 Merge pull request #619 from InsanusMokrassar/0.26.6
0.26.6
2025-10-19 16:24:37 +06:00
89e16b7bdb update dependencies
io.ktor:ktor-* 3.3.0 -> 3.3.1
com.squareup.okio:okio 3.16.0 -> 3.16.2
org.jetbrains.dokka:dokka-gradle-plugin 2.0.0 -> 2.1.0
2025-10-19 16:23:23 +06:00
c2965da341 start 0.26.6 2025-10-19 15:24:41 +06:00
ffb072dc5f Merge pull request #613 from InsanusMokrassar/0.26.5
0.26.5
2025-10-04 14:39:30 +06:00
a247dbcb02 rollback sqlite update 2025-10-04 14:20:18 +06:00
1dd71175f4 update dependencies 2025-09-30 23:29:04 +06:00
bbe62c0e7b start 0.26.5 2025-09-30 23:06:42 +06:00
9822ff321b Merge pull request #607 from InsanusMokrassar/0.26.4
0.26.4
2025-09-02 01:33:50 +06:00
b485d485ef MPPFilePathSeparator 2025-09-01 22:40:26 +06:00
0b3d445109 start 0.26.4 2025-09-01 22:34:32 +06:00
d7e48940bc Merge pull request #606 from InsanusMokrassar/0.26.3
0.26.3
2025-08-20 19:17:26 +06:00
1049eb0fe7 update dependencies 2025-08-20 19:15:24 +06:00
c871ef5635 start 0.26.3 2025-08-20 18:39:59 +06:00
7edfcb20c4 Merge pull request #593 from InsanusMokrassar/0.26.2
0.26.2
2025-07-31 15:22:34 +06:00
18 changed files with 700 additions and 166 deletions

View File

@@ -1,5 +1,53 @@
# Changelog # Changelog
## 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`:
* `Ktor`: `3.3.0` -> `3.3.1`
* `Okio`: `3.16.0` -> `3.16.2`
## 0.26.5
* `Versions`:
* `Kotlin`: `2.2.10` -> `2.2.20`
* `KSLog`: `1.5.0` -> `1.5.1`
* `Ktor`: `3.2.3` -> `3.3.0`
* `KotlinX Browser`: `0.3` -> `0.5.0`
* `Koin`: `4.1.0` -> `4.1.1`
## 0.26.4
* `Common`:
* Add expect/actual `MPPFilePathSeparator`
* Fix `FileName` realization to take care about system file path separator
## 0.26.3
* `Versions`:
* `Kotlin`: `2.2.0` -> `2.2.10`
* `KSP`: `2.2.0-2.0.2` -> `2.2.10-2.0.2`
* `Android CoreKTX`: `1.16.0` -> `1.17.0`
* `Android Fragment`: `1.8.8` -> `1.8.9`
## 0.26.2 ## 0.26.2
* `Versions`: * `Versions`:

View File

@@ -13,6 +13,7 @@ kotlin {
androidMain { androidMain {
dependencies { dependencies {
api project(":micro_utils.android.smalltextfield") api project(":micro_utils.android.smalltextfield")
api libs.jb.compose.icons
} }
} }
} }

View File

@@ -7,7 +7,7 @@ import kotlin.jvm.JvmInline
@JvmInline @JvmInline
value class FileName(val string: String) { value class FileName(val string: String) {
val name: String val name: String
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } get() = withoutSlashAtTheEnd.takeLastWhile { it != MPPFilePathSeparator }
val extension: String val extension: String
get() = name.takeLastWhile { it != '.' } get() = name.takeLastWhile { it != '.' }
val nameWithoutExtension: String val nameWithoutExtension: String
@@ -18,7 +18,7 @@ value class FileName(val string: String) {
} ?: filename } ?: filename
} }
val withoutSlashAtTheEnd: String val withoutSlashAtTheEnd: String
get() = string.dropLastWhile { it == '/' } get() = string.dropLastWhile { it == MPPFilePathSeparator }
override fun toString(): String = string override fun toString(): String = string
} }
@@ -26,6 +26,7 @@ value class FileName(val string: String) {
expect class MPPFile expect class MPPFile
expect val MPPFile.filename: FileName expect val MPPFile.filename: FileName
expect val MPPFilePathSeparator: Char
expect val MPPFile.filesize: Long expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator

View File

@@ -35,6 +35,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/** /**
* @suppress * @suppress
*/ */

View File

@@ -14,6 +14,10 @@ actual typealias MPPFile = File
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = File.separatorChar
/** /**
* @suppress * @suppress
*/ */

View File

@@ -11,6 +11,10 @@ actual typealias MPPFile = Path
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(toString()) get() = FileName(toString())
actual val MPPFilePathSeparator: Char = Path.DIRECTORY_SEPARATOR.first()
/** /**
* @suppress * @suppress
*/ */

View File

@@ -37,6 +37,10 @@ private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().awa
*/ */
actual val MPPFile.filename: FileName actual val MPPFile.filename: FileName
get() = FileName(name) get() = FileName(name)
actual val MPPFilePathSeparator: Char
get() = '/'
/** /**
* @suppress * @suppress
*/ */

View File

@@ -72,7 +72,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits acquiredPermits != checkedPermits
} }
if (shouldContinue) { if (shouldContinue) {
waitRelease() waitRelease(checkedPermits - acquiredPermits)
} }
} while (shouldContinue && currentCoroutineContext().isActive) } while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
@@ -13,184 +14,517 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests { class SmartKeyRWLockerTests {
@Test private lateinit var locker: SmartKeyRWLocker<String>
fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite()
assertTrue { locker.isWriteLocked() } @BeforeTest
fun setup() {
assertFails { locker = SmartKeyRWLocker()
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) }
} }
// ==================== Global Read Tests ====================
@Test @Test
fun writeLockKeyFailedOnGlobalReadLockTest() = runTest { fun testGlobalReadAllowsMultipleConcurrentReads() = runTest {
val locker = SmartKeyRWLocker<String>() val results = mutableListOf<Boolean>()
val testKey = "test"
locker.acquireRead() locker.acquireRead()
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits) val jobs = List(5) {
launch {
assertFails { locker.acquireRead()
realWithTimeout(1.seconds) { delay(100.milliseconds)
locker.lockWrite(testKey) 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() locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits) writeJob.join()
realWithTimeout(1.seconds) { assertTrue(writeAcquired, "Write should succeed after read released")
locker.lockWrite(testKey)
}
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
@Test @Test
fun readLockFailedOnWriteLockKeyTest() = runTest { fun testGlobalReadBlocksAllKeyWrites() = runTest {
val locker = SmartKeyRWLocker<String>() locker.acquireRead()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) } val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
assertFails { val jobs = keys.map { key ->
realWithTimeout(1.seconds) { 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() 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) delay(200.milliseconds)
assertFalse { locker.isWriteLocked(testKey) } 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.unlockWrite()
locker.acquireRead() jobs.joinAll()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits) assertTrue(globalReadAcquired)
assertTrue { locker.releaseRead() } assertTrue(keyReadAcquired)
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits) assertTrue(keyWriteAcquired)
} }
@Test @Test
fun writeLockFailedOnWriteLockKeyTest() = runTest { fun testGlobalWriteIsExclusive() = runTest {
val locker = SmartKeyRWLocker<String>() locker.lockWrite()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) } var secondWriteAcquired = false
val job = launch {
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite()
}
}
assertFalse(locker.isWriteLocked())
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.lockWrite() locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
} }
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() } delay(200.milliseconds)
assertFalse(locker.isWriteLocked()) assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
} }
// ==================== Key Read Tests ====================
@Test @Test
fun readsBlockingGlobalWrite() = runTest { fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() val key = "testKey"
val results = mutableListOf<Boolean>()
val testKeys = (0 until 100).map { "test$it" } locker.acquireRead(key)
for (i in testKeys.indices) { val jobs = List(5) {
val it = testKeys[i] launch {
locker.acquireRead(it) locker.acquireRead(key)
val previous = testKeys.take(i) delay(50.milliseconds)
val next = testKeys.drop(i + 1) results.add(true)
locker.releaseRead(key)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
} }
} }
for (i in testKeys.indices) { jobs.joinAll()
val it = testKeys[i] locker.releaseRead(key)
assertFails {
realWithTimeout(13.milliseconds) { locker.lockWrite() } assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
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() locker.releaseRead()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.releaseRead(it)
} }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } delay(200.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() } assertFalse(globalReadAcquired, "Global read should be blocked by key write")
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() } locker.unlockWrite("key1")
} job.join()
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE } assertTrue(globalReadAcquired)
} }
@Test @Test
fun writesBlockingGlobalWrite() = runTest { fun testKeyWriteIsExclusiveForSameKey() = runTest {
val locker = SmartKeyRWLocker<String>() 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) { delay(200.milliseconds)
val it = testKeys[i] assertFalse(secondWriteAcquired, "Second write for same key should be blocked")
locker.lockWrite(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach { locker.unlockWrite(key)
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true } job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
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<Job>()
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<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
} }
} }
for (i in testKeys.indices) { delay(200.milliseconds)
val it = testKeys[i] assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
val readPermitsBeforeLock = locker.readSemaphore().freePermits locker.unlockWrite(key)
assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } } readers.joinAll()
assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
locker.unlockWrite(it) assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
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 } delay(50.milliseconds)
realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails { launch {
realWithTimeout(13.milliseconds) { locker.acquireRead() } 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<Job>()
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")
} }
} }

View File

@@ -8,8 +8,8 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# https://github.com/google/ksp/issues/2491 ## https://github.com/google/ksp/issues/2491
ksp.useKSP2=false #ksp.useKSP2=false
# JS NPM # JS NPM
@@ -18,5 +18,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.26.2 version=0.26.7
android_code_version=301 android_code_version=306

View File

@@ -1,43 +1,46 @@
[versions] [versions]
kt = "2.2.0" kt = "2.2.21"
kt-serialization = "1.9.0" kt-serialization = "1.9.0"
kt-coroutines = "1.10.2" kt-coroutines = "1.10.2"
kotlinx-browser = "0.3" kotlinx-browser = "0.5.0"
kslog = "1.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-exposed = "0.61.0"
jb-dokka = "2.0.0" jb-dokka = "2.1.0"
sqlite = "3.50.1.0" # 3.51.0.0 contains bug, checking with ./gradlew :micro_utils.repos.exposed:jvmTest
sqlite = "3.51.0.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.2.3" ktor = "3.3.1"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.1.0" koin = "4.1.1"
okio = "3.16.0" okio = "3.16.2"
ksp = "2.2.0-2.0.2" ksp = "2.3.1"
kotlin-poet = "2.2.0" kotlin-poet = "2.2.0"
versions = "0.52.0" versions = "0.52.0"
nmcp = "1.0.2" nmcp = "1.2.0"
android-gradle = "8.9.+" android-gradle = "8.10.+"
dexcount = "4.0.0" dexcount = "4.0.0"
android-coreKtx = "1.16.0" android-coreKtx = "1.17.0"
android-recyclerView = "1.4.0" android-recyclerView = "1.4.0"
android-appCompat = "1.7.1" android-appCompat = "1.7.1"
android-fragment = "1.8.8" android-fragment = "1.8.9"
android-espresso = "3.7.0" android-espresso = "3.7.0"
android-test = "1.3.0" android-test = "1.3.0"
@@ -88,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" } jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } 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-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" } android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -27,6 +27,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName 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.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin import org.koin.core.Koin
@@ -240,9 +241,14 @@ class Processor(
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = safeClassName { it.type } val type = safeClassName { it.type }
val targetType = runCatching { 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 -> }.getOrElse { e ->
when (e) { when (e) {
is IllegalArgumentException if (e.message ?.contains("no type argument") == true) -> return@getOrElse type
is KSTypeNotPresentException -> e.ksType.toClassName() is KSTypeNotPresentException -> e.ksType.toClassName()
} }
if (e is KSTypesNotPresentException) { if (e is KSTypesNotPresentException) {
@@ -251,14 +257,32 @@ class Processor(
throw e throw e
} }
}.copy( }.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 { ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach {
val targetType = TypeVariableName("T", Any::class) 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 { }.build().let {
File( File(

View File

@@ -10,6 +10,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.* import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.withNoSuchElementWorkaround
import dev.inmo.micro_ksp.generator.writeFile import dev.inmo.micro_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded import dev.inmo.micro_utils.ksp.classcasts.ClassCastsExcluded
import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded import dev.inmo.micro_utils.ksp.classcasts.ClassCastsIncluded
@@ -25,7 +26,11 @@ class Processor(
) { ) {
val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first() val rootAnnotation = ksClassDeclaration.getAnnotationsByType(ClassCastsIncluded::class).first()
val (includeRegex: Regex?, excludeRegex: Regex?) = rootAnnotation.let { 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<KSClassDeclaration, MutableSet<KSClassDeclaration>>() val classesSubtypes = mutableMapOf<KSClassDeclaration, MutableSet<KSClassDeclaration>>()
@@ -49,7 +54,9 @@ class Processor(
when { when {
potentialSubtype === ksClassDeclaration -> {} potentialSubtype === ksClassDeclaration -> {}
potentialSubtype.isAnnotationPresent(ClassCastsExcluded::class) -> return@forEach 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 excludeRegex ?.matches(simpleName) == true -> return@forEach
includeRegex ?.matches(simpleName) == false -> {} includeRegex ?.matches(simpleName) == false -> {}
else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype) else -> classesSubtypes.getOrPut(ksClassDeclaration) { mutableSetOf() }.add(potentialSubtype)
@@ -96,7 +103,9 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(ClassCastsIncluded::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix val prefix = withNoSuchElementWorkaround("") {
it.getAnnotationsByType(ClassCastsIncluded::class).first().outputFilePrefix
}
it.writeFile(prefix = prefix, suffix = "ClassCasts") { it.writeFile(prefix = prefix, suffix = "ClassCasts") {
FileSpec.builder( FileSpec.builder(
it.packageName.asString(), it.packageName.asString(),

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_ksp.generator
inline fun <T> withNoSuchElementWorkaround(
default: T,
block: () -> T
): T = runCatching(block).getOrElse {
if (it is NoSuchElementException) {
default
} else {
throw it
}
}

View File

@@ -12,6 +12,7 @@ import com.squareup.kotlinpoet.ksp.toClassName
import dev.inmo.micro_ksp.generator.buildSubFileName import dev.inmo.micro_ksp.generator.buildSubFileName
import dev.inmo.micro_ksp.generator.companion import dev.inmo.micro_ksp.generator.companion
import dev.inmo.micro_ksp.generator.findSubClasses 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_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedTypesWorkaround
import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround import dev.inmo.micro_utils.ksp.sealed.GenerateSealedWorkaround
@@ -113,7 +114,7 @@ class Processor(
val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation val annotation = ksClassDeclaration.getGenerateSealedTypesWorkaroundAnnotation
val subClasses = ksClassDeclaration.resolveSubclasses( val subClasses = ksClassDeclaration.resolveSubclasses(
searchIn = resolver.getAllFiles(), searchIn = resolver.getAllFiles(),
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false
).distinct() ).distinct()
val subClassesNames = subClasses.filter { val subClassesNames = subClasses.filter {
it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0 it.getAnnotationsByType(GenerateSealedTypesWorkaround.Exclude::class).count() == 0
@@ -164,7 +165,15 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().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.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") { it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
@@ -184,7 +193,9 @@ class Processor(
} }
} }
(resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(GenerateSealedTypesWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
val prefix = (it.getGenerateSealedTypesWorkaroundAnnotation) ?.prefix ?.takeIf { val prefix = withNoSuchElementWorkaround("") {
(it.getGenerateSealedTypesWorkaroundAnnotation)?.prefix
} ?.takeIf {
it.isNotEmpty() it.isNotEmpty()
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "") } ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") { it.writeFile(prefix = prefix, suffix = "SealedTypesWorkaround") {

View File

@@ -16,6 +16,7 @@ import com.squareup.kotlinpoet.ksp.toTypeName
import dev.inmo.micro_ksp.generator.convertToClassName import dev.inmo.micro_ksp.generator.convertToClassName
import dev.inmo.micro_ksp.generator.convertToClassNames import dev.inmo.micro_ksp.generator.convertToClassNames
import dev.inmo.micro_ksp.generator.findSubClasses 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_ksp.generator.writeFile
import dev.inmo.micro_utils.ksp.variations.GenerateVariations import dev.inmo.micro_utils.ksp.variations.GenerateVariations
import dev.inmo.micro_utils.ksp.variations.GenerationVariant import dev.inmo.micro_utils.ksp.variations.GenerationVariant
@@ -218,7 +219,9 @@ class Processor(
@OptIn(KspExperimental::class) @OptIn(KspExperimental::class)
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
(resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach { (resolver.getSymbolsWithAnnotation(GenerateVariations::class.qualifiedName!!)).filterIsInstance<KSFunctionDeclaration>().forEach {
val prefix = (it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix ?.takeIf { val prefix = withNoSuchElementWorkaround("") {
(it.getAnnotationsByType(GenerateVariations::class)).firstOrNull() ?.prefix
} ?.takeIf {
it.isNotEmpty() it.isNotEmpty()
} ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "") } ?: it.simpleName.asString().replaceFirst(it.simpleName.asString(), "")
it.writeFile(prefix = prefix, suffix = "GeneratedVariation") { it.writeFile(prefix = prefix, suffix = "GeneratedVariation") {

View File

@@ -1,10 +1,81 @@
package dev.inmo.micro_utils.repos.exposed package dev.inmo.micro_utils.repos.exposed
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SchemaUtils.addMissingColumnsStatements
import org.jetbrains.exposed.sql.SchemaUtils.checkMappingConsistence
import org.jetbrains.exposed.sql.SchemaUtils.createStatements
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
/**
* Code in this function mostly duplicates Exposed [SchemaUtils.createMissingTablesAndColumns]. It made due to deprecation
* status of the last one and potential lost of it in future updates.
*
* Code doing:
*
* * Creating missed tables
* * Altering missed tables (where possible)
* * Calculate problems with [checkMappingConsistence] and add them in execution too
*
* All changes made in [transaction] with [database] as its `db` argument
*/
fun initTablesInTransaction(vararg tables: Table, database: Database, inBatch: Boolean = false, withLogs: Boolean = true) {
transaction(database) {
fun <R> logTimeSpent(message: String, withLogs: Boolean, block: () -> R): R {
return if (withLogs) {
val start = System.currentTimeMillis()
val answer = block()
exposedLogger.info(message + " took " + (System.currentTimeMillis() - start) + "ms")
answer
} else {
block()
}
}
fun Transaction.execStatements(inBatch: Boolean, statements: List<String>) {
if (inBatch) {
execInBatch(statements)
} else {
for (statement in statements) {
exec(statement)
}
}
}
with(TransactionManager.current()) {
db.dialect.resetCaches()
val createStatements = logTimeSpent("Preparing create tables statements", withLogs) {
createStatements(*tables)
}
logTimeSpent("Executing create tables statements", withLogs) {
execStatements(inBatch, createStatements)
commit()
}
val alterStatements = logTimeSpent("Preparing alter table statements", withLogs) {
addMissingColumnsStatements(tables = tables, withLogs)
}
logTimeSpent("Executing alter table statements", withLogs) {
execStatements(inBatch, alterStatements)
commit()
}
val executedStatements = createStatements + alterStatements
logTimeSpent("Checking mapping consistence", withLogs) {
val modifyTablesStatements = checkMappingConsistence(
tables = tables,
withLogs
).filter { it !in executedStatements }
execStatements(inBatch, modifyTablesStatements)
commit()
}
db.dialect.resetCaches()
}
}
}
fun Table.initTable(database: Database, inBatch: Boolean, withLogs: Boolean) {
initTablesInTransaction(this, database = database, inBatch = inBatch, withLogs = withLogs)
}
fun Table.initTable(database: Database) { fun Table.initTable(database: Database) {
transaction(database) { SchemaUtils.createMissingTablesAndColumns(this@initTable) } initTable(database = database, inBatch = false, withLogs = true)
} }
fun <T> T.initTable() where T: ExposedRepo, T: Table = initTable(database) fun <T> T.initTable() where T: ExposedRepo, T: Table = initTable(database)