Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ dependencies {
implementation libs.leakcanary.android

testImplementation project(':testlib')
testImplementation project(":libanki:testutils")

// A path for a testing library which provide Parameterized Test
testImplementation libs.junit.jupiter
Expand Down
3 changes: 1 addition & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/CollectionManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ object CollectionManager {
}

/**
* Close the currently cached backend and discard it. Useful when enabling the V16 scheduler in the
* dev preferences, or if the active language changes. Saves and closes the collection if open.
* Close the currently cached backend and discard it. Saves and closes the collection if open.
*/
suspend fun discardBackend() {
withQueue {
Expand Down
11 changes: 11 additions & 0 deletions AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import com.ichi2.anki.observability.undoableOp
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.compat.customtabs.CustomTabActivityHelper
import com.ichi2.testutils.AndroidTest
import com.ichi2.testutils.CollectionManagerTestAdapter
import com.ichi2.testutils.TaskSchedulerRule
import com.ichi2.testutils.TestClass
import com.ichi2.testutils.common.FailOnUnhandledExceptionRule
Expand All @@ -59,6 +60,8 @@ import com.ichi2.testutils.filter
import com.ichi2.utils.InMemorySQLiteOpenHelperFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import net.ankiweb.rsdroid.BackendException
import net.ankiweb.rsdroid.testing.RustBackendLoader
Expand Down Expand Up @@ -113,6 +116,9 @@ open class RobolectricTest :
@get:Rule
val timeoutRule: TimeoutRule = TimeoutRule.seconds(60)

override val collectionManager: CollectionManagerTestAdapter
get() = CollectionManagerTestAdapter

@Before
@CallSuper
open fun setUp() {
Expand Down Expand Up @@ -521,6 +527,11 @@ open class RobolectricTest :
throw e
}
}

override suspend fun TestScope.runTestInner(testBody: suspend TestScope.() -> Unit) {
collectionManager.setTestDispatcher(UnconfinedTestDispatcher(testScheduler))
testBody()
}
}

private fun getLatestAlertDialog(): AlertDialog =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 David Allison <davidallisongithub@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.testutils

import com.ichi2.anki.CollectionManager
import com.ichi2.anki.libanki.testutils.TestCollectionManager
import kotlinx.coroutines.CoroutineDispatcher

/**
* Adapts [CollectionManager] to [TestCollectionManager]
*/
object CollectionManagerTestAdapter : TestCollectionManager {
override suspend fun discardBackend() {
CollectionManager.discardBackend()
}

/** @see CollectionManager.setTestDispatcher */
fun setTestDispatcher(dispatcher: CoroutineDispatcher) {
CollectionManager.setTestDispatcher(dispatcher)
}
}
10 changes: 10 additions & 0 deletions AnkiDroid/src/test/java/com/ichi2/testutils/JvmTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.ichi2.anki.libanki.Storage
import com.ichi2.anki.observability.ChangeManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import net.ankiweb.rsdroid.BackendException
import net.ankiweb.rsdroid.testing.RustBackendLoader
Expand All @@ -50,6 +52,9 @@ open class JvmTest : TestClass {
RustBackendLoader.ensureSetup()
}

override val collectionManager: CollectionManagerTestAdapter
get() = CollectionManagerTestAdapter

override val col: Collection
get() {
if (_col == null) {
Expand Down Expand Up @@ -121,4 +126,9 @@ open class JvmTest : TestClass {
) {
Assume.assumeThat(actual, matcher)
}

override suspend fun TestScope.runTestInner(testBody: suspend TestScope.() -> Unit) {
collectionManager.setTestDispatcher(UnconfinedTestDispatcher(testScheduler))
testBody()
}
}
12 changes: 9 additions & 3 deletions AnkiDroid/src/test/java/com/ichi2/testutils/TestClass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.ichi2.testutils

import android.annotation.SuppressLint
import com.ichi2.anki.CollectionManager
import com.ichi2.anki.ioDispatcher
import com.ichi2.anki.isCollectionEmpty
import com.ichi2.anki.libanki.Card
Expand All @@ -31,6 +30,7 @@ import com.ichi2.anki.libanki.NotetypeJson
import com.ichi2.anki.libanki.Notetypes
import com.ichi2.anki.libanki.QueueType
import com.ichi2.anki.libanki.exception.ConfirmModSchemaException
import com.ichi2.anki.libanki.testutils.TestCollectionManager
import com.ichi2.testutils.ext.addNote
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
Expand All @@ -50,6 +50,8 @@ import kotlin.time.Duration.Companion.milliseconds
interface TestClass {
val col: Collection

val collectionManager: TestCollectionManager

fun addBasicNote(
front: String = "Front",
back: String = "Back",
Expand Down Expand Up @@ -322,9 +324,13 @@ interface TestClass {
repeat(times) {
if (times != 1) Timber.d("------ Executing test $it/$times ------")
kotlinx.coroutines.test.runTest(context, dispatchTimeoutMs.milliseconds) {
CollectionManager.setTestDispatcher(UnconfinedTestDispatcher(testScheduler))
testBody()
runTestInner(testBody)
}
}
}

/** Runs [testBody], supporting [TestScope]-specific setup & teardown */
suspend fun TestScope.runTestInner(testBody: suspend TestScope.() -> Unit) {
testBody()
}
}
2 changes: 2 additions & 0 deletions libanki/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ dependencies {
testImplementation(libs.junit.jupiter)
testImplementation(libs.json)

testImplementation(project(":libanki:testutils"))

// project lint checks
// PERF: some rules do not need to be applied... but the full run was 3s
lintChecks(project(":lint-rules"))
Expand Down
1 change: 1 addition & 0 deletions libanki/testutils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
69 changes: 69 additions & 0 deletions libanki/testutils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 David Allison <davidallisongithub@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}

android {
namespace = "com.ichi2.anki.libanki.testutils"
compileSdk =
libs.versions.compileSdk
.get()
.toInt()

defaultConfig {
minSdk =
libs.versions.minSdk
.get()
.toInt()
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}
}

dependencies {
implementation(project(":common"))
implementation(project(":libanki"))

val localProperties = Properties()
if (project.rootProject.file("local.properties").exists()) {
localProperties.load(project.rootProject.file("local.properties").inputStream())
}
if (localProperties["local_backend"] == "true") {
implementation(files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar"))
testImplementation(files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing.jar"))
} else {
implementation(libs.ankiBackend.backend)
testImplementation(libs.ankiBackend.testing)
}

implementation(libs.jakewharton.timber)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.test)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 David Allison <davidallisongithub@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.libanki.testutils

/**
* Trimmed down version of `com.ichi2.anki.CollectionManager` which can be used without a reference
* to Android & AnkiDroid app logic
*/
interface TestCollectionManager {
/**
* Close the currently cached backend and discard it. Saves and closes the collection if open.
*/
suspend fun discardBackend()
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ dependencyResolutionManagement {
}
}

include(":lint-rules", ":api", ":AnkiDroid", ":testlib", ":common", ":libanki")
include(":lint-rules", ":api", ":AnkiDroid", ":testlib", ":common", ":libanki", ":libanki:testutils")