0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-19 11:32:15 +02:00

Replace CRLF with LF

This commit is contained in:
CLIDragon 2024-07-15 17:11:59 +10:00 committed by David Allison
parent 1feb8b31b8
commit 2c7c0c5bda
6 changed files with 438 additions and 438 deletions

View File

@ -1,208 +1,208 @@
/*
* Copyright (c) 2023
*
* 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
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.tests.libanki.RetryRule
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
import com.ichi2.anki.testutil.ThreadUtils
import com.ichi2.anki.testutil.grantPermissions
import com.ichi2.anki.testutil.notificationPermission
import com.ichi2.libanki.Collection
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.AssertionError
@RunWith(AndroidJUnit4::class)
class ReviewerTest : InstrumentedTest() {
// Launch IntroductionActivity instead of DeckPicker activity because in CI
// builds, it seems to create IntroductionActivity after the DeckPicker,
// causing the DeckPicker activity to be destroyed. As a consequence, this
// will throw RootViewWithoutFocusException when Espresso tries to interact
// with an already destroyed activity. By launching IntroductionActivity, we
// ensure that IntroductionActivity is launched first and navigate to the
// DeckPicker -> Reviewer activities
@get:Rule
val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java)
@get:Rule
val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission)
@get:Rule
val retry = RetryRule(10)
@Test
fun testCustomSchedulerWithCustomData() {
col.cardStateCustomizer =
"""
states.good.normal.review.easeFactor = 3.0;
states.good.normal.review.scheduledDays = 123;
customData.good.c += 1;
"""
val note = addNoteUsingBasicModel("foo", "bar")
val card = note.firstCard(col)
val deck = col.decks.get(note.notetype.did)!!
card.moveToReviewQueue()
col.backend.updateCards(
listOf(
card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build()
),
true
)
closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName(deck.name)
var cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(card.factor))
assertThat(cardFromDb.interval, equalTo(card.ivl))
assertThat(cardFromDb.customData, equalTo("""{"c":1}"""))
clickShowAnswerAndAnswerGood()
fun runAssertion() {
cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(3000))
assertThat(cardFromDb.interval, equalTo(123))
assertThat(cardFromDb.customData, equalTo("""{"c":2}"""))
}
try {
runAssertion()
} catch (e: Exception) {
// Give separate threads a greater chance of doing the custom scheduling
// if the card scheduling values aren't updated immediately
ThreadUtils.sleep(2000)
runAssertion()
}
}
@Test
fun testCustomSchedulerWithRuntimeError() {
// Issue 15035 - runtime errors weren't handled
col.cardStateCustomizer = "states.this_is_not_defined.normal.review = 12;"
addNoteUsingBasicModel()
closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName("Default")
clickShowAnswer()
ensureAnswerButtonsAreDisplayed()
}
private fun clickOnDeckWithName(deckName: String) {
onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName))))
onView(withId(R.id.files)).perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(deckName)),
click()
)
)
}
private fun clickOnStudyButtonIfExists() {
onView(withId(R.id.studyoptions_start))
.withFailureHandler { _, _ -> }
.perform(click())
}
private fun reviewDeckWithName(deckName: String) {
clickOnDeckWithName(deckName)
// Adding cards directly to the database while in the Deck Picker screen
// will not update the page with correct card counts. Hence, clicking
// on the deck will bring us to the study options page where we need to
// click on the Study button. If we have added cards to the database
// before the Deck Picker screen has fully loaded, then we skip clicking
// the Study button
clickOnStudyButtonIfExists()
}
private fun clickShowAnswerAndAnswerGood() {
clickShowAnswer()
ensureAnswerButtonsAreDisplayed()
try {
// ...on the command line it has resource name "good_button"...
onView(withResourceName("good_button")).perform(click())
} catch (e: NoMatchingViewException) {
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
onView(withResourceName("flashcard_layout_ease3")).perform(click())
}
}
private fun clickShowAnswer() {
try {
// ... on the command line, it has resource name "show_answer"...
onView(withResourceName("show_answer")).perform(click())
} catch (e: NoMatchingViewException) {
// ... but in Android Studio it has resource name "flashcard_layout_flip" !?
onView(withResourceName("flashcard_layout_flip")).perform(click())
}
}
private fun ensureAnswerButtonsAreDisplayed() {
// We need to wait for the card to fully load to allow enough time for
// the messages to be passed in and out of the WebView when evaluating
// the custom JS scheduler code. The ease buttons are hidden until the
// custom scheduler has finished running
try {
// ...on the command line it has resource name "good_button"...
onView(withResourceName("good_button")).checkWithTimeout(
matches(isDisplayed()),
100
)
} catch (e: AssertionError) {
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
onView(withResourceName("flashcard_layout_ease3")).checkWithTimeout(
matches(isDisplayed()),
100
)
}
}
}
private var Collection.cardStateCustomizer: String?
get() = config.get("cardStateCustomizer")
set(value) { config.set("cardStateCustomizer", value) }
fun closeGetStartedScreenIfExists() {
onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click())
}
fun closeBackupCollectionDialogIfExists() {
onView(withText(R.string.button_backup_later))
.withFailureHandler { _, _ -> }
.perform(click())
}
/*
* Copyright (c) 2023
*
* 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
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.tests.libanki.RetryRule
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
import com.ichi2.anki.testutil.ThreadUtils
import com.ichi2.anki.testutil.grantPermissions
import com.ichi2.anki.testutil.notificationPermission
import com.ichi2.libanki.Collection
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.AssertionError
@RunWith(AndroidJUnit4::class)
class ReviewerTest : InstrumentedTest() {
// Launch IntroductionActivity instead of DeckPicker activity because in CI
// builds, it seems to create IntroductionActivity after the DeckPicker,
// causing the DeckPicker activity to be destroyed. As a consequence, this
// will throw RootViewWithoutFocusException when Espresso tries to interact
// with an already destroyed activity. By launching IntroductionActivity, we
// ensure that IntroductionActivity is launched first and navigate to the
// DeckPicker -> Reviewer activities
@get:Rule
val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java)
@get:Rule
val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission)
@get:Rule
val retry = RetryRule(10)
@Test
fun testCustomSchedulerWithCustomData() {
col.cardStateCustomizer =
"""
states.good.normal.review.easeFactor = 3.0;
states.good.normal.review.scheduledDays = 123;
customData.good.c += 1;
"""
val note = addNoteUsingBasicModel("foo", "bar")
val card = note.firstCard(col)
val deck = col.decks.get(note.notetype.did)!!
card.moveToReviewQueue()
col.backend.updateCards(
listOf(
card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build()
),
true
)
closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName(deck.name)
var cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(card.factor))
assertThat(cardFromDb.interval, equalTo(card.ivl))
assertThat(cardFromDb.customData, equalTo("""{"c":1}"""))
clickShowAnswerAndAnswerGood()
fun runAssertion() {
cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(3000))
assertThat(cardFromDb.interval, equalTo(123))
assertThat(cardFromDb.customData, equalTo("""{"c":2}"""))
}
try {
runAssertion()
} catch (e: Exception) {
// Give separate threads a greater chance of doing the custom scheduling
// if the card scheduling values aren't updated immediately
ThreadUtils.sleep(2000)
runAssertion()
}
}
@Test
fun testCustomSchedulerWithRuntimeError() {
// Issue 15035 - runtime errors weren't handled
col.cardStateCustomizer = "states.this_is_not_defined.normal.review = 12;"
addNoteUsingBasicModel()
closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName("Default")
clickShowAnswer()
ensureAnswerButtonsAreDisplayed()
}
private fun clickOnDeckWithName(deckName: String) {
onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName))))
onView(withId(R.id.files)).perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(deckName)),
click()
)
)
}
private fun clickOnStudyButtonIfExists() {
onView(withId(R.id.studyoptions_start))
.withFailureHandler { _, _ -> }
.perform(click())
}
private fun reviewDeckWithName(deckName: String) {
clickOnDeckWithName(deckName)
// Adding cards directly to the database while in the Deck Picker screen
// will not update the page with correct card counts. Hence, clicking
// on the deck will bring us to the study options page where we need to
// click on the Study button. If we have added cards to the database
// before the Deck Picker screen has fully loaded, then we skip clicking
// the Study button
clickOnStudyButtonIfExists()
}
private fun clickShowAnswerAndAnswerGood() {
clickShowAnswer()
ensureAnswerButtonsAreDisplayed()
try {
// ...on the command line it has resource name "good_button"...
onView(withResourceName("good_button")).perform(click())
} catch (e: NoMatchingViewException) {
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
onView(withResourceName("flashcard_layout_ease3")).perform(click())
}
}
private fun clickShowAnswer() {
try {
// ... on the command line, it has resource name "show_answer"...
onView(withResourceName("show_answer")).perform(click())
} catch (e: NoMatchingViewException) {
// ... but in Android Studio it has resource name "flashcard_layout_flip" !?
onView(withResourceName("flashcard_layout_flip")).perform(click())
}
}
private fun ensureAnswerButtonsAreDisplayed() {
// We need to wait for the card to fully load to allow enough time for
// the messages to be passed in and out of the WebView when evaluating
// the custom JS scheduler code. The ease buttons are hidden until the
// custom scheduler has finished running
try {
// ...on the command line it has resource name "good_button"...
onView(withResourceName("good_button")).checkWithTimeout(
matches(isDisplayed()),
100
)
} catch (e: AssertionError) {
// ...but in Android Studio it has resource name "flashcard_layout_ease3" !?
onView(withResourceName("flashcard_layout_ease3")).checkWithTimeout(
matches(isDisplayed()),
100
)
}
}
}
private var Collection.cardStateCustomizer: String?
get() = config.get("cardStateCustomizer")
set(value) { config.set("cardStateCustomizer", value) }
fun closeGetStartedScreenIfExists() {
onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click())
}
fun closeBackupCollectionDialogIfExists() {
onView(withText(R.string.button_backup_later))
.withFailureHandler { _, _ -> }
.perform(click())
}

View File

@ -1,99 +1,99 @@
/*
* Copyright (c) 2023 Abdo <abdo@abdnh.net>
*
* 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.pages
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.dialogs.DiscardChangesDialog
import org.json.JSONObject
import timber.log.Timber
class ImageOcclusion : PageFragment(R.layout.image_occlusion) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(requireActivity()) {
onBackPressedDispatcher.addCallback(this) {
DiscardChangesDialog.showDialog(this@with) {
finish()
}
}
}
view.findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
if (it.itemId == R.id.action_save) {
Timber.i("save item selected")
webView.evaluateJavascript("anki.imageOcclusion.save()", null)
}
return@setOnMenuItemClickListener true
}
}
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
return object : PageWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val kind = requireArguments().getString(ARG_KEY_KIND)
val noteOrNotetypeId = requireArguments().getLong(ARG_KEY_ID)
val imagePath = requireArguments().getString(ARG_KEY_PATH)
val options = JSONObject()
options.put("kind", kind)
if (kind == "add") {
options.put("imagePath", imagePath)
options.put("notetypeId", noteOrNotetypeId)
} else {
options.put("noteId", noteOrNotetypeId)
}
view?.evaluateJavascript("globalThis.anki.imageOcclusion.mode = $options") {
super.onPageFinished(view, url)
}
}
}
}
companion object {
private const val ARG_KEY_KIND = "kind"
private const val ARG_KEY_ID = "id"
private const val ARG_KEY_PATH = "imagePath"
fun getIntent(context: Context, kind: String, noteOrNotetypeId: Long, imagePath: String?): Intent {
val suffix = if (kind == "edit") {
"/$noteOrNotetypeId"
} else {
imagePath
}
val arguments = bundleOf(
ARG_KEY_KIND to kind,
ARG_KEY_ID to noteOrNotetypeId,
ARG_KEY_PATH to imagePath,
PATH_ARG_KEY to "image-occlusion$suffix"
)
return SingleFragmentActivity.getIntent(context, ImageOcclusion::class, arguments)
}
}
}
/*
* Copyright (c) 2023 Abdo <abdo@abdnh.net>
*
* 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.pages
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.dialogs.DiscardChangesDialog
import org.json.JSONObject
import timber.log.Timber
class ImageOcclusion : PageFragment(R.layout.image_occlusion) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(requireActivity()) {
onBackPressedDispatcher.addCallback(this) {
DiscardChangesDialog.showDialog(this@with) {
finish()
}
}
}
view.findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
if (it.itemId == R.id.action_save) {
Timber.i("save item selected")
webView.evaluateJavascript("anki.imageOcclusion.save()", null)
}
return@setOnMenuItemClickListener true
}
}
override fun onCreateWebViewClient(savedInstanceState: Bundle?): PageWebViewClient {
return object : PageWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val kind = requireArguments().getString(ARG_KEY_KIND)
val noteOrNotetypeId = requireArguments().getLong(ARG_KEY_ID)
val imagePath = requireArguments().getString(ARG_KEY_PATH)
val options = JSONObject()
options.put("kind", kind)
if (kind == "add") {
options.put("imagePath", imagePath)
options.put("notetypeId", noteOrNotetypeId)
} else {
options.put("noteId", noteOrNotetypeId)
}
view?.evaluateJavascript("globalThis.anki.imageOcclusion.mode = $options") {
super.onPageFinished(view, url)
}
}
}
}
companion object {
private const val ARG_KEY_KIND = "kind"
private const val ARG_KEY_ID = "id"
private const val ARG_KEY_PATH = "imagePath"
fun getIntent(context: Context, kind: String, noteOrNotetypeId: Long, imagePath: String?): Intent {
val suffix = if (kind == "edit") {
"/$noteOrNotetypeId"
} else {
imagePath
}
val arguments = bundleOf(
ARG_KEY_KIND to kind,
ARG_KEY_ID to noteOrNotetypeId,
ARG_KEY_PATH to imagePath,
PATH_ARG_KEY to "image-occlusion$suffix"
)
return SingleFragmentActivity.getIntent(context, ImageOcclusion::class, arguments)
}
}
}

View File

@ -1,31 +1,31 @@
/*
* Copyright (c) 2022 Brayan Oliveira <brayandso.dev@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.preferences
import com.ichi2.anki.R
/**
* Fragment with preferences related to notifications
*/
class AccessibilitySettingsFragment : SettingsFragment() {
override val preferenceResource: Int
get() = R.xml.preferences_accessibility
override val analyticsScreenNameConstant: String
get() = "prefs.accessibility"
override fun initSubscreen() {
}
}
/*
* Copyright (c) 2022 Brayan Oliveira <brayandso.dev@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.preferences
import com.ichi2.anki.R
/**
* Fragment with preferences related to notifications
*/
class AccessibilitySettingsFragment : SettingsFragment() {
override val preferenceResource: Int
get() = R.xml.preferences_accessibility
override val analyticsScreenNameConstant: String
get() = "prefs.accessibility"
override fun initSubscreen() {
}
}

View File

@ -1,55 +1,55 @@
/*
* Copyright (c) 2024 WPum <27683756+WPum@users.noreply.github.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
import com.ichi2.anki.notifications.NotificationId
import com.ichi2.anki.worker.UniqueWorkNames
import org.junit.Test
import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
class ConstantUniquenessTest {
@Test
fun testConstantUniqueness() {
assertConstantUniqueness(NotificationId::class)
assertConstantUniqueness(UniqueWorkNames::class)
}
companion object {
/**
* To check whether all PUBLIC CONST values in an object are unique.
*/
fun <T : Any> assertConstantUniqueness(clazz: KClass<T>) {
assertNotNull(clazz.objectInstance, "Can only check objects for uniqueness")
val valueSet = HashSet<Any?>()
for (prop in clazz.declaredMemberProperties) {
if (!prop.isConst || prop.visibility != KVisibility.PUBLIC) {
continue
}
// use .call() since clazz represents an object
val value = prop.call()
assertFalse(valueSet.contains(value), "Duplicate value ('$value') for constant in ${clazz.qualifiedName}")
valueSet.add(value)
}
}
}
}
/*
* Copyright (c) 2024 WPum <27683756+WPum@users.noreply.github.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
import com.ichi2.anki.notifications.NotificationId
import com.ichi2.anki.worker.UniqueWorkNames
import org.junit.Test
import kotlin.reflect.KClass
import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberProperties
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
class ConstantUniquenessTest {
@Test
fun testConstantUniqueness() {
assertConstantUniqueness(NotificationId::class)
assertConstantUniqueness(UniqueWorkNames::class)
}
companion object {
/**
* To check whether all PUBLIC CONST values in an object are unique.
*/
fun <T : Any> assertConstantUniqueness(clazz: KClass<T>) {
assertNotNull(clazz.objectInstance, "Can only check objects for uniqueness")
val valueSet = HashSet<Any?>()
for (prop in clazz.declaredMemberProperties) {
if (!prop.isConst || prop.visibility != KVisibility.PUBLIC) {
continue
}
// use .call() since clazz represents an object
val value = prop.call()
assertFalse(valueSet.contains(value), "Duplicate value ('$value') for constant in ${clazz.qualifiedName}")
valueSet.add(value)
}
}
}
}

View File

@ -1,30 +1,30 @@
/*
* Copyright (c) 2024 Brayan Oliveira <brayandso.dev@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.previewer
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
class PreviewerViewModelTest {
@Test
fun `type answer fields are removed in questions`() {
assertThat(
PreviewerViewModel.typeAnsQuestionFilter("creu [[type:leu]]"),
equalTo("creu ")
)
}
}
/*
* Copyright (c) 2024 Brayan Oliveira <brayandso.dev@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.previewer
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
class PreviewerViewModelTest {
@Test
fun `type answer fields are removed in questions`() {
assertThat(
PreviewerViewModel.typeAnsQuestionFilter("creu [[type:leu]]"),
equalTo("creu ")
)
}
}

View File

@ -1,15 +1,15 @@
# We want control over the SDK used to build since we need newer JDKs
# Use the latest LTS supported by temurin with version format xx.yy.zz-tem (ignore sub-version)
# You may find versions e.g. for JDK21 like so https://adoptium.net/temurin/archive/?version=21
# You may verify that a JDK is installed by looking at the "sdk list java" output from, for example:
# https://jitpack.io/com/github/ankidroid/Anki-Android/v2.18alpha7/build.log (just use a recent tag,
# if it has not built yet you will have to wait for it to build then on refresh the build log should exist)
before_install:
- sdk update
- sdk list java
- sdk install java 21.0.2-tem
- sdk use java 21.0.2-tem
# We can do the absolute minimum to build the API module, no need to build AnkiDroid module
install:
- ./gradlew :api:publishToMavenLocal
# We want control over the SDK used to build since we need newer JDKs
# Use the latest LTS supported by temurin with version format xx.yy.zz-tem (ignore sub-version)
# You may find versions e.g. for JDK21 like so https://adoptium.net/temurin/archive/?version=21
# You may verify that a JDK is installed by looking at the "sdk list java" output from, for example:
# https://jitpack.io/com/github/ankidroid/Anki-Android/v2.18alpha7/build.log (just use a recent tag,
# if it has not built yet you will have to wait for it to build then on refresh the build log should exist)
before_install:
- sdk update
- sdk list java
- sdk install java 21.0.2-tem
- sdk use java 21.0.2-tem
# We can do the absolute minimum to build the API module, no need to build AnkiDroid module
install:
- ./gradlew :api:publishToMavenLocal