mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Merge main and common module to simplify.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package testhelpers
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import testhelpers.logging.JUnitLogger
|
||||
|
||||
|
||||
open class BaseTest {
|
||||
init {
|
||||
Logging.clearAll()
|
||||
Logging.install(JUnitLogger())
|
||||
testClassName = this.javaClass.simpleName
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var testClassName: String? = null
|
||||
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
fun onTestClassFinished() {
|
||||
unmockkAll()
|
||||
log(testClassName!!, VERBOSE) { "onTestClassFinished()" }
|
||||
Logging.clearAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package testhelpers
|
||||
|
||||
class IsAUnitTest
|
||||
@@ -0,0 +1,23 @@
|
||||
package testhelpers.coroutine
|
||||
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TestDispatcherProvider(private val context: CoroutineContext? = null) : DispatcherProvider {
|
||||
override val Default: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val Main: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val MainImmediate: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val Unconfined: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val IO: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
}
|
||||
|
||||
fun CoroutineScope.asDispatcherProvider() = this.coroutineContext.asDispatcherProvider()
|
||||
|
||||
fun CoroutineContext.asDispatcherProvider() = TestDispatcherProvider(context = this)
|
||||
@@ -0,0 +1,38 @@
|
||||
package testhelpers.coroutine
|
||||
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun runTest2(
|
||||
autoCancel: Boolean = false,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
expectedError: KClass<out Throwable>? = null,
|
||||
testBody: suspend TestScope.() -> Unit
|
||||
) {
|
||||
try {
|
||||
val scope = TestScope(context = context)
|
||||
try {
|
||||
scope.runTest {
|
||||
testBody()
|
||||
if (autoCancel) scope.cancel("autoCancel")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val isExpected = expectedError?.isInstance(e) ?: false
|
||||
if (!isExpected) throw e
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
if (e.message == "autoCancel" && autoCancel) {
|
||||
io.kotest.mpp.log { "Test was auto-cancelled ${e.asLog()}" }
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package testhelpers.flow
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
fun <T> Flow<T>.test(
|
||||
tag: String? = null,
|
||||
scope: CoroutineScope
|
||||
): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = scope)
|
||||
|
||||
fun <T> Flow<T>.createTest(
|
||||
tag: String? = null
|
||||
): TestCollector<T> = TestCollector(this, tag ?: "FlowTest")
|
||||
|
||||
class TestCollector<T>(
|
||||
private val flow: Flow<T>,
|
||||
private val tag: String
|
||||
|
||||
) {
|
||||
private var error: Throwable? = null
|
||||
private lateinit var job: Job
|
||||
private val cache = MutableSharedFlow<T>(
|
||||
replay = Int.MAX_VALUE,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
onBufferOverflow = BufferOverflow.SUSPEND
|
||||
)
|
||||
private var latestInternal: T? = null
|
||||
private val collectedValuesMutex = Mutex()
|
||||
private val collectedValues = mutableListOf<T>()
|
||||
|
||||
var silent = false
|
||||
|
||||
fun start(scope: CoroutineScope) = apply {
|
||||
flow
|
||||
.buffer(capacity = Int.MAX_VALUE)
|
||||
.onStart { log(tag) { "Setting up." } }
|
||||
.onCompletion { log(tag) { "Final." } }
|
||||
.onEach {
|
||||
collectedValuesMutex.withLock {
|
||||
if (!silent) log(tag) { "Collecting: $it" }
|
||||
latestInternal = it
|
||||
collectedValues.add(it)
|
||||
cache.emit(it)
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
log(tag, WARN) { "Caught error: ${e.asLog()}" }
|
||||
error = e
|
||||
}
|
||||
.launchIn(scope)
|
||||
.also { job = it }
|
||||
}
|
||||
|
||||
fun emissions(): Flow<T> = cache
|
||||
|
||||
val latestValue: T?
|
||||
get() = collectedValues.last()
|
||||
|
||||
val latestValues: List<T>
|
||||
get() = collectedValues
|
||||
|
||||
fun await(
|
||||
timeout: Long = 10_000,
|
||||
condition: (List<T>, T) -> Boolean
|
||||
): T = runBlocking {
|
||||
withTimeout(timeMillis = timeout) {
|
||||
emissions().first {
|
||||
condition(collectedValues, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitFinal(cancel: Boolean = false) = apply {
|
||||
if (cancel) job.cancel()
|
||||
try {
|
||||
job.join()
|
||||
} catch (e: Exception) {
|
||||
error = e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun assertNoErrors() = apply {
|
||||
awaitFinal()
|
||||
require(error == null) { "Error was not null: $error" }
|
||||
}
|
||||
|
||||
suspend fun cancelAndJoin() {
|
||||
if (job.isCompleted) throw IllegalStateException("Flow is already canceled.")
|
||||
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package testhelpers.json
|
||||
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.Moshi
|
||||
import okio.Buffer
|
||||
import okio.ByteString.Companion.encode
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
|
||||
|
||||
fun String.toComparableJson(): String {
|
||||
val value = Buffer().use {
|
||||
it.writeUtf8(this)
|
||||
val reader = JsonReader.of(it)
|
||||
reader.readJsonValue()
|
||||
}
|
||||
|
||||
val adapter = Moshi.Builder().build().adapter(Any::class.java).indent(" ")
|
||||
|
||||
return adapter.toJson(value)
|
||||
}
|
||||
|
||||
fun String.writeToFile(file: File) = encode().let { text ->
|
||||
require(!file.exists())
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
file.sink().buffer().use { it.write(text) }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package testhelpers.livedata
|
||||
|
||||
import androidx.arch.core.executor.ArchTaskExecutor
|
||||
import androidx.arch.core.executor.TaskExecutor
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(
|
||||
object : TaskExecutor() {
|
||||
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun isMainThread(): Boolean = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package testhelpers.logging
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
|
||||
class JUnitLogger(private val minLogLevel: Logging.Priority = Logging.Priority.VERBOSE) : Logging.Logger {
|
||||
|
||||
override fun isLoggable(priority: Logging.Priority): Boolean = priority.intValue >= minLogLevel.intValue
|
||||
|
||||
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
|
||||
println("${System.currentTimeMillis()} ${priority.shortLabel}/$tag: $message")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import eu.darken.capod.common.preferences.FlowPreference
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun <T> mockFlowPreference(
|
||||
defaultValue: T
|
||||
): FlowPreference<T> {
|
||||
val instance = mockk<FlowPreference<T>>()
|
||||
val flow = MutableStateFlow(defaultValue)
|
||||
every { instance.flow } answers { flow }
|
||||
every { instance.value } answers { flow.value }
|
||||
every { instance.update(any()) } answers {
|
||||
val updateCall = arg<(T) -> T>(0)
|
||||
flow.value = updateCall(flow.value)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class MockSharedPreferences : SharedPreferences {
|
||||
private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>()
|
||||
private val dataMap = mutableMapOf<String, Any>()
|
||||
val dataMapPeek: Map<String, Any>
|
||||
get() = dataMap.toMap()
|
||||
|
||||
override fun getAll(): MutableMap<String, *> = dataMap
|
||||
|
||||
override fun getString(key: String, defValue: String?): String? =
|
||||
dataMap[key] as? String ?: defValue
|
||||
|
||||
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String> {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defValue: Int): Int =
|
||||
dataMap[key] as? Int ?: defValue
|
||||
|
||||
override fun getLong(key: String, defValue: Long): Long =
|
||||
dataMap[key] as? Long ?: defValue
|
||||
|
||||
override fun getFloat(key: String, defValue: Float): Float {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean =
|
||||
dataMap[key] as? Boolean ?: defValue
|
||||
|
||||
override fun contains(key: String): Boolean = dataMap.contains(key)
|
||||
|
||||
override fun edit(): SharedPreferences.Editor = createEditor(dataMap.toMap()) { newData ->
|
||||
dataMap.clear()
|
||||
dataMap.putAll(newData)
|
||||
}
|
||||
|
||||
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun createEditor(
|
||||
toEdit: Map<String, Any>,
|
||||
onSave: (Map<String, Any>) -> Unit
|
||||
): SharedPreferences.Editor {
|
||||
return object : SharedPreferences.Editor {
|
||||
private val editorData = toEdit.toMutableMap()
|
||||
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply {
|
||||
value?.let { editorData[key] = it } ?: editorData.remove(key)
|
||||
}
|
||||
|
||||
override fun putStringSet(
|
||||
key: String?,
|
||||
values: MutableSet<String>?
|
||||
): SharedPreferences.Editor {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun remove(key: String): SharedPreferences.Editor = apply {
|
||||
editorData.remove(key)
|
||||
}
|
||||
|
||||
override fun clear(): SharedPreferences.Editor = apply {
|
||||
editorData.clear()
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
onSave(editorData)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
onSave(editorData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import androidx.core.content.edit
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class MockSharedPreferencesTest : BaseTest() {
|
||||
|
||||
private fun createInstance() = MockSharedPreferences()
|
||||
|
||||
@Test
|
||||
fun `test boolean insertion`() {
|
||||
val prefs = createInstance()
|
||||
prefs.dataMapPeek shouldBe emptyMap()
|
||||
prefs.getBoolean("key", true) shouldBe true
|
||||
prefs.edit { putBoolean("key", false) }
|
||||
prefs.getBoolean("key", true) shouldBe false
|
||||
prefs.dataMapPeek["key"] shouldBe false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user