diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 95bf00e74f89..990718b12344 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,7 +20,3 @@ updates: - dependency-name: org.apache.commons:commons-compress versions: - ">= 1.12, < 1.23" - # We cannot update this one until minimum API24. Ignore range should slide with known versions so we stay informed. - - dependency-name: com.fasterxml.jackson.core:jackson-databind - versions: - - ">= 2.13.5, < 2.16" diff --git a/.github/workflows/tests_unit.yml b/.github/workflows/tests_unit.yml index d23a135f7dfe..6c0df7a48efd 100644 --- a/.github/workflows/tests_unit.yml +++ b/.github/workflows/tests_unit.yml @@ -39,16 +39,12 @@ concurrency: jobs: unit: - name: JUnit Tests (${{ matrix.os}}, legacy_schema = ${{ matrix.legacy_schema }}) + name: JUnit Tests (${{ matrix.os}} timeout-minutes: 40 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - legacy_schema: [true, false] - exclude: - - os: macos-latest - legacy_schema: false runs-on: ${{ matrix.os }} #env: # CODACY_TOKEN: ${{ secrets.CODACY_TOKEN }} @@ -103,9 +99,6 @@ jobs: max_attempts: 3 command: ./gradlew robolectricSdkDownload --daemon - - name: Set legacy schema property - run: echo "legacy_schema = ${{ matrix.legacy_schema }}" >> local.properties - - name: Run Unit Tests uses: gradle/gradle-build-action@v2 with: diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index aab7bad0413e..e0f2f38552a6 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -1,6 +1,7 @@ plugins { // Gradle plugin portal id 'com.github.triplet.play' version '3.8.4' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.0' } apply plugin: 'com.android.application' @@ -46,7 +47,6 @@ android { defaultConfig { applicationId "com.ichi2.anki" buildConfigField "Boolean", "CI", (System.getenv("CI") == "true").toString() - buildConfigField "Boolean", "LEGACY_SCHEMA", "true" buildConfigField "String", "ACRA_URL", '"https://ankidroid.org/acra/report"' buildConfigField "String", "BACKEND_VERSION", "\"$ankidroid_backend_version\"" buildConfigField "Boolean", "ENABLE_LEAK_CANARY", "false" @@ -77,7 +77,7 @@ android { // needed for upgrades to be offered correctly. versionCode=21700101 versionName="2.17alpha1" - minSdkVersion 21 + minSdkVersion 23 // change api/build.gradle // change robolectricDownloader.gradle // After #13695: change .tests_emulator.yml @@ -113,10 +113,6 @@ android { if (localProperties['enable_languages'] == "false") { android.defaultConfig.resConfigs "en" } - // allow overriding default schema version - if (localProperties["legacy_schema"] != null) { - buildConfigField "Boolean", "LEGACY_SCHEMA", localProperties["legacy_schema"] - } // allows the scoped storage migration when the user is not logged in if (localProperties["allow_unsafe_migration"] != null) { buildConfigField "Boolean", "ALLOW_UNSAFE_MIGRATION", localProperties["allow_unsafe_migration"] @@ -237,7 +233,6 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_11 } - ndkVersion "22.0.7026061" } play { @@ -317,14 +312,13 @@ dependencies { // Note: the design support library can be quite buggy, so test everything thoroughly before updating it // Changing the version from 1.8.0 to 1.7.0 because the item in navigation drawer is getting bold unnecessarily implementation 'com.google.android.material:material:1.7.0' - // noinspection GradleDependency jackson-databind 2.13 requires SDK 24+: https://github.com/FasterXML/jackson-databind#compatibility - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' implementation 'com.vanniktech:android-image-cropper:4.5.0' implementation 'org.nanohttpd:nanohttpd:2.3.1' + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" // Backend libraries - implementation 'com.google.protobuf:protobuf-kotlin:3.24.1' // This is required when loading from a file + implementation 'com.google.protobuf:protobuf-kotlin-lite:3.24.1' // This is required when loading from a file Properties localProperties = new Properties() if (project.rootProject.file('local.properties').exists()) { @@ -332,9 +326,7 @@ dependencies { } 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-${ankidroid_backend_version}.jar") - // On Windows, you can use something like - // implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar") + testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing.jar") } else { implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" @@ -352,10 +344,8 @@ dependencies { implementation 'com.afollestad.material-dialogs:core:3.3.0' implementation 'com.afollestad.material-dialogs:input:3.3.0' - // io.github.java-diff-utils:java-diff-utils is the natural successor here, but requires API24, #7091 - implementation 'org.bitbucket.cowwoc:diff-match-patch:1.2' // noinspection GradleDependency - commons-compress 1.12 - later versions use `File.toPath`; API26 can remove? - implementation 'org.apache.commons:commons-compress:1.12' // #6419 - handle >2GB apkg files + implementation 'org.apache.commons:commons-compress:1.12' implementation 'org.apache.commons:commons-collections4:4.4' // SetUniqueList implementation 'commons-io:commons-io:2.13.0' // FileUtils.contentEquals implementation 'net.mikehardy:google-analytics-java7:2.0.13' diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt index 0077f0add41c..64861b60ac55 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTabOrderTest.kt @@ -89,6 +89,6 @@ class NoteEditorTabOrderTest : NoteEditorTest() { } private fun ensureCollectionLoaded() { - CollectionHelper.instance.getCol(targetContext) + CollectionHelper.instance.getColUnsafe(targetContext) } } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt index e6f3525631b9..f71e00155a08 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt @@ -30,11 +30,8 @@ import com.ichi2.anki.exception.ConfirmModSchemaException import com.ichi2.anki.testutil.DatabaseUtils.cursorFillWindow import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission import com.ichi2.anki.testutil.grantPermissions -import com.ichi2.async.TaskManager.Companion.waitToFinish import com.ichi2.libanki.* -import com.ichi2.utils.BlocksSchemaUpgrade import com.ichi2.utils.KotlinCleanup -import net.ankiweb.rsdroid.BackendFactory.defaultLegacySchema import org.hamcrest.MatcherAssert.* import org.hamcrest.Matchers.* import org.json.JSONObject @@ -45,8 +42,6 @@ import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Assume.* -import org.junit.runner.RunWith -import org.junit.runners.Parameterized import timber.log.Timber import java.util.* import kotlin.test.assertNotNull @@ -59,13 +54,7 @@ import kotlin.test.junit.JUnitAsserter.assertNotNull * These tests should cover all supported operations for each URI. */ @KotlinCleanup("is -> equalTo") -@RunWith(Parameterized::class) class ContentProviderTest : InstrumentedTest() { - @JvmField // required for Parameter - @Parameterized.Parameter - @KotlinCleanup("lateinit") - var schedVersion = 0 - @get:Rule var runtimePermissionRule = grantPermissions(storagePermission, FlashCardsContract.READ_WRITE_PERMISSION) @@ -87,32 +76,16 @@ class ContentProviderTest : InstrumentedTest() { * Initially create one note for each model. */ @Before - @BlocksSchemaUpgrade("some of these tests are failing; need to investigate why") @Throws( Exception::class ) @KotlinCleanup("remove 'requireNoNulls' and fix mDummyFields") fun setUp() { - assumeThat(defaultLegacySchema, `is`(true)) Timber.i("setUp()") mCreatedNotes = ArrayList() val col = col - // We have parameterized the "schedVersion" variable, if we are on an emulator - // (so it is safe) we will try to run with multiple scheduler versions - mTearDown = false - if (isEmulator()) { - col.changeSchedulerVer(schedVersion) - } else { - if (schedVersion == 1) { - assumeThat(col.sched.name, equalTo("std")) - } else { - assumeThat(col.sched.name, equalTo("std2")) - } - } mTearDown = true - // Do not teardown if setup was aborted - // Add a new basic model that we use for testing purposes (existing models could potentially be corrupted) val model = StdModels.BASIC_MODEL.add(col, BASIC_MODEL_NAME) mModelId = model.getLong("id") @@ -132,10 +105,7 @@ class ContentProviderTest : InstrumentedTest() { /* If parent already exists, don't add the deck, so * that we are sure it won't get deleted at * set-down, */ - if (col.decks.byName(partialName!!) != null) { - continue - } - val did = col.decks.id(partialName) + val did = col.decks.byName(partialName!!)?.id ?: col.decks.id(partialName) mTestDeckIds.add(did) mCreatedNotes.add(setupNewNote(col, mModelId, did, mDummyFields.requireNoNulls(), TEST_TAG)) partialName += "::" @@ -161,7 +131,7 @@ class ContentProviderTest : InstrumentedTest() { if (remnantNotes.isNotEmpty()) { val noteIds = remnantNotes.toLongArray() col.remNotes(noteIds) - col.save() + assertEquals( "Check that remnant notes have been deleted", 0, @@ -169,10 +139,7 @@ class ContentProviderTest : InstrumentedTest() { ) } // delete test decks - for (did in mTestDeckIds) { - col.decks.rem(did, cardsToo = true, childrenToo = true) - } - col.decks.flush() + col.decks.removeDecks(mTestDeckIds) assertEquals( "Check that all created decks have been deleted", mNumDecksBeforeTest, @@ -186,10 +153,10 @@ class ContentProviderTest : InstrumentedTest() { @Throws(Exception::class) private fun removeAllModelsByName(col: com.ichi2.libanki.Collection, name: String) { - var testModel = col.models.byName(name) + var testModel = col.notetypes.byName(name) while (testModel != null) { - col.models.rem(testModel) - testModel = col.models.byName(name) + col.notetypes.rem(testModel) + testModel = col.notetypes.byName(name) } } @@ -248,7 +215,7 @@ class ContentProviderTest : InstrumentedTest() { TEST_NOTE_FIELDS ) assertEquals("Check that tag was set correctly", TEST_TAG, addedNote.tags[0]) - val model: JSONObject? = col.models.get(mModelId) + val model: JSONObject? = col.notetypes.get(mModelId) assertNotNull("Check model", model) val expectedNumCards = model!!.getJSONArray("tmpls").length() assertEquals("Check that correct number of cards generated", expectedNumCards, addedNote.numberOfCards()) @@ -272,7 +239,7 @@ class ContentProviderTest : InstrumentedTest() { val cr = contentResolver var col = col // Add a new basic model that we use for testing purposes (existing models could potentially be corrupted) - var model: Model? = StdModels.BASIC_MODEL.add(col, BASIC_MODEL_NAME) + var model: NotetypeJson? = StdModels.BASIC_MODEL.add(col, BASIC_MODEL_NAME) val modelId = model!!.getLong("id") // Add the note val modelUri = ContentUris.withAppendedId(FlashCardsContract.Model.CONTENT_URI, modelId) @@ -297,7 +264,7 @@ class ContentProviderTest : InstrumentedTest() { templateUri!! ) ) - model = col.models.get(modelId) + model = col.notetypes.get(modelId) assertNotNull("Check model", model) val template = model!!.getJSONArray("tmpls").getJSONObject(expectedOrd) assertEquals( @@ -314,7 +281,7 @@ class ContentProviderTest : InstrumentedTest() { assertEquals("Check afmt", TEST_MODEL_AFMT[testIndex], template.getString("afmt")) assertEquals("Check bqfmt", TEST_MODEL_QFMT[testIndex], template.getString("bqfmt")) assertEquals("Check bafmt", TEST_MODEL_AFMT[testIndex], template.getString("bafmt")) - col.models.rem(model) + col.notetypes.rem(model) } /** @@ -326,7 +293,7 @@ class ContentProviderTest : InstrumentedTest() { // Get required objects for test val cr = contentResolver var col = col - var model: Model? = StdModels.BASIC_MODEL.add(col, BASIC_MODEL_NAME) + var model: NotetypeJson? = StdModels.BASIC_MODEL.add(col, BASIC_MODEL_NAME) val modelId = model!!.getLong("id") val initialFieldsArr = model.getJSONArray("flds") val initialFieldCount = initialFieldsArr.length() @@ -337,7 +304,7 @@ class ContentProviderTest : InstrumentedTest() { assertNotNull("Check field uri", fieldUri) // Ensure that the changes are physically saved to the DB col = reopenCol() - model = col.models.get(modelId) + model = col.notetypes.get(modelId) // Test the field is as expected val fieldId = ContentUris.parseId(fieldUri!!) assertEquals("Check field id", initialFieldCount.toLong(), fieldId) @@ -353,7 +320,7 @@ class ContentProviderTest : InstrumentedTest() { TEST_FIELD_NAME, fldsArr.getJSONObject(fldsArr.length() - 1).optString("name", "") ) - col.models.rem(model) + col.notetypes.rem(model) } /** @@ -562,7 +529,7 @@ class ContentProviderTest : InstrumentedTest() { val mid = modelUri.lastPathSegment!!.toLong() var col = reopenCol() try { - var model: JSONObject? = col.models.get(mid) + var model: JSONObject? = col.notetypes.get(mid) assertNotNull("Check model", model) assertEquals("Check model name", TEST_MODEL_NAME, model!!.getString("name")) assertEquals( @@ -591,7 +558,7 @@ class ContentProviderTest : InstrumentedTest() { `is`(greaterThan(0)) ) col = reopenCol() - model = col.models.get(mid) + model = col.notetypes.get(mid) assertNotNull("Check model", model) assertEquals("Check css", TEST_MODEL_CSS, model!!.getString("css")) // Update each of the templates in model (to test updating MODELS_ID_TEMPLATES_ID Uri) @@ -615,7 +582,7 @@ class ContentProviderTest : InstrumentedTest() { ) ) col = reopenCol() - model = col.models.get(mid) + model = col.notetypes.get(mid) assertNotNull("Check model", model) val template = model!!.getJSONArray("tmpls").getJSONObject(i) assertEquals( @@ -632,9 +599,9 @@ class ContentProviderTest : InstrumentedTest() { // Delete the model (this will force a full-sync) col.modSchemaNoCheck() try { - val model = col.models.get(mid) + val model = col.notetypes.get(mid) assertNotNull("Check model", model) - col.models.rem(model!!) + col.notetypes.rem(model!!) } catch (e: ConfirmModSchemaException) { // This will never happen } @@ -910,7 +877,7 @@ class ContentProviderTest : InstrumentedTest() { it.getLong(it.getColumnIndex(FlashCardsContract.Deck.DECK_ID)) val deckName = it.getString(it.getColumnIndex(FlashCardsContract.Deck.DECK_NAME)) - val deck = decks.get(deckID) + val deck = decks.get(deckID)!! assertNotNull("Check that the deck we received actually exists", deck) assertEquals( "Check that the received deck has the correct name", @@ -940,7 +907,7 @@ class ContentProviderTest : InstrumentedTest() { decksCursor.getLong(decksCursor.getColumnIndex(FlashCardsContract.Deck.DECK_ID)) val returnedDeckName = decksCursor.getString(decksCursor.getColumnIndex(FlashCardsContract.Deck.DECK_NAME)) - val realDeck = col.decks.get(deckId) + val realDeck = col.decks.get(deckId)!! assertEquals( "Check that received deck ID equals real deck ID", deckId, @@ -978,11 +945,8 @@ class ContentProviderTest : InstrumentedTest() { reviewInfoCursor.getLong(reviewInfoCursor.getColumnIndex(FlashCardsContract.ReviewInfo.NOTE_ID)) var nextCard: Card? = null for (i in 0..9) { // minimizing fails, when sched.reset() randomly chooses between multiple cards - col.reset() nextCard = sched.card - waitToFinish() if (nextCard != null && nextCard.note().id == noteID && nextCard.ord == cardOrd) break - waitToFinish() } assertNotNull("Check that there actually is a next scheduled card", nextCard) assertEquals( @@ -1033,7 +997,6 @@ class ContentProviderTest : InstrumentedTest() { col.decks.select(deckToTest) var nextCard: Card? = null for (i in 0..9) { // minimizing fails, when sched.reset() randomly chooses between multiple cards - col.reset() nextCard = sched.card if (nextCard != null && nextCard.note().id == noteID && nextCard.ord == cardOrd) break try { @@ -1079,7 +1042,6 @@ class ContentProviderTest : InstrumentedTest() { private fun getFirstCardFromScheduler(col: com.ichi2.libanki.Collection): Card? { val deckId = mTestDeckIds[0] col.decks.select(deckId) - col.reset() return col.sched.card } @@ -1098,8 +1060,7 @@ class ContentProviderTest : InstrumentedTest() { val reviewInfoUri = FlashCardsContract.ReviewInfo.CONTENT_URI val noteId = card.note().id val cardOrd = card.ord - val earlyGraduatingEase = - if (schedVersion == 1) AbstractFlashcardViewer.EASE_3 else AbstractFlashcardViewer.EASE_4 + val earlyGraduatingEase = AbstractFlashcardViewer.EASE_4 val values = ContentValues().apply { val timeTaken: Long = 5000 // 5 seconds put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId) @@ -1113,7 +1074,6 @@ class ContentProviderTest : InstrumentedTest() { Thread.currentThread().join(500) } catch (e: Exception) { /* do nothing */ } - col.reset() val newCard = col.sched.card if (newCard != null) { if (newCard.note().id == card.note().id && newCard.ord == card.ord) { @@ -1167,7 +1127,7 @@ class ContentProviderTest : InstrumentedTest() { // QUEUE_TYPE_MANUALLY_BURIED was also used for SIBLING_BURIED in sched v1 assertEquals( "Card is user-buried", - if (schedVersion == 1) Consts.QUEUE_TYPE_SIBLING_BURIED else Consts.QUEUE_TYPE_MANUALLY_BURIED, + Consts.QUEUE_TYPE_MANUALLY_BURIED, cardAfterUpdate.queue ) @@ -1220,8 +1180,7 @@ class ContentProviderTest : InstrumentedTest() { // cleanup, unsuspend card and reschedule // -------------------------------------- - col.sched.unsuspendCards(longArrayOf(cardId)) - col.reset() + col.sched.unsuspendCards(listOf(cardId)) } /** @@ -1272,8 +1231,8 @@ class ContentProviderTest : InstrumentedTest() { isEmulator() ) val col = col - col.models.all()[0].put("did", JSONObject.NULL) - col.save() + col.notetypes.all()[0].put("did", JSONObject.NULL) + val cr = contentResolver // Query all available models val allModels = cr.query(FlashCardsContract.Model.CONTENT_URI, null, null, null, null) @@ -1281,7 +1240,7 @@ class ContentProviderTest : InstrumentedTest() { } private fun reopenCol(): com.ichi2.libanki.Collection { - CollectionHelper.instance.closeCollection(false, "ContentProviderTest: reopenCol") + CollectionHelper.instance.closeCollection("ContentProviderTest: reopenCol") return col } @@ -1289,13 +1248,6 @@ class ContentProviderTest : InstrumentedTest() { get() = testContext.contentResolver companion object { - @Parameterized.Parameters - @JvmStatic // required for initParameters - fun initParameters(): Collection> { - // This does one run with schedVersion injected as 1, and one run as 2 - return listOf(arrayOf(1), arrayOf(2)) - } - private const val BASIC_MODEL_NAME = "com.ichi2.anki.provider.test.basic.x94oa3F" private const val TEST_FIELD_NAME = "TestFieldName" private const val TEST_FIELD_VALUE = "test field value" @@ -1326,7 +1278,7 @@ class ContentProviderTest : InstrumentedTest() { fields: Array, tag: String ): Uri { - val newNote = Note(col, col.models.get(mid)!!) + val newNote = Note(col, col.notetypes.get(mid)!!) for (idx in fields.indices) { newNote.setField(idx, fields[idx]) } diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt index 54e31efff036..37c63fe08146 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt @@ -29,11 +29,11 @@ import java.io.IOException abstract class InstrumentedTest { protected val col: Collection - get() = CollectionHelper.instance.getCol(testContext)!! + get() = CollectionHelper.instance.getColUnsafe(testContext)!! @get:Throws(IOException::class) protected val emptyCol: Collection - get() = Shared.getEmptyCol(testContext) + get() = Shared.getEmptyCol() @get:Rule val ensureAllFilesAccessRule = EnsureAllFilesAccessRule() diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/RustTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/RustTest.kt deleted file mode 100644 index fb9728b0ebf4..000000000000 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/RustTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2020 David Allison - * - * 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 . - */ -package com.ichi2.anki.tests - -import com.ichi2.libanki.Storage -import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendFactory -import org.hamcrest.MatcherAssert -import org.hamcrest.Matchers.equalTo -import org.junit.Assume.assumeThat -import org.junit.Rule -import org.junit.Test -import org.junit.rules.Timeout -import java.io.IOException -import java.util.concurrent.TimeUnit - -class RustTest : InstrumentedTest() { - /** Ensure that the database isn't be locked - * This happened before the database code was converted to use the Rust backend. - */ - @get:Rule - var timeout = Timeout(30, TimeUnit.SECONDS) - - @Test - @Throws(BackendException::class, IOException::class) - fun collectionIsVersion11AfterOpen() { - assumeThat(BackendFactory.defaultLegacySchema, equalTo(true)) - // This test will be decommissioned, but before we get an upgrade strategy, we need to ensure we're not upgrading the database. - val path = Shared.getTestFilePath(testContext, "initial_version_2_12_1.anki2") - val collection = Storage.collection(testContext, path) - val ver = collection.db.queryScalar("select ver from col") - MatcherAssert.assertThat(ver, equalTo(11)) - } -} diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt index a98a33485c02..9b97e80d320b 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/Shared.kt @@ -31,13 +31,13 @@ import java.io.IOException @KotlinCleanup("maybe delete Shared object and make inner functions as top level") object Shared { @Throws(IOException::class) - fun getEmptyCol(context: Context): Collection { + fun getEmptyCol(): Collection { val f = File.createTempFile("test", ".anki2") // Provide a string instead of an actual File. Storage.Collection won't populate the DB // if the file already exists (it assumes it's an existing DB). val path = f.absolutePath assertTrue(f.delete()) - return Storage.collection(context, path) + return Storage.collection(path) } /** diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/HttpTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/HttpTest.kt deleted file mode 100644 index 70273bea0fcd..000000000000 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/HttpTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -/*************************************************************************************** - * * - * Copyright (c) 2018 Mike Hardy * - * * - * 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 . * - ****************************************************************************************/ - -package com.ichi2.anki.tests.libanki - -import android.Manifest -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission -import com.ichi2.anki.testutil.grantPermissions -import com.ichi2.async.Connection -import com.ichi2.libanki.sync.HostNum -import com.ichi2.utils.NetworkUtils -import org.junit.Assert -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class HttpTest { - - @get:Rule - var runtimeStoragePermissionRule = grantPermissions( - storagePermission, - Manifest.permission.INTERNET, - Manifest.permission.ACCESS_NETWORK_STATE - ) - - // #7108: AsyncTask - @Suppress("DEPRECATION") - @Test - fun testLogin() { - val username = "AnkiDroidInstrumentedTestUser" - val password = "AnkiDroidInstrumentedTestInvalidPass" - val invalidPayload = Connection.Payload(arrayOf(username, password, HostNum(null))) - val testListener = TestTaskListener(invalidPayload) - - // We have to carefully run things on the main thread here or the threading protections in BaseAsyncTask throw - // The first one is just to run the static initializer, really - val onlineRunnable = Runnable { - try { - Class.forName("com.ichi2.async.Connection") - } catch (e: Exception) { - Assert.fail("Unable to load Connection class: " + e.message) - } - } - InstrumentationRegistry.getInstrumentation().runOnMainSync(onlineRunnable) - - // If we are not online this test is not nearly as interesting - // TODO simulate offline programmatically - currently exercised by manually toggling an emulator offline pre-test - if (!NetworkUtils.isOnline) { - Connection.login(testListener, invalidPayload) - Assert.assertFalse( - "Successful login despite being offline", - testListener.getPayload()!!.success - ) - Assert.assertTrue( - "onDisconnected not called despite being offline", - testListener.disconnectedCalled - ) - return - } - - val r = Runnable { - val conn = Connection.login(testListener, invalidPayload) - try { - // This forces us to synchronously wait for the AsyncTask to do it's work - conn!!.get() - } catch (e: Exception) { - Assert.fail("Caught exception while trying to login: " + e.message) - } - } - InstrumentationRegistry.getInstrumentation().runOnMainSync(r) - Assert.assertFalse( - "Successful login despite invalid credentials", - testListener.getPayload()!!.success - ) - } - - class TestTaskListener(payload: Connection.Payload) : - Connection.TaskListener { - - private var payload: Connection.Payload? = null - var disconnectedCalled = false - private fun setPayload(payload: Connection.Payload) { - this.payload = payload - } - - override fun onPreExecute() { - // do nothing - } - - override fun onProgressUpdate(vararg values: Any?) { - // do nothing - } - - fun getPayload(): Connection.Payload? { - return payload - } - - override fun onPostExecute(data: Connection.Payload) { - // do nothing - } - - override fun onDisconnected() { - disconnectedCalled = true - } - - init { - setPayload(payload) - } - } -} diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/ImportTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/ImportTest.kt deleted file mode 100644 index 3dfae8df7118..000000000000 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/ImportTest.kt +++ /dev/null @@ -1,243 +0,0 @@ -/**************************************************************************************** - * Copyright (c) 2016 Houssam Salem * - * * - * 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 . * - ****************************************************************************************/ -package com.ichi2.anki.tests.libanki - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ichi2.anki.exception.ImportExportException -import com.ichi2.anki.tests.InstrumentedTest -import com.ichi2.anki.tests.Shared -import com.ichi2.anki.testutil.GrantStoragePermission -import com.ichi2.libanki.Collection -import com.ichi2.libanki.importer.Anki2Importer -import com.ichi2.libanki.importer.AnkiPackageImporter -import com.ichi2.libanki.importer.Importer -import net.ankiweb.rsdroid.BackendFactory.defaultLegacySchema -import org.hamcrest.Matchers.equalTo -import org.json.JSONException -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Assume.assumeThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - -@RunWith(AndroidJUnit4::class) -class ImportTest : InstrumentedTest() { - private lateinit var testCol: Collection - - @get:Rule - var runtimePermissionRule = GrantStoragePermission.instance - - // testAnki2Mediadupes() failed on Travis API=22 EMU_FLAVOR=default ABI=armeabi-v7a - // com.ichi2.anki.tests.libanki.ImportTest > testAnki2Mediadupes[test(AVD) - 5.1.1] FAILED - // error: - // android.database.sqlite.SQLiteReadOnlyDatabaseException: attempt to write a readonly database (code 1032) - // at io.requery.android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method) - // :AnkiDroid:connectedDebugAndroidTest FAILED - // - // Error code 1032 is https://www.sqlite.org/rescode.html#readonly_dbmoved - which should be impossible - // - // I was unable to reproduce it on the same emulator locally, even with thousands of iterations. - // Allowing it to re-run now, 3 times, in case it flakes again. - @get:Rule - var retry = RetryRule(10) - - @Before - @Throws(IOException::class) - fun setUp() { - testCol = emptyCol - // the backend provides its own importing methods - assumeThat(defaultLegacySchema, equalTo(true)) - } - - @After - fun tearDown() { - testCol.close() - } - - @Test - @Throws(IOException::class, JSONException::class, ImportExportException::class) - fun testAnki2Mediadupes() { - // add a note that references a sound - var n = testCol.newNote() - n.setField(0, "[sound:foo.mp3]") - val mid = n.model().getLong("id") - testCol.addNote(n) - // add that sound to the media directory - var os = FileOutputStream(File(testCol.media.dir(), "foo.mp3"), false) - os.write("foo".toByteArray()) - os.close() - testCol.close() - // it should be imported correctly into an empty deck - val empty = emptyCol - var imp: Importer = Anki2Importer(empty, testCol.path) - imp.run() - var expected = listOf("foo.mp3") - var actual = File(empty.media.dir()).list()!!.toMutableList() - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - // and importing again will not duplicate, as the file content matches - empty.removeCardsAndOrphanedNotes(empty.db.queryLongList("select id from cards")) - imp = Anki2Importer(empty, testCol.path) - imp.run() - expected = listOf("foo.mp3") - actual = mutableListOf(*File(empty.media.dir()).list()!!) - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - n = empty.getNote(empty.db.queryLongScalar("select id from notes")) - assertTrue("foo.mp3" in n.fields[0]) - // if the local file content is different, and import should trigger a rename - empty.removeCardsAndOrphanedNotes(empty.db.queryLongList("select id from cards")) - os = FileOutputStream(File(empty.media.dir(), "foo.mp3"), false) - os.write("bar".toByteArray()) - os.close() - imp = Anki2Importer(empty, testCol.path) - imp.run() - expected = listOf("foo.mp3", "foo_$mid.mp3") - actual = mutableListOf(*File(empty.media.dir()).list()!!) - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - n = empty.getNote(empty.db.queryLongScalar("select id from notes")) - assertTrue(n.fields[0].contains("_")) - // if the localized media file already exists, we rewrite the note and media - empty.removeCardsAndOrphanedNotes(empty.db.queryLongList("select id from cards")) - os = FileOutputStream(File(empty.media.dir(), "foo.mp3")) - os.write("bar".toByteArray()) - os.close() - imp = Anki2Importer(empty, testCol.path) - imp.run() - expected = listOf("foo.mp3", "foo_$mid.mp3") - actual = mutableListOf(*File(empty.media.dir()).list()!!) - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - n = empty.getNote(empty.db.queryLongScalar("select id from notes")) - assertTrue(n.fields[0].contains("_")) - empty.close() - } - - @Test - @Throws(IOException::class, ImportExportException::class) - fun testApkg() { - val apkg = Shared.getTestFilePath(testContext, "media.apkg") - var imp: Importer = AnkiPackageImporter(testCol, apkg) - var expected: List = emptyList() - var actual = mutableListOf( - *File( - testCol.media.dir() - ).list()!! - ) - actual.retainAll(expected) - assertEquals(actual.size.toLong(), 0) - imp.run() - expected = listOf("foo.wav") - actual = mutableListOf(*File(testCol.media.dir()).list()!!) - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - // import again should be idempotent in terms of media - testCol.removeCardsAndOrphanedNotes(testCol.db.queryLongList("select id from cards")) - imp = AnkiPackageImporter(testCol, apkg) - imp.run() - expected = listOf("foo.wav") - actual = mutableListOf(*File(testCol.media.dir()).list()!!) - actual.retainAll(expected) - assertEquals(expected.size.toLong(), actual.size.toLong()) - // but if the local file has different data, it will rename - testCol.removeCardsAndOrphanedNotes(testCol.db.queryLongList("select id from cards")) - val os = FileOutputStream(File(testCol.media.dir(), "foo.wav"), false) - os.write("xyz".toByteArray()) - os.close() - imp = AnkiPackageImporter(testCol, apkg) - imp.run() - assertEquals(2, File(testCol.media.dir()).list()!!.size.toLong()) - } - - @Test - @Throws(IOException::class, JSONException::class, ImportExportException::class) - fun testAnki2DiffmodelTemplates() { - // different from the above as this one tests only the template text being - // changed, not the number of cards/fields - // import the first version of the model - var tmp = Shared.getTestFilePath(testContext, "diffmodeltemplates-1.apkg") - var imp = AnkiPackageImporter(testCol, tmp) - imp.setDupeOnSchemaChange(true) - imp.run() - // then the version with updated template - tmp = Shared.getTestFilePath(testContext, "diffmodeltemplates-2.apkg") - imp = AnkiPackageImporter(testCol, tmp) - imp.setDupeOnSchemaChange(true) - imp.run() - // collection should contain the note we imported - assertEquals(1, testCol.noteCount().toLong()) - // the front template should contain the text added in the 2nd package - val tcid = testCol.findCards("")[0] - val tnote = testCol.getCard(tcid).note() - assertTrue( - testCol.findTemplates(tnote)[0].getString("qfmt").contains("Changed Front Template") - ) - } - - @Test - @Throws(IOException::class, ImportExportException::class) - fun testAnki2Updates() { - // create a new empty deck - var tmp = Shared.getTestFilePath(testContext, "update1.apkg") - var imp = AnkiPackageImporter(testCol, tmp) - imp.run() - assertEquals(0, imp.dupes) - assertEquals(1, imp.added) - assertEquals(0, imp.updated) - // importing again should be idempotent - imp = AnkiPackageImporter(testCol, tmp) - imp.run() - assertEquals(1, imp.dupes) - assertEquals(0, imp.added) - assertEquals(0, imp.updated) - // importing a newer note should update - assertEquals(1, testCol.noteCount().toLong()) - assertTrue(testCol.db.queryString("select flds from notes").startsWith("hello")) - tmp = Shared.getTestFilePath(testContext, "update2.apkg") - imp = AnkiPackageImporter(testCol, tmp) - imp.run() - assertEquals(1, imp.dupes) - assertEquals(0, imp.added) - assertEquals(1, imp.updated) - assertTrue(testCol.db.queryString("select flds from notes").startsWith("goodbye")) - } - - /** - * Custom tests for AnkiDroid. - */ - @Test - @Throws(IOException::class, ImportExportException::class) - fun testDupeIgnore() { - // create a new empty deck - var tmp = Shared.getTestFilePath(testContext, "update1.apkg") - var imp = AnkiPackageImporter(testCol, tmp) - imp.run() - tmp = Shared.getTestFilePath(testContext, "update3.apkg") - imp = AnkiPackageImporter(testCol, tmp) - imp.run() - // there is a dupe, but it was ignored - assertEquals(1, imp.dupes) - assertEquals(0, imp.added) - assertEquals(0, imp.updated) - } -} diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt index 1648d5b7d77f..b2bc4392feaa 100644 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt +++ b/AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/libanki/MediaTest.kt @@ -22,9 +22,6 @@ import com.ichi2.anki.testutil.GrantStoragePermission import com.ichi2.libanki.Collection import com.ichi2.libanki.Media import com.ichi2.libanki.exception.EmptyMediaException -import net.ankiweb.rsdroid.BackendFactory.defaultLegacySchema -import org.hamcrest.MatcherAssert -import org.hamcrest.Matchers import org.junit.* import org.junit.runner.RunWith import java.io.File @@ -32,7 +29,6 @@ import java.io.FileOutputStream import java.io.IOException import kotlin.test.assertEquals import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail @@ -99,71 +95,11 @@ class MediaTest : InstrumentedTest() { } } - @Test - @Suppress("SpellCheckingInspection") - fun testStrings() { - val mid = mTestCol!!.models.getModels().entries.iterator().next().key - - var expected: List = emptyList() - var actual = mTestCol!!.media.filesInStr(mid, "aoeu").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.jpg") - actual = mTestCol!!.media.filesInStr(mid, "aoeuao").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.jpg", "bar.jpg") - actual = mTestCol!!.media.filesInStr(mid, """aoeuao""").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.jpg") - actual = mTestCol!!.media.filesInStr(mid, "aoeuao").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("one", "two") - actual = mTestCol!!.media.filesInStr(mid, "").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.jpg") - actual = mTestCol!!.media.filesInStr(mid, """aoeuao""").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.jpg", "fo") - actual = - mTestCol!!.media.filesInStr(mid, """aoeuao""").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - expected = listOf("foo.mp3") - actual = mTestCol!!.media.filesInStr(mid, "aou[sound:foo.mp3]aou").toMutableList() - actual.retainAll(expected) - assertEquals(expected.size, actual.size) - - assertEquals("aoeu", mTestCol!!.media.strip("aoeu")) - assertEquals("aoeuaoeu", mTestCol!!.media.strip("aoeu[sound:foo.mp3]aoeu")) - assertEquals("aoeu", mTestCol!!.media.strip("aoeu")) - assertEquals("aoeu", Media.escapeImages("aoeu")) - assertEquals( - "", - Media.escapeImages("") - ) - assertEquals( - """""", - Media.escapeImages("""""") - ) - } - @Test @Throws(IOException::class, EmptyMediaException::class) fun testDeckIntegration() { // create a media dir - mTestCol!!.media.dir() + mTestCol!!.media.dir // Put a file into it val file = createNonEmptyFile("fake.png") mTestCol!!.media.addFile(file) @@ -178,7 +114,7 @@ class MediaTest : InstrumentedTest() { f.setField(1, "") mTestCol!!.addNote(f) // and add another file which isn't used - val os = FileOutputStream(File(mTestCol!!.media.dir(), "foo.jpg"), false) + val os = FileOutputStream(File(mTestCol!!.media.dir, "foo.jpg"), false) os.write("test".toByteArray()) os.close() // check media @@ -193,86 +129,6 @@ class MediaTest : InstrumentedTest() { assertEquals(expected.size, actual.size) } - @Test - @Suppress("SpellCheckingInspection") - fun testAudioTags() { - assertEquals("aoeu", mTestCol!!.media.strip("a