diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9613500944..efd653ad48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -302,6 +302,7 @@ dependencies { testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.test.runtime) androidTestImplementation(libs.bundles.test.android) + testImplementation(kotlinx.coroutines.test) } tasks { diff --git a/app/src/test/java/dev/yokai/core/migration/MigratorTest.kt b/app/src/test/java/dev/yokai/core/migration/MigratorTest.kt new file mode 100644 index 0000000000..d22a9e55de --- /dev/null +++ b/app/src/test/java/dev/yokai/core/migration/MigratorTest.kt @@ -0,0 +1,157 @@ +package dev.yokai.core.migration + +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MigratorTest { + lateinit var migrationCompletedListener: MigrationCompletedListener + lateinit var migrationContext: MigrationContext + lateinit var migrationJobFactory: MigrationJobFactory + lateinit var migrationStrategyFactory: MigrationStrategyFactory + + @BeforeEach + fun initilize() { + migrationContext = MigrationContext(false) + migrationJobFactory = spyk(MigrationJobFactory(migrationContext, CoroutineScope(Dispatchers.Main + Job()))) + migrationCompletedListener = spyk<() -> Unit>({}) + migrationStrategyFactory = spyk(MigrationStrategyFactory(migrationJobFactory, migrationCompletedListener)) + } + + @Test + fun initialVersion() = runBlocking { + val strategy = migrationStrategyFactory.create(0, 1) + assertInstanceOf(InitialMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(1, migrations.captured.size) + verify { migrationCompletedListener() } + } + + @Test + fun sameVersion() = runBlocking { + val strategy = migrationStrategyFactory.create(1, 1) + assertInstanceOf(NoopMigrationStrategy::class.java, strategy) + + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false })) + + val result = execute.await() + assertFalse(result) + + verify(exactly = 0) { migrationJobFactory.create(any()) } + } + + @Test + fun noMigrations() = runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val execute = strategy(emptyList()) + + val result = execute.await() + assertFalse(result) + + verify(exactly = 0) { migrationJobFactory.create(any()) } + } + + @Test + fun smallMigration() = runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = strategy(listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true })) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(2, migrations.captured.size) + verify { migrationCompletedListener() } + } + + @Test + fun largeMigration() = runBlocking { + val input = listOf( + Migration.of(Migration.ALWAYS) { true }, + Migration.of(2f) { true }, + Migration.of(3f) { true }, + Migration.of(4f) { true }, + Migration.of(5f) { true }, + Migration.of(6f) { true }, + Migration.of(7f) { true }, + Migration.of(8f) { true }, + Migration.of(9f) { true }, + Migration.of(10f) { true }, + ) + + val strategy = migrationStrategyFactory.create(1, 10) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = strategy(input) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(10, migrations.captured.size) + verify { migrationCompletedListener() } + } + + @Test + fun withinRangeMigration() = runBlocking { + val strategy = migrationStrategyFactory.create(1, 2) + assertInstanceOf(VersionRangeMigrationStrategy::class.java, strategy) + + val migrations = slot>() + val execute = strategy( + listOf( + Migration.of(Migration.ALWAYS) { true }, + Migration.of(2f) { true }, + Migration.of(3f) { false } + ) + ) + + execute.await() + + verify { migrationJobFactory.create(capture(migrations)) } + assertEquals(2, migrations.captured.size) + verify { migrationCompletedListener() } + } + + companion object { + + val mainThreadSurrogate = newSingleThreadContext("UI thread") + + @BeforeAll + @JvmStatic + fun setUp() { + Dispatchers.setMain(mainThreadSurrogate) + } + + @AfterAll + @JvmStatic + fun tearDown() { + Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher + mainThreadSurrogate.close() + } + } +} diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index f6b5c427f0..b6ef6bf683 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -7,6 +7,7 @@ xml_serialization = "0.86.3" [libraries] coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }