improve tests

This commit is contained in:
Aria Moradi
2021-10-31 17:31:46 +03:30
parent d460d3ccdf
commit 5fe69becf3
9 changed files with 243 additions and 52 deletions

View File

@@ -1,204 +0,0 @@
package suwayomi
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import rx.Observable
import suwayomi.tachidesk.manga.impl.Source.getSourceList
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup
import xyz.nulldev.ts.config.CONFIG_PREFIX
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestExtensions {
private val logger = KotlinLogging.logger {}
private lateinit var extensions: List<ExtensionDataClass>
private lateinit var sources: List<HttpSource>
private val mangaToFetch = mutableListOf<Pair<HttpSource, SManga>>()
private val failedToFetch = mutableListOf<Pair<HttpSource, Exception>>()
private val mangaFailedToFetch = mutableListOf<Triple<HttpSource, SManga, Exception>>()
private val chaptersToFetch = mutableListOf<Triple<HttpSource, SManga, SChapter>>()
private val chaptersFailedToFetch = mutableListOf<Triple<HttpSource, SManga, Throwable>>()
private val chaptersPageListFailedToFetch = mutableListOf<Triple<HttpSource, Pair<SManga, SChapter>, Exception>>()
@BeforeAll
fun setup() {
val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
applicationSetup()
setLoggingEnabled(false)
runBlocking {
extensions = getExtensionList()
extensions.forEach {
when {
it.obsolete -> {
uninstallExtension(it.pkgName)
}
it.hasUpdate -> {
updateExtension(it.pkgName)
}
else -> {
uninstallExtension(it.pkgName)
installExtension(it.pkgName)
}
}
}
sources = getSourceList().map { getCatalogueSource(it.id.toLong())!! as HttpSource }
}
setLoggingEnabled(true)
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
}
@Test
fun runTest() {
runBlocking(Dispatchers.Default) {
val semaphore = Semaphore(10)
val popularCount = AtomicInteger(1)
sources.map { source ->
async {
semaphore.withPermit {
logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" }
try {
mangaToFetch += source to (
source.fetchPopularManga(1)
.awaitSingleRepeat().mangas.firstOrNull()
?: throw Exception("Source returned no manga")
)
} catch (e: Exception) {
logger.warn { "Failed to fetch popular manga from $source: ${e.message}" }
failedToFetch += source to e
}
}
}
}.awaitAll()
File("$BASE_PATH/failedToFetch.txt").writeText(
failedToFetch.joinToString("\n") { (source, exception) ->
"${source.name} (${source.lang.uppercase()}, ${source.id}):" +
" ${exception.message}"
}
)
logger.info { "Now fetching manga info from ${mangaToFetch.size} sources" }
val mangaCount = AtomicInteger(1)
mangaToFetch.map { (source, manga) ->
async {
semaphore.withPermit {
logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" }
try {
manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat())
manga.initialized = true
} catch (e: Exception) {
logger.warn {
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
}
mangaFailedToFetch += Triple(source, manga, e)
}
}
}
}.awaitAll()
File("$BASE_PATH/MangaFailedToFetch.txt").writeText(
mangaFailedToFetch.joinToString("\n") { (source, manga, exception) ->
"${source.name} (${source.lang}, ${source.id}):" +
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
" ${exception.message}"
}
)
logger.info { "Now fetching manga chapters from ${mangaToFetch.size} sources" }
val chapterCount = AtomicInteger(1)
mangaToFetch.filter { it.second.initialized }.map { (source, manga) ->
async {
semaphore.withPermit {
logger.info { "${chapterCount.getAndIncrement()} - Now fetching manga chapters from $source" }
try {
chaptersToFetch += Triple(
source,
manga,
source.fetchChapterList(manga).awaitSingleRepeat().firstOrNull() ?: throw Exception("Source returned no chapters")
)
} catch (e: Exception) {
logger.warn {
"Failed to fetch manga chapters from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
}
chaptersFailedToFetch += Triple(source, manga, e)
} catch (e: NoClassDefFoundError) {
logger.warn {
"Failed to fetch manga chapters from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
}
chaptersFailedToFetch += Triple(source, manga, e)
}
}
}
}.awaitAll()
File("$BASE_PATH/ChaptersFailedToFetch.txt").writeText(
chaptersFailedToFetch.joinToString("\n") { (source, manga, exception) ->
"${source.name} (${source.lang}, ${source.id}):" +
" ${manga.title} (${source.mangaDetailsRequest(manga).url}):" +
" ${exception.message}"
}
)
val pageListCount = AtomicInteger(1)
chaptersToFetch.map { (source, manga, chapter) ->
async {
semaphore.withPermit {
logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" }
try {
source.fetchPageList(chapter).awaitSingleRepeat()
} catch (e: Exception) {
logger.warn {
"Failed to fetch manga info from $source for ${manga.title} (${source.mangaDetailsRequest(manga).url}): ${e.message}"
}
chaptersPageListFailedToFetch += Triple(source, manga to chapter, e)
}
}
}
}.awaitAll()
File("$BASE_PATH/ChapterPageListFailedToFetch.txt").writeText(
chaptersPageListFailedToFetch.joinToString("\n") { (source, manga, exception) ->
"${source.name} (${source.lang}, ${source.id}):" +
" ${manga.first.title} (${source.mangaDetailsRequest(manga.first).url}):" +
" ${manga.second.name} (${manga.second.url}): ${exception.message}"
}
)
}
}
private suspend fun <T> Observable<T>.awaitSingleRepeat(): T {
for (i in 1..2) {
try {
return awaitSingle()
} catch (e: Exception) {}
}
return awaitSingle()
}
}

View File

@@ -1,21 +0,0 @@
package suwayomi
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 ch.qos.logback.classic.Level
import mu.KotlinLogging
import org.slf4j.Logger
fun setLoggingEnabled(enabled: Boolean = true) {
val logger = (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger)
logger.level = if (enabled) {
Level.DEBUG
} else Level.ERROR
}
const val BASE_PATH = "build/tmp/TestDesk"

View File

@@ -0,0 +1,157 @@
package suwayomi.tachidesk
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.source.local.LocalSource
import mu.KotlinLogging
import org.jetbrains.exposed.sql.Database
import org.junit.jupiter.api.BeforeAll
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.conf.global
import org.kodein.di.singleton
import masstest.BASE_PATH
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.JavalinSetup
import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.androidCompat
import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.systemTrayInstance
import suwayomi.tachidesk.server.util.AppMutex
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.CONFIG_PREFIX
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
import java.util.Locale
open class ApplicationTest {
companion object {
@BeforeAll
@JvmStatic
fun beforeAll() {
if (!initializedTheApp) {
val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
testingSetup()
databaseSetup()
initializedTheApp = true
}
}
private val logger = KotlinLogging.logger {}
private var initializedTheApp = false
fun testingSetup() {
// Application dirs
val applicationDirs = ApplicationDirs()
DI.global.addImport(
DI.Module("Server") {
bind<ApplicationDirs>() with singleton { applicationDirs }
}
)
logger.debug("Data Root directory is set to: ${applicationDirs.dataRoot}")
// make dirs we need
listOf(
applicationDirs.dataRoot,
applicationDirs.extensionsRoot,
applicationDirs.extensionsRoot + "/icon",
applicationDirs.mangaThumbnailsRoot,
applicationDirs.animeThumbnailsRoot,
applicationDirs.mangaRoot,
applicationDirs.localMangaRoot,
).forEach {
File(it).mkdirs()
}
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// Make sure only one instance of the app is running
AppMutex.handleAppMutex()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
// create conf file if doesn't exist
try {
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
if (!dataConfFile.exists()) {
JavalinSetup::class.java.getResourceAsStream("/server-reference.conf").use { input ->
dataConfFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
logger.error("Exception while creating initial server.conf", e)
}
// copy local source icon
try {
val localSourceIconFile = File("${applicationDirs.extensionsRoot}/icon/localSource.png")
if (!localSourceIconFile.exists()) {
JavalinSetup::class.java.getResourceAsStream("/icon/localSource.png").use { input ->
localSourceIconFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
logger.error("Exception while copying Local source's icon", e)
}
// create system tray
if (serverConfig.systemTrayEnabled) {
try {
systemTrayInstance
} 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")
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings
if (serverConfig.socksProxyEnabled) {
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
}
}
fun databaseSetup() {
// fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
Locale.setDefault(Locale.ENGLISH)
// 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")
databaseUp(db)
LocalSource.addDbRecords()
}
}
}

View File

@@ -1,27 +1,22 @@
package suwayomi.tachidesk.manga.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import suwayomi.BASE_PATH
import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.server.applicationSetup
import xyz.nulldev.ts.config.CONFIG_PREFIX
import java.io.File
internal class CategoryControllerTest {
@BeforeEach
internal fun setUp() {
val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
applicationSetup()
}
import suwayomi.tachidesk.ApplicationTest
internal class CategoryControllerTest : ApplicationTest() {
@Test
fun categoryReorder() {
Category.createCategory("foo")

View File

@@ -0,0 +1,46 @@
package suwayomi.tachidesk.manga.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* 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 org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.ApplicationTest
@TestInstance(PER_CLASS)
internal class SourceControllerTest : ApplicationTest() {
@Test
fun categoryReorder() {
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)
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)
}
@AfterEach
internal fun tearDown() {
transaction {
CategoryTable.deleteAll()
}
}
}

View File

@@ -13,10 +13,9 @@ import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import suwayomi.BASE_PATH
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
@@ -26,59 +25,49 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable.name
import suwayomi.tachidesk.manga.model.table.ChapterTable.sourceOrder
import suwayomi.tachidesk.manga.model.table.ChapterTable.url
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.applicationSetup
import xyz.nulldev.ts.config.CONFIG_PREFIX
import java.io.File
import suwayomi.tachidesk.ApplicationTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CategoryMangaTest {
@BeforeEach
fun setUp() {
val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
applicationSetup()
}
class CategoryMangaTest : ApplicationTest() {
@Test
fun getCategoryMangaList() {
val emptyCats = CategoryManga.getCategoryMangaList(0).size
val emptyCats = CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size
assertEquals(0, emptyCats, "Default category should be empty at start")
val mangaId = createManga("Psyren")
val mangaId = createLibraryManga("Psyren")
createChapters(mangaId, 10, true)
assertEquals(1, CategoryManga.getCategoryMangaList(0).size, "Default category should have one member")
assertEquals(1, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size, "Default category should have one member")
assertEquals(
0, CategoryManga.getCategoryMangaList(0)[0].unreadCount,
0, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
"Manga should not have any unread chapters"
)
createChapters(mangaId, 10, false)
assertEquals(
10, CategoryManga.getCategoryMangaList(0)[0].unreadCount,
10, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
"Manga should have unread chapters"
)
Category.createCategory("Old") // category id 1
val categoryId = Category.createCategory("Old")
assertEquals(
0,
CategoryManga.getCategoryMangaList(1).size,
CategoryManga.getCategoryMangaList(categoryId).size,
"Newly created category shouldn't have any Mangas"
)
CategoryManga.addMangaToCategory(mangaId, 1)
CategoryManga.addMangaToCategory(mangaId, categoryId)
assertEquals(
1, CategoryManga.getCategoryMangaList(1).size,
1, CategoryManga.getCategoryMangaList(categoryId).size,
"Manga should been moved"
)
assertEquals(
10, CategoryManga.getCategoryMangaList(1)[0].unreadCount,
10, CategoryManga.getCategoryMangaList(categoryId)[0].unreadCount,
"Manga should keep it's unread count in moved category"
)
assertEquals(
0, CategoryManga.getCategoryMangaList(0).size,
0, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size,
"Manga shouldn't be member of default category after moving"
)
}
private fun createManga(
private fun createLibraryManga(
_title: String
): Int {
return transaction {