Переглянути джерело

Update chapter recognition and related tests

Includes 3e07100dc2725cb2d42050571232dd5d485b4de5

Co-authored-by: Saud-97 <[email protected]>
arkon 2 роки тому
батько
коміт
4a71022a60

+ 7 - 3
.github/workflows/build_pull_request.yml

@@ -5,6 +5,10 @@ on:
       - '**.md'
       - 'app/src/main/res/**/strings.xml'
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+  cancel-in-progress: true
+
 permissions:
   contents: read
 
@@ -21,7 +25,7 @@ jobs:
         uses: gradle/wrapper-validation-action@v1
 
       - name: Dependency Review
-        uses: actions/dependency-review-action@v1
+        uses: actions/dependency-review-action@v2
 
       - name: Set up JDK 11
         uses: actions/setup-java@v3
@@ -29,7 +33,7 @@ jobs:
           java-version: 11
           distribution: adopt
 
-      - name: Build app
+      - name: Build app and run unit tests
         uses: gradle/gradle-command-action@v2
         with:
-          arguments: assembleStandardRelease
+          arguments: assembleStandardRelease testStandardReleaseUnitTest

+ 6 - 8
.github/workflows/build_push.yml

@@ -6,18 +6,16 @@ on:
     tags:
       - v*
 
+concurrency:
+  group: ${{ github.workflow }}
+  cancel-in-progress: true
+
 jobs:
   build:
     name: Build app
     runs-on: ubuntu-latest
 
     steps:
-      - name: Cancel previous runs
-        uses: styfle/[email protected]
-        with:
-          access_token: ${{ github.token }}
-          all_but_latest: true
-
       - name: Clone repo
         uses: actions/checkout@v3
 
@@ -30,10 +28,10 @@ jobs:
           java-version: 11
           distribution: adopt
 
-      - name: Build app
+      - name: Build app and run unit tests
         uses: gradle/gradle-command-action@v2
         with:
-          arguments: assembleStandardRelease
+          arguments: assembleStandardRelease testStandardReleaseUnitTest
 
       # Sign APK and create release for tags
 

+ 0 - 16
.github/workflows/cancel_pull_request.yml

@@ -1,16 +0,0 @@
-name: Cancel old pull request workflows
-
-on:
-  workflow_run:
-    workflows: ["PR build check"]
-    types:
-      - requested
-
-jobs:
-  cancel:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: styfle/[email protected]
-      with:
-        all_but_latest: true
-        workflow_id: ${{ github.event.workflow.id }}

+ 8 - 4
app/build.gradle.kts

@@ -1,3 +1,4 @@
+import org.gradle.api.tasks.testing.logging.TestLogEvent
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -242,16 +243,19 @@ dependencies {
 
     // Tests
     testImplementation(libs.junit)
-    testImplementation(libs.assertj.core)
-    testImplementation(libs.mockito.core)
-
-    testImplementation(libs.bundles.robolectric)
 
     // For detecting memory leaks; see https://square.github.io/leakcanary/
     // debugImplementation(libs.leakcanary.android)
 }
 
 tasks {
+    withType<Test> {
+        useJUnitPlatform()
+        testLogging {
+            events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
+        }
+    }
+
     // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
     withType<KotlinCompile> {
         kotlinOptions.freeCompilerArgs += listOf(

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt

@@ -46,8 +46,8 @@ object ChapterRecognition {
         // Get chapter title with lower case
         var name = chapter.name.lowercase()
 
-        // Remove comma's from chapter.
-        name = name.replace(',', '.')
+        // Remove comma's or hyphens.
+        name = name.replace(',', '.').replace('-', '.')
 
         // Remove unwanted white spaces.
         unwantedWhiteSpace.findAll(name).let {

+ 0 - 12
app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt

@@ -1,12 +0,0 @@
-package eu.kanade.tachiyomi
-
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.manifest.AndroidManifest
-
-class CustomRobolectricGradleTestRunner(klass: Class<*>) : RobolectricTestRunner(klass) {
-
-    override fun getAppManifest(config: Config): AndroidManifest {
-        return super.getAppManifest(config).apply { packageName = "eu.kanade.tachiyomi" }
-    }
-}

+ 0 - 8
app/src/test/java/eu/kanade/tachiyomi/TestApp.kt

@@ -1,8 +0,0 @@
-package eu.kanade.tachiyomi
-
-open class TestApp : App() {
-
-    override fun setupAcra() {
-        // Do nothing
-    }
-}

+ 0 - 377
app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt

@@ -1,377 +0,0 @@
-package eu.kanade.tachiyomi.data.backup
-
-import android.app.Application
-import android.content.Context
-import android.os.Build
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
-import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
-import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
-import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.ChapterImpl
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaImpl
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.database.models.TrackImpl
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
-import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.buildJsonObject
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.RETURNS_DEEP_STUBS
-import org.mockito.Mockito.anyLong
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
-import org.robolectric.RuntimeEnvironment
-import org.robolectric.annotation.Config
-import rx.Observable
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.InjektModule
-import uy.kohesive.injekt.api.InjektRegistrar
-import uy.kohesive.injekt.api.addSingleton
-
-/**
- * Test class for the [LegacyBackupManager].
- * Note that this does not include the backup create/restore services.
- */
-@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
-@RunWith(CustomRobolectricGradleTestRunner::class)
-class BackupTest {
-    // Create root object
-    var root = Backup()
-
-    // Create information object
-    var information = buildJsonObject {}
-
-    lateinit var app: Application
-    lateinit var context: Context
-    lateinit var source: HttpSource
-
-    lateinit var legacyBackupManager: LegacyBackupManager
-
-    lateinit var db: DatabaseHelper
-
-    @Before
-    fun setup() {
-        app = RuntimeEnvironment.application
-        context = app.applicationContext
-        legacyBackupManager = LegacyBackupManager(context, 2)
-        db = legacyBackupManager.databaseHelper
-
-        // Mock the source manager
-        val module = object : InjektModule {
-            override fun InjektRegistrar.registerInjectables() {
-                addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS))
-            }
-        }
-        Injekt.importModule(module)
-
-        source = mock(HttpSource::class.java)
-        `when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source)
-    }
-
-    /**
-     * Test that checks if no crashes when no categories in library.
-     */
-    @Test
-    fun testRestoreEmptyCategory() {
-        // Restore Json
-        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
-
-        // Check if empty
-        val dbCats = db.getCategories().executeAsBlocking()
-        assertThat(dbCats).isEmpty()
-    }
-
-    /**
-     * Test to check if single category gets restored
-     */
-    @Test
-    fun testRestoreSingleCategory() {
-        // Create category and add to json
-        val category = addSingleCategory("category")
-
-        // Restore Json
-        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
-
-        // Check if successful
-        val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
-        assertThat(dbCats).hasSize(1)
-        assertThat(dbCats[0].name).isEqualTo(category.name)
-    }
-
-    /**
-     * Test to check if multiple categories get restored.
-     */
-    @Test
-    fun testRestoreMultipleCategories() {
-        // Create category and add to json
-        val category = addSingleCategory("category")
-        val category2 = addSingleCategory("category2")
-        val category3 = addSingleCategory("category3")
-        val category4 = addSingleCategory("category4")
-        val category5 = addSingleCategory("category5")
-
-        // Insert category to test if no duplicates on restore.
-        db.insertCategory(category).executeAsBlocking()
-
-        // Restore Json
-        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
-
-        // Check if successful
-        val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
-        assertThat(dbCats).hasSize(5)
-        assertThat(dbCats[0].name).isEqualTo(category.name)
-        assertThat(dbCats[1].name).isEqualTo(category2.name)
-        assertThat(dbCats[2].name).isEqualTo(category3.name)
-        assertThat(dbCats[3].name).isEqualTo(category4.name)
-        assertThat(dbCats[4].name).isEqualTo(category5.name)
-    }
-
-    /**
-     * Test if restore of manga is successful
-     */
-    @Test
-    fun testRestoreManga() {
-        // Add manga to database
-        val manga = getSingleManga("One Piece")
-        manga.readingModeType = ReadingModeType.VERTICAL.flagValue
-        manga.orientationType = OrientationType.PORTRAIT.flagValue
-        manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
-
-        var favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
-        assertThat(favoriteManga).hasSize(1)
-        assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
-        assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
-
-        // Change manga in database to default values
-        val dbManga = getSingleManga("One Piece")
-        dbManga.id = manga.id
-        db.insertManga(dbManga).executeAsBlocking()
-
-        favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
-        assertThat(favoriteManga).hasSize(1)
-        assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.DEFAULT.flagValue)
-        assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.DEFAULT.flagValue)
-
-        // Restore local manga
-        legacyBackupManager.restoreMangaNoFetch(manga, dbManga)
-
-        // Test if restore successful
-        favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
-        assertThat(favoriteManga).hasSize(1)
-        assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
-        assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
-
-        // Clear database to test manga fetch
-        clearDatabase()
-
-        // Test if successful
-        favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
-        assertThat(favoriteManga).hasSize(0)
-
-        // Restore Json
-        // Create JSON from manga to test parser
-        val json = legacyBackupManager.parser.encodeToString(manga)
-        // Restore JSON from manga to test parser
-        val jsonManga = legacyBackupManager.parser.decodeFromString<Manga>(json)
-
-        // Restore manga with fetch observable
-        val networkManga = getSingleManga("One Piece")
-        networkManga.description = "This is a description"
-        `when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
-
-        runBlocking {
-            legacyBackupManager.fetchManga(source, jsonManga)
-
-            // Check if restore successful
-            val dbCats = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
-            assertThat(dbCats).hasSize(1)
-            assertThat(dbCats[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
-            assertThat(dbCats[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
-            assertThat(dbCats[0].description).isEqualTo("This is a description")
-        }
-    }
-
-    /**
-     * Test if chapter restore is successful
-     */
-    @Test
-    fun testRestoreChapters() {
-        // Insert manga
-        val manga = getSingleManga("One Piece")
-        manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
-
-        // Create restore list
-        val chapters = mutableListOf<Chapter>()
-        for (i in 1..8) {
-            val chapter = getSingleChapter("Chapter $i")
-            chapter.read = true
-            chapters.add(chapter)
-        }
-
-        // Check parser
-        val chaptersJson = legacyBackupManager.parser.encodeToString(chapters)
-        val restoredChapters = legacyBackupManager.parser.decodeFromString<List<Chapter>>(chaptersJson)
-
-        // Fetch chapters from upstream
-        // Create list
-        val chaptersRemote = mutableListOf<Chapter>()
-        (1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
-        `when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
-
-        runBlocking {
-            legacyBackupManager.restoreChapters(source, manga, restoredChapters)
-
-            val dbCats = legacyBackupManager.databaseHelper.getChapters(manga).executeAsBlocking()
-            assertThat(dbCats).hasSize(10)
-            assertThat(dbCats[0].read).isEqualTo(true)
-        }
-    }
-
-    /**
-     * Test to check if history restore works
-     */
-    @Test
-    fun restoreHistoryForManga() {
-        val manga = getSingleManga("One Piece")
-        manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
-
-        // Create chapter
-        val chapter = getSingleChapter("Chapter 1")
-        chapter.manga_id = manga.id
-        chapter.read = true
-        chapter.id = legacyBackupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId()
-
-        val historyJson = getSingleHistory(chapter)
-
-        val historyList = mutableListOf<DHistory>()
-        historyList.add(historyJson)
-
-        // Check parser
-        val historyListJson = legacyBackupManager.parser.encodeToString(historyList)
-        val history = legacyBackupManager.parser.decodeFromString<List<DHistory>>(historyListJson)
-
-        // Restore categories
-        legacyBackupManager.restoreHistoryForManga(history)
-
-        val historyDB = legacyBackupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
-        assertThat(historyDB).hasSize(1)
-        assertThat(historyDB[0].last_read).isEqualTo(1000)
-    }
-
-    /**
-     * Test to check if tracking restore works
-     */
-    @Test
-    fun restoreTrackForManga() {
-        // Create mangas
-        val manga = getSingleManga("One Piece")
-        val manga2 = getSingleManga("Bleach")
-        manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
-        manga2.id = legacyBackupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId()
-
-        // Create track and add it to database
-        // This tests duplicate errors.
-        val track = getSingleTrack(manga)
-        track.last_chapter_read = 5F
-        legacyBackupManager.databaseHelper.insertTrack(track).executeAsBlocking()
-        var trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
-        assertThat(trackDB).hasSize(1)
-        assertThat(trackDB[0].last_chapter_read).isEqualTo(5)
-        track.last_chapter_read = 7F
-
-        // Create track for different manga to test track not in database
-        val track2 = getSingleTrack(manga2)
-        track2.last_chapter_read = 10F
-
-        // Check parser and restore already in database
-        var trackList = listOf(track)
-        // Check parser
-        var trackListJson = legacyBackupManager.parser.encodeToString(trackList)
-        var trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
-        legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
-
-        // Assert if restore works.
-        trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
-        assertThat(trackDB).hasSize(1)
-        assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
-
-        // Check parser and restore already in database with lower chapter_read
-        track.last_chapter_read = 5F
-        trackList = listOf(track)
-        legacyBackupManager.restoreTrackForManga(manga, trackList)
-
-        // Assert if restore works.
-        trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking()
-        assertThat(trackDB).hasSize(1)
-        assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
-
-        // Check parser and restore, track not in database
-        trackList = listOf(track2)
-
-        // Check parser
-        trackListJson = legacyBackupManager.parser.encodeToString(trackList)
-        trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
-        legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
-
-        // Assert if restore works.
-        trackDB = legacyBackupManager.databaseHelper.getTracks(manga2).executeAsBlocking()
-        assertThat(trackDB).hasSize(1)
-        assertThat(trackDB[0].last_chapter_read).isEqualTo(10)
-    }
-
-    private fun clearJson() {
-        root = Backup()
-        information = buildJsonObject {}
-    }
-
-    private fun addSingleCategory(name: String): Category {
-        val category = Category.create(name)
-        root.categories = listOf(category)
-        return category
-    }
-
-    private fun clearDatabase() {
-        db.deleteMangas().executeAsBlocking()
-        db.deleteHistory().executeAsBlocking()
-    }
-
-    private fun getSingleHistory(chapter: Chapter): DHistory {
-        return DHistory(chapter.url, 1000)
-    }
-
-    private fun getSingleTrack(manga: Manga): TrackImpl {
-        val track = TrackImpl()
-        track.title = manga.title
-        track.manga_id = manga.id!!
-        track.sync_id = 1
-        return track
-    }
-
-    private fun getSingleManga(title: String): MangaImpl {
-        val manga = MangaImpl()
-        manga.source = 1
-        manga.title = title
-        manga.url = "/manga/$title"
-        manga.favorite = true
-        return manga
-    }
-
-    private fun getSingleChapter(name: String): ChapterImpl {
-        val chapter = ChapterImpl()
-        chapter.name = name
-        chapter.url = "/read-online/$name-page-1.html"
-        return chapter
-    }
-}

+ 0 - 109
app/src/test/java/eu/kanade/tachiyomi/data/database/CategoryTest.kt

@@ -1,109 +0,0 @@
-package eu.kanade.tachiyomi.data.database
-
-import android.os.Build
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
-import eu.kanade.tachiyomi.data.database.models.CategoryImpl
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.RuntimeEnvironment
-import org.robolectric.annotation.Config
-
-@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
-@RunWith(CustomRobolectricGradleTestRunner::class)
-class CategoryTest {
-
-    lateinit var db: DatabaseHelper
-
-    @Before
-    fun setup() {
-        val app = RuntimeEnvironment.application
-        db = DatabaseHelper(app)
-
-        // Create 5 manga
-        createManga("a")
-        createManga("b")
-        createManga("c")
-        createManga("d")
-        createManga("e")
-    }
-
-    @Test
-    fun testHasCategories() {
-        // Create 2 categories
-        createCategory("Reading")
-        createCategory("Hold")
-
-        val categories = db.getCategories().executeAsBlocking()
-        assertThat(categories).hasSize(2)
-    }
-
-    @Test
-    fun testHasLibraryMangas() {
-        val mangas = db.getLibraryMangas().executeAsBlocking()
-        assertThat(mangas).hasSize(5)
-    }
-
-    @Test
-    fun testHasCorrectFavorites() {
-        val m = Manga.create(0)
-        m.title = "title"
-        m.author = ""
-        m.artist = ""
-        m.thumbnail_url = ""
-        m.genre = "a list of genres"
-        m.description = "long description"
-        m.url = "url to manga"
-        m.favorite = false
-        db.insertManga(m).executeAsBlocking()
-        val mangas = db.getLibraryMangas().executeAsBlocking()
-        assertThat(mangas).hasSize(5)
-    }
-
-    @Test
-    fun testMangaInCategory() {
-        // Create 2 categories
-        createCategory("Reading")
-        createCategory("Hold")
-
-        // It should not have 0 as id
-        val c = db.getCategories().executeAsBlocking()[0]
-        assertThat(c.id).isNotZero
-
-        // Add a manga to a category
-        val m = db.getLibraryMangas().executeAsBlocking()[0]
-        val mc = MangaCategory.create(m, c)
-        db.insertMangaCategory(mc).executeAsBlocking()
-
-        // Get mangas from library and assert manga category is the same
-        val mangas = db.getLibraryMangas().executeAsBlocking()
-        for (manga in mangas) {
-            if (manga.id == m.id) {
-                assertThat(manga.category).isEqualTo(c.id)
-            }
-        }
-    }
-
-    private fun createManga(title: String) {
-        val m = Manga.create(0)
-        m.title = title
-        m.author = ""
-        m.artist = ""
-        m.thumbnail_url = ""
-        m.genre = "a list of genres"
-        m.description = "long description"
-        m.url = "url to manga"
-        m.favorite = true
-        db.insertManga(m).executeAsBlocking()
-    }
-
-    private fun createCategory(name: String) {
-        val c = CategoryImpl()
-        c.name = name
-        db.insertCategory(c).executeAsBlocking()
-    }
-}

+ 0 - 497
app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt

@@ -1,497 +0,0 @@
-package eu.kanade.tachiyomi.data.database
-
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Before
-import org.junit.Test
-
-class ChapterRecognitionTest {
-    /**
-     * The manga containing manga title
-     */
-    lateinit var manga: Manga
-
-    /**
-     * The chapter containing chapter name
-     */
-    lateinit var chapter: Chapter
-
-    /**
-     * Set chapter title
-     * @param name name of chapter
-     * @return chapter object
-     */
-    private fun createChapter(name: String): Chapter {
-        chapter = Chapter.create()
-        chapter.name = name
-        return chapter
-    }
-
-    /**
-     * Set manga title
-     * @param title title of manga
-     * @return manga object
-     */
-    private fun createManga(title: String): Manga {
-        manga.title = title
-        return manga
-    }
-
-    /**
-     * Called before test
-     */
-    @Before
-    fun setup() {
-        manga = Manga.create(0).apply { title = "random" }
-        chapter = Chapter.create()
-    }
-
-    /**
-     * Ch.xx base case
-     */
-    @Test
-    fun ChCaseBase() {
-        createManga("Mokushiroku Alice")
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4f)
-    }
-
-    /**
-     * Ch. xx base case but space after period
-     */
-    @Test
-    fun ChCaseBase2() {
-        createManga("Mokushiroku Alice")
-
-        createChapter("Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4f)
-    }
-
-    /**
-     * Ch.xx.x base case
-     */
-    @Test
-    fun ChCaseDecimal() {
-        createManga("Mokushiroku Alice")
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4.1f)
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4.4f)
-    }
-
-    /**
-     * Ch.xx.a base case
-     */
-    @Test
-    fun ChCaseAlpha() {
-        createManga("Mokushiroku Alice")
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4.1f)
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4.2f)
-
-        createChapter("Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4.99f)
-    }
-
-    /**
-     * Name containing one number base case
-     */
-    @Test
-    fun OneNumberCaseBase() {
-        createManga("Bleach")
-
-        createChapter("Bleach 567 Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567f)
-    }
-
-    /**
-     * Name containing one number and decimal case
-     */
-    @Test
-    fun OneNumberCaseDecimal() {
-        createManga("Bleach")
-
-        createChapter("Bleach 567.1 Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567.1f)
-
-        createChapter("Bleach 567.4 Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567.4f)
-    }
-
-    /**
-     * Name containing one number and alpha case
-     */
-    @Test
-    fun OneNumberCaseAlpha() {
-        createManga("Bleach")
-
-        createChapter("Bleach 567.a Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567.1f)
-
-        createChapter("Bleach 567.b Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567.2f)
-
-        createChapter("Bleach 567.extra Down With Snowwhite")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(567.99f)
-    }
-
-    /**
-     * Chapter containing manga title and number base case
-     */
-    @Test
-    fun MangaTitleCaseBase() {
-        createManga("Solanin")
-
-        createChapter("Solanin 028 Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28f)
-    }
-
-    /**
-     * Chapter containing manga title and number decimal case
-     */
-    @Test
-    fun MangaTitleCaseDecimal() {
-        createManga("Solanin")
-
-        createChapter("Solanin 028.1 Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.1f)
-
-        createChapter("Solanin 028.4 Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.4f)
-    }
-
-    /**
-     * Chapter containing manga title and number alpha case
-     */
-    @Test
-    fun MangaTitleCaseAlpha() {
-        createManga("Solanin")
-
-        createChapter("Solanin 028.a Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.1f)
-
-        createChapter("Solanin 028.b Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.2f)
-
-        createChapter("Solanin 028.extra Vol. 2")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.99f)
-    }
-
-    /**
-     * Extreme base case
-     */
-    @Test
-    fun ExtremeCaseBase() {
-        createManga("Onepunch-Man")
-
-        createChapter("Onepunch-Man Punch Ver002 028")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28f)
-    }
-
-    /**
-     * Extreme base case decimal
-     */
-    @Test
-    fun ExtremeCaseDecimal() {
-        createManga("Onepunch-Man")
-
-        createChapter("Onepunch-Man Punch Ver002 028.1")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.1f)
-
-        createChapter("Onepunch-Man Punch Ver002 028.4")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.4f)
-    }
-
-    /**
-     * Extreme base case alpha
-     */
-    @Test
-    fun ExtremeCaseAlpha() {
-        createManga("Onepunch-Man")
-
-        createChapter("Onepunch-Man Punch Ver002 028.a")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.1f)
-
-        createChapter("Onepunch-Man Punch Ver002 028.b")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.2f)
-
-        createChapter("Onepunch-Man Punch Ver002 028.extra")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(28.99f)
-    }
-
-    /**
-     * Chapter containing .v2
-     */
-    @Test
-    fun dotV2Case() {
-        createChapter("Vol.1 Ch.5v.2: Alones")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(5f)
-    }
-
-    /**
-     * Check for case with number in manga title
-     */
-    @Test
-    fun numberInMangaTitleCase() {
-        createManga("Ayame 14")
-        createChapter("Ayame 14 1 - The summer of 14")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(1f)
-    }
-
-    /**
-     * Case with space between ch. x
-     */
-    @Test
-    fun spaceAfterChapterCase() {
-        createManga("Mokushiroku Alice")
-        createChapter("Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(4f)
-    }
-
-    /**
-     * Chapter containing mar(ch)
-     */
-    @Test
-    fun marchInChapterCase() {
-        createManga("Ayame 14")
-        createChapter("Vol.1 Ch.1: March 25 (First Day Cohabiting)")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(1f)
-    }
-
-    /**
-     * Chapter containing range
-     */
-    @Test
-    fun rangeInChapterCase() {
-        createChapter("Ch.191-200 Read Online")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(191f)
-    }
-
-    /**
-     * Chapter containing multiple zeros
-     */
-    @Test
-    fun multipleZerosCase() {
-        createChapter("Vol.001 Ch.003: Kaguya Doesn't Know Much")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(3f)
-    }
-
-    /**
-     * Chapter with version before number
-     */
-    @Test
-    fun chapterBeforeNumberCase() {
-        createManga("Onepunch-Man")
-        createChapter("Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(86f)
-    }
-
-    /**
-     * Case with version attached to chapter number
-     */
-    @Test
-    fun vAttachedToChapterCase() {
-        createManga("Ansatsu Kyoushitsu")
-        createChapter("Ansatsu Kyoushitsu 011v002: Assembly Time")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(11f)
-    }
-
-    /**
-     * Case where the chapter title contains the chapter
-     * But wait it's not actual the chapter number.
-     */
-    @Test
-    fun NumberAfterMangaTitleWithChapterInChapterTitleCase() {
-        createChapter("Tokyo ESP 027: Part 002: Chapter 001")
-        createManga("Tokyo ESP")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(027f)
-    }
-
-    /**
-     * unParsable chapter
-     */
-    @Test
-    fun unParsableCase() {
-        createChapter("Foo")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(-1f)
-    }
-
-    /**
-     * chapter with time in title
-     */
-    @Test
-    fun timeChapterCase() {
-        createChapter("Fairy Tail 404: 00:00")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404f)
-    }
-
-    /**
-     * chapter with alpha without dot
-     */
-    @Test
-    fun alphaWithoutDotCase() {
-        createChapter("Asu No Yoichi 19a")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(19.1f)
-    }
-
-    /**
-     * Chapter title containing extra and vol
-     */
-    @Test
-    fun chapterContainingExtraCase() {
-        createManga("Fairy Tail")
-
-        createChapter("Fairy Tail 404.extravol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.99f)
-
-        createChapter("Fairy Tail 404 extravol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.99f)
-
-        createChapter("Fairy Tail 404.evol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.5f)
-    }
-
-    /**
-     * Chapter title containing omake (japanese extra) and vol
-     */
-    @Test
-    fun chapterContainingOmakeCase() {
-        createManga("Fairy Tail")
-
-        createChapter("Fairy Tail 404.omakevol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.98f)
-
-        createChapter("Fairy Tail 404 omakevol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.98f)
-
-        createChapter("Fairy Tail 404.ovol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.15f)
-    }
-
-    /**
-     * Chapter title containing special and vol
-     */
-    @Test
-    fun chapterContainingSpecialCase() {
-        createManga("Fairy Tail")
-
-        createChapter("Fairy Tail 404.specialvol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.97f)
-
-        createChapter("Fairy Tail 404 specialvol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.97f)
-
-        createChapter("Fairy Tail 404.svol002")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(404.19f)
-    }
-
-    /**
-     * Chapter title containing comma's
-     */
-    @Test
-    fun chapterContainingCommasCase() {
-        createManga("One Piece")
-
-        createChapter("One Piece 300,a")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(300.1f)
-
-        createChapter("One Piece Ch,123,extra")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(123.99f)
-
-        createChapter("One Piece the sunny, goes swimming 024,005")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(24.005f)
-    }
-
-    /**
-     * Test for chapters containing season
-     */
-    @Test
-    fun chapterContainingSeasonCase() {
-        createManga("D.I.C.E")
-
-        createChapter("D.I.C.E[Season 001] Ep. 007")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(7f)
-    }
-
-    /**
-     * Test for chapters in format sx - chapter xx
-     */
-    @Test
-    fun chapterContainingSeasonCase2() {
-        createManga("The Gamer")
-
-        createChapter("S3 - Chapter 20")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(20f)
-    }
-
-    /**
-     * Test for chapters ending with s
-     */
-    @Test
-    fun chaptersEndingWithS() {
-        createManga("One Outs")
-
-        createChapter("One Outs 001")
-        ChapterRecognition.parseChapterNumber(chapter, manga)
-        assertThat(chapter.chapter_number).isEqualTo(1f)
-    }
-}

+ 0 - 136
app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt

@@ -1,136 +0,0 @@
-package eu.kanade.tachiyomi.data.library
-
-import android.app.Application
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.LibraryManga
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.online.HttpSource
-import kotlinx.coroutines.runBlocking
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Matchers.anyLong
-import org.mockito.Mockito.RETURNS_DEEP_STUBS
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
-import org.robolectric.Robolectric
-import org.robolectric.RuntimeEnvironment
-import org.robolectric.annotation.Config
-import rx.Observable
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.InjektModule
-import uy.kohesive.injekt.api.InjektRegistrar
-import uy.kohesive.injekt.api.addSingleton
-
-@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M])
-@RunWith(CustomRobolectricGradleTestRunner::class)
-class LibraryUpdateServiceTest {
-
-    lateinit var app: Application
-    lateinit var context: Context
-    lateinit var service: LibraryUpdateService
-    lateinit var source: HttpSource
-
-    @Before
-    fun setup() {
-        app = RuntimeEnvironment.application
-        context = app.applicationContext
-
-        // Mock the source manager
-        val module = object : InjektModule {
-            override fun InjektRegistrar.registerInjectables() {
-                addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS))
-            }
-        }
-        Injekt.importModule(module)
-
-        service = Robolectric.setupService(LibraryUpdateService::class.java)
-        source = mock(HttpSource::class.java)
-        `when`(service.sourceManager.get(anyLong())).thenReturn(source)
-    }
-
-    @Test
-    fun testLifecycle() {
-        // Smoke test
-        Robolectric.buildService(LibraryUpdateService::class.java)
-            .attach()
-            .create()
-            .startCommand(0, 0)
-            .destroy()
-            .get()
-    }
-
-    @Test
-    fun testUpdateManga() {
-        val manga = createManga("/manga1")[0]
-        manga.id = 1L
-        service.db.insertManga(manga).executeAsBlocking()
-
-        val sourceChapters = createChapters("/chapter1", "/chapter2")
-
-        `when`(source.fetchChapterList(manga)).thenReturn(Observable.just(sourceChapters))
-
-        runBlocking {
-            service.updateManga(manga)
-
-            assertThat(service.db.getChapters(manga).executeAsBlocking()).hasSize(2)
-        }
-    }
-
-    @Test
-    fun testContinuesUpdatingWhenAMangaFails() {
-        var favManga = createManga("/manga1", "/manga2", "/manga3")
-        service.db.insertMangas(favManga).executeAsBlocking()
-        favManga = service.db.getLibraryMangas().executeAsBlocking()
-
-        val chapters = createChapters("/chapter1", "/chapter2")
-        val chapters3 = createChapters("/achapter1", "/achapter2")
-
-        // One of the updates will fail
-        `when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters))
-        `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error(Exception()))
-        `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
-
-        val intent = Intent()
-        val categoryId = intent.getIntExtra(LibraryUpdateService.KEY_CATEGORY, -1)
-        val target = LibraryUpdateService.Target.CHAPTERS
-        runBlocking {
-            service.addMangaToQueue(categoryId, target)
-            service.updateChapterList()
-
-            // There are 3 network attempts and 2 insertions (1 request failed)
-            assertThat(service.db.getChapters(favManga[0]).executeAsBlocking()).hasSize(2)
-            assertThat(service.db.getChapters(favManga[1]).executeAsBlocking()).hasSize(0)
-            assertThat(service.db.getChapters(favManga[2]).executeAsBlocking()).hasSize(2)
-        }
-    }
-
-    private fun createChapters(vararg urls: String): List<Chapter> {
-        val list = mutableListOf<Chapter>()
-        for (url in urls) {
-            val c = Chapter.create()
-            c.url = url
-            c.name = url.substring(1)
-            list.add(c)
-        }
-        return list
-    }
-
-    private fun createManga(vararg urls: String): List<LibraryManga> {
-        val list = mutableListOf<LibraryManga>()
-        for (url in urls) {
-            val m = LibraryManga()
-            m.url = url
-            m.title = url.substring(1)
-            m.favorite = true
-            list.add(m)
-        }
-        return list
-    }
-}

+ 275 - 0
app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt

@@ -0,0 +1,275 @@
+package eu.kanade.tachiyomi.util.chapter
+
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode
+
+@Execution(ExecutionMode.CONCURRENT)
+class ChapterRecognitionTest {
+
+    @Test
+    fun `Basic Ch prefix`() {
+        val mangaTitle = "Mokushiroku Alice"
+
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4: Misrepresentation", 4f)
+    }
+
+    @Test
+    fun `Basic Ch prefix with space after period`() {
+        val mangaTitle = "Mokushiroku Alice"
+
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation", 4f)
+    }
+
+    @Test
+    fun `Basic Ch prefix with decimal`() {
+        val mangaTitle = "Mokushiroku Alice"
+
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation", 4.1f)
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation", 4.4f)
+    }
+
+    @Test
+    fun `Basic Ch prefix with alpha postfix`() {
+        val mangaTitle = "Mokushiroku Alice"
+
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation", 4.1f)
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation", 4.2f)
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation", 4.99f)
+    }
+
+    @Test
+    fun `Name containing one number`() {
+        val mangaTitle = "Bleach"
+
+        assertChapter(mangaTitle, "Bleach 567 Down With Snowwhite", 567f)
+    }
+
+    @Test
+    fun `Name containing one number and decimal`() {
+        val mangaTitle = "Bleach"
+
+        assertChapter(mangaTitle, "Bleach 567.1 Down With Snowwhite", 567.1f)
+        assertChapter(mangaTitle, "Bleach 567.4 Down With Snowwhite", 567.4f)
+    }
+
+    @Test
+    fun `Name containing one number and alpha`() {
+        val mangaTitle = "Bleach"
+
+        assertChapter(mangaTitle, "Bleach 567.a Down With Snowwhite", 567.1f)
+        assertChapter(mangaTitle, "Bleach 567.b Down With Snowwhite", 567.2f)
+        assertChapter(mangaTitle, "Bleach 567.extra Down With Snowwhite", 567.99f)
+    }
+
+    @Test
+    fun `Chapter containing manga title and number`() {
+        val mangaTitle = "Solanin"
+
+        assertChapter(mangaTitle, "Solanin 028 Vol. 2", 28f)
+    }
+
+    @Test
+    fun `Chapter containing manga title and number decimal`() {
+        val mangaTitle = "Solanin"
+
+        assertChapter(mangaTitle, "Solanin 028.1 Vol. 2", 28.1f)
+        assertChapter(mangaTitle, "Solanin 028.4 Vol. 2", 28.4f)
+    }
+
+    @Test
+    fun `Chapter containing manga title and number alpha`() {
+        val mangaTitle = "Solanin"
+
+        assertChapter(mangaTitle, "Solanin 028.a Vol. 2", 28.1f)
+        assertChapter(mangaTitle, "Solanin 028.b Vol. 2", 28.2f)
+        assertChapter(mangaTitle, "Solanin 028.extra Vol. 2", 28.99f)
+    }
+
+    @Test
+    fun `Extreme case`() {
+        val mangaTitle = "Onepunch-Man"
+
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028", 28f)
+    }
+
+    @Test
+    fun `Extreme case with decimal`() {
+        val mangaTitle = "Onepunch-Man"
+
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.1", 28.1f)
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.4", 28.4f)
+    }
+
+    @Test
+    fun `Extreme case with alpha`() {
+        val mangaTitle = "Onepunch-Man"
+
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.a", 28.1f)
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.b", 28.2f)
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.extra", 28.99f)
+    }
+
+    @Test
+    fun `Chapter containing dot v2`() {
+        val mangaTitle = "random"
+
+        assertChapter(mangaTitle, "Vol.1 Ch.5v.2: Alones", 5f)
+    }
+
+    @Test
+    fun `Number in manga title`() {
+        val mangaTitle = "Ayame 14"
+
+        assertChapter(mangaTitle, "Ayame 14 1 - The summer of 14", 1f)
+    }
+
+    @Test
+    fun `Space between ch x`() {
+        val mangaTitle = "Mokushiroku Alice"
+
+        assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation", 4f)
+    }
+
+    @Test
+    fun `Chapter title with ch substring`() {
+        val mangaTitle = "Ayame 14"
+
+        assertChapter(mangaTitle, "Vol.1 Ch.1: March 25 (First Day Cohabiting)", 1f)
+    }
+
+    @Test
+    fun `Chapter containing multiple zeros`() {
+        val mangaTitle = "random"
+
+        assertChapter(mangaTitle, "Vol.001 Ch.003: Kaguya Doesn't Know Much", 3f)
+    }
+
+    @Test
+    fun `Chapter with version before number`() {
+        val mangaTitle = "Onepunch-Man"
+
+        assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]", 86f)
+    }
+
+    @Test
+    fun `Version attached to chapter number`() {
+        val mangaTitle = "Ansatsu Kyoushitsu"
+
+        assertChapter(mangaTitle, "Ansatsu Kyoushitsu 011v002: Assembly Time", 11f)
+    }
+
+    /**
+     * Case where the chapter title contains the chapter
+     * But wait it's not actual the chapter number.
+     */
+    @Test
+    fun `Number after manga title with chapter in chapter title case`() {
+        val mangaTitle = "Tokyo ESP"
+
+        assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027f)
+    }
+
+    @Test
+    fun `Unparseable chapter`() {
+        val mangaTitle = "random"
+
+        assertChapter(mangaTitle, "Foo", -1f)
+    }
+
+    @Test
+    fun `Chapter with time in title`() {
+        val mangaTitle = "random"
+
+        assertChapter(mangaTitle, "Fairy Tail 404: 00:00", 404f)
+    }
+
+    @Test
+    fun `Chapter with alpha without dot`() {
+        val mangaTitle = "random"
+
+        assertChapter(mangaTitle, "Asu No Yoichi 19a", 19.1f)
+    }
+
+    @Test
+    fun `Chapter title containing extra and vol`() {
+        val mangaTitle = "Fairy Tail"
+
+        assertChapter(mangaTitle, "Fairy Tail 404.extravol002", 404.99f)
+        assertChapter(mangaTitle, "Fairy Tail 404 extravol002", 404.99f)
+        assertChapter(mangaTitle, "Fairy Tail 404.evol002", 404.5f)
+    }
+
+    @Test
+    fun `Chapter title containing omake (japanese extra) and vol`() {
+        val mangaTitle = "Fairy Tail"
+
+        assertChapter(mangaTitle, "Fairy Tail 404.omakevol002", 404.98f)
+        assertChapter(mangaTitle, "Fairy Tail 404 omakevol002", 404.98f)
+        assertChapter(mangaTitle, "Fairy Tail 404.ovol002", 404.15f)
+    }
+
+    @Test
+    fun `Chapter title containing special and vol`() {
+        val mangaTitle = "Fairy Tail"
+
+        assertChapter(mangaTitle, "Fairy Tail 404.specialvol002", 404.97f)
+        assertChapter(mangaTitle, "Fairy Tail 404 specialvol002", 404.97f)
+        assertChapter(mangaTitle, "Fairy Tail 404.svol002", 404.19f)
+    }
+
+    @Test
+    fun `Chapter title containing commas`() {
+        val mangaTitle = "One Piece"
+
+        assertChapter(mangaTitle, "One Piece 300,a", 300.1f)
+        assertChapter(mangaTitle, "One Piece Ch,123,extra", 123.99f)
+        assertChapter(mangaTitle, "One Piece the sunny, goes swimming 024,005", 24.005f)
+    }
+
+    @Test
+    fun `Chapter title containing hyphens`() {
+        val mangaTitle = "Solo Leveling"
+
+        assertChapter(mangaTitle, "ch 122-a", 122.1f)
+        assertChapter(mangaTitle, "Solo Leveling Ch.123-extra", 123.99f)
+        assertChapter(mangaTitle, "Solo Leveling, 024-005", 24.005f)
+        assertChapter(mangaTitle, "Ch.191-200 Read Online", 191.200f)
+    }
+
+    @Test
+    fun `Chapters containing season`() {
+        assertChapter("D.I.C.E", "D.I.C.E[Season 001] Ep. 007", 7f)
+    }
+
+    @Test
+    fun `Chapters in format sx - chapter xx`() {
+        assertChapter("The Gamer", "S3 - Chapter 20", 20f)
+    }
+
+    @Test
+    fun `Chapters ending with s`() {
+        assertChapter("One Outs", "One Outs 001", 1f)
+    }
+
+    private fun assertChapter(mangaTitle: String, name: String, expected: Float) {
+        val chapter = createChapter(name)
+        ChapterRecognition.parseChapterNumber(chapter, createManga(mangaTitle))
+        assertEquals(expected, chapter.chapter_number)
+    }
+
+    private fun createManga(title: String): Manga {
+        val manga = Manga.create(0)
+        manga.title = title
+        return manga
+    }
+
+    private fun createChapter(name: String): Chapter {
+        val chapter = Chapter.create()
+        chapter.name = name
+        return chapter
+    }
+}

+ 1 - 8
gradle/libs.versions.toml

@@ -6,7 +6,6 @@ coil_version = "2.0.0-rc03"
 conductor_version = "3.1.5"
 flowbinding_version = "1.2.0"
 shizuku_version = "12.1.0"
-robolectric_version = "3.1.4"
 
 [libraries]
 android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
@@ -87,12 +86,7 @@ aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibr
 shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
 shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
 
-junit = "junit:junit:4.13.2"
-assertj-core = "org.assertj:assertj-core:3.16.1"
-mockito-core = "org.mockito:mockito-core:1.10.19"
-
-robolectric-core = { module = "org.robolectric:robolectric", version.ref = "robolectric_version" }
-robolectric-playservices = { module = "org.robolectric:shadows-play-services", version.ref = "robolectric_version" }
+junit = "org.junit.jupiter:junit-jupiter:5.9.0"
 
 leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
 
@@ -106,7 +100,6 @@ coil = ["coil-core","coil-gif",]
 flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
 conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
 shizuku = ["shizuku-api","shizuku-provider"]
-robolectric = ["robolectric-core","robolectric-playservices"]
 
 [plugins]
 kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"}