* Fix test setup

* Fix tests

* Disable broken CloudflareTest

* Add a basic test for Android's Looper
This commit is contained in:
Constantin Piber
2026-05-18 20:04:39 +02:00
committed by GitHub
parent 762d5bdbe6
commit 779229a48a
11 changed files with 149 additions and 31 deletions

View File

@@ -88,4 +88,6 @@ object SettingsRegistry {
fun get(name: String): SettingMetadata? = settings[name] fun get(name: String): SettingMetadata? = settings[name]
fun getAll(): Map<String, SettingMetadata> = settings.toMap() fun getAll(): Map<String, SettingMetadata> = settings.toMap()
fun clear() = settings.clear()
} }

View File

@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import java.util.Objects
import kotlin.math.min import kotlin.math.min
open class PaginatedList<T>( open class PaginatedList<T>(
val page: List<T>, val page: List<T>,
val hasNextPage: Boolean, val hasNextPage: Boolean,
) ) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PaginatedList<T>) return false
return page == other.page && hasNextPage == other.hasNextPage
}
override fun hashCode(): Int = Objects.hash(page, hasNextPage)
}
const val PAGINATION_FACTOR = 50 const val PAGINATION_FACTOR = 50

View File

@@ -22,7 +22,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.config4k.toConfig import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson3
import io.javalin.json.JsonMapper import io.javalin.json.JsonMapper
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -222,7 +222,7 @@ fun serverModule(applicationDirs: ApplicationDirs): Module =
module { module {
single { applicationDirs } single { applicationDirs }
single<IUpdater> { Updater() } single<IUpdater> { Updater() }
single<JsonMapper> { JavalinJackson() } single<JsonMapper> { JavalinJackson3() }
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)

View File

@@ -144,14 +144,15 @@ object DBManager {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun databaseUp() { fun databaseUp(givenDb: Database? = null) {
val db = val db =
try { givenDb
DBManager.setupDatabase() ?: try {
} catch (e: Exception) { DBManager.setupDatabase()
logger.error(e) { "Failed to setup Database" } } catch (e: Exception) {
return logger.error(e) { "Failed to setup Database" }
} return
}
logger.info { logger.info {
"Using ${db.vendor} database version ${db.version}" "Using ${db.vendor} database version ${db.version}"

View File

@@ -1,17 +1,22 @@
package masstest package masstest
import android.os.Looper
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
import suwayomi.tachidesk.test.setLoggingEnabled import suwayomi.tachidesk.test.setLoggingEnabled
import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.CONFIG_PREFIX
@@ -25,8 +30,11 @@ class CloudFlareTest {
fun setup() { fun setup() {
val dataRoot = File(BASE_PATH).absolutePath val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
Looper.clearMainLooperForTest()
SettingsRegistry.clear()
applicationSetup() applicationSetup()
setLoggingEnabled(false) setLoggingEnabled(false)
return
runBlocking { runBlocking {
val extensions = ExtensionsList.getExtensionList() val extensions = ExtensionsList.getExtensionList()
@@ -48,9 +56,15 @@ class CloudFlareTest {
setLoggingEnabled(true) setLoggingEnabled(true)
} }
@AfterAll
fun teardown() {
stopKoin()
}
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@Test @Test
@Disabled
fun `test nhentai browse`() = fun `test nhentai browse`() =
runTest { runTest {
assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) { assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {

View File

@@ -7,6 +7,7 @@ package masstest
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -17,9 +18,11 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.manga.impl.Source.getSourceList import suwayomi.tachidesk.manga.impl.Source.getSourceList
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension 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.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
import suwayomi.tachidesk.test.setLoggingEnabled import suwayomi.tachidesk.test.setLoggingEnabled
import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.CONFIG_PREFIX
@@ -51,6 +55,8 @@ class TestExtensionCompatibility {
fun setup() { fun setup() {
val dataRoot = File(BASE_PATH).absolutePath val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
Looper.clearMainLooperForTest()
SettingsRegistry.clear()
applicationSetup() applicationSetup()
setLoggingEnabled(false) 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) setLoggingEnabled(true)
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" }) File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
} }
@AfterAll
fun teardown() {
stopKoin()
}
@Test @Test
fun runTest() { fun runTest() {
runBlocking(Dispatchers.Default) { runBlocking(Dispatchers.Default) {

View File

@@ -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()
}
}

View File

@@ -18,19 +18,22 @@ import suwayomi.tachidesk.test.clearTables
class CategoryControllerTest : ApplicationTest() { class CategoryControllerTest : ApplicationTest() {
@Test @Test
fun categoryReorder() { fun categoryReorder() {
clearTables(
CategoryTable,
)
Category.createCategory("foo") Category.createCategory("foo")
Category.createCategory("bar") Category.createCategory("bar")
val cats = Category.getCategoryList() val cats = Category.getCategoryList()
val foo = cats.asSequence().filter { it.name == "foo" }.first() val foo = cats.asSequence().filter { it.name == "foo" }.first()
val bar = cats.asSequence().filter { it.name == "bar" }.first() val bar = cats.asSequence().filter { it.name == "bar" }.first()
assertEquals(1, foo.order) assertEquals(0, foo.order)
assertEquals(2, bar.order) assertEquals(1, bar.order)
Category.reorderCategory(1, 2) Category.reorderCategory(1, 2)
val catsReordered = Category.getCategoryList() val catsReordered = Category.getCategoryList()
val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first() val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first()
val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first() val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first()
assertEquals(2, fooReordered.order) assertEquals(1, fooReordered.order)
assertEquals(1, barReordered.order) assertEquals(0, barReordered.order)
} }
@AfterEach @AfterEach

View File

@@ -35,7 +35,7 @@ class CategoryMangaTest : ApplicationTest() {
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
"Manga should not have any unread chapters", "Manga should not have any unread chapters",
) )
createChapters(mangaId, 10, false) createChapters(mangaId, 10, false, start = 11)
assertEquals( assertEquals(
10, 10,
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,

View File

@@ -12,9 +12,12 @@ import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.oshai.kotlinlogging.KotlinLogging 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.jetbrains.exposed.v1.jdbc.Database
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
@@ -22,7 +25,9 @@ import suwayomi.tachidesk.server.androidCompat
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.serverModule import suwayomi.tachidesk.server.serverModule
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
import suwayomi.tachidesk.server.util.SystemTray import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -55,6 +60,13 @@ open class ApplicationTest {
private var initializedTheApp = false private var initializedTheApp = false
fun testingSetup() { fun testingSetup() {
// register Tachidesk's config which is dubbed "ServerConfig"
SettingsRegistry.clear()
ConfigTypeRegistration.registerCustomTypes()
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
)
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
@@ -72,13 +84,9 @@ open class ApplicationTest {
File(it).mkdirs() File(it).mkdirs()
} }
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
)
// initialize Koin modules // initialize Koin modules
val app = App() val app = App()
stopKoin()
startKoin { startKoin {
modules( modules(
createAppModule(app), createAppModule(app),
@@ -128,14 +136,14 @@ open class ApplicationTest {
} }
// create system tray // create system tray
if (serverConfig.systemTrayEnabled.value) { // if (serverConfig.systemTrayEnabled.value) {
try { // try {
SystemTray.create() // SystemTray.create()
} catch (e: Throwable) { // } catch (e: Throwable) {
// cover both java.lang.Exception and java.lang.Error // // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() // e.printStackTrace()
} // }
} // }
// Disable jetty's logging // Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false") 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() // fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
Locale.setDefault(Locale.ENGLISH) 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 // 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) databaseUp(db)

View File

@@ -55,8 +55,9 @@ fun createChapters(
mangaId: Int, mangaId: Int,
amount: Int, amount: Int,
read: Boolean, 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 { transaction {
ChapterTable ChapterTable
.batchInsert(list) { .batchInsert(list) {