commonMain.com.copperleaf.ballast.test.internal.ViewModelTestSuiteScopeImpl.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ballast-test-jvm Show documentation
Show all versions of ballast-test-jvm Show documentation
Opinionated Application State Management framework for Kotlin Multiplatform
package com.copperleaf.ballast.test.internal
import com.copperleaf.ballast.BallastInterceptor
import com.copperleaf.ballast.EventHandler
import com.copperleaf.ballast.InputFilter
import com.copperleaf.ballast.InputHandler
import com.copperleaf.ballast.InputStrategy
import com.copperleaf.ballast.core.LifoInputStrategy
import com.copperleaf.ballast.test.ViewModelTestScenarioScope
import com.copperleaf.ballast.test.ViewModelTestSuiteScope
import com.copperleaf.ballast.test.internal.vm.TestEventHandler
import com.copperleaf.ballast.test.internal.vm.TestInputFilter
import com.copperleaf.ballast.test.internal.vm.TestInterceptor
import com.copperleaf.ballast.test.internal.vm.TestInterceptorWrapper
import com.copperleaf.ballast.test.internal.vm.TestViewModel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
@ExperimentalCoroutinesApi
@ExperimentalTime
internal class ViewModelTestSuiteScopeImpl(
private val inputHandler: InputHandler,
private val eventHandler: EventHandler,
private val filter: InputFilter?,
) : ViewModelTestSuiteScope {
private var suiteLogger: (String) -> Unit = { }
private var defaultTimeout: Duration = Duration.seconds(30)
private var inputStrategy: InputStrategy = LifoInputStrategy()
internal val interceptors: MutableList<() -> BallastInterceptor, Events, State>> =
mutableListOf()
private var defaultInitialStateBlock: (() -> State)? = null
private val scenarioBlocks = mutableListOf>()
override fun logger(block: (String) -> Unit) {
this.suiteLogger = block
}
override fun defaultTimeout(timeout: () -> Duration) {
this.defaultTimeout = timeout()
}
override fun addInterceptor(interceptor: () -> BallastInterceptor) {
this.interceptors += { TestInterceptorWrapper(interceptor()) }
}
override fun defaultInputStrategy(inputStrategy: () -> InputStrategy) {
this.inputStrategy = inputStrategy()
}
override fun defaultInitialState(block: () -> State) {
defaultInitialStateBlock = block
}
override fun scenario(name: String, block: ViewModelTestScenarioScope.() -> Unit) {
scenarioBlocks += ViewModelTestScenarioScopeImpl(name).apply(block)
}
private suspend fun runScenario(
scenario: ViewModelTestScenarioScopeImpl
) = supervisorScope {
val scenarioLogger = scenario.logger ?: suiteLogger
val scenarioTimeout = scenario.timeout ?: defaultTimeout
val scenarioInputStrategy = scenario.inputStrategy ?: inputStrategy
val otherInterceptors = scenario.interceptors + interceptors
scenarioLogger("Scenario '${scenario.name}'")
scenarioLogger("before runScenario")
val testViewModel = TestViewModel(
logger = scenarioLogger,
testInterceptor = TestInterceptor(),
otherInterceptors = otherInterceptors.map { it() },
initialState = scenario.givenBlock?.invoke()
?: defaultInitialStateBlock?.invoke()
?: error("No initial state given"),
inputHandler = inputHandler,
filter = filter?.let { TestInputFilter(it) },
inputStrategy = scenarioInputStrategy,
name = scenario.name,
)
// start running the VM in the background
val viewModelJob = launch(start = CoroutineStart.UNDISPATCHED) {
testViewModel.impl.start(this)
}
val eventHandlerJob = launch(start = CoroutineStart.UNDISPATCHED) {
testViewModel.impl.attachEventHandler(TestEventHandler(eventHandler))
}
val inputSequenceScope = ViewModelTestScenarioInputSequenceScopeImpl(scenarioLogger, testViewModel)
// run the scenario input sequence
scenarioLogger(" before onInputSequenceBlock")
scenario.onInputSequenceBlock(inputSequenceScope)
scenarioLogger(" after onInputSequenceBlock")
scenarioLogger(" before awaitSideEffectsCompletion")
withTimeoutOrNull(scenarioTimeout) {
testViewModel.impl.awaitSideEffectsCompletion()
}
scenarioLogger(" after awaitSideEffectsCompletion")
// await test completion
scenarioLogger(" before completing whole test")
inputSequenceScope.finish()
scenarioLogger(" after completing whole test")
// if the test passed, manually clear everything
scenarioLogger(" before cleanup")
viewModelJob.cancelAndJoin()
eventHandlerJob.cancelAndJoin()
testViewModel.onCleared()
scenarioLogger(" after cleanup")
// make assertions on the VM. Errors should get captured and thrown by this coroutine scope, cancelling
// everything if there are failures
scenarioLogger(" before verification")
scenario.verifyBlock(testViewModel.testInterceptor.getResults())
scenarioLogger(" after verification")
scenarioLogger("after runScenario")
}
private sealed class ScenarioResult {
abstract val scenario: ViewModelTestScenarioScopeImpl
abstract fun printResults(): String
data class Passed(
override val scenario: ViewModelTestScenarioScopeImpl,
val time: Duration
) : ScenarioResult() {
override fun printResults(): String {
return "Scenario '${scenario.name}': Passed ($time)"
}
}
data class Failed(
override val scenario: ViewModelTestScenarioScopeImpl,
val time: Duration,
val reason: Throwable,
) : ScenarioResult() {
override fun printResults(): String {
return "Scenario '${scenario.name}': Failed ($time)"
}
}
data class Skipped(
override val scenario: ViewModelTestScenarioScopeImpl,
) : ScenarioResult() {
override fun printResults(): String {
return "Scenario '${scenario.name}': Skipped"
}
}
}
internal suspend fun runTest() = supervisorScope {
val totalTestTime = measureTime {
val hasSoloScenario = scenarioBlocks.any { it.solo }
val actualScenariosToRun = if (hasSoloScenario) {
scenarioBlocks.filter { it.solo }
} else {
scenarioBlocks
}
val results: List> = actualScenariosToRun
.map { scenario ->
if (scenario.skip) {
CompletableDeferred(ScenarioResult.Skipped(scenario))
} else {
async {
val result: Result
val scenarioTestTime = measureTime {
result = runCatching { runScenario(scenario) }
}
result.fold(
onSuccess = { ScenarioResult.Passed(scenario, scenarioTestTime) },
onFailure = { ScenarioResult.Failed(scenario, scenarioTestTime, it) },
)
}
}
}
.awaitAll()
results.forEach {
val scenarioLogger = it.scenario.logger ?: suiteLogger
scenarioLogger(it.printResults())
}
results.filterIsInstance>().firstOrNull()?.let {
throw it.reason
}
}
suiteLogger("All scenarios completed in $totalTestTime")
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy