From 779229a48ae671ed2a65f3d855376a09fe4636a0 Mon Sep 17 00:00:00 2001 From: Constantin Piber <59023762+cpiber@users.noreply.github.com> Date: Mon, 18 May 2026 20:04:39 +0200 Subject: [PATCH] Fix tests (#2049) * Fix test setup * Fix tests * Disable broken CloudflareTest * Add a basic test for Android's Looper --- .../server/settings/SettingsRegistry.kt | 2 + .../manga/model/dataclass/PaginatedList.kt | 11 +++- .../suwayomi/tachidesk/server/ServerSetup.kt | 4 +- .../tachidesk/server/database/DBManager.kt | 15 ++--- .../test/kotlin/masstest/CloudFlareTest.kt | 14 +++++ .../masstest/TestExtensionCompatibility.kt | 18 +++++- .../kotlin/suwayomi/tachidesk/LooperTest.kt | 56 +++++++++++++++++++ .../controller/CategoryControllerTest.kt | 11 ++-- .../tachidesk/manga/impl/CategoryMangaTest.kt | 2 +- .../tachidesk/test/ApplicationTest.kt | 44 ++++++++++----- .../suwayomi/tachidesk/test/TestUtils.kt | 3 +- 11 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 server/src/test/kotlin/suwayomi/tachidesk/LooperTest.kt diff --git a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt index 3347a847f..4012428a7 100644 --- a/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt +++ b/server/server-config/src/main/kotlin/suwayomi/tachidesk/server/settings/SettingsRegistry.kt @@ -88,4 +88,6 @@ object SettingsRegistry { fun get(name: String): SettingMetadata? = settings[name] fun getAll(): Map = settings.toMap() + + fun clear() = settings.clear() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/PaginatedList.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/PaginatedList.kt index 5de0fc6f5..5423b7cd3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/PaginatedList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/dataclass/PaginatedList.kt @@ -7,12 +7,21 @@ package suwayomi.tachidesk.manga.model.dataclass * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import java.util.Objects import kotlin.math.min open class PaginatedList( val page: List, val hasNextPage: Boolean, -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PaginatedList) return false + return page == other.page && hasNextPage == other.hasNextPage + } + + override fun hashCode(): Int = Objects.hash(page, hasNextPage) +} const val PAGINATION_FACTOR = 50 diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index 4e12490ac..f164a493f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -22,7 +22,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.local.LocalSource import io.github.config4k.toConfig import io.github.oshai.kotlinlogging.KotlinLogging -import io.javalin.json.JavalinJackson +import io.javalin.json.JavalinJackson3 import io.javalin.json.JsonMapper import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -222,7 +222,7 @@ fun serverModule(applicationDirs: ApplicationDirs): Module = module { single { applicationDirs } single { Updater() } - single { JavalinJackson() } + single { JavalinJackson3() } } @OptIn(DelicateCoroutinesApi::class) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBManager.kt index d83b35a2d..2891274cd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBManager.kt @@ -144,14 +144,15 @@ object DBManager { private val logger = KotlinLogging.logger {} -fun databaseUp() { +fun databaseUp(givenDb: Database? = null) { val db = - try { - DBManager.setupDatabase() - } catch (e: Exception) { - logger.error(e) { "Failed to setup Database" } - return - } + givenDb + ?: try { + DBManager.setupDatabase() + } catch (e: Exception) { + logger.error(e) { "Failed to setup Database" } + return + } logger.info { "Using ${db.vendor} database version ${db.version}" diff --git a/server/src/test/kotlin/masstest/CloudFlareTest.kt b/server/src/test/kotlin/masstest/CloudFlareTest.kt index 3a02bfc0a..fd9f1cb0a 100644 --- a/server/src/test/kotlin/masstest/CloudFlareTest.kt +++ b/server/src/test/kotlin/masstest/CloudFlareTest.kt @@ -1,17 +1,22 @@ package masstest +import android.os.Looper import eu.kanade.tachiyomi.source.online.HttpSource import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.koin.core.context.stopKoin import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.server.applicationSetup +import suwayomi.tachidesk.server.settings.SettingsRegistry import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.setLoggingEnabled import xyz.nulldev.ts.config.CONFIG_PREFIX @@ -25,8 +30,11 @@ class CloudFlareTest { fun setup() { val dataRoot = File(BASE_PATH).absolutePath System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) + Looper.clearMainLooperForTest() + SettingsRegistry.clear() applicationSetup() setLoggingEnabled(false) + return runBlocking { val extensions = ExtensionsList.getExtensionList() @@ -48,9 +56,15 @@ class CloudFlareTest { setLoggingEnabled(true) } + @AfterAll + fun teardown() { + stopKoin() + } + private val logger = KotlinLogging.logger {} @Test + @Disabled fun `test nhentai browse`() = runTest { assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) { diff --git a/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt b/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt index f50551124..cc569bb11 100644 --- a/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt +++ b/server/src/test/kotlin/masstest/TestExtensionCompatibility.kt @@ -7,6 +7,7 @@ package masstest * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import android.os.Looper import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource @@ -17,9 +18,11 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.koin.core.context.stopKoin import suwayomi.tachidesk.manga.impl.Source.getSourceList import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension @@ -28,6 +31,7 @@ import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.server.applicationSetup +import suwayomi.tachidesk.server.settings.SettingsRegistry import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.setLoggingEnabled import xyz.nulldev.ts.config.CONFIG_PREFIX @@ -51,6 +55,8 @@ class TestExtensionCompatibility { fun setup() { val dataRoot = File(BASE_PATH).absolutePath System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) + Looper.clearMainLooperForTest() + SettingsRegistry.clear() applicationSetup() setLoggingEnabled(false) @@ -72,12 +78,22 @@ class TestExtensionCompatibility { } } } - sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource } + sources = + getSourceList() + .filter { + // filter local source + it.id.toLong() != 0L + }.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource } } setLoggingEnabled(true) File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" }) } + @AfterAll + fun teardown() { + stopKoin() + } + @Test fun runTest() { runBlocking(Dispatchers.Default) { diff --git a/server/src/test/kotlin/suwayomi/tachidesk/LooperTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/LooperTest.kt new file mode 100644 index 000000000..321b30122 --- /dev/null +++ b/server/src/test/kotlin/suwayomi/tachidesk/LooperTest.kt @@ -0,0 +1,56 @@ +package suwayomi.tachidesk + +import android.os.Handler +import android.os.Looper +import android.os.Message +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +import kotlin.text.StringBuilder + +class LooperThread : Thread() { + var mHandler: Handler? = null + val latch = CountDownLatch(1) + + override fun run() { + Looper.prepare() + mHandler = Handler(Looper.myLooper()) + latch.countDown() + Looper.loop() + } +} + +class LooperTest { + @Test + fun multiplePostWork() { + val thread = LooperThread() + thread.start() + val sb = StringBuilder() + val latch = CountDownLatch(1) + assertTrue(thread.latch.await(5, TimeUnit.SECONDS)) + + thread.mHandler!!.post { + Thread.sleep(100) + sb.append("a_b_c") + } + thread.mHandler!!.post { + Thread.sleep(100) + sb.append("_d_e_f") + } + thread.mHandler!!.post { + Thread.sleep(100) + sb.append("_g_h_i") + latch.countDown() + } + + assertNotEquals("a_b_c_d_e_f_g_h_i", sb.toString()) + assertTrue(latch.await(5, TimeUnit.SECONDS)) + + assertEquals("a_b_c_d_e_f_g_h_i", sb.toString()) + thread.mHandler!!.looper.quit() + // thread.join() + } +} diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt index 589e52d96..a09fc2b68 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt @@ -18,19 +18,22 @@ import suwayomi.tachidesk.test.clearTables class CategoryControllerTest : ApplicationTest() { @Test fun categoryReorder() { + clearTables( + CategoryTable, + ) Category.createCategory("foo") Category.createCategory("bar") val cats = Category.getCategoryList() val foo = cats.asSequence().filter { it.name == "foo" }.first() val bar = cats.asSequence().filter { it.name == "bar" }.first() - assertEquals(1, foo.order) - assertEquals(2, bar.order) + assertEquals(0, foo.order) + assertEquals(1, bar.order) Category.reorderCategory(1, 2) val catsReordered = Category.getCategoryList() val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first() val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first() - assertEquals(2, fooReordered.order) - assertEquals(1, barReordered.order) + assertEquals(1, fooReordered.order) + assertEquals(0, barReordered.order) } @AfterEach diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt index 731356d99..0534e73e3 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt @@ -35,7 +35,7 @@ class CategoryMangaTest : ApplicationTest() { CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, "Manga should not have any unread chapters", ) - createChapters(mangaId, 10, false) + createChapters(mangaId, 10, false, start = 11) assertEquals( 10, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, diff --git a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt index 9adab3bcf..b10ac751a 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt @@ -12,9 +12,12 @@ import eu.kanade.tachiyomi.createAppModule import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.local.LocalSource import io.github.oshai.kotlinlogging.KotlinLogging +import org.jetbrains.exposed.v1.core.DatabaseConfig +import org.jetbrains.exposed.v1.core.ExperimentalKeywordApi import org.jetbrains.exposed.v1.jdbc.Database import org.junit.jupiter.api.BeforeAll import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.ServerConfig @@ -22,7 +25,9 @@ import suwayomi.tachidesk.server.androidCompat import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverModule +import suwayomi.tachidesk.server.settings.SettingsRegistry import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex +import suwayomi.tachidesk.server.util.ConfigTypeRegistration import suwayomi.tachidesk.server.util.SystemTray import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -55,6 +60,13 @@ open class ApplicationTest { private var initializedTheApp = false fun testingSetup() { + // register Tachidesk's config which is dubbed "ServerConfig" + SettingsRegistry.clear() + ConfigTypeRegistration.registerCustomTypes() + GlobalConfigManager.registerModule( + ServerConfig.register { GlobalConfigManager.config }, + ) + // Application dirs val applicationDirs = ApplicationDirs() @@ -72,13 +84,9 @@ open class ApplicationTest { File(it).mkdirs() } - // register Tachidesk's config which is dubbed "ServerConfig" - GlobalConfigManager.registerModule( - ServerConfig.register { GlobalConfigManager.config }, - ) - // initialize Koin modules val app = App() + stopKoin() startKoin { modules( createAppModule(app), @@ -128,14 +136,14 @@ open class ApplicationTest { } // create system tray - if (serverConfig.systemTrayEnabled.value) { - try { - SystemTray.create() - } catch (e: Throwable) { - // cover both java.lang.Exception and java.lang.Error - e.printStackTrace() - } - } + // if (serverConfig.systemTrayEnabled.value) { + // try { + // SystemTray.create() + // } catch (e: Throwable) { + // // cover both java.lang.Exception and java.lang.Error + // e.printStackTrace() + // } + // } // Disable jetty's logging System.setProperty("org.eclipse.jetty.util.log.announce", "false") @@ -154,8 +162,16 @@ open class ApplicationTest { // fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase() Locale.setDefault(Locale.ENGLISH) + val dbConfig = + DatabaseConfig { + useNestedTransactions = true + @OptIn(ExperimentalKeywordApi::class) + preserveKeywordCasing = false + defaultSchema = null + } + // in-memory database, don't discard database between connections/transactions - val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver") + val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver", databaseConfig = dbConfig) databaseUp(db) diff --git a/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt b/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt index eb59d9370..de9a0c064 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt @@ -55,8 +55,9 @@ fun createChapters( mangaId: Int, amount: Int, read: Boolean, + start: Int = 1, ) { - val list = listOf((0 until amount)).flatten().map { 1 } + val list = listOf((0 until amount)).flatten().map { it + start } transaction { ChapterTable .batchInsert(list) {