diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt index 5bd386403e0..003c40369b5 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt @@ -51,14 +51,16 @@ class AdministratorControlsViewModel @Inject constructor( private fun processGetDeviceSettingsResult( deviceSettingsResult: AsyncResult ): DeviceSettings { - if (deviceSettingsResult.isFailure()) { - oppiaLogger.e( - "AdministratorControlsFragment", - "Failed to retrieve profile", - deviceSettingsResult.getErrorOrNull()!! - ) + return when (deviceSettingsResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "AdministratorControlsFragment", "Failed to retrieve profile", deviceSettingsResult.error + ) + DeviceSettings.getDefaultInstance() + } + is AsyncResult.Pending -> DeviceSettings.getDefaultInstance() + is AsyncResult.Success -> deviceSettingsResult.value } - return deviceSettingsResult.getOrDefault(DeviceSettings.getDefaultInstance()) } private fun processAdministratorControlsList( diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt index 8dded74ea84..861862fe597 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt @@ -7,6 +7,7 @@ import org.oppia.android.app.model.DeviceSettings import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData /** [ViewModel] for the recycler view in [AdministratorControlsFragment]. */ @@ -31,11 +32,11 @@ class AdministratorControlsDownloadPermissionsViewModel( .observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic update on wifi permission", - it.getErrorOrNull()!! + it.error ) } } @@ -49,11 +50,11 @@ class AdministratorControlsDownloadPermissionsViewModel( ).toLiveData().observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic auto update permission", - it.getErrorOrNull()!! + it.error ) } } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt index 4db3b84d049..4086552343e 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt @@ -48,14 +48,18 @@ class CompletedStoryListViewModel @Inject constructor( private fun processCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "CompletedStoryListFragment", - "Failed to retrieve CompletedStory list: ", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "CompletedStoryListFragment", + "Failed to retrieve CompletedStory list: ", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun processCompletedStoryList( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt index bcc09604a72..24d1b7fc4e7 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkChaptersCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkChaptersCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkChaptersCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt index b105bbc5a31..8ec76b5d69e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkStoriesCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkStoriesCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkStoriesCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt index ca48064520c..fd2d06e094b 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt @@ -45,14 +45,16 @@ class MarkTopicsCompletedViewModel @Inject constructor( } private fun processAllTopicsResult(allTopics: AsyncResult>): List { - if (allTopics.isFailure()) { - oppiaLogger.e( - "MarkTopicsCompletedFragment", - "Failed to retrieve all topics", - allTopics.getErrorOrNull()!! - ) + return when (allTopics) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkTopicsCompletedFragment", "Failed to retrieve all topics", allTopics.error + ) + mutableListOf() + } + is AsyncResult.Pending -> mutableListOf() + is AsyncResult.Success -> allTopics.value } - return allTopics.getOrDefault(mutableListOf()) } private fun processAllTopics(allTopics: List): List { diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index d367016774a..1caee17fa2d 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -164,14 +164,14 @@ class NavigationDrawerFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("NavigationDrawerFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun getCompletedStoryListCount(): LiveData { @@ -193,14 +193,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun getOngoingTopicListCount(): LiveData { @@ -222,14 +226,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun openActivityByMenuItemId(menuItemId: Int) { diff --git a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt index 24a7efb509f..16e3011628b 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt @@ -25,6 +25,7 @@ import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -94,14 +95,18 @@ class HomeViewModel( */ val homeItemViewModelListLiveData: LiveData> by lazy { Transformations.map(homeItemViewModelListDataProvider.toLiveData()) { itemListResult -> - if (itemListResult.isFailure()) { - oppiaLogger.e( - "HomeFragment", - "No home fragment available -- failed to retrieve fragment data.", - itemListResult.getErrorOrNull() - ) + return@map when (itemListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HomeFragment", + "No home fragment available -- failed to retrieve fragment data.", + itemListResult.error + ) + listOf() + } + is AsyncResult.Pending -> listOf() + is AsyncResult.Success -> itemListResult.value } - return@map itemListResult.getOrDefault(listOf()) } } diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt index c86e6e050bc..1a2647ff393 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt @@ -171,11 +171,12 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( } private fun getAssumedSuccessfulPromotedActivityList(): LiveData { - // If there's an error loading the data, assume the default. return Transformations.map(ongoingStoryListSummaryResultLiveData) { - it.getOrDefault( - PromotedActivityList.getDefaultInstance() - ) + when (it) { + // If there's an error loading the data, assume the default. + is AsyncResult.Failure, is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> it.value + } } } @@ -252,7 +253,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( internalProfileId, @@ -260,9 +261,9 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( promotedStory.storyId, promotedStory.explorationId, backflowScreen = null, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( promotedStory.topicId, @@ -298,17 +299,14 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "RecentlyPlayedFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("RecentlyPlayedFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("RecentlyPlayedFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt index bac0611a5b7..9d3e2fbd9fb 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt @@ -50,14 +50,18 @@ class OngoingTopicListViewModel @Inject constructor( private fun processOngoingTopicResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "OngoingTopicListFragment", - "Failed to retrieve OngoingTopicList: ", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "OngoingTopicListFragment", + "Failed to retrieve OngoingTopicList: ", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun processOngoingTopicList( diff --git a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt index 4c27f79469d..acb9712e054 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt @@ -73,10 +73,14 @@ class OptionControlsViewModel @Inject constructor( } private fun processProfileResult(profile: AsyncResult): Profile { - if (profile.isFailure()) { - oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.getErrorOrNull()!!) + return when (profile) { + is AsyncResult.Failure -> { + oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profile.value } - return profile.getOrDefault(Profile.getDefaultInstance()) } private fun processProfileList(profile: Profile): List { diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index e4b632d3d32..369f0efa623 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -21,6 +21,7 @@ import org.oppia.android.databinding.OptionStoryTextSizeBinding import org.oppia.android.databinding.OptionsFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import java.security.InvalidParameterException import javax.inject.Inject @@ -193,14 +194,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: small text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: small text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -212,14 +213,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: medium text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: medium text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -231,14 +232,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -251,14 +252,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: extra large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: extra large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -276,14 +277,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -295,14 +293,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.HINDI_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.HINDI_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -314,14 +309,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.CHINESE_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.CHINESE_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -333,14 +325,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.FRENCH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.FRENCH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -359,14 +348,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.NO_AUDIO - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: No Audio", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.NO_AUDIO + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: No Audio", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -378,14 +364,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -397,14 +380,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -416,14 +396,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -435,14 +412,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt index d552408ca6e..14c5478b70f 100755 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt @@ -71,10 +71,9 @@ class AudioFragmentPresenter @Inject constructor( .observe( fragment, Observer> { - if (it.isSuccess()) { - val prefs = it.getOrDefault(CellularDataPreference.getDefaultInstance()) - showCellularDataDialog = !(prefs.hideDialog) - useCellularData = prefs.useCellularData + if (it is AsyncResult.Success) { + showCellularDataDialog = !it.value.hideDialog + useCellularData = it.value.useCellularData } } ) @@ -143,10 +142,15 @@ class AudioFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): String { - if (profileResult.isFailure()) { - oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.getErrorOrNull()!!) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return getAudioLanguage(profileResult.getOrDefault(Profile.getDefaultInstance()).audioLanguage) + return getAudioLanguage(profile.audioLanguage) } /** Sets selected language code in presenter and ViewModel */ diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt index d18048e74f1..5c1981790e5 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt @@ -142,26 +142,26 @@ class AudioViewModel @Inject constructor( } private fun processDurationResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().duration + return playProgressResult.value.duration } private fun processPositionResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().position + return playProgressResult.value.position } private fun processPlayStatusResultLiveData( playProgressResult: AsyncResult ): UiAudioPlayStatus { - return when { - playProgressResult.isPending() -> UiAudioPlayStatus.LOADING - playProgressResult.isFailure() -> UiAudioPlayStatus.FAILED - else -> when (playProgressResult.getOrThrow().type) { + return when (playProgressResult) { + is AsyncResult.Pending -> UiAudioPlayStatus.LOADING + is AsyncResult.Failure -> UiAudioPlayStatus.FAILED + is AsyncResult.Success -> when (playProgressResult.value.type) { PlayStatus.PREPARED -> { if (autoPlay) audioPlayerController.play() autoPlay = false diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index aa1edfe9e04..4f131159d98 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -238,18 +238,15 @@ class ExplorationActivityPresenter @Inject constructor( fun stopExploration() { fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) - explorationDataController.stopPlayingExploration() + explorationDataController.stopPlayingExploration().toLiveData() .observe( activity, Observer> { - when { - it.isPending() -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") - it.isFailure() -> oppiaLogger.e( - "ExplorationActivity", - "Failed to stop exploration", - it.getErrorOrNull()!! - ) - else -> { + when (it) { + is AsyncResult.Pending -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ExplorationActivity", "Failed to stop exploration", it.error) + is AsyncResult.Success -> { oppiaLogger.d("ExplorationActivity", "Successfully stopped exploration") backPressActivitySelector(backflowScreen) (activity as ExplorationActivity).finish() @@ -320,14 +317,16 @@ class ExplorationActivityPresenter @Inject constructor( /** Helper for subscribeToExploration. */ private fun processExploration(ephemeralStateResult: AsyncResult): Exploration { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + Exploration.getDefaultInstance() + } + is AsyncResult.Pending -> Exploration.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(Exploration.getDefaultInstance()) } private fun backPressActivitySelector(backflowScreen: Int?) { @@ -420,15 +419,17 @@ class ExplorationActivityPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { - oldestCheckpointExplorationId = it.getOrThrow().explorationId - oldestCheckpointExplorationTitle = it.getOrThrow().explorationTitle - } else if (it.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve oldest saved checkpoint details.", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> { + oldestCheckpointExplorationId = it.value.explorationId + oldestCheckpointExplorationTitle = it.value.explorationTitle + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve oldest saved checkpoint details.", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for an actual result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt index 28dc4156aac..4df78475693 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt @@ -45,13 +45,15 @@ class ExplorationManagerFragmentPresenter @Inject constructor( private fun processReadingTextSizeResult( readingTextSizeResult: AsyncResult ): ReadingTextSize { - if (readingTextSizeResult.isFailure()) { - oppiaLogger.e( - "ExplorationManagerFragment", - "Failed to retrieve profile", - readingTextSizeResult.getErrorOrNull()!! - ) - } - return readingTextSizeResult.getOrDefault(Profile.getDefaultInstance()).readingTextSize + return when (readingTextSizeResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationManagerFragment", "Failed to retrieve profile", readingTextSizeResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> readingTextSizeResult.value + }.readingTextSize } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt index a8460e014ff..2f7e4613911 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt @@ -40,25 +40,25 @@ class HintsAndSolutionExplorationManagerFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "HintsAndSolutionExplorationManagerFragmentPresenter", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HintsAndSolutionExplorationManagerFragmentPresenter", + "Failed to retrieve ephemeral state", + result.error + ) + } // Display nothing until a valid result is available. - return - } - - val ephemeralState = result.getOrThrow() - - // Check if hints are available for this state. - if (ephemeralState.state.interaction.hintList.size != 0) { - (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( - ephemeralState.state, ephemeralState.writtenTranslationContext - ) + is AsyncResult.Pending -> {} + is AsyncResult.Success -> { + // Check if hints are available for this state. + val ephemeralState = result.value + if (ephemeralState.state.interaction.hintList.size != 0) { + (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( + ephemeralState.state, ephemeralState.writtenTranslationContext + ) + } + } } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 4af453b01d8..e3eafd4baee 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -39,6 +39,7 @@ import org.oppia.android.domain.exploration.ExplorationProgressController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.StoryProgressController import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType @@ -282,19 +283,15 @@ class StateFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return + when (result) { + is AsyncResult.Failure -> + oppiaLogger.e("StateFragment", "Failed to retrieve ephemeral state", result.error) + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> processEphemeralState(result.value) } + } - val ephemeralState = result.getOrThrow() + private fun processEphemeralState(ephemeralState: EphemeralState) { explorationCheckpointState = ephemeralState.checkpointState val shouldSplit = splitScreenManager.shouldSplitScreen(ephemeralState.state.interaction.id) if (shouldSplit) { @@ -333,14 +330,12 @@ class StateFragmentPresenter @Inject constructor( } /** Subscribes to the result of requesting to show a hint or solution. */ - private fun subscribeToHintSolution(resultLiveData: LiveData>) { - resultLiveData.observe( + private fun subscribeToHintSolution(resultDataProvider: DataProvider) { + resultDataProvider.toLiveData().observe( fragment, { result -> - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", "Failed to retrieve hint/solution", result.getErrorOrNull()!! - ) + if (result is AsyncResult.Failure) { + oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. viewModel.setHintOpenedAndUnRevealedVisibility(false) @@ -389,18 +384,20 @@ class StateFragmentPresenter @Inject constructor( private fun processAnswerOutcome( ephemeralStateResult: AsyncResult ): AnswerOutcome { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateFragment", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + AnswerOutcome.getDefaultInstance() + } + is AsyncResult.Pending -> AnswerOutcome.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(AnswerOutcome.getDefaultInstance()) } private fun handleSubmitAnswer(answer: UserAnswer) { - subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer)) + subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer).toLiveData()) } fun dismissConceptCard() { @@ -413,7 +410,7 @@ class StateFragmentPresenter @Inject constructor( private fun moveToNextState() { viewModel.setCanSubmitAnswer(canSubmitAnswer = false) - explorationProgressController.moveToNextState().observe( + explorationProgressController.moveToNextState().toLiveData().observe( fragment, Observer { recyclerViewAssembler.collapsePreviousResponses() diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 8375b12f475..9d32680b3e0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -220,7 +220,7 @@ class StatePlayerRecyclerViewAssembler private constructor( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.pendingState.wrongAnswerList, - /* isCorrectAnswer= */ false, + isLastAnswerCorrect = false, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -251,7 +251,7 @@ class StatePlayerRecyclerViewAssembler private constructor( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.completedState.answerList, - /* isCorrectAnswer= */ true, + isLastAnswerCorrect = true, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -346,7 +346,7 @@ class StatePlayerRecyclerViewAssembler private constructor( pendingItemList: MutableList, rightPendingItemList: MutableList, answersAndResponses: List, - isCorrectAnswer: Boolean, + isLastAnswerCorrect: Boolean, gcsEntityId: String, writtenTranslationContext: WrittenTranslationContext ) { @@ -369,6 +369,8 @@ class StatePlayerRecyclerViewAssembler private constructor( hasPreviousResponsesExpanded for (answerAndResponse in answersAndResponses.take(answersAndResponses.size - 1)) { if (playerFeatureSet.pastAnswerSupport) { + // Earlier answers can't be correct (since otherwise new answers wouldn't be able to be + // submitted), hence the assumption that these aren't. createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, @@ -396,17 +398,17 @@ class StatePlayerRecyclerViewAssembler private constructor( } answersAndResponses.lastOrNull()?.let { answerAndResponse -> if (playerFeatureSet.pastAnswerSupport) { - if (isCorrectAnswer && isSplitView.get()!!) { + if (isLastAnswerCorrect && isSplitView.get()!!) { rightPendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - /* isAnswerCorrect= */ true + isAnswerCorrect = true ) } else { pendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - this.isCorrectAnswer.get()!! + isLastAnswerCorrect || answerAndResponse.isCorrectAnswer ) } } @@ -1055,6 +1057,9 @@ class StatePlayerRecyclerViewAssembler private constructor( when (userAnswer.textualAnswerCase) { UserAnswer.TextualAnswerCase.HTML_ANSWER -> { showSingleAnswer(binding) + val accessibleAnswer = if (userAnswer.contentDescription.isNotEmpty()) { + userAnswer.contentDescription + } else null val htmlParser = htmlParserFactory.create( resourceBucketName, entityType, @@ -1067,7 +1072,8 @@ class StatePlayerRecyclerViewAssembler private constructor( userAnswer.htmlAnswer, binding.submittedAnswerTextView, supportsConceptCards = submittedAnswerViewModel.supportsConceptCards - ) + ), + accessibleAnswer ) } UserAnswer.TextualAnswerCase.LIST_OF_HTML_ANSWERS -> { diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 3ca8d4771b4..9929a4077ee 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -19,6 +19,7 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject private const val TEST_ACTIVITY_TAG = "TestActivity" @@ -96,24 +97,20 @@ class StateFragmentTestActivityPresenter @Inject constructor( explorationId, shouldSavePartialProgress, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - .observe( - activity, - Observer> { result -> - when { - result.isPending() -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") - result.isFailure() -> oppiaLogger.e( - TEST_ACTIVITY_TAG, - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { - oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") - initializeExploration(profileId, topicId, storyId, explorationId) - } + ).toLiveData().observe( + activity, + Observer> { result -> + when (result) { + is AsyncResult.Pending -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e(TEST_ACTIVITY_TAG, "Failed to load exploration", result.error) + is AsyncResult.Success -> { + oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") + initializeExploration(profileId, topicId, storyId, explorationId) } } - ) + } + ) } /** diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt index ce0a67e81db..82a0ad1a982 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt @@ -274,26 +274,30 @@ class AddProfileActivityPresenter @Inject constructor( result: AsyncResult, binding: AddProfileActivityBinding ) { - if (result.isSuccess()) { - val intent = Intent(activity, ProfileChooserActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { - is ProfileManagementController.ProfileNameNotUniqueException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_not_unique + when (result) { + is AsyncResult.Success -> { + val intent = Intent(activity, ProfileChooserActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivity(intent) + } + is AsyncResult.Failure -> { + when (result.error) { + is ProfileManagementController.ProfileNameNotUniqueException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_not_unique + ) ) - ) - is ProfileManagementController.ProfileNameOnlyLettersException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_only_letters + is ProfileManagementController.ProfileNameOnlyLettersException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_only_letters + ) ) - ) + } + binding.addProfileActivityScrollView.smoothScrollTo(0, 0) } - binding.addProfileActivityScrollView.smoothScrollTo(0, 0) + is AsyncResult.Pending -> {} // Wait for an actual result. } } diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt index aae69e73de2..8a7a082abb1 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt @@ -15,6 +15,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AdminPinActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -114,7 +115,7 @@ class AdminPinActivityPresenter @Inject constructor( profileManagementController.updatePin(profileId, inputPin).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { when (activity.intent.getIntExtra(ADMIN_PIN_ENUM_EXTRA_KEY, 0)) { AdminAuthEnum.PROFILE_ADMIN_CONTROLS.value -> { activity.startActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index b8d4b3c8dab..1894391df81 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -18,6 +18,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.PinPasswordActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -88,7 +89,7 @@ class PinPasswordActivityPresenter @Inject constructor( .observe( activity, { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity((HomeActivity.createHomeActivity(activity, profileId))) } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt index d7d111158ba..edd0ba366ac 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt @@ -48,14 +48,14 @@ class PinPasswordViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "PinPasswordActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("PinPasswordActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) correctPin.set(profile.pin) isAdmin.set(profile.isAdmin) name.set(profile.name) diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt index dcbf77608db..5d12e64ec28 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt @@ -123,14 +123,18 @@ class ProfileChooserFragmentPresenter @Inject constructor( private fun processWasProfileEverBeenAddedResult( wasProfileEverBeenAddedResult: AsyncResult ): Boolean { - if (wasProfileEverBeenAddedResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserFragment", - "Failed to retrieve the information on wasProfileEverBeenAdded", - wasProfileEverBeenAddedResult.getErrorOrNull()!! - ) + return when (wasProfileEverBeenAddedResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserFragment", + "Failed to retrieve the information on wasProfileEverBeenAdded", + wasProfileEverBeenAddedResult.error + ) + false + } + is AsyncResult.Pending -> false + is AsyncResult.Success -> wasProfileEverBeenAddedResult.value } - return wasProfileEverBeenAddedResult.getOrDefault(/* defaultValue= */ false) } /** Randomly selects a color for the new profile that is not already in use. */ @@ -173,7 +177,7 @@ class ProfileChooserFragmentPresenter @Inject constructor( profileManagementController.loginToProfile(model.profile.id).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity( ( HomeActivity.createHomeActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt index 217a72fcbb1..d78a40767e7 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt @@ -42,14 +42,16 @@ class ProfileChooserViewModel @Inject constructor( private fun processGetProfilesResult( profilesResult: AsyncResult> ): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) - } - val profileList = profilesResult.getOrDefault(emptyList()).map { + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value + }.map { ProfileChooserUiModel.newBuilder().setProfile(it).build() }.toMutableList() diff --git a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt index 852417f4263..0b6d5beb5b6 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt @@ -16,6 +16,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResetPinDialogBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -95,7 +96,7 @@ class ResetPinDialogFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { routeDialogInterface.routeToSuccessDialog() } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt index 6c3a8a2fb67..9a487c48a1a 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt @@ -80,14 +80,14 @@ class ProfilePictureActivityPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfilePictureActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfilePictureActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun setProfileAvatar(avatar: ProfileAvatar) { diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt index e84fea15ab5..c6036860a9d 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt @@ -74,14 +74,14 @@ class ProfileProgressViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfileProgressFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private val promotedActivityListResultLiveData: @@ -113,16 +113,20 @@ class ProfileProgressViewModel @Inject constructor( } private fun processPromotedActivityListResult( - promotedActivityListtResult: AsyncResult + promotedActivityListResult: AsyncResult ): PromotedActivityList { - if (promotedActivityListtResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve promoted story list: ", - promotedActivityListtResult.getErrorOrNull()!! - ) + return when (promotedActivityListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve promoted story list: ", + promotedActivityListResult.error + ) + PromotedActivityList.getDefaultInstance() + } + is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> promotedActivityListResult.value } - return promotedActivityListtResult.getOrDefault(PromotedActivityList.getDefaultInstance()) } private fun processPromotedActivityList( @@ -169,14 +173,18 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun subscribeToOngoingTopicListLiveData() { @@ -198,13 +206,17 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index 16cc17775b9..f49c2081f62 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -139,14 +139,18 @@ class ResumeLessonFragmentPresenter @Inject constructor( private fun processChapterSummaryResult( chapterSummaryResult: AsyncResult ): ChapterSummary { - if (chapterSummaryResult.isFailure()) { - oppiaLogger.e( - "ResumeLessonFragment", - "Failed to retrieve chapter summary for the explorationId $explorationId: ", - chapterSummaryResult.getErrorOrNull() - ) + return when (chapterSummaryResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ResumeLessonFragment", + "Failed to retrieve chapter summary for the explorationId $explorationId: ", + chapterSummaryResult.error + ) + ChapterSummary.getDefaultInstance() + } + is AsyncResult.Pending -> ChapterSummary.getDefaultInstance() + is AsyncResult.Success -> chapterSummaryResult.value } - return chapterSummaryResult.getOrDefault(ChapterSummary.getDefaultInstance()) } private fun playExploration( @@ -166,17 +170,14 @@ class ResumeLessonFragmentPresenter @Inject constructor( // ResumeLessonFragment implies that learner has not completed the lesson. shouldSavePartialProgress = true, explorationCheckpoint - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "ResumeLessonFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ResumeLessonFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("ResumeLessonFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index 9839f15b8a8..10bc638a31b 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -14,6 +14,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.databinding.ProfileEditFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -96,11 +97,9 @@ class ProfileEditFragmentPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( - "ProfileEditActivityPresenter", - "Failed to updated allow download access", - it.getErrorOrNull()!! + "ProfileEditActivityPresenter", "Failed to updated allow download access", it.error ) } } @@ -126,7 +125,7 @@ class ProfileEditFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { if (fragment.requireContext().resources.getBoolean(R.bool.isTablet)) { val intent = Intent(fragment.requireContext(), AdministratorControlsActivity::class.java) diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt index 549c3be7798..75a3a98b5dd 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt @@ -44,14 +44,18 @@ class ProfileEditViewModel @Inject constructor( /** Fetches the profile of a user asynchronously. */ private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileEditViewModel", - "Failed to retrieve the profile with ID: ${profileId.internalId}", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileEditViewModel", + "Failed to retrieve the profile with ID: ${profileId.internalId}", + profileResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) isAllowedDownloadAccessMutableLiveData.value = profile.allowDownloadAccess isAdmin = profile.isAdmin return profile diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt index a8b82176459..dfa4e50e6fe 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt @@ -28,14 +28,16 @@ class ProfileListViewModel @Inject constructor( } private fun processGetProfilesResult(profilesResult: AsyncResult>): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileListViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileListViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value } - val profileList = profilesResult.getOrDefault(emptyList()) val sortedProfileList = profileList.sortedBy { machineLocale.run { it.name.toMachineLowerCase() } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt index 7ab38ce0f16..6350c2e4581 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt @@ -101,12 +101,12 @@ class ProfileRenameFragmentPresenter @Inject constructor( } private fun handleAddProfileResult(result: AsyncResult, profileId: Int) { - if (result.isSuccess()) { + if (result is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { + } else if (result is AsyncResult.Failure) { + when (result.error) { is ProfileManagementController.ProfileNameNotUniqueException -> renameViewModel.nameErrorMsg.set( resourceHandler.getStringInLocale( diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt index da948172c4c..b4afe1d73e1 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt @@ -16,6 +16,7 @@ import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextCha import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileResetPinFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -133,7 +134,7 @@ class ProfileResetPinFragmentPresenter @Inject constructor( .observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt index 8d779fb14fd..84fe06a1197 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt @@ -115,16 +115,15 @@ class SplashActivityPresenter @Inject constructor( private fun processInitState( initStateResult: AsyncResult ): SplashInitState { - if (initStateResult.isFailure()) { - oppiaLogger.e( - "SplashActivity", - "Failed to compute initial state", - initStateResult.getErrorOrNull() - ) - } - // If there's an error loading the data, assume the default. - return initStateResult.getOrDefault(SplashInitState.computeDefault(localeController)) + return when (initStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("SplashActivity", "Failed to compute initial state", initStateResult.error) + SplashInitState.computeDefault(localeController) + } + is AsyncResult.Pending -> SplashInitState.computeDefault(localeController) + is AsyncResult.Success -> initStateResult.value + } } private fun getDeprecationNoticeDialogFragment(): AutomaticAppDeprecationNoticeDialogFragment? { diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index fd52ed1ee7e..9c03a1b5b48 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -35,6 +35,7 @@ import org.oppia.android.databinding.StoryHeaderViewBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.parser.html.TopicHtmlParserEntityType @@ -273,17 +274,14 @@ class StoryFragmentPresenter @Inject constructor( shouldSavePartialProgress = shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("Story Fragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "Story Fragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("Story Fragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("Story Fragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("Story Fragment", "Successfully loaded exploration: $explorationId") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt index 6b7970a08cf..c127eae3707 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt @@ -70,15 +70,14 @@ class StoryViewModel @Inject constructor( } private fun processStoryResult(storyResult: AsyncResult): StorySummary { - if (storyResult.isFailure()) { - oppiaLogger.e( - "StoryFragment", - "Failed to retrieve Story: ", - storyResult.getErrorOrNull()!! - ) + return when (storyResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("StoryFragment", "Failed to retrieve Story: ", storyResult.error) + StorySummary.getDefaultInstance() + } + is AsyncResult.Pending -> StorySummary.getDefaultInstance() + is AsyncResult.Success -> storyResult.value } - - return storyResult.getOrDefault(StorySummary.getDefaultInstance()) } private fun processStoryChapterList(storySummary: StorySummary): List { diff --git a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt index 10bee4d6ddc..db9186002dd 100644 --- a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt @@ -56,7 +56,7 @@ class StoryChapterSummaryViewModel( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, @@ -66,9 +66,9 @@ class StoryChapterSummaryViewModel( canExplorationBeResumed = true, shouldSavePartialProgress, backflowId = 1, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index 36ae33e2d6e..5fda9abfdb1 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -13,6 +13,7 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject private const val INTERNAL_PROFILE_ID = 0 @@ -47,24 +48,22 @@ class ExplorationTestActivityPresenter @Inject constructor( EXPLORATION_ID, shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( activity, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Loading exploration") - result.isFailure() -> oppiaLogger.e( - TAG_EXPLORATION_TEST_ACTIVITY, - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> + oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e(TAG_EXPLORATION_TEST_ACTIVITY, "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Successfully loaded exploration") routeToExplorationListener.routeToExploration( INTERNAL_PROFILE_ID, TOPIC_ID, STORY_ID, EXPLORATION_ID, - /* backflowScreen= */ null, + backflowScreen = null, isCheckpointingEnabled = false ) } diff --git a/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt index 4bceb84f036..483f745c470 100644 --- a/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt @@ -48,7 +48,7 @@ class SplashTestActivityPresenter @Inject constructor( } private fun processPlatformParameters(loadingStatus: AsyncResult): Boolean { - return loadingStatus.isSuccess() + return loadingStatus is AsyncResult.Success } private fun showToastIfAllowed() { diff --git a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt index 1165eb155f2..46d591d347a 100644 --- a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt @@ -52,14 +52,13 @@ class TopicViewModel @Inject constructor( } private fun processTopicResult(topicResult: AsyncResult): Topic { - if (topicResult.isFailure()) { - oppiaLogger.e( - "TopicFragment", - "Failed to retrieve Topic: ", - topicResult.getErrorOrNull()!! - ) + return when (topicResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicFragment", "Failed to retrieve Topic: ", topicResult.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topicResult.value } - - return topicResult.getOrDefault(Topic.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt index f360fa5c359..70964188b58 100644 --- a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt @@ -41,13 +41,15 @@ class ConceptCardViewModel @Inject constructor( private fun processConceptCardResult( conceptCardResult: AsyncResult ): EphemeralConceptCard { - if (conceptCardResult.isFailure()) { - oppiaLogger.e( - "ConceptCardFragment", - "Failed to retrieve Concept Card", - conceptCardResult.getErrorOrNull()!! - ) + return when (conceptCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ConceptCardFragment", "Failed to retrieve Concept Card", conceptCardResult.error + ) + EphemeralConceptCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralConceptCard.getDefaultInstance() + is AsyncResult.Success -> conceptCardResult.value } - return conceptCardResult.getOrDefault(EphemeralConceptCard.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt index c1ece29b555..9195ce8ebb5 100644 --- a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt @@ -95,10 +95,14 @@ class TopicInfoFragmentPresenter @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", topic.getErrorOrNull()!!) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun controlSeeMoreTextVisibility() { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt index 930b512dd0d..4bf478275cb 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt @@ -47,14 +47,14 @@ class TopicLessonViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicLessonFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicLessonFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun processTopic(topic: Topic): List { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt index 2e437007ed7..c8c8e4c218a 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt @@ -243,7 +243,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( internalProfileId, @@ -251,9 +251,9 @@ class TopicLessonsFragmentPresenter @Inject constructor( storyId, explorationId, backflowScreen = 0, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( internalProfileId, @@ -292,17 +292,14 @@ class TopicLessonsFragmentPresenter @Inject constructor( shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("TopicLessonsFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "TopicLessonsFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("TopicLessonsFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("TopicLessonsFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("TopicLessonsFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt index 1cb085fcf87..16f7e7777ff 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt @@ -52,14 +52,14 @@ class TopicPracticeViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicPracticeFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicPracticeFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun processTopicPracticeSkillList(topic: Topic): List { diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt index c6432134ffe..deeee084741 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt @@ -40,25 +40,24 @@ class HintsAndSolutionQuestionManagerFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "HintsAndSolutionQuestionManagerFragmentPresenter", - "Failed to retrieve ephemeral state", result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return - } - - val ephemeralQuestionState = result.getOrThrow() - - // Check if hints are available for this state. - if (ephemeralQuestionState.ephemeralState.state.interaction.hintList.size != 0) { - (activity as HintsAndSolutionQuestionManagerListener).onQuestionStateLoaded( - ephemeralQuestionState.ephemeralState.state, - ephemeralQuestionState.ephemeralState.writtenTranslationContext - ) + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HintsAndSolutionQuestionManagerFragmentPresenter", + "Failed to retrieve ephemeral state", + result.error + ) + } + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> { + // Check if hints are available for this state. + if (result.value.ephemeralState.state.interaction.hintList.size != 0) { + (activity as HintsAndSolutionQuestionManagerListener).onQuestionStateLoaded( + result.value.ephemeralState.state, + result.value.ephemeralState.writtenTranslationContext + ) + } + } } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt index 632d13a5608..6e58b27a68e 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.databinding.QuestionPlayerActivityBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionTrainingController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject @@ -101,20 +102,14 @@ class QuestionPlayerActivityPresenter @Inject constructor( startDataProvider.toLiveData().observe( activity, { - when { - it.isPending() -> oppiaLogger.d( - "QuestionPlayerActivity", - "Starting training session" - ) - it.isFailure() -> { - oppiaLogger.e( - "QuestionPlayerActivity", - "Failed to start training session", - it.getErrorOrNull()!! - ) + when (it) { + is AsyncResult.Pending -> + oppiaLogger.d("QuestionPlayerActivity", "Starting training session") + is AsyncResult.Failure -> { + oppiaLogger.e("QuestionPlayerActivity", "Failed to start training session", it.error) activity.finish() // Can't recover from the session failing to start. } - else -> { + is AsyncResult.Success -> { oppiaLogger.d("QuestionPlayerActivity", "Successfully started training session") callback() } @@ -127,20 +122,14 @@ class QuestionPlayerActivityPresenter @Inject constructor( questionTrainingController.stopQuestionTrainingSession().toLiveData().observe( activity, { - when { - it.isPending() -> oppiaLogger.d( - "QuestionPlayerActivity", - "Stopping training session" - ) - it.isFailure() -> { - oppiaLogger.e( - "QuestionPlayerActivity", - "Failed to stop training session", - it.getErrorOrNull()!! - ) + when (it) { + is AsyncResult.Pending -> + oppiaLogger.d("QuestionPlayerActivity", "Stopping training session") + is AsyncResult.Failure -> { + oppiaLogger.e("QuestionPlayerActivity", "Failed to stop training session", it.error) activity.finish() // Can't recover from the session failing to stop. } - else -> { + is AsyncResult.Success -> { oppiaLogger.d("QuestionPlayerActivity", "Successfully stopped training session") callback() } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index 540cf538e7b..02a4ab6daab 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -32,6 +32,7 @@ import org.oppia.android.databinding.QuestionPlayerFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionAssessmentProgressController import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.QuestionResourceBucketName import org.oppia.android.util.system.OppiaClock @@ -187,17 +188,18 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } private fun processEphemeralQuestionResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "QuestionPlayerFragment", - "Failed to retrieve ephemeral question", - result.getErrorOrNull()!! - ) - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "QuestionPlayerFragment", "Failed to retrieve ephemeral question", result.error + ) + } + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> processEphemeralQuestion(result.value) } - val ephemeralQuestion = result.getOrThrow() + } + + private fun processEphemeralQuestion(ephemeralQuestion: EphemeralQuestion) { // TODO(#497): Update this to properly link to question assets. val skillId = ephemeralQuestion.question.linkedSkillIdsList.firstOrNull() ?: "" @@ -253,7 +255,7 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } private fun handleSubmitAnswer(answer: UserAnswer) { - subscribeToAnswerOutcome(questionAssessmentProgressController.submitAnswer(answer)) + subscribeToAnswerOutcome(questionAssessmentProgressController.submitAnswer(answer).toLiveData()) } /** This function listens to and processes the result of submitAnswer from QuestionAssessmentProgressController. */ @@ -277,14 +279,12 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } /** Subscribes to the result of requesting to show a hint or solution. */ - private fun subscribeToHintSolution(resultLiveData: LiveData>) { - resultLiveData.observe( + private fun subscribeToHintSolution(resultDataProvider: DataProvider) { + resultDataProvider.toLiveData().observe( fragment, { result -> - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", "Failed to retrieve hint/solution", result.getErrorOrNull()!! - ) + if (result is AsyncResult.Failure) { + oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. questionViewModel.setHintOpenedAndUnRevealedVisibility(false) @@ -297,14 +297,18 @@ class QuestionPlayerFragmentPresenter @Inject constructor( private fun processAnsweredQuestionOutcome( answeredQuestionOutcomeResult: AsyncResult ): AnsweredQuestionOutcome { - if (answeredQuestionOutcomeResult.isFailure()) { - oppiaLogger.e( - "QuestionPlayerFragment", - "Failed to retrieve answer outcome", - answeredQuestionOutcomeResult.getErrorOrNull()!! - ) + return when (answeredQuestionOutcomeResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "QuestionPlayerFragment", + "Failed to retrieve answer outcome", + answeredQuestionOutcomeResult.error + ) + AnsweredQuestionOutcome.getDefaultInstance() + } + is AsyncResult.Pending -> AnsweredQuestionOutcome.getDefaultInstance() + is AsyncResult.Success -> answeredQuestionOutcomeResult.value } - return answeredQuestionOutcomeResult.getOrDefault(AnsweredQuestionOutcome.getDefaultInstance()) } private fun moveToNextState() { diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt index 0849e5f4f7f..a1574b5ca5b 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt @@ -54,14 +54,14 @@ class TopicRevisionViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicRevisionFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicRevisionFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } fun setTopicId(topicId: String) { diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt index f67bf0289fd..78ce25bfb86 100644 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt @@ -118,15 +118,17 @@ class RevisionCardActivityPresenter @Inject constructor( private fun processSubtopicTitleResult( revisionCardResult: AsyncResult ): String { - if (revisionCardResult.isFailure()) { - oppiaLogger.e( - "RevisionCardActivity", - "Failed to retrieve Revision Card", - revisionCardResult.getErrorOrNull()!! - ) - } val ephemeralRevisionCard = - revisionCardResult.getOrDefault(EphemeralRevisionCard.getDefaultInstance()) + when (revisionCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "RevisionCardActivity", "Failed to retrieve Revision Card", revisionCardResult.error + ) + EphemeralRevisionCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralRevisionCard.getDefaultInstance() + is AsyncResult.Success -> revisionCardResult.value + } return ephemeralRevisionCard.revisionCard.subtopicTitle } diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt index 8a2e1fa3cc1..01e859e2d1a 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt @@ -54,13 +54,15 @@ class RevisionCardViewModel @Inject constructor( private fun processRevisionCard( revisionCardResult: AsyncResult ): EphemeralRevisionCard { - if (revisionCardResult.isFailure()) { - oppiaLogger.e( - "RevisionCardFragment", - "Failed to retrieve Revision Card", - revisionCardResult.getErrorOrNull()!! - ) + return when (revisionCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "RevisionCardFragment", "Failed to retrieve Revision Card", revisionCardResult.error + ) + EphemeralRevisionCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralRevisionCard.getDefaultInstance() + is AsyncResult.Success -> revisionCardResult.value } - return revisionCardResult.getOrDefault(EphemeralRevisionCard.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt index e811c558b74..18b3f88b912 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt @@ -69,30 +69,34 @@ class AppLanguageWatcherMixin @Inject constructor( activity, object : Observer> { override fun onChanged(localeResult: AsyncResult) { - if (localeResult.isSuccess()) { - // Only recreate the activity if the locale actually changed (to avoid an infinite - // recreation loop). - if (appLanguageLocaleHandler.updateLocale(localeResult.getOrThrow())) { - // Recreate the activity to apply the latest locale state. Note that in some cases - // this may result in 2 recreations for the user: one to notify that there's a new - // system locale, and a second to actually apply that locale. This is due to a - // limitation in the infrastructure where the app can't know which system locale it - // can use without a LiveData trigger (this class). While this isn't an ideal user - // experience, the expectation is that the recreation should happen fairly quickly. - // If, in practice, that's not the case, the team will need to look into ways of - // synchronizing the UI-kept locale faster (maybe by short-circuiting some of the - // system locale selection code since the underlying I/O state is technically fixed - // and doesn't need a DataProvider past the splash screen). Finally, if the decision - // is made to recreate the activity then ensure it can never happen again in this - // activity by removing this observer. - liveData.removeObserver(this) - activityRecreator.recreate(activity) + when (localeResult) { + is AsyncResult.Success -> { + // Only recreate the activity if the locale actually changed (to avoid an infinite + // recreation loop). + if (appLanguageLocaleHandler.updateLocale(localeResult.value)) { + // Recreate the activity to apply the latest locale state. Note that in some cases + // this may result in 2 recreations for the user: one to notify that there's a new + // system locale, and a second to actually apply that locale. This is due to a + // limitation in the infrastructure where the app can't know which system locale it + // can use without a LiveData trigger (this class). While this isn't an ideal user + // experience, the expectation is that the recreation should happen fairly quickly. + // If, in practice, that's not the case, the team will need to look into ways of + // synchronizing the UI-kept locale faster (maybe by short-circuiting some of the + // system locale selection code since the underlying I/O state is technically fixed + // and doesn't need a DataProvider past the splash screen). Finally, if the decision + // is made to recreate the activity then ensure it can never happen again in this + // activity by removing this observer. + liveData.removeObserver(this) + activityRecreator.recreate(activity) + } } - } else if (localeResult.isFailure()) { - oppiaLogger.e( - "AppLanguageWatcherMixin", - "Failed to retrieve app string locale for activity: $activity" - ) + is AsyncResult.Failure -> { + oppiaLogger.e( + "AppLanguageWatcherMixin", + "Failed to retrieve app string locale for activity: $activity" + ) + } + is AsyncResult.Pending -> {} // Wait for an actual result. } } } diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt index b39919ee66a..a49ae9378ea 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt @@ -93,14 +93,14 @@ class WalkthroughFinalFragmentPresenter @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "WalkthroughFinalFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("WalkthroughFinalFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } override fun goBack() { diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt index a02fbce4815..19eb3fbea61 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt @@ -37,14 +37,18 @@ class WalkthroughTopicViewModel @Inject constructor( } private fun processTopicListResult(topicSummaryListResult: AsyncResult): TopicList { - if (topicSummaryListResult.isFailure()) { - oppiaLogger.e( - "WalkthroughTopicSummaryListFragment", - "Failed to retrieve TopicSummary list: ", - topicSummaryListResult.getErrorOrNull()!! - ) + return when (topicSummaryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "WalkthroughTopicSummaryListFragment", + "Failed to retrieve TopicSummary list: ", + topicSummaryListResult.error + ) + TopicList.getDefaultInstance() + } + is AsyncResult.Pending -> TopicList.getDefaultInstance() + is AsyncResult.Success -> topicSummaryListResult.value } - return topicSummaryListResult.getOrDefault(TopicList.getDefaultInstance()) } private fun processCompletedTopicList(topicList: TopicList): List { diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt index 04fc2404c9d..3c49e08cbc1 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt @@ -87,14 +87,16 @@ class WalkthroughWelcomeFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "WalkthroughWelcomeFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "WalkthroughWelcomeFragment", "Failed to retrieve profile", profileResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun setProfileName() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index dd1143bf18f..0253a8aa5f1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -765,6 +765,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testStateFragment_loadDragDropExp_correctAnswer_contentDescriptionIsCorrect() { launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index 879a6fe580a..21df562faf4 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -87,7 +87,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -340,7 +339,7 @@ class ProfileChooserFragmentTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = true - ).toLiveData() + ) launch(createProfileChooserActivityIntent()).use { testCoroutineDispatchers.runCurrent() onView( @@ -364,7 +363,7 @@ class ProfileChooserFragmentTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = true - ).toLiveData() + ) launch(createProfileChooserActivityIntent()).use { testCoroutineDispatchers.runCurrent() onView(withId(R.id.administrator_controls_linear_layout)).perform(click()) diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt index 583421ecc35..88c56f45a4c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt @@ -85,7 +85,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -413,7 +412,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -434,7 +433,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -456,7 +455,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -479,7 +478,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -500,7 +499,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -521,7 +520,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index c4a06775983..697edb45f15 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -514,6 +514,7 @@ class TopicLessonsFragmentTest { targetViewId = R.id.chapter_recycler_view ) ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + testCoroutineDispatchers.runCurrent() intended( allOf( hasExtra( diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 18fe1fd9127..aecd0697a62 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -16,6 +16,8 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//model/src/main/proto:test_models", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_constants", diff --git a/data/build.gradle b/data/build.gradle index d045d3d746b..9c6ce1210e0 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -82,6 +82,7 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'com.google.dagger:dagger:2.24', 'com.google.truth:truth:1.1.3', + 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 'com.squareup.okhttp3:mockwebserver:4.7.2', 'com.squareup.okhttp3:okhttp:4.7.2', 'junit:junit:4.12', diff --git a/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt b/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt index 19608033e7b..c0305216ab8 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt +++ b/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt @@ -64,7 +64,7 @@ class PersistentCacheStore private constructor( // First, determine whether the current cache has been attempted to be retrieved from disk. if (cachePayload.state == CacheState.UNLOADED) { deferLoadFile() - return AsyncResult.pending() + return AsyncResult.Pending() } // Second, check if a previous deferred read failed. The store stays in a failed state until @@ -74,13 +74,13 @@ class PersistentCacheStore private constructor( failureLock.withLock { deferredLoadCacheFailure?.let { // A previous read failed. - return AsyncResult.failed(it) + return AsyncResult.Failure(it) } } // Finally, check if there's an in-memory cached value that can be loaded now. // Otherwise, there should be a guaranteed in-memory value to use, instead. - return AsyncResult.success(cachePayload.value) + return AsyncResult.Success(cachePayload.value) } } diff --git a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt index 45f77380839..11d58728a53 100644 --- a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt +++ b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt @@ -2,11 +2,10 @@ package org.oppia.android.data.persistence import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import com.google.protobuf.MessageLite +import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat import dagger.BindsInstance import dagger.Component import dagger.Module @@ -16,26 +15,16 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.TestMessage import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.threading.BackgroundDispatcher @@ -51,57 +40,36 @@ private const val CACHE_NAME_1 = "test_cache_1" private const val CACHE_NAME_2 = "test_cache_2" /** Tests for [PersistentCacheStore]. */ +// Same parameter value: helpers reduce test context, even if they are used by 1 test. +// Function name: test names are conventionally named with underscores. +@Suppress("SameParameterValue", "FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = PersistentCacheStoreTest.TestApplication::class) class PersistentCacheStoreTest { private companion object { - private val TEST_MESSAGE_VERSION_1 = TestMessage.newBuilder().setIntValue(1).build() - private val TEST_MESSAGE_VERSION_2 = TestMessage.newBuilder().setIntValue(2).build() + private val TEST_MESSAGE_V1 = TestMessage.newBuilder().setIntValue(1).build() + private val TEST_MESSAGE_V2 = TestMessage.newBuilder().setIntValue(2).build() } - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() + @Inject lateinit var cacheFactory: PersistentCacheStore.Factory + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject - lateinit var cacheFactory: PersistentCacheStore.Factory - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - @field:BackgroundDispatcher - lateinit var backgroundDispatcher: CoroutineDispatcher - - @Mock - lateinit var mockUserAppHistoryObserver1: Observer> - - @Mock - lateinit var mockUserAppHistoryObserver2: Observer> - - @Captor - lateinit var userAppHistoryResultCaptor1: ArgumentCaptor> - - @Captor - lateinit var userAppHistoryResultCaptor2: ArgumentCaptor> - - private val backgroundDispatcherScope by lazy { - CoroutineScope(backgroundDispatcher) - } + private val backgroundDispatcherScope by lazy { CoroutineScope(backgroundDispatcher) } @Before fun setUp() { setUpTestApplicationComponent() } - // TODO(#59): Create a test-only proto for this test rather than needing to reuse a production-facing proto. + // TODO(#59): Create a test-only proto for this test rather than needing to reuse a + // production-facing proto. @Test @ExperimentalCoroutinesApi - fun testCache_toLiveData_initialState_isPending() { + fun testCache_initialState_isPending() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) // Directly call retrieveData() to get the very initial state of the provider. Relying on @@ -116,289 +84,221 @@ class PersistentCacheStoreTest { } testCoroutineDispatchers.advanceUntilIdle() - val result = deferredResult.getCompleted() - assertThat(result.isPending()).isTrue() + assertThat(deferredResult.getCompleted()).isPending() } @Test - fun testCache_toLiveData_loaded_providesInitialValue() { + fun testCache_loaded_providesInitialValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore, mockUserAppHistoryObserver1) + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) // The initial cache state should be the default cache value. - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(value).isEqualToDefaultInstance() } @Test - fun testCache_nonDefaultInitialState_toLiveData_loaded_providesCorrectInitialVal() { - val cacheStore = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_1) + fun testCache_nonDefaultInitialState_loaded_providesCorrectInitialVal() { + val cacheStore = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V1) - observeCache(cacheStore, mockUserAppHistoryObserver1) + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) // Caches can have non-default initial states. - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(value).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_registerObserver_updateAfter_observerNotifiedOfNewValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + monitorFactory.waitForNextSuccessfulResult(cacheStore) + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The store operation should be completed, and the observer should be notified of the changed value. + // The store operation should be completed, and the observer should be notified of the changed + // value. assertThat(storeOp.isCompleted).isTrue() - verify(mockUserAppHistoryObserver1).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_registerObserver_updateBefore_observesUpdatedStateInitially() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The store operation should be completed, and the observer's only call should be the updated state. + // The store operation should be completed, and the observer's only call should be the updated + // state. + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) assertThat(storeOp.isCompleted).isTrue() - verify(mockUserAppHistoryObserver1).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(value).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_noMemoryCacheUpdate_updateAfterReg_observerNotNotified() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) + val monitor = monitorFactory.createMonitor(cacheStore) - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + monitor.waitForNextSuccessResult() + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The store operation should be completed, but the observe will not be notified of changes since the in-memory - // cache was not changed. + // The store operation should be completed, but the observe will not be notified of changes + // since the in-memory cache was not changed. assertThat(storeOp.isCompleted).isTrue() - verifyZeroInteractions(mockUserAppHistoryObserver1) + monitor.verifyProviderIsNotUpdated() } @Test fun testCache_noMemoryCacheUpdate_updateBeforeReg_observesUpdatedState() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The store operation should be completed, but the observer will receive the updated state since the cache wasn't - // primed and no previous observers initialized it. - // NB: This may not be ideal behavior long-term; the store may need to be updated to be more resilient to these - // types of scenarios. + // The store operation should be completed, but the observer will receive the updated state + // since the cache wasn't primed and no previous observers initialized it. + // NB: This may not be ideal behavior long-term; the store may need to be updated to be more + // resilient to these types of scenarios. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_updated_newCache_newObserver_observesNewValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // The new cache should have the updated value since it points to the same file as the first cache. This is - // simulating something closer to an app restart or non-UI Dagger component refresh since UI components should share - // the same cache instance via an application-bound controller object. + // The new cache should have the updated value since it points to the same file as the first + // cache. This is simulating something closer to an app restart or non-UI Dagger component + // refresh since UI components should share the same cache instance via an application-bound + // controller object. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_updated_noInMemoryCacheUpdate_newCache_newObserver_observesNewVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // The new cache should have the updated value since it points to the same file as the first cache, even though the - // update operation did not update the in-memory cache (the new cache has a separate in-memory cache). This is - // simulating something closer to an app restart or non-UI Dagger component refresh since UI components should share - // the same cache instance via an application-bound controller object. + // The new cache should have the updated value since it points to the same file as the first + // cache, even though the update operation did not update the in-memory cache (the new cache has + // a separate in-memory cache). This is simulating something closer to an app restart or non-UI + // Dagger component refresh since UI components should share the same cache instance via an + // application-bound controller object. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testExistingDiskCache_newCacheObject_updateNoMemThenRead_receivesNewValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name and update it, then observe it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp2 = - cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // Both operations should be complete, and the observer will receive the latest value since the update was posted - // before the read occurred. + // Both operations should be complete, and the observer will receive the latest value since the + // update was posted before the read occurred. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testExistingDiskCache_newObject_updateNoMemThenRead_primed_receivesPrevVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // Create a new cache with the same name and update it, then observe it. However, first prime it. + // Create a new cache with the same name and update it, then observe it. However, first prime + // it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val primeOp = cacheStore2.primeCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val storeOp2 = - cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // All operations should be complete, but the observer will receive the previous update rather than th elatest since - // it wasn't updated in memory and the cache was pre-primed. + // All operations should be complete, but the observer will receive the previous update rather + // than the latest since it wasn't updated in memory and the cache was pre-primed. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testExistingDiskCache_newObject_updateMemThenRead_primed_receivesNewVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // Create a new cache with the same name and update it, then observe it. However, first prime it. + // Create a new cache with the same name and update it, then observe it. However, first prime + // it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val primeOp = cacheStore2.primeCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val storeOp2 = cacheStore2.storeDataAsync { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // Similar to the previous test, except due to the in-memory update the observer will receive the latest result - // regardless of the cache priming. + // Similar to the previous test, except due to the in-memory update the observer will receive + // the latest result regardless of the cache priming. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testCache_primed_afterStoreUpdateWithoutMemUpdate_notForced_observesOldValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache( - cacheStore, - mockUserAppHistoryObserver1 - ) // Force initializing the store's in-memory cache + // Force initializing the store's in-memory cache + monitorFactory.waitForNextSuccessfulResult(cacheStore) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val primeOp = cacheStore.primeCacheAsync(forceUpdate = false) testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver2) - // Both ops will succeed, and the observer will receive the old value due to the update not changing the in-memory - // cache, and the prime no-oping due to the cache already being initialized. + // Both ops will succeed, and the observer will receive the old value due to the update not + // changing the in-memory cache, and the prime no-oping due to the cache already being + // initialized. assertThat(storeOp.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_primed_afterStoreUpdateWithoutMemoryUpdate_forced_observesNewValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache( - cacheStore, - mockUserAppHistoryObserver1 - ) // Force initializing the store's in-memory cache + monitorFactory.waitForNextSuccessfulResult(cacheStore) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val primeOp = cacheStore.primeCacheAsync(forceUpdate = true) testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver2) - // The observer will receive the new value because the prime was forced. This ensures the store's in-memory cache is - // now up-to-date with the on-disk representation. + // The observer will receive the new value because the prime was forced. This ensures the + // store's in-memory cache is now up-to-date with the on-disk representation. assertThat(storeOp.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test @@ -407,110 +307,76 @@ class PersistentCacheStoreTest { val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The new observer should observe the store with its default state since nothing needed to be cleared. + // The new observer should observe the store with its default state since nothing needed to be + // cleared. assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_update_clear_resetsCacheToInitialState() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) // The new observer should observe that the store is cleared. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_update_existingObserver_clear_isNotifiedOfClear() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) + val monitor = monitorFactory.createMonitor(cacheStore) + monitor.waitForNextSuccessResult() val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() // The observer should not be notified the cache was cleared. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + monitor.verifyProviderIsNotUpdated() } @Test fun testCache_update_newCache_observesInitialState() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val clearOp = cacheStore1.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_2) - observeCache(cacheStore2, mockUserAppHistoryObserver1) + val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V2) - // The new observer should observe that there's no persisted on-disk store since it has a different default value - // that would only be used if there wasn't already on-disk storage. + // The new observer should observe that there's no persisted on-disk store since it has a + // different default value that would only be used if there wasn't already on-disk storage. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testMultipleCaches_oneUpdates_newCacheSameNameDiffInit_observesUpdatedValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_2) - observeCache(cacheStore2, mockUserAppHistoryObserver1) + val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V2) - // The new cache should observe the updated on-disk value rather than its new default since an on-disk value exists. - // This isn't a very realistic test since all caches should use default proto instances for initialization, but it's - // a possible edge case that should at least have established behavior that can be adjusted later if it isn't - // desirable in some circumstances. + // The new cache should observe the updated on-disk value rather than its new default since an + // on-disk value exists. This isn't a very realistic test since all caches should use default + // proto instances for initialization, but it's a possible edge case that should at least have + // established behavior that can be adjusted later if it isn't desirable in some circumstances. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test @@ -518,85 +384,58 @@ class PersistentCacheStoreTest { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val cacheStore2 = cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - observeCache(cacheStore1, mockUserAppHistoryObserver1) - observeCache(cacheStore2, mockUserAppHistoryObserver2) - reset(mockUserAppHistoryObserver2) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val monitor1 = monitorFactory.createMonitor(cacheStore1) + val monitor2 = monitorFactory.createMonitor(cacheStore2) + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The observer of the second store will be not notified of the change to the first store since they have different - // names. + // The observer of the second store will be not notified of the change to the first store since + // they have different names. assertThat(storeOp.isCompleted).isTrue() - verifyZeroInteractions(mockUserAppHistoryObserver2) + monitor1.verifyProviderIsNotUpdated() + monitor2.verifyProviderIsNotUpdated() } @Test fun testMultipleCaches_diffNames_oneUpdates_cachesRecreated_onlyOneObservesVal() { val cacheStore1a = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1a.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1a.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Recreate the stores and observe them. val cacheStore1b = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val cacheStore2b = cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - observeCache(cacheStore1b, mockUserAppHistoryObserver1) - observeCache(cacheStore2b, mockUserAppHistoryObserver2) - // Only the observer of the first cache should notice the update since they are different caches. + // Only the observer of the first cache should notice the update since they are different + // caches. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor2.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) - assertThat(userAppHistoryResultCaptor2.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor2.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore1b)).isEqualTo(TEST_MESSAGE_V1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2b)).isEqualToDefaultInstance() } @Test fun testNewCache_fileCorrupted_providesError() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Simulate the file being corrupted & reopen the file in a new store. corruptFileCache(CACHE_NAME_1) val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) // The new observer should receive an IOException error when trying to read the file. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isFailure()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getErrorOrNull()).isInstanceOf( - IOException::class.java - ) - } - - private fun observeCache( - cacheStore: PersistentCacheStore, - observer: Observer> - ) { - cacheStore.toLiveData().observeForever(observer) - testCoroutineDispatchers.advanceUntilIdle() + val error = monitorFactory.waitForNextFailureResult(cacheStore2) + assertThat(error).isInstanceOf(IOException::class.java) } private fun corruptFileCache(cacheName: String) { - // NB: This is unfortunately tied to the implementation details of PersistentCacheStore. If this ends up being an - // issue, the store should be updated to call into a file path provider that can also be used in this test to - // retrieve the file cache. This may also be needed for downstream profile work if per-profile data stores are done - // via subdirectories or altered filenames. + // NB: This is unfortunately tied to the implementation details of PersistentCacheStore. If this + // ends up being an issue, the store should be updated to call into a file path provider that + // can also be used in this test to retrieve the file cache. This may also be needed for + // downstream profile work if per-profile data stores are done via subdirectories or altered + // filenames. val cacheFileName = "$cacheName.cache" val cacheFile = File( ApplicationProvider.getApplicationContext().filesDir, cacheFileName diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index ef961b09449..fed8f4ec528 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -196,6 +196,7 @@ TEST_DEPS = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", "//domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader:fake_log_uploader", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/network:network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt index 8e4e8c14f67..756177c31ef 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt +++ b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt @@ -40,7 +40,7 @@ class AudioPlayerController @Inject constructor( ) { inner class AudioMutableLiveData : - MutableLiveData>(AsyncResult.pending()) { + MutableLiveData>(AsyncResult.Pending()) { override fun onActive() { super.onActive() audioLock.withLock { @@ -128,17 +128,17 @@ class AudioPlayerController @Inject constructor( completed = true stopUpdatingSeekBar() playProgress?.value = - AsyncResult.success(PlayProgress(PlayStatus.COMPLETED, 0, duration)) + AsyncResult.Success(PlayProgress(PlayStatus.COMPLETED, 0, duration)) } mediaPlayer.setOnPreparedListener { prepared = true duration = it.duration playProgress?.value = - AsyncResult.success(PlayProgress(PlayStatus.PREPARED, 0, duration)) + AsyncResult.Success(PlayProgress(PlayStatus.PREPARED, 0, duration)) } mediaPlayer.setOnErrorListener { _, what, extra -> playProgress?.value = - AsyncResult.failed( + AsyncResult.Failure( AudioPlayerException("Audio Player put in error state with what: $what and extra: $extra") ) releaseMediaPlayer() @@ -186,7 +186,7 @@ class AudioPlayerController @Inject constructor( exceptionsController.logNonFatalException(e) oppiaLogger.e("AudioPlayerController", "Failed to set data source for media player", e) } - playProgress?.value = AsyncResult.pending() + playProgress?.value = AsyncResult.Pending() } /** @@ -212,7 +212,7 @@ class AudioPlayerController @Inject constructor( check(prepared) { "Media Player not in a prepared state" } if (mediaPlayer.isPlaying) { playProgress?.value = - AsyncResult.success( + AsyncResult.Success( PlayProgress(PlayStatus.PAUSED, mediaPlayer.currentPosition, duration) ) mediaPlayer.pause() @@ -239,7 +239,7 @@ class AudioPlayerController @Inject constructor( val position = if (completed) 0 else mediaPlayer.currentPosition completed = false playProgress?.postValue( - AsyncResult.success( + AsyncResult.Success( PlayProgress(PlayStatus.PLAYING, position, mediaPlayer.duration) ) ) diff --git a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt index c9f5aad125d..5bd95b9f858 100644 --- a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt @@ -44,7 +44,7 @@ class ModifyLessonProgressController @Inject constructor( val topicId = topicSummary.topicId listOfTopics.add(topicController.retrieveTopic(topicId)) } - AsyncResult.success(listOfTopics.toList()) + AsyncResult.Success(listOfTopics.toList()) } val topicProgressListDataProvider = storyProgressController.retrieveTopicProgressListDataProvider(profileId) @@ -71,7 +71,7 @@ class ModifyLessonProgressController @Inject constructor( listOfTopics.forEach { topic -> storyMap[topic.topicId] = topic.storyList } - AsyncResult.success(storyMap.toMap()) + AsyncResult.Success(storyMap.toMap()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt index 62657e1387d..1fbb83eec34 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.exploration -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId @@ -40,12 +38,15 @@ class ExplorationDataController @Inject constructor( } /** - * Begins playing an exploration of the specified ID. This method is not expected to fail. - * [ExplorationProgressController] should be used to manage the play state, and monitor the load success/failure of - * the exploration. + * Begins playing an exploration of the specified ID. * - * This must be called only if no active exploration is being played. The previous exploration must have first been - * stopped using [stopPlayingExploration] otherwise an exception will be thrown. + * This method is not expected to fail. + * + * [ExplorationProgressController] should be used to manage the play state, and monitor the load + * success/failure of the exploration. + * + * This must be called only if no active exploration is being played. The previous exploration + * must have first been stopped using [stopPlayingExploration], otherwise the operation will fail. * * @param internalProfileId the ID corresponding to the profile for which exploration has to be * played @@ -55,7 +56,7 @@ class ExplorationDataController @Inject constructor( * @param shouldSavePartialProgress the boolean that indicates if partial progress has to be saved * for the current exploration * @param explorationCheckpoint the checkpoint which may be used to resume the exploration - * @return a one-time [LiveData] to observe whether initiating the play request succeeded. + * @return a one-time [DataProvider] to observe whether initiating the play request succeeded. * The exploration may still fail to load, but this provides early-failure detection. */ fun startPlayingExploration( @@ -65,36 +66,26 @@ class ExplorationDataController @Inject constructor( explorationId: String, shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint - ): LiveData> { - return try { - explorationProgressController.beginExplorationAsync( - internalProfileId, - topicId, - storyId, - explorationId, - shouldSavePartialProgress, - explorationCheckpoint - ) - MutableLiveData(AsyncResult.success(null)) - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - MutableLiveData(AsyncResult.failed(e)) - } + ): DataProvider { + return explorationProgressController.beginExplorationAsync( + internalProfileId, + topicId, + storyId, + explorationId, + shouldSavePartialProgress, + explorationCheckpoint + ) } /** - * Finishes the most recent exploration started by [startPlayingExploration]. This method should only be called if an - * active exploration is being played, otherwise an exception will be thrown. + * Finishes the most recent exploration started by [startPlayingExploration], and returns a + * one-off [DataProvider] indicating whether the operation succeeded. + * + * This method should only be called if an active exploration is being played, otherwise the + * operation will fail. */ - fun stopPlayingExploration(): LiveData> { - return try { - explorationProgressController.finishExplorationAsync() - MutableLiveData(AsyncResult.success(null)) - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - MutableLiveData(AsyncResult.failed(e)) - } - } + fun stopPlayingExploration(): DataProvider = + explorationProgressController.finishExplorationAsync() /** * Fetches the details of the oldest saved exploration for a specified profileId. @@ -125,10 +116,10 @@ class ExplorationDataController @Inject constructor( @Suppress("RedundantSuspendModifier") private suspend fun retrieveExplorationById(explorationId: String): AsyncResult { return try { - AsyncResult.success(explorationRetriever.loadExploration(explorationId)) + AsyncResult.Success(explorationRetriever.loadExploration(explorationId)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index 19ec0d29f29..11c403a259c 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.exploration -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.AnswerOutcome import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.EphemeralState @@ -20,6 +18,7 @@ import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncDataSubscriptionManager import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider +import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.system.OppiaClock @@ -28,7 +27,21 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock -private const val CURRENT_STATE_DATA_PROVIDER_ID = "current_state_data_provider_id" +private const val BEGIN_EXPLORATION_RESULT_PROVIDER_ID = + "ExplorationProgressController.begin_exploration_result" +private const val FINISH_EXPLORATION_RESULT_PROVIDER_ID = + "ExplorationProgressController.finish_exploration_result" +private const val SUBMIT_ANSWER_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_answer_result" +private const val SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_hint_revealed_result" +private const val SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_solution_revealed_result" +private const val MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID = + "ExplorationProgressController.move_to_previous_state_result" +private const val MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID = + "ExplorationProgressController.move_to_next_state_result" +private const val CURRENT_STATE_PROVIDER_ID = "ExplorationProgressController.current_state" /** * Controller that tracks and reports the learner's ephemeral/non-persisted progress through an @@ -50,7 +63,8 @@ class ExplorationProgressController @Inject constructor( private val oppiaClock: OppiaClock, private val oppiaLogger: OppiaLogger, private val hintHandlerFactory: HintHandler.Factory, - private val translationController: TranslationController + private val translationController: TranslationController, + private val dataProviders: DataProviders ) : HintHandler.HintMonitor { // TODO(#179): Add support for parameters. // TODO(#3622): Update the internal locking of this controller to use something like an in-memory @@ -69,7 +83,10 @@ class ExplorationProgressController @Inject constructor( private val explorationProgressLock = ReentrantLock() private lateinit var hintHandler: HintHandler - /** Resets this controller to begin playing the specified [Exploration]. */ + /** + * Resets this controller to begin playing the specified [Exploration], and returns a + * [DataProvider] indicating whether the start was successful. + */ internal fun beginExplorationAsync( internalProfileId: Int, topicId: String, @@ -77,34 +94,57 @@ class ExplorationProgressController @Inject constructor( explorationId: String, shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint - ) { - explorationProgressLock.withLock { - check(explorationProgress.playStage == ExplorationProgress.PlayStage.NOT_PLAYING) { - "Expected to finish previous exploration before starting a new one." - } + ): DataProvider { + return explorationProgressLock.withLock { + try { + check(explorationProgress.playStage == ExplorationProgress.PlayStage.NOT_PLAYING) { + "Expected to finish previous exploration before starting a new one." + } - explorationProgress.apply { - currentProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() - currentTopicId = topicId - currentStoryId = storyId - currentExplorationId = explorationId - this.shouldSavePartialProgress = shouldSavePartialProgress - checkpointState = CheckpointState.CHECKPOINT_UNSAVED - this.explorationCheckpoint = explorationCheckpoint + explorationProgress.apply { + currentProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + currentTopicId = topicId + currentStoryId = storyId + currentExplorationId = explorationId + this.shouldSavePartialProgress = shouldSavePartialProgress + checkpointState = CheckpointState.CHECKPOINT_UNSAVED + this.explorationCheckpoint = explorationCheckpoint + } + hintHandler = hintHandlerFactory.create(this) + explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.LOADING_EXPLORATION) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return@withLock dataProviders.createInMemoryDataProvider( + BEGIN_EXPLORATION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + BEGIN_EXPLORATION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - hintHandler = hintHandlerFactory.create(this) - explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.LOADING_EXPLORATION) - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) } } - /** Indicates that the current exploration being played is now completed. */ - internal fun finishExplorationAsync() { - explorationProgressLock.withLock { - check(explorationProgress.playStage != ExplorationProgress.PlayStage.NOT_PLAYING) { - "Cannot finish playing an exploration that hasn't yet been started" + /** + * Indicates that the current exploration being played is now completed, and returns a + * [DataProvider] indicating whether the cleanup was successful. + */ + internal fun finishExplorationAsync(): DataProvider { + return explorationProgressLock.withLock { + try { + check(explorationProgress.playStage != ExplorationProgress.PlayStage.NOT_PLAYING) { + "Cannot finish playing an exploration that hasn't yet been started" + } + explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.NOT_PLAYING) + return@withLock dataProviders.createInMemoryDataProvider( + FINISH_EXPLORATION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + FINISH_EXPLORATION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.NOT_PLAYING) } } @@ -112,22 +152,23 @@ class ExplorationProgressController @Inject constructor( explorationProgressLock.withLock { saveExplorationCheckpoint() } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) } /** * Submits an answer to the current state and returns how the UI should respond to this answer. - * The returned [LiveData] will only have at most two results posted: a pending result, and then a - * completed success/failure result. Failures in this case represent a failure of the app + * + * The returned [DataProvider] will only have at most two results posted: a pending result, and + * then a completed success/failure result. Failures in this case represent a failure of the app * (possibly due to networking conditions). The app should report this error in a consumable way * to the user so that they may take action on it. No additional values will be reported to the - * [LiveData]. Each call to this method returns a new, distinct, [LiveData] object that must be - * observed. Note also that the returned [LiveData] is not guaranteed to begin with a pending - * state. + * [DataProvider]. Each call to this method returns a new, distinct, [DataProvider] object that + * must be observed. Note also that the returned [DataProvider] is not guaranteed to begin with a + * pending state. * - * If the app undergoes a configuration change, calling code should rely on the [LiveData] from - * [getCurrentState] to know whether a current answer is pending. That [LiveData] will have its - * state changed to pending during answer submission and until answer resolution. + * If the app undergoes a configuration change, calling code should rely on the [DataProvider] + * from [getCurrentState] to know whether a current answer is pending. That [DataProvider] will + * have its state changed to pending during answer submission and until answer resolution. * * Submitting an answer should result in the learner staying in the current state, moving to a new * state in the exploration, being shown a concept card, or being navigated to another exploration @@ -141,11 +182,11 @@ class ExplorationProgressController @Inject constructor( * to submit an answer while a previous answer is pending. That scenario will also result in a * failed answer submission. * - * No assumptions should be made about the completion order of the returned [LiveData] vs. the - * [LiveData] from [getCurrentState]. Also note that the returned [LiveData] will only have a - * single value and not be reused after that point. + * No assumptions should be made about the completion order of the returned [DataProvider] vs. the + * [DataProvider] from [getCurrentState]. Also note that the returned [DataProvider] will only + * have a single value and not be reused after that point. */ - fun submitAnswer(userAnswer: UserAnswer): LiveData> { + fun submitAnswer(userAnswer: UserAnswer): DataProvider { try { explorationProgressLock.withLock { check( @@ -169,7 +210,7 @@ class ExplorationProgressController @Inject constructor( // Notify observers that the submitted answer is currently pending. explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.SUBMITTING_ANSWER) - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) lateinit var answerOutcome: AnswerOutcome try { @@ -212,13 +253,17 @@ class ExplorationProgressController @Inject constructor( explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(answerOutcome)) + return dataProviders.createInMemoryDataProvider(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + answerOutcome + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -227,10 +272,10 @@ class ExplorationProgressController @Inject constructor( * * @param hintIndex index of the hint that was revealed in the hint list of the current pending * state - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitHintIsRevealed(hintIndex: Int): LiveData> { + fun submitHintIsRevealed(hintIndex: Int): DataProvider { try { explorationProgressLock.withLock { check( @@ -259,22 +304,26 @@ class ExplorationProgressController @Inject constructor( // exception. explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider(SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID) { + null + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } /** * Notifies the controller that the user has revealed the solution to the current state. * - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitSolutionIsRevealed(): LiveData> { + fun submitSolutionIsRevealed(): DataProvider { try { explorationProgressLock.withLock { check( @@ -304,12 +353,16 @@ class ExplorationProgressController @Inject constructor( explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { null } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -318,13 +371,13 @@ class ExplorationProgressController @Inject constructor( * this method will throw an exception. Calling code is responsible for ensuring this method is * only called when it's possible to navigate backward. * - * @return a one-time [LiveData] indicating whether the movement to the previous state was + * @return a one-time [DataProvider] indicating whether the movement to the previous state was * successful, or a failure if state navigation was attempted at an invalid time in the state * graph (e.g. if currently viewing the initial state of the exploration). It's recommended * that calling code only listen to this result for failures, and instead rely on * [getCurrentState] for observing a successful transition to another state. */ - fun moveToPreviousState(): LiveData> { + fun moveToPreviousState(): DataProvider { try { explorationProgressLock.withLock { check( @@ -347,12 +400,16 @@ class ExplorationProgressController @Inject constructor( } hintHandler.navigateToPreviousState() explorationProgress.stateDeck.navigateToPreviousState() - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -365,13 +422,13 @@ class ExplorationProgressController @Inject constructor( * that routes to a later state via [submitAnswer] in order for the current state to change to a * completed state before forward navigation can occur. * - * @return a one-time [LiveData] indicating whether the movement to the next state was successful, - * or a failure if state navigation was attempted at an invalid time in the state graph (e.g. - * if the current state is pending or terminal). It's recommended that calling code only - * listen to this result for failures, and instead rely on [getCurrentState] for observing a - * successful transition to another state. + * @return a one-time [DataProvider] indicating whether the movement to the next state was + * successful, or a failure if state navigation was attempted at an invalid time in the state + * graph (e.g. if the current state is pending or terminal). It's recommended that calling + * code only listen to this result for failures, and instead rely on [getCurrentState] for + * observing a successful transition to another state. */ - fun moveToNextState(): LiveData> { + fun moveToNextState(): DataProvider { try { explorationProgressLock.withLock { check( @@ -401,12 +458,16 @@ class ExplorationProgressController @Inject constructor( // will not be marked on any of the completed states. saveExplorationCheckpoint() } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync(MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -434,7 +495,7 @@ class ExplorationProgressController @Inject constructor( fun getCurrentState(): DataProvider { return translationController.getWrittenTranslationContentLocale( explorationProgress.currentProfileId - ).transformAsync(CURRENT_STATE_DATA_PROVIDER_ID) { contentLocale -> + ).transformAsync(CURRENT_STATE_PROVIDER_ID) { contentLocale -> return@transformAsync retrieveCurrentStateAsync(contentLocale) } } @@ -446,7 +507,7 @@ class ExplorationProgressController @Inject constructor( retrieveCurrentStateWithinCacheAsync(writtenTranslationContentLocale) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } @@ -476,20 +537,20 @@ class ExplorationProgressController @Inject constructor( " to ${explorationProgress.currentExplorationId}" } return when (explorationProgress.playStage) { - ExplorationProgress.PlayStage.NOT_PLAYING -> AsyncResult.pending() + ExplorationProgress.PlayStage.NOT_PLAYING -> AsyncResult.Pending() ExplorationProgress.PlayStage.LOADING_EXPLORATION -> { try { // The exploration must be available for this stage since it was loaded above. finishLoadExploration(exploration!!, explorationProgress) - AsyncResult.success(computeCurrentEphemeralState(writtenTranslationContentLocale)) + AsyncResult.Success(computeCurrentEphemeralState(writtenTranslationContentLocale)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } ExplorationProgress.PlayStage.VIEWING_STATE -> - AsyncResult.success(computeCurrentEphemeralState(writtenTranslationContentLocale)) - ExplorationProgress.PlayStage.SUBMITTING_ANSWER -> AsyncResult.pending() + AsyncResult.Success(computeCurrentEphemeralState(writtenTranslationContentLocale)) + ExplorationProgress.PlayStage.SUBMITTING_ANSWER -> AsyncResult.Pending() } } } @@ -645,7 +706,7 @@ class ExplorationProgressController @Inject constructor( } explorationProgress.updateCheckpointState(newCheckpointState) // Notify observers that the checkpoint state has changed. - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt index f86d1402d55..21bf8e7767b 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt @@ -134,7 +134,7 @@ class ExplorationCheckpointController @Inject constructor( return dataProviders.createInMemoryDataProviderAsync( RECORD_EXPLORATION_CHECKPOINT_DATA_PROVIDER_ID ) { - return@createInMemoryDataProviderAsync AsyncResult.success(deferred.await()) + return@createInMemoryDataProviderAsync AsyncResult.Success(deferred.await()) } } @@ -153,10 +153,10 @@ class ExplorationCheckpointController @Inject constructor( when { checkpoint != null && exploration.version == checkpoint.explorationVersion -> { - AsyncResult.success(checkpoint) + AsyncResult.Success(checkpoint) } checkpoint != null && exploration.version != checkpoint.explorationVersion -> { - AsyncResult.failed( + AsyncResult.Failure( OutdatedExplorationCheckpointException( "checkpoint with version: ${checkpoint.explorationVersion} cannot be used to " + "resume exploration $explorationId with version: ${exploration.version}" @@ -164,7 +164,7 @@ class ExplorationCheckpointController @Inject constructor( ) } else -> { - AsyncResult.failed( + AsyncResult.Failure( ExplorationCheckpointNotFoundException( "Checkpoint with the explorationId $explorationId was not found " + "for profileId ${profileId.internalId}." @@ -201,9 +201,9 @@ class ExplorationCheckpointController @Inject constructor( .setExplorationTitle(oldestCheckpoint.value.explorationTitle) .setExplorationVersion(oldestCheckpoint.value.explorationVersion) .build() - AsyncResult.success(explorationCheckpointDetails) + AsyncResult.Success(explorationCheckpointDetails) } else { - AsyncResult.failed( + AsyncResult.Failure( ExplorationCheckpointNotFoundException( "No saved checkpoints in $CACHE_NAME for profileId ${profileId.internalId}." ) @@ -256,14 +256,13 @@ class ExplorationCheckpointController @Inject constructor( ): AsyncResult { return when (deferred.await()) { ExplorationCheckpointActionStatus.CHECKPOINT_NOT_FOUND -> - AsyncResult.failed( + AsyncResult.Failure( ExplorationCheckpointNotFoundException( "No saved checkpoint with explorationId ${explorationId!!} found for " + "the profileId ${profileId!!.internalId}." ) ) - ExplorationCheckpointActionStatus.SUCCESS -> - AsyncResult.success(null) + ExplorationCheckpointActionStatus.SUCCESS -> AsyncResult.Success(null) } } @@ -287,8 +286,7 @@ class ExplorationCheckpointController @Inject constructor( throwable?.let { oppiaLogger.e( "ExplorationCheckpointController", - "Failed to prime cache ahead of LiveData conversion " + - "for ExplorationCheckpointController.", + "Failed to prime cache ahead of data retrieval for ExplorationCheckpointController.", it ) } diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt index ec90dae8f02..09b9e1eac99 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt @@ -215,7 +215,7 @@ class LocaleController @Inject constructor( fun retrieveSystemLanguage(): DataProvider { val providerId = SYSTEM_LANGUAGE_DATA_PROVIDER_ID return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile -> - AsyncResult.success( + AsyncResult.Success( retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language ?: OppiaLanguage.LANGUAGE_UNSPECIFIED ) @@ -303,8 +303,8 @@ class LocaleController @Inject constructor( @Suppress("UNCHECKED_CAST") // as? should always be a safe cast, even if unchecked. val locale = computeLocale(language, systemLocaleProfile, usageMode) as? T return locale?.let { - AsyncResult.success(it) - } ?: AsyncResult.failed( + AsyncResult.Success(it) + } ?: AsyncResult.Failure( IllegalStateException( "Language $language for usage $usageMode doesn't match supported language definitions" ) diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt index 545b6ceba9c..7d8c9db9f06 100644 --- a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt +++ b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt @@ -37,9 +37,7 @@ class AppStartupStateController @Inject constructor( onboardingFlowStore.primeCacheAsync().invokeOnCompletion { it?.let { oppiaLogger.e( - "DOMAIN", - "Failed to prime cache ahead of LiveData conversion for user onboarding data.", - it + "DOMAIN", "Failed to prime cache ahead of data retrieval for user onboarding data.", it ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index 38af73c4546..0faf401f0fa 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -31,7 +31,10 @@ kt_android_library( srcs = [ "LogStorageModule.kt", ], - visibility = ["//domain/src/main/java/org/oppia/android/domain/oppialogger:__subpackages__"], + visibility = [ + "//:oppia_testing_visibility", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:__subpackages__", + ], deps = [ ":dagger", ], diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt index 7fe6d744842..7298fe27e61 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt @@ -84,7 +84,7 @@ class PlatformParameterController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - PlatformParameterCachingStatus.SUCCESS -> AsyncResult.success(null) + PlatformParameterCachingStatus.SUCCESS -> AsyncResult.Success(null) } } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt index 83bb3d4b274..87c91472a0d 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt @@ -17,6 +17,7 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.platformparameter.PlatformParameterController import org.oppia.android.domain.util.getStringFromData +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.threading.BackgroundDispatcher import retrofit2.Response import java.lang.IllegalArgumentException @@ -114,8 +115,8 @@ class PlatformParameterSyncUpWorker private constructor( val cachingResult = platformParameterController .updatePlatformParameterDatabase(platformParameterList) .retrieveData() - if (cachingResult.isFailure()) { - throw IllegalStateException(cachingResult.getErrorOrNull()) + if (cachingResult is AsyncResult.Failure) { + throw IllegalStateException(cachingResult.error) } Result.success() } else { diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt index 85de12651d3..700c456168e 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt @@ -125,7 +125,7 @@ class ProfileManagementController @Inject constructor( it?.let { oppiaLogger.e( "DOMAIN", - "Failed to prime cache ahead of LiveData conversion for ProfileManagementController.", + "Failed to prime cache ahead of data retrieval for ProfileManagementController.", it ) } @@ -144,9 +144,9 @@ class ProfileManagementController @Inject constructor( return profileDataStore.transformAsync(GET_PROFILE_PROVIDER_ID) { val profile = it.profilesMap[profileId.internalId] if (profile != null) { - AsyncResult.success(profile) + AsyncResult.Success(profile) } else { - AsyncResult.failed( + AsyncResult.Failure( ProfileNotFoundException( "ProfileId ${profileId.internalId} does" + " not match an existing Profile" @@ -160,7 +160,7 @@ class ProfileManagementController @Inject constructor( fun getWasProfileEverAdded(): DataProvider { return profileDataStore.transformAsync(GET_WAS_PROFILE_EVER_ADDED_PROVIDER_ID) { val wasProfileEverAdded = it.wasProfileEverAdded - AsyncResult.success(wasProfileEverAdded) + AsyncResult.Success(wasProfileEverAdded) } } @@ -169,9 +169,9 @@ class ProfileManagementController @Inject constructor( return profileDataStore.transformAsync(GET_DEVICE_SETTINGS_PROVIDER_ID) { val deviceSettings = it.deviceSettings if (deviceSettings != null) { - AsyncResult.success(deviceSettings) + AsyncResult.Success(deviceSettings) } else { - AsyncResult.failed(DeviceSettingsNotFoundException("Device Settings not found.")) + AsyncResult.Failure(DeviceSettingsNotFoundException("Device Settings not found.")) } } } @@ -578,9 +578,9 @@ class ProfileManagementController @Inject constructor( val profileDatabase = profileDataStore.readDataAsync().await() if (profileDatabase.profilesMap.containsKey(profileId.internalId)) { currentProfileId = profileId.internalId - return@createInMemoryDataProviderAsync AsyncResult.success(0) + return@createInMemoryDataProviderAsync AsyncResult.Success(0) } - AsyncResult.failed( + AsyncResult.Failure( ProfileNotFoundException( "ProfileId ${profileId.internalId} is" + " not associated with an existing profile" @@ -643,41 +643,49 @@ class ProfileManagementController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - ProfileActionStatus.SUCCESS -> AsyncResult.success(null) - ProfileActionStatus.INVALID_PROFILE_NAME -> AsyncResult.failed( - ProfileNameOnlyLettersException("$name does not contain only letters") - ) - ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> AsyncResult.failed( - ProfileNameNotUniqueException("$name is not unique to other profiles") - ) - ProfileActionStatus.FAILED_TO_STORE_IMAGE -> AsyncResult.failed( - FailedToStoreImageException( - "Failed to store user's selected avatar image" + ProfileActionStatus.SUCCESS -> AsyncResult.Success(null) + ProfileActionStatus.INVALID_PROFILE_NAME -> + AsyncResult.Failure( + ProfileNameOnlyLettersException("$name does not contain only letters") ) - ) - ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> AsyncResult.failed( - FailedToGenerateGravatarException("Failed to generate a gravatar url") - ) - ProfileActionStatus.FAILED_TO_DELETE_DIR -> AsyncResult.failed( - FailedToDeleteDirException( - "Failed to delete directory with ${profileId?.internalId}" + ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> + AsyncResult.Failure( + ProfileNameNotUniqueException("$name is not unique to other profiles") ) - ) - ProfileActionStatus.PROFILE_NOT_FOUND -> AsyncResult.failed( - ProfileNotFoundException( - "ProfileId ${profileId?.internalId} does not match an existing Profile" + ProfileActionStatus.FAILED_TO_STORE_IMAGE -> + AsyncResult.Failure( + FailedToStoreImageException( + "Failed to store user's selected avatar image" + ) ) - ) - ProfileActionStatus.PROFILE_NOT_ADMIN -> AsyncResult.failed( - ProfileNotAdminException( - "ProfileId ${profileId?.internalId} does not match an existing admin" + ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> + AsyncResult.Failure( + FailedToGenerateGravatarException("Failed to generate a gravatar url") ) - ) - ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> AsyncResult.failed( - ProfileAlreadyHasAdminException( - "Profile cannot be an admin" + ProfileActionStatus.FAILED_TO_DELETE_DIR -> + AsyncResult.Failure( + FailedToDeleteDirException( + "Failed to delete directory with ${profileId?.internalId}" + ) + ) + ProfileActionStatus.PROFILE_NOT_FOUND -> + AsyncResult.Failure( + ProfileNotFoundException( + "ProfileId ${profileId?.internalId} does not match an existing Profile" + ) + ) + ProfileActionStatus.PROFILE_NOT_ADMIN -> + AsyncResult.Failure( + ProfileNotAdminException( + "ProfileId ${profileId?.internalId} does not match an existing admin" + ) + ) + ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> + AsyncResult.Failure( + ProfileAlreadyHasAdminException( + "Profile cannot be an admin" + ) ) - ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt index 476b1c76b88..8a06f674b31 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.question -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.AnsweredQuestionOutcome import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.EphemeralState @@ -29,15 +27,26 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock -private const val CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID = - "create_current_question_data_provider_id" -private const val CREATE_CURRENT_QUESTION_DATA_WITH_TRANSLATION_CONTEXT_PROVIDER_ID = - "create_current_question_data_with_translation_context_provider_id" -private const val BEGIN_QUESTION_TRAINING_SESSION_PROVIDER_ID = - "begin_question_training_session_provider_id" -private const val CREATE_EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID = - "create_empty_questions_list_data_provider_id" -private const val SUBMIT_ANSWER_PROVIDER_ID = "submit_answer_provider_id" +private const val BEGIN_SESSION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.begin_session_result" +private const val FINISH_SESSION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.finish_session_result" +private const val SUBMIT_ANSWER_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_answer_result" +private const val SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_hint_revealed_result" +private const val SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_solution_revealed_result" +private const val MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.move_to_next_question_result" +private const val CURRENT_QUESTION_PROVIDER_ID = + "QuestionAssessmentProgressController.current_question" +private const val CALCULATE_SCORES_PROVIDER_ID = + "QuestionAssessmentProgressController.calculate_scores" +private const val LOCALIZED_QUESTION_PROVIDER_ID = + "QuestionAssessmentProgressController.localized_question" +private const val EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID = + "QuestionAssessmentProgressController.create_empty_questions_list_data_provider_id" /** * Controller that tracks and reports the learner's ephemeral/non-persisted progress through a @@ -72,55 +81,84 @@ class QuestionAssessmentProgressController @Inject constructor( private val currentQuestionDataProvider: NestedTransformedDataProvider = createCurrentQuestionDataProvider(createEmptyQuestionsListDataProvider()) + /** + * Begins a training session based on the specified question list data provider and [ProfileId], + * and returns a [DataProvider] indicating whether the session was successfully started. + */ internal fun beginQuestionTrainingSession( questionsListDataProvider: DataProvider>, profileId: ProfileId - ) { - progressLock.withLock { - check(progress.trainStage == TrainStage.NOT_IN_TRAINING_SESSION) { - "Cannot start a new training session until the previous one is completed." - } - progress.currentProfileId = profileId + ): DataProvider { + return progressLock.withLock { + try { + check(progress.trainStage == TrainStage.NOT_IN_TRAINING_SESSION) { + "Cannot start a new training session until the previous one is completed." + } + progress.currentProfileId = profileId - hintHandler = hintHandlerFactory.create(this) - progress.advancePlayStageTo(TrainStage.LOADING_TRAINING_SESSION) - currentQuestionDataProvider.setBaseDataProvider( - questionsListDataProvider, - this::retrieveCurrentQuestionAsync - ) - asyncDataSubscriptionManager.notifyChangeAsync(BEGIN_QUESTION_TRAINING_SESSION_PROVIDER_ID) + hintHandler = hintHandlerFactory.create(this) + progress.advancePlayStageTo(TrainStage.LOADING_TRAINING_SESSION) + currentQuestionDataProvider.setBaseDataProvider( + questionsListDataProvider, + this::retrieveCurrentQuestionAsync + ) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return@withLock dataProviders.createInMemoryDataProvider(BEGIN_SESSION_RESULT_PROVIDER_ID) { + null + } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + BEGIN_SESSION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } + } } } - internal fun finishQuestionTrainingSession() { - progressLock.withLock { - check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { - "Cannot stop a new training session which wasn't started." + /** + * Ends the current training session and returns a [DataProvider] that indicates whether it was + * successfully ended. + */ + internal fun finishQuestionTrainingSession(): DataProvider { + return progressLock.withLock { + try { + check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { + "Cannot stop a new training session which wasn't started." + } + progress.advancePlayStageTo(TrainStage.NOT_IN_TRAINING_SESSION) + currentQuestionDataProvider.setBaseDataProvider( + createEmptyQuestionsListDataProvider(), this::retrieveCurrentQuestionAsync + ) + return@withLock dataProviders.createInMemoryDataProvider( + FINISH_SESSION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + FINISH_SESSION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - progress.advancePlayStageTo(TrainStage.NOT_IN_TRAINING_SESSION) - currentQuestionDataProvider.setBaseDataProvider( - createEmptyQuestionsListDataProvider(), this::retrieveCurrentQuestionAsync - ) } } override fun onHelpIndexChanged() { - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) } /** * Submits an answer to the current question and returns how the UI should respond to this answer. - * The returned [LiveData] will only have at most two results posted: a pending result, and then a - * completed success/failure result. Failures in this case represent a failure of the app + * + * The returned [DataProvider] will only have at most two results posted: a pending result, and + * then a completed success/failure result. Failures in this case represent a failure of the app * (possibly due to networking conditions). The app should report this error in a consumable way * to the user so that they may take action on it. No additional values will be reported to the - * [LiveData]. Each call to this method returns a new, distinct, [LiveData] object that must be - * observed. Note also that the returned [LiveData] is not guaranteed to begin with a pending - * state. + * [DataProvider]. Each call to this method returns a new, distinct, [DataProvider] object that + * must be observed. Note also that the returned [DataProvider] is not guaranteed to begin with a + * pending state. * - * If the app undergoes a configuration change, calling code should rely on the [LiveData] from - * [getCurrentQuestion] to know whether a current answer is pending. That [LiveData] will have its - * state changed to pending during answer submission and until answer resolution. + * If the app undergoes a configuration change, calling code should rely on the [DataProvider] + * from [getCurrentQuestion] to know whether a current answer is pending. That [DataProvider] will + * have its state changed to pending during answer submission and until answer resolution. * * Submitting an answer should result in the learner staying in the current question or moving to * a new question in the training session. Note that once a correct answer is processed, the @@ -133,11 +171,11 @@ class QuestionAssessmentProgressController @Inject constructor( * allow users to submit an answer while a previous answer is pending. That scenario will also * result in a failed answer submission. * - * No assumptions should be made about the completion order of the returned [LiveData] vs. the - * [LiveData] from [getCurrentQuestion]. Also note that the returned [LiveData] will only have a - * single value and not be reused after that point. + * No assumptions should be made about the completion order of the returned [DataProvider] vs. the + * [DataProvider] from [getCurrentQuestion]. Also note that the returned [DataProvider] will only + * have a single value and not be reused after that point. */ - fun submitAnswer(answer: UserAnswer): LiveData> { + fun submitAnswer(answer: UserAnswer): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -152,7 +190,7 @@ class QuestionAssessmentProgressController @Inject constructor( // Notify observers that the submitted answer is currently pending. progress.advancePlayStageTo(TrainStage.SUBMITTING_ANSWER) - asyncDataSubscriptionManager.notifyChangeAsync(SUBMIT_ANSWER_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) lateinit var answeredQuestionOutcome: AnsweredQuestionOutcome try { @@ -198,13 +236,17 @@ class QuestionAssessmentProgressController @Inject constructor( progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(answeredQuestionOutcome)) + return dataProviders.createInMemoryDataProvider(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + answeredQuestionOutcome + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -213,10 +255,10 @@ class QuestionAssessmentProgressController @Inject constructor( * * @param hintIndex index of the hint that was revealed in the hint list of the current pending * state - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitHintIsRevealed(hintIndex: Int): LiveData> { + fun submitHintIsRevealed(hintIndex: Int): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -237,22 +279,26 @@ class QuestionAssessmentProgressController @Inject constructor( // exception. progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider(SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID) { + null + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } /** * Notifies the controller that the user has revealed the solution to the current state. * - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitSolutionIsRevealed(): LiveData> { + fun submitSolutionIsRevealed(): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -274,12 +320,16 @@ class QuestionAssessmentProgressController @Inject constructor( progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { null } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -291,13 +341,13 @@ class QuestionAssessmentProgressController @Inject constructor( * Note that if the current question is pending, the user needs to submit a correct answer via * [submitAnswer] before forward navigation can occur. * - * @return a one-time [LiveData] indicating whether the movement to the next question was + * @return a one-time [DataProvider] indicating whether the movement to the next question was * successful, or a failure if question navigation was attempted at an invalid time (such as * if the current question is pending or terminal). It's recommended that calling code only * listen to this result for failures, and instead rely on [getCurrentQuestion] for observing * a successful transition to another question. */ - fun moveToNextQuestion(): LiveData> { + fun moveToNextQuestion(): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -314,12 +364,16 @@ class QuestionAssessmentProgressController @Inject constructor( hintHandler.navigateBackToLatestPendingState() progress.processNavigationToNewQuestion() } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return dataProviders.createInMemoryDataProviderAsync( + MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -346,7 +400,7 @@ class QuestionAssessmentProgressController @Inject constructor( * success or failure state back to pending. */ fun getCurrentQuestion(): DataProvider = progressLock.withLock { - val providerId = CREATE_CURRENT_QUESTION_DATA_WITH_TRANSLATION_CONTEXT_PROVIDER_ID + val providerId = LOCALIZED_QUESTION_PROVIDER_ID return translationController.getWrittenTranslationContentLocale( progress.currentProfileId ).combineWith(currentQuestionDataProvider, providerId) { contentLocale, currentQuestion -> @@ -363,9 +417,7 @@ class QuestionAssessmentProgressController @Inject constructor( */ fun calculateScores(skillIdList: List): DataProvider = progressLock.withLock { - return dataProviders.createInMemoryDataProviderAsync( - "user_assessment_performance" - ) { + dataProviders.createInMemoryDataProviderAsync(CALCULATE_SCORES_PROVIDER_ID) { retrieveUserAssessmentPerformanceAsync(skillIdList) } } @@ -380,7 +432,7 @@ class QuestionAssessmentProgressController @Inject constructor( progressLock.withLock { val scoreCalculator = scoreCalculatorFactory.create(skillIdList, progress.questionSessionMetrics) - return AsyncResult.success(scoreCalculator.computeAll()) + return AsyncResult.Success(scoreCalculator.computeAll()) } } @@ -388,7 +440,7 @@ class QuestionAssessmentProgressController @Inject constructor( questionsListDataProvider: DataProvider> ): NestedTransformedDataProvider { return questionsListDataProvider.transformNested( - CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID, + CURRENT_QUESTION_PROVIDER_ID, this::retrieveCurrentQuestionAsync ) } @@ -400,24 +452,24 @@ class QuestionAssessmentProgressController @Inject constructor( progressLock.withLock { return try { when (progress.trainStage) { - TrainStage.NOT_IN_TRAINING_SESSION -> AsyncResult.pending() + TrainStage.NOT_IN_TRAINING_SESSION -> AsyncResult.Pending() TrainStage.LOADING_TRAINING_SESSION -> { // If the assessment hasn't yet been initialized, initialize it // now that a list of questions is available. initializeAssessment(questionsList) progress.advancePlayStageTo(TrainStage.VIEWING_STATE) - AsyncResult.success( + AsyncResult.Success( retrieveEphemeralQuestionState(questionsList) ) } - TrainStage.VIEWING_STATE -> AsyncResult.success( + TrainStage.VIEWING_STATE -> AsyncResult.Success( retrieveEphemeralQuestionState(questionsList) ) - TrainStage.SUBMITTING_ANSWER -> AsyncResult.pending() + TrainStage.SUBMITTING_ANSWER -> AsyncResult.Pending() } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -464,8 +516,8 @@ class QuestionAssessmentProgressController @Inject constructor( /** Returns a temporary [DataProvider] that always provides an empty list of [Question]s. */ private fun createEmptyQuestionsListDataProvider(): DataProvider> { - return dataProviders.createInMemoryDataProvider(CREATE_EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID) { - listOf() + return dataProviders.createInMemoryDataProvider(EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID) { + listOf() } } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt index 935e17cca38..f55e83bb117 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt @@ -7,6 +7,7 @@ import org.oppia.android.domain.topic.TopicController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders +import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.transform import javax.inject.Inject import javax.inject.Singleton @@ -16,8 +17,6 @@ private const val RETRIEVE_QUESTION_FOR_SKILLS_ID_PROVIDER_ID = "retrieve_question_for_skills_id_provider_id" private const val START_QUESTION_TRAINING_SESSION_PROVIDER_ID = "start_question_training_session_provider_id" -private const val STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID = - "stop_question_training_session_provider_id" /** Controller for retrieving a set of questions. */ @Singleton @@ -47,18 +46,23 @@ class QuestionTrainingController @Inject constructor( fun startQuestionTrainingSession( profileId: ProfileId, skillIdsList: List - ): DataProvider { + ): DataProvider { return try { val retrieveQuestionsDataProvider = retrieveQuestionsForSkillIds(skillIdsList) - questionAssessmentProgressController.beginQuestionTrainingSession( - retrieveQuestionsDataProvider, profileId - ) - // Convert the data provider type to 'Any' via a transformation. - retrieveQuestionsDataProvider.transform(START_QUESTION_TRAINING_SESSION_PROVIDER_ID) { it } + val beginSessionDataProvider = + questionAssessmentProgressController.beginQuestionTrainingSession( + questionsListDataProvider = retrieveQuestionsDataProvider, profileId + ) + // Combine the data providers to ensure their results are tied together, but only take the + // result from the begin session provider (since that's the one that indicates session start + // success/failure, assuming the questions loaded successfully). + retrieveQuestionsDataProvider.combineWith( + beginSessionDataProvider, START_QUESTION_TRAINING_SESSION_PROVIDER_ID + ) { _, sessionResult -> sessionResult } } catch (e: Exception) { exceptionsController.logNonFatalException(e) dataProviders.createInMemoryDataProviderAsync(START_QUESTION_TRAINING_SESSION_PROVIDER_ID) { - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -112,15 +116,6 @@ class QuestionTrainingController @Inject constructor( * method should only be called if there is a training session is being played, otherwise an * exception will be thrown. */ - fun stopQuestionTrainingSession(): DataProvider { - return try { - questionAssessmentProgressController.finishQuestionTrainingSession() - dataProviders.createInMemoryDataProvider(STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID) { } - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - dataProviders.createInMemoryDataProviderAsync(STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID) { - AsyncResult.failed(e) - } - } - } + fun stopQuestionTrainingSession(): DataProvider = + questionAssessmentProgressController.finishQuestionTrainingSession() } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt index e2830055195..30304fd0575 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt @@ -304,9 +304,9 @@ class StoryProgressController @Inject constructor( .transformAsync(RETRIEVE_CHAPTER_PLAY_STATE_DATA_PROVIDER_ID) { val chapterProgress = it.chapterProgressMap[explorationId] if (chapterProgress != null) { - AsyncResult.success(chapterProgress.chapterPlayState) + AsyncResult.Success(chapterProgress.chapterPlayState) } else { - AsyncResult.success(ChapterPlayState.NOT_STARTED) + AsyncResult.Success(ChapterPlayState.NOT_STARTED) } } } @@ -319,7 +319,7 @@ class StoryProgressController @Inject constructor( .transformAsync(RETRIEVE_TOPIC_PROGRESS_LIST_DATA_PROVIDER_ID) { topicProgressDatabase -> val topicProgressList = mutableListOf() topicProgressList.addAll(topicProgressDatabase.topicProgressMap.values) - AsyncResult.success(topicProgressList.toList()) + AsyncResult.Success(topicProgressList.toList()) } } @@ -330,7 +330,7 @@ class StoryProgressController @Inject constructor( ): DataProvider { return retrieveCacheStore(profileId) .transformAsync(RETRIEVE_TOPIC_PROGRESS_DATA_PROVIDER_ID) { - AsyncResult.success(it.topicProgressMap[topicId] ?: TopicProgress.getDefaultInstance()) + AsyncResult.Success(it.topicProgressMap[topicId] ?: TopicProgress.getDefaultInstance()) } } @@ -342,7 +342,7 @@ class StoryProgressController @Inject constructor( ): DataProvider { return retrieveTopicProgressDataProvider(profileId, topicId) .transformAsync(RETRIEVE_STORY_PROGRESS_DATA_PROVIDER_ID) { - AsyncResult.success(it.storyProgressMap[storyId] ?: StoryProgress.getDefaultInstance()) + AsyncResult.Success(it.storyProgressMap[storyId] ?: StoryProgress.getDefaultInstance()) } } @@ -350,7 +350,7 @@ class StoryProgressController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - StoryProgressActionStatus.SUCCESS -> AsyncResult.success(null) + StoryProgressActionStatus.SUCCESS -> AsyncResult.Success(null) } } @@ -374,7 +374,7 @@ class StoryProgressController @Inject constructor( it?.let { it -> oppiaLogger.e( "StoryProgressController", - "Failed to prime cache ahead of LiveData conversion for StoryProgressController.", + "Failed to prime cache ahead of data retrieval for StoryProgressController.", it ) } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt index 2e8c0485fbe..d9f94efb97f 100755 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt @@ -115,7 +115,7 @@ class TopicController @Inject constructor( fun getTopic(profileId: ProfileId, topicId: String): DataProvider { val topicDataProvider = dataProviders.createInMemoryDataProviderAsync(GET_TOPIC_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveTopic(topicId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveTopic(topicId)) } val topicProgressDataProvider = storyProgressController.retrieveTopicProgressDataProvider(profileId, topicId) @@ -142,7 +142,7 @@ class TopicController @Inject constructor( ): DataProvider { val storyDataProvider = dataProviders.createInMemoryDataProviderAsync(GET_STORY_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveStory(topicId, storyId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveStory(topicId, storyId)) } val storyProgressDataProvider = storyProgressController.retrieveStoryProgressDataProvider(profileId, topicId, storyId) @@ -168,13 +168,13 @@ class TopicController @Inject constructor( explorationId: String ): DataProvider { return dataProviders.createInMemoryDataProviderAsync(GET_STORY_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveStory(topicId, storyId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveStory(topicId, storyId)) }.transformAsync(GET_CHAPTER_PROVIDER_ID) { storySummary -> val chapterSummary = fetchChapter(storySummary, explorationId) if (chapterSummary != null) { - AsyncResult.success(chapterSummary) + AsyncResult.Success(chapterSummary) } else { - AsyncResult.failed( + AsyncResult.Failure( ChapterNotFoundException( "Chapter for exploration $explorationId not found in story $storyId and topic $topicId" ) @@ -246,7 +246,7 @@ class TopicController @Inject constructor( ) ) } - AsyncResult.success(completedStoryListBuilder.build()) + AsyncResult.Success(completedStoryListBuilder.build()) } } @@ -258,7 +258,7 @@ class TopicController @Inject constructor( profileId ).transformAsync(GET_ONGOING_TOPIC_LIST_PROVIDER_ID) { val ongoingTopicList = createOngoingTopicListFromProgress(it) - AsyncResult.success(ongoingTopicList) + AsyncResult.Success(ongoingTopicList) } } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt index 6d37567193f..6a6c80366af 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt @@ -121,7 +121,7 @@ class TopicListController @Inject constructor( return storyProgressController.retrieveTopicProgressListDataProvider(profileId) .transformAsync(GET_PROMOTED_ACTIVITY_LIST_PROVIDER_ID) { val promotedActivityList = computePromotedActivityList(it) - AsyncResult.success(promotedActivityList) + AsyncResult.Success(promotedActivityList) } } diff --git a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt index 6cf23c39d04..6774e0cb70b 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt @@ -122,7 +122,7 @@ class TranslationController @Inject constructor( fun updateAppLanguage(profileId: ProfileId, selection: AppLanguageSelection): DataProvider { return dataProviders.createInMemoryDataProviderAsync(UPDATE_APP_LANGUAGE_DATA_PROVIDER_ID) { updateAppLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } @@ -173,7 +173,7 @@ class TranslationController @Inject constructor( val providerId = UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID return dataProviders.createInMemoryDataProviderAsync(providerId) { updateWrittenTranslationContentLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } @@ -224,7 +224,7 @@ class TranslationController @Inject constructor( val providerId = UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID return dataProviders.createInMemoryDataProviderAsync(providerId) { updateAudioTranslationContentLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } diff --git a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt index 972bd2ea1ba..561b3beb570 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt @@ -28,6 +28,7 @@ import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule @@ -51,6 +52,8 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [AudioPlayerControllerTest]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) @@ -146,8 +149,9 @@ class AudioPlayerControllerTest { arrangeMediaPlayer() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PREPARED) + } } @Test @@ -159,7 +163,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -169,9 +173,10 @@ class AudioPlayerControllerTest { shadowMediaPlayer.invokeCompletionListener() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.COMPLETED) + assertThat(position).isEqualTo(0) + } } @Test @@ -181,7 +186,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL2) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -192,7 +197,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL2) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -203,8 +208,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.runCurrent() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PLAYING) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PLAYING) + } } @Test @@ -218,7 +224,7 @@ class AudioPlayerControllerTest { verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) val results = audioPlayerResultCaptor.allValues - val pendingIndex = results.indexOfLast { it.isPending() } + val pendingIndex = results.indexOfLast { it is AsyncResult.Pending } val preparedIndex = results.indexOfLast { it.hasStatus(PlayStatus.PREPARED) } val playingIndex = results.indexOfLast { it.hasStatus(PlayStatus.PLAYING) } val completedIndex = results.indexOfLast { it.hasStatus(PlayStatus.COMPLETED) } @@ -238,8 +244,9 @@ class AudioPlayerControllerTest { audioPlayerController.pause() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PAUSED) + } } @Test @@ -247,7 +254,9 @@ class AudioPlayerControllerTest { arrangeMediaPlayer() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(0) + } } @Test @@ -260,7 +269,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.runCurrent() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(500) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(500) + } } @Test @@ -270,7 +281,9 @@ class AudioPlayerControllerTest { audioPlayerController.play() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().duration).isEqualTo(2000) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(duration).isEqualTo(2000) + } } @Test @@ -283,7 +296,9 @@ class AudioPlayerControllerTest { audioPlayerController.play() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(0) + } } @Test @@ -295,8 +310,9 @@ class AudioPlayerControllerTest { verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) // If the observer was still getting updates, the result would be pending - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PREPARED) + } } @Test @@ -309,7 +325,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.advanceTimeBy(2000) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PAUSED) + } // Verify: If the test does not hang, the behavior is correct. } @@ -323,7 +341,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.advanceTimeBy(2000) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.COMPLETED) + } // Verify: If the test does not hang, the behavior is correct. } @@ -367,7 +387,7 @@ class AudioPlayerControllerTest { shadowMediaPlayer.invokePreparedListener() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isFailure()).isTrue() + assertThat(audioPlayerResultCaptor.value).isFailure() } @Test @@ -440,7 +460,7 @@ class AudioPlayerControllerTest { } private fun AsyncResult.hasStatus(playStatus: PlayStatus): Boolean { - return isCompleted() && getOrThrow().type == playStatus + return (this is AsyncResult.Success) && value.type == playStatus } private fun setUpTestApplicationComponent() { diff --git a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt index 4aaafe0af54..6bb9a706e3c 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.audio import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,25 +10,15 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.CellularDataPreference import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -43,25 +32,15 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = CellularAudioDialogControllerTest.TestApplication::class) class CellularAudioDialogControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var cellularAudioDialogController: CellularAudioDialogController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCellularDataObserver: Observer> - - @Captor - lateinit var cellularDataResultCaptor: ArgumentCaptor> + @Inject lateinit var cellularAudioDialogController: CellularAudioDialogController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -73,80 +52,64 @@ class CellularAudioDialogControllerTest { } @Test - fun testController_providesInitialLiveData_indicatesToNotHideDialogAndNotUseCellularData() { - val cellularDataPreference = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - cellularDataPreference.observeForever(mockCellularDataObserver) - testCoroutineDispatchers.advanceUntilIdle() + fun testController_providesInitialState_indicatesToNotHideDialogAndNotUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isFalse() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isFalse() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setNeverUseCellularDataPref_providesLiveData_indicatesToHideDialogAndNotUseCellularData() { // ktlint-disable max-line-length - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() + fun testController_setNeverUseCellularDataPref_indicatesToHideDialogAndNotUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - appHistory.observeForever(mockCellularDataObserver) cellularAudioDialogController.setNeverUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setAlwaysUseCellularDataPref_providesLiveData_indicatesToHideDialogAndUseCellularData() { // ktlint-disable max-line-length - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() + fun testController_setAlwaysUseCellularDataPref_indicatesToHideDialogAndUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - appHistory.observeForever(mockCellularDataObserver) cellularAudioDialogController.setAlwaysUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isTrue() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isTrue() } @Test - fun testController_setNeverUseCellularDataPref_observedNewController_indicatesToHideDialogAndNotUseCellularData() { // ktlint-disable max-line-length + fun testController_setNeverUseCellDataPref_observeNewController_indicatesHideDialogNotUseCell() { // Pause immediate dispatching to avoid an infinite loop within the provider pipeline. cellularAudioDialogController.setNeverUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() setUpTestApplicationComponent() - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - appHistory.observeForever(mockCellularDataObserver) testCoroutineDispatchers.advanceUntilIdle() + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setAlwaysUseCellularDataPref_observedNewController_indicatesToHideDialogAndUseCellularData() { // ktlint-disable max-line-length + fun testController_setAlwaysUseCellDataPref_observeNewController_indicatesHideDialogAndUseCell() { cellularAudioDialogController.setAlwaysUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() setUpTestApplicationComponent() - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - appHistory.observeForever(mockCellularDataObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isTrue() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isTrue() } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt index 4bd222ebe06..60827cf127d 100644 --- a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.devoptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,21 +10,15 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.StorySummary import org.oppia.android.app.model.Topic import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.story.StoryProgressTestHelper @@ -36,8 +29,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -52,6 +43,8 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [ModifyLessonProgressController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ModifyLessonProgressControllerTest.TestApplication::class) @@ -71,43 +64,15 @@ class ModifyLessonProgressControllerTest { private const val RATIOS_STORY_ID_1 = "xBSdg4oOClga" private const val TEST_EXPLORATION_ID_2 = "test_exp_id_2" - private const val TEST_EXPLORATION_ID_4 = "test_exp_id_4" - private const val TEST_EXPLORATION_ID_5 = "13" private const val FRACTIONS_EXPLORATION_ID_0 = "umPkwp0L1M0-" private const val FRACTIONS_EXPLORATION_ID_1 = "MjZzEVOG47_1" - private const val RATIOS_EXPLORATION_ID_0 = "2mzzFVDLuAj8" - private const val RATIOS_EXPLORATION_ID_1 = "5NWuolNcwH6e" - private const val RATIOS_EXPLORATION_ID_2 = "k2bQ7z5XHNbK" - private const val RATIOS_EXPLORATION_ID_3 = "tIoSb3HZFN6e" } - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var modifyLessonProgressController: ModifyLessonProgressController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockAllTopicsObserver: Observer>> - - @Captor - lateinit var allTopicsResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockAllStoriesObserver: Observer>>> - - @Captor - lateinit var allStoriesResultCaptor: ArgumentCaptor>>> + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var modifyLessonProgressController: ModifyLessonProgressController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId: ProfileId @@ -120,24 +85,22 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_isSuccessful() { - val allTopicsLiveData = - modifyLessonProgressController.getAllTopicsWithProgress(profileId).toLiveData() - allTopicsLiveData.observeForever(mockAllTopicsObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllTopicsObserver).onChanged(allTopicsResultCaptor.capture()) - val allTopicsResult = allTopicsResultCaptor.value - assertThat(allTopicsResult!!.isSuccess()).isTrue() + val topicsProvider = modifyLessonProgressController.getAllTopicsWithProgress(profileId) + + monitorFactory.waitForNextSuccessfulResult(topicsProvider) } @Test fun testRetrieveAllTopics_providesListOfMultipleTopics() { val allTopics = retrieveAllTopics() + assertThat(allTopics.size).isGreaterThan(1) } @Test fun testRetrieveAllTopics_firstTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.name).isEqualTo("First Test Topic") @@ -146,6 +109,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_secondTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val secondTopic = allTopics[1] assertThat(secondTopic.topicId).isEqualTo(TEST_TOPIC_ID_1) assertThat(secondTopic.name).isEqualTo("Second Test Topic") @@ -154,6 +118,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_fractionsTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val fractionsTopic = allTopics[2] assertThat(fractionsTopic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(fractionsTopic.name).isEqualTo("Fractions") @@ -162,6 +127,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_ratiosTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val ratiosTopic = allTopics[3] assertThat(ratiosTopic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(ratiosTopic.name).isEqualTo("Ratios and Proportional Reasoning") @@ -177,6 +143,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_firstTopic_withoutAnyProgress_correctProgressFound() { val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.storyList[0].chapterList[0].chapterPlayState) @@ -190,7 +157,9 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_firstTopic_withTopicCompleted_correctProgressFound() { markFirstTestTopicCompleted() + val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.storyList[0].chapterList[0].chapterPlayState) @@ -202,43 +171,47 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_withoutAnyProgress_noTopicIsCompleted() { val allTopics = retrieveAllTopics() - allTopics.forEach { topic -> - val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(topic) - assertThat(isCompleted).isFalse() - } + + val topicsProgress = allTopics.map(modifyLessonProgressController::checkIfTopicIsCompleted) + + // None of the topics have progress. + assertThat(topicsProgress.all { !it }).isTrue() } @Test fun markFirstTestTopicCompleted_testRetrieveAllTopics_onlyFirstTestTopicIsCompleted() { markFirstTestTopicCompleted() val allTopics = retrieveAllTopics() - allTopics.forEach { topic -> - val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(topic) - if (topic.topicId.equals(TEST_TOPIC_ID_0)) assertThat(isCompleted).isTrue() - else assertThat(isCompleted).isFalse() - } + + val topicsProgress = + allTopics.associateBy(Topic::getTopicId).mapValues { (_, topic) -> + modifyLessonProgressController.checkIfTopicIsCompleted(topic) + } + + // All topics except the test topic 0 should not have progress. + val nonTestTopics = topicsProgress.filterNot { (id, _) -> id == TEST_TOPIC_ID_0 } + assertThat(nonTestTopics.values.count { !it }).isEqualTo(allTopics.size - 1) + assertThat(topicsProgress[TEST_TOPIC_ID_0]).isTrue() } @Test fun testRetrieveAllStories_isSuccessful() { - val allStoriesLiveData = - modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() - allStoriesLiveData.observeForever(mockAllStoriesObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllStoriesObserver).onChanged(allStoriesResultCaptor.capture()) - val allStoriesResult = allStoriesResultCaptor.value - assertThat(allStoriesResult!!.isSuccess()).isTrue() + val storyProgressProvider = modifyLessonProgressController.getStoryMapWithProgress(profileId) + + monitorFactory.waitForNextSuccessfulResult(storyProgressProvider) } @Test fun testRetrieveAllStories_providesListOfMultipleStories() { val allStories = retrieveAllStories() + assertThat(allStories.size).isGreaterThan(1) } @Test fun testRetrieveAllStories_firstStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.storyName).isEqualTo("First Story") @@ -247,6 +220,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_otherStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val secondStory = allStories[1] assertThat(secondStory.storyId).isEqualTo(TEST_STORY_ID_2) assertThat(secondStory.storyName).isEqualTo("Other Interesting Story") @@ -255,6 +229,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_fractionsStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val fractionsStory = allStories[2] assertThat(fractionsStory.storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(fractionsStory.storyName).isEqualTo("Matthew Goes to the Bakery") @@ -263,6 +238,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_ratiosStory1_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val ratiosStory1 = allStories[3] assertThat(ratiosStory1.storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(ratiosStory1.storyName).isEqualTo("Ratios: Part 1") @@ -271,6 +247,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_ratiosStory2_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val ratiosStory2 = allStories[4] assertThat(ratiosStory2.storyId).isEqualTo(RATIOS_STORY_ID_1) assertThat(ratiosStory2.storyName).isEqualTo("Ratios: Part 2") @@ -279,6 +256,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_firstStory_withoutAnyProgress_correctProgressFound() { val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.NOT_STARTED) @@ -291,7 +269,9 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_firstStory_withStoryCompleted_correctProgressFound() { markFirstStoryCompleted() + val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) @@ -301,21 +281,27 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_withoutAnyProgress_noStoryIsCompleted() { val allStories = retrieveAllStories() - allStories.forEach { storySummary -> - val isCompleted = modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) - assertThat(isCompleted).isFalse() - } + + val storiesProgress = allStories.map(modifyLessonProgressController::checkIfStoryIsCompleted) + + // None of the stories have progress. + assertThat(storiesProgress.all { !it }).isTrue() } @Test fun markFirstStoryCompleted_testRetrieveAllStories_onlyFirstStoryIsCompleted() { markFirstStoryCompleted() val allStories = retrieveAllStories() - allStories.forEach { storySummary -> - val isCompleted = modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) - if (storySummary.storyId.equals(TEST_STORY_ID_0)) assertThat(isCompleted).isTrue() - else assertThat(isCompleted).isFalse() - } + + val storiesProgress = + allStories.associateBy(StorySummary::getStoryId).mapValues { (_, storySummary) -> + modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) + } + + // All stories except the test story 0 should not have progress. + val nonTestStories = storiesProgress.filterNot { (id, _) -> id == TEST_STORY_ID_0 } + assertThat(nonTestStories.values.count { !it }).isEqualTo(allStories.size - 1) + assertThat(storiesProgress[TEST_STORY_ID_0]).isTrue() } @Test @@ -324,6 +310,7 @@ class ModifyLessonProgressControllerTest { profileId, listOf(TEST_TOPIC_ID_0, FRACTIONS_TOPIC_ID) ) + val allTopics = retrieveAllTopics() val firstTopic = allTopics[0] val fractionsTopic = allTopics[2] @@ -345,6 +332,7 @@ class ModifyLessonProgressControllerTest { profileId, mapOf(TEST_STORY_ID_0 to TEST_TOPIC_ID_0, RATIOS_STORY_ID_1 to RATIOS_TOPIC_ID) ) + val allStories = retrieveAllStories() val firstStory = allStories[0] val ratios2Story = allStories[4] @@ -366,6 +354,7 @@ class ModifyLessonProgressControllerTest { FRACTIONS_EXPLORATION_ID_1 to Pair(FRACTIONS_STORY_ID_0, FRACTIONS_TOPIC_ID) ) ) + val allStories = retrieveAllStories() val firstStory = allStories[0] val fractionsStory = allStories[2] @@ -396,21 +385,13 @@ class ModifyLessonProgressControllerTest { } private fun retrieveAllTopics(): List { - val allTopicsLiveData = - modifyLessonProgressController.getAllTopicsWithProgress(profileId).toLiveData() - allTopicsLiveData.observeForever(mockAllTopicsObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllTopicsObserver).onChanged(allTopicsResultCaptor.capture()) - return allTopicsResultCaptor.value.getOrThrow() + val topicsProvider = modifyLessonProgressController.getAllTopicsWithProgress(profileId) + return monitorFactory.waitForNextSuccessfulResult(topicsProvider) } private fun retrieveAllStories(): List { - val allStoriesLiveData = - modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() - allStoriesLiveData.observeForever(mockAllStoriesObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllStoriesObserver).onChanged(allStoriesResultCaptor.capture()) - return allStoriesResultCaptor.value.getOrThrow().values.flatten() + val storyProgressProvider = modifyLessonProgressController.getStoryMapWithProgress(profileId) + return monitorFactory.waitForNextSuccessfulResult(storyProgressProvider).values.flatten() } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 361bedc9528..461e240ed31 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.exploration import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,18 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.EphemeralState -import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.domain.classify.InteractionsModule import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule @@ -56,6 +45,7 @@ import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_1 import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.lightweightcheckpointing.ExplorationCheckpointTestHelper import org.oppia.android.testing.robolectric.RobolectricModule @@ -66,8 +56,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -82,39 +70,18 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [ExplorationDataController]. */ +// Function name: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationDataControllerTest.TestApplication::class) class ExplorationDataControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() + @Inject lateinit var explorationDataController: ExplorationDataController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject - lateinit var explorationDataController: ExplorationDataController - - @Inject - lateinit var explorationProgressController: ExplorationProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCurrentStateLiveDataObserver: Observer> - - @Mock - lateinit var mockExplorationObserver: Observer> - - @Captor - lateinit var explorationResultCaptor: ArgumentCaptor> - - val internalProfileId: Int = -1 + private val internalProfileId: Int = -1 @Before fun setUp() { @@ -126,96 +93,60 @@ class ExplorationDataControllerTest { } @Test - fun testController_providesInitialLiveDataForFractions0Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_0).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) + fun testController_providesInitialStateForFractions0Exploration() { + val explorationResult = explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_0) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("What is a Fraction?") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(25) } @Test - fun testController_providesInitialLiveDataForFractions1Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_1).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForFractions1Exploration() { + val explorationResult = explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_1) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("The Meaning of \"Equal Parts\"") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(18) } @Test - fun testController_providesInitialLiveDataForRatios0Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_0).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios0Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_0) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("What is a Ratio?") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(26) } @Test - fun testController_providesInitialLiveDataForRatios1Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_1).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios1Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_1) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Order is Important") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(22) } @Test - fun testController_providesInitialLiveDataForRatios2Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_2).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios2Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_2) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Equivalent Ratios") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(24) } @Test - fun testController_providesInitialLiveDataForRatios3Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_3).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios3Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_3) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Writing Ratios in Simplest Form") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(21) @@ -223,13 +154,9 @@ class ExplorationDataControllerTest { @Test fun testController_returnsFailedForNonExistentExploration() { - val explorationLiveData = - explorationDataController.getExplorationById("NON_EXISTENT_TEST").toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + val explorationResult = explorationDataController.getExplorationById("NON_EXISTENT_TEST") - verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(explorationResult) val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST") @@ -237,13 +164,9 @@ class ExplorationDataControllerTest { @Test fun testController_returnsFailed_logsException() { - val explorationLiveData = - explorationDataController.getExplorationById("NON_EXISTENT_TEST").toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + val explorationResult = explorationDataController.getExplorationById("NON_EXISTENT_TEST") - verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(explorationResult) val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST") diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index 337a0a98132..4ed0eade1d2 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.exploration import android.app.Application import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -16,14 +14,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.AnswerOutcome import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.ClickOnImage @@ -91,8 +81,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -132,64 +120,18 @@ class ExplorationProgressControllerTest { // - testSubmitAnswer_whileSubmittingAnotherAnswer_failsWithError // - testMoveToPrevious_whileSubmittingAnswer_failsWithError - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - @get:Rule val oppiaTestRule = OppiaTestRule() - @Inject - lateinit var context: Context - - @Inject - lateinit var explorationDataController: ExplorationDataController - - @Inject - lateinit var explorationProgressController: ExplorationProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var oppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockAsyncResultLiveDataObserver: Observer> - - @Mock - lateinit var mockAsyncAnswerOutcomeObserver: Observer> - - @Mock - lateinit var mockAsyncHintObserver: Observer> - - @Mock - lateinit var mockAsyncSolutionObserver: Observer> - - @Captor - lateinit var asyncResultCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncAnswerOutcomeCaptor: ArgumentCaptor> - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var explorationDataController: ExplorationDataController + @Inject lateinit var explorationProgressController: ExplorationProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var oppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var translationController: TranslationController private val profileId = ProfileId.newBuilder().setInternalId(0).build() @@ -208,7 +150,7 @@ class ExplorationProgressControllerTest { @Test fun testPlayExploration_invalid_returnsSuccess() { - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, INVALID_TOPIC_ID, @@ -217,13 +159,10 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // An invalid exploration is not known until it's fully loaded, and that's observed via // getCurrentState. - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(resultDataProvider) } @Test @@ -244,7 +183,7 @@ class ExplorationProgressControllerTest { @Test fun testPlayExploration_valid_returnsSuccess() { - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -253,11 +192,8 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(resultDataProvider) } @Test @@ -311,13 +247,10 @@ class ExplorationProgressControllerTest { @Test fun testFinishExploration_beforePlaying_failWithError() { - val resultLiveData = explorationDataController.stopPlayingExploration() - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val resultDataProvider = explorationDataController.stopPlayingExploration() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(resultDataProvider) + assertThat(error) .hasMessageThat() .contains("Cannot finish playing an exploration that hasn't yet been started") } @@ -335,7 +268,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() // Try playing another exploration without finishing the previous one. - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -344,12 +277,9 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(resultDataProvider) + assertThat(error) .hasMessageThat() .contains("Expected to finish previous exploration before starting a new one.") } @@ -388,50 +318,15 @@ class ExplorationProgressControllerTest { @Test fun testSubmitAnswer_beforePlaying_failsWithError() { - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(result) + assertThat(error) .hasMessageThat() .contains("Cannot submit an answer if an exploration is not being played.") } - @Test - fun testSubmitAnswer_whileLoading_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() - - // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot submit an answer while the exploration is being loaded.") - } - @Test fun testSubmitAnswer_forMultipleChoice_correctAnswer_succeeds() { playExploration( @@ -445,17 +340,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -471,17 +359,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -499,17 +380,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -525,17 +399,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("Try again.") } @@ -618,37 +485,13 @@ class ExplorationProgressControllerTest { @Test fun testMoveToNext_beforePlaying_failsWithError() { val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a next state if an exploration is not being played.") } - @Test - fun testMoveToNext_whileLoadingExploration_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot navigate to a next state if an exploration is being loaded.") - } - @Test fun testMoveToNext_forPendingInitialState_failsWithError() { playExploration( @@ -662,13 +505,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // Verify that we can't move ahead since the current state isn't yet completed. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -687,11 +527,8 @@ class ExplorationProgressControllerTest { submitPrototypeState1Answer() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToStateResult) } @Test @@ -728,57 +565,26 @@ class ExplorationProgressControllerTest { moveToNextState() // Try skipping past the current state. - val moveToStateResult = - explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToNextState() // Verify we can't move ahead since the new state isn't yet completed. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @Test fun testMoveToPrevious_beforePlaying_failsWithError() { - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) + val moveToStateResult = explorationProgressController.moveToPreviousState() testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a previous state if an exploration is not being played.") } - @Test - fun testMoveToPrevious_whileLoadingExploration_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot navigate to a previous state if an exploration is being loaded.") - } - @Test fun testMoveToPrevious_onPendingInitialState_failsWithError() { playExploration( @@ -791,15 +597,11 @@ class ExplorationProgressControllerTest { ) waitForGetCurrentStateSuccessfulLoad() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Verify we can't move behind since the current state is the initial exploration state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -817,15 +619,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() submitPrototypeState1Answer() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Still can't navigate behind for a completed initial state since there's no previous state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -843,15 +641,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() playThroughPrototypeState1AndMoveToNextState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Verify that we can navigate to the previous state since the current state is complete and not // initial. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToStateResult) } @Test @@ -890,16 +684,12 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() moveToPreviousState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // The first previous navigation should succeed (see above), but the second will fail since // we're back at the initial state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -917,17 +707,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -945,18 +728,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) // Verify that the answer was wrong, and that there's no handler for it so the default outcome // is returned. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("Not quite.") } @@ -1003,7 +779,9 @@ class ExplorationProgressControllerTest { // Submit 2 wrong answers to trigger a hint becoming available. submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) // Verify that the current state updates. It should stay pending, on submission of wrong answer. val ephemeralState = waitForGetCurrentStateSuccessfulLoad() @@ -1029,15 +807,17 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Verify that the current state updates. It should stay pending, on submission of wrong answer. waitForGetCurrentStateSuccessfulLoad() - val result = explorationProgressController.submitSolutionIsRevealed() - result.observeForever(mockAsyncSolutionObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitSolutionIsRevealed() + ) // Verify that the current state updates. Solution revealed is true. val ephemeralState = waitForGetCurrentStateSuccessfulLoad() @@ -1129,11 +909,10 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) - testCoroutineDispatchers.runCurrent() // Verify that the helpIndex.IndexTypeCase is equal LATEST_REVEALED_HINT_INDEX because a new // revealed hint is visible. + monitorFactory.waitForNextSuccessfulResult(result) val ephemeralState = waitForGetCurrentStateSuccessfulLoad() assertThat(ephemeralState.isHintRevealed(0)).isTrue() assertThat(ephemeralState.isSolutionRevealed()).isFalse() @@ -1157,8 +936,7 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() // The solution should be visible after 30 seconds of the last hint being reveled. @@ -1189,8 +967,7 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() submitWrongAnswerForPrototypeState2() @@ -1222,16 +999,14 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val hintResult = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - hintResult.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() // The solution should be visible after 30 seconds of the last hint being reveled. testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) testCoroutineDispatchers.runCurrent() - val solutionResult = explorationProgressController.submitSolutionIsRevealed() - solutionResult.observeForever(mockAsyncSolutionObserver) + explorationProgressController.submitSolutionIsRevealed() testCoroutineDispatchers.runCurrent() // Verify that the helpIndex.IndexTypeCase is equal EVERYTHING_IS_REVEALED because a new the @@ -1284,9 +1059,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should now be completed with the correct answer. @@ -1311,9 +1084,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish ")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Finnish ")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. The submitted answer should have a textual version @@ -1340,9 +1111,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should stay pending, and the wrong answer should be @@ -1527,15 +1296,9 @@ class ExplorationProgressControllerTest { navigateToPrototypeNumericInputState() val result = explorationProgressController.submitAnswer(createNumericInputAnswer(121.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -1553,18 +1316,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeNumericInputState() - val result = explorationProgressController.submitAnswer( - createNumericInputAnswer(122.0) - ) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createNumericInputAnswer(122.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("It's less than that.") } @@ -1583,15 +1338,9 @@ class ExplorationProgressControllerTest { // The first state of the exploration is the Continue interaction. val result = explorationProgressController.submitAnswer(createContinueButtonAnswer()) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the continue button succeeds by default. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Continuing onward") } @@ -1657,13 +1406,10 @@ class ExplorationProgressControllerTest { playThroughPrototypeExploration() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // Verify we can't navigate past the last state of the exploration. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -1743,11 +1489,10 @@ class ExplorationProgressControllerTest { @Test fun testMoveToNext_beforePlaying_failsWithError_logsException() { - val moveToStateResult = - explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - val exception = fakeExceptionLogger.getMostRecentException() + explorationProgressController.moveToNextState() + testCoroutineDispatchers.runCurrent() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot navigate to a next state if an exploration is not being played.") @@ -1767,26 +1512,22 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() moveToPreviousState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) + explorationProgressController.moveToPreviousState() testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) - assertThat(exception).hasMessageThat() + assertThat(exception) + .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @Test fun testSubmitAnswer_beforePlaying_failsWithError_logsException() { - val result = explorationProgressController.submitAnswer( - createMultipleChoiceAnswer(0) - ) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot submit an answer if an exploration is not being played.") @@ -1802,10 +1543,10 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) + waitForGetCurrentStateFailureLoad() val exception = fakeExceptionLogger.getMostRecentException() - assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: $INVALID_EXPLORATION_ID") } @@ -1822,13 +1563,12 @@ class ExplorationProgressControllerTest { ) waitForGetCurrentStateSuccessfulLoad() - val retrieveCheckpointLiveData = + val result = explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - TEST_EXPLORATION_ID_2 - ).toLiveData() + profileId, TEST_EXPLORATION_ID_2 + ) - verifyOperationSucceeds(retrieveCheckpointLiveData) + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -2107,7 +1847,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) verifyCheckpointHasCorrectHelpIndex( profileId, TEST_EXPLORATION_ID_2, @@ -2134,7 +1876,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) @@ -2164,11 +1908,15 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) - verifyOperationSucceeds(explorationProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitSolutionIsRevealed() + ) verifyCheckpointHasCorrectHelpIndex( profileId, TEST_EXPLORATION_ID_2, @@ -2692,7 +2440,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2727,7 +2477,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2765,7 +2517,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2788,7 +2542,7 @@ class ExplorationProgressControllerTest { } @Test - fun testCheckpointing_SolutionIsVisible_resumeExp_unrevealedSolutionIsVisibleOnPendingState() { + fun testCheckpointing_solutionIsVisible_resumeExp_unrevealedSolutionIsVisibleOnPendingState() { playExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -2804,7 +2558,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) endExploration() @@ -2842,14 +2598,15 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) // The solution should be visible after 30 seconds of the last hint being reveled. testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) testCoroutineDispatchers.runCurrent() - val solutionResult = explorationProgressController.submitSolutionIsRevealed() - solutionResult.observeForever(mockAsyncSolutionObserver) + explorationProgressController.submitSolutionIsRevealed() testCoroutineDispatchers.runCurrent() endExploration() @@ -3043,21 +2800,9 @@ class ExplorationProgressControllerTest { profileId: ProfileId, explorationId: String ): ExplorationCheckpoint { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - return explorationCheckpointCaptor.value.getOrThrow() + val explorationCheckpointDataProvider = + explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) + return monitorFactory.waitForNextSuccessfulResult(explorationCheckpointDataProvider) } private fun playExploration( @@ -3068,7 +2813,7 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint ) { - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( explorationDataController.startPlayingExploration( internalProfileId, topicId, @@ -3133,7 +2878,9 @@ class ExplorationProgressControllerTest { } private fun submitAnswer(userAnswer: UserAnswer): EphemeralState { - verifyOperationSucceeds(explorationProgressController.submitAnswer(userAnswer)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitAnswer(userAnswer) + ) return waitForGetCurrentStateSuccessfulLoad() } @@ -3312,17 +3059,17 @@ class ExplorationProgressControllerTest { } private fun moveToNextState(): EphemeralState { - verifyOperationSucceeds(explorationProgressController.moveToNextState()) + monitorFactory.waitForNextSuccessfulResult(explorationProgressController.moveToNextState()) return waitForGetCurrentStateSuccessfulLoad() } private fun moveToPreviousState(): EphemeralState { - verifyOperationSucceeds(explorationProgressController.moveToPreviousState()) + monitorFactory.waitForNextSuccessfulResult(explorationProgressController.moveToPreviousState()) return waitForGetCurrentStateSuccessfulLoad() } private fun endExploration() { - verifyOperationSucceeds(explorationDataController.stopPlayingExploration()) + monitorFactory.waitForNextSuccessfulResult(explorationDataController.stopPlayingExploration()) } private fun createContinueButtonAnswer() = @@ -3452,22 +3199,8 @@ class ExplorationProgressControllerTest { explorationId: String, pendingStateName: String ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingStateName) - .isEqualTo(pendingStateName) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.pendingStateName).isEqualTo(pendingStateName) } private fun verifyCheckpointHasCorrectCountOfAnswers( @@ -3475,22 +3208,8 @@ class ExplorationProgressControllerTest { explorationId: String, countOfAnswers: Int ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingUserAnswersCount) - .isEqualTo(countOfAnswers) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.pendingUserAnswersCount).isEqualTo(countOfAnswers) } private fun verifyCheckpointHasCorrectStateIndex( @@ -3498,22 +3217,8 @@ class ExplorationProgressControllerTest { explorationId: String, stateIndex: Int ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().stateIndex) - .isEqualTo(stateIndex) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.stateIndex).isEqualTo(stateIndex) } private fun verifyCheckpointHasCorrectHelpIndex( @@ -3521,40 +3226,8 @@ class ExplorationProgressControllerTest { explorationId: String, helpIndex: HelpIndex ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().helpIndex).isEqualTo(helpIndex) - } - - /** - * Verifies that the specified live data provides at least one successful operation. This will - * change test-wide mock state, and synchronizes background execution. - */ - private fun verifyOperationSucceeds(liveData: LiveData>) { - reset(mockAsyncResultLiveDataObserver) - liveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - asyncResultCaptor.value.apply { - // This bit of conditional logic is used to add better error reporting when failures occur. - if (isFailure()) { - throw AssertionError("Operation failed", getErrorOrNull()) - } - assertThat(isSuccess()).isTrue() - } - reset(mockAsyncResultLiveDataObserver) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.helpIndex).isEqualTo(helpIndex) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel new file mode 100644 index 00000000000..5b8b31df22c --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel @@ -0,0 +1,57 @@ +""" +Tests for lightweight checkpointing domain components. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "ExplorationCheckpointControllerTest", + srcs = ["ExplorationCheckpointControllerTest.kt"], + custom_package = "org.oppia.android.domain.exploration.lightweightcheckpointing", + test_class = "org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointControllerTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +oppia_android_test( + name = "ExplorationStorageModuleTest", + srcs = ["ExplorationStorageModuleTest.kt"], + custom_package = "org.oppia.android.domain.exploration.lightweightcheckpointing", + test_class = "org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt index 58abada089d..02daf2d19eb 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.exploration.lightweightcheckpointing import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,25 +10,18 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.ExplorationCheckpoint -import org.oppia.android.app.model.ExplorationCheckpointDetails import org.oppia.android.app.model.ProfileId +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.OutdatedExplorationCheckpointException import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.lightweightcheckpointing.ExplorationCheckpointTestHelper import org.oppia.android.testing.lightweightcheckpointing.FRACTIONS_EXPLORATION_0_TITLE @@ -46,8 +38,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -81,47 +71,18 @@ private const val BASE_TEST_EXPLORATION_TITLE = "Test Exploration " * For testing this controller, checkpoints of hypothetical explorations are saved, updated, * retrieved and deleted. These hypothetical explorations are referred to as "test explorations". */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationCheckpointControllerTest.TestApplication::class) class ExplorationCheckpointControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Mock - lateinit var mockResultObserver: Observer> - - @Captor - lateinit var resultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> - - @Mock - lateinit var mockCheckpointDetailsObserver: Observer> - - @Captor - lateinit var checkpointDetailsCaptor: ArgumentCaptor> + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var context: Context + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val firstTestProfile = ProfileId.newBuilder().setInternalId(0).build() private val secondTestProfile = ProfileId.newBuilder().setInternalId(1).build() @@ -134,28 +95,27 @@ class ExplorationCheckpointControllerTest { @Test fun testController_saveCheckpoint_databaseNotFull_isSuccessfulWithDatabaseInCorrectState() { - saveCheckpoint(firstTestProfile, index = 0) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT - ) + val result = saveCheckpoint(firstTestProfile, index = 0) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT) } @Test fun testController_saveCheckpoint_databaseFull_isSuccessfulWithDatabaseInCorrectState() { saveMultipleCheckpoints(firstTestProfile, numberOfCheckpoints = 2) - saveCheckpoint(firstTestProfile, index = 3) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_EXCEEDED_LIMIT - ) + + val result = saveCheckpoint(firstTestProfile, index = 3) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_EXCEEDED_LIMIT) } @Test fun testController_databaseFullForFirstTestProfile_checkDatabaseNotFullForSecondTestProfile() { saveMultipleCheckpoints(firstTestProfile, numberOfCheckpoints = 3) - saveCheckpoint(secondTestProfile, index = 0) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT - ) + + val result = saveCheckpoint(secondTestProfile, index = 0) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT) } @Test @@ -164,12 +124,14 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + + monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) } @Test @@ -178,16 +140,15 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_1 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_1 + ) + + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -197,16 +158,14 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - secondTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + secondTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -220,17 +179,15 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - val updatedCheckpoint = - explorationCheckpointCaptor.value.getOrDefault(ExplorationCheckpoint.getDefaultInstance()) + val updatedCheckpoint = monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) assertThat(updatedCheckpoint.pendingStateName) - .matches(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } @Test @@ -239,34 +196,30 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - explorationCheckpointTestHelper.saveCheckpointForFractionsStory0Exploration1( profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - val oldestCheckpointDetailsLiveData = - explorationCheckpointController - .retrieveOldestSavedExplorationCheckpointDetails(firstTestProfile).toLiveData() - oldestCheckpointDetailsLiveData.observeForever(mockCheckpointDetailsObserver) - verifyMockObserverIsSuccessful(mockCheckpointDetailsObserver, checkpointDetailsCaptor) + val checkpointProvider = + explorationCheckpointController.retrieveOldestSavedExplorationCheckpointDetails( + firstTestProfile + ) - val oldestCheckpointDetails = checkpointDetailsCaptor.value.getOrThrow() + val oldestCheckpointDetails = monitorFactory.waitForNextSuccessfulResult(checkpointProvider) assertThat(oldestCheckpointDetails.explorationId).isEqualTo(FRACTIONS_EXPLORATION_ID_0) assertThat(oldestCheckpointDetails.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) } @Test fun testCheckpointController_databaseEmpty_retrieveOldestCheckpointDetails_isFailure() { - val oldestCheckpointDetailsLiveData = - explorationCheckpointController - .retrieveOldestSavedExplorationCheckpointDetails(firstTestProfile).toLiveData() - oldestCheckpointDetailsLiveData.observeForever(mockCheckpointDetailsObserver) - verifyMockObserverIsFailure(mockCheckpointDetailsObserver, checkpointDetailsCaptor) - - assertThat(checkpointDetailsCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val checkpointProvider = + explorationCheckpointController.retrieveOldestSavedExplorationCheckpointDetails( + firstTestProfile + ) + + val error = monitorFactory.waitForNextFailureResult(checkpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -276,62 +229,68 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val deleteCheckpointLiveData = + val deleteCheckpointProvider = explorationCheckpointController.deleteSavedExplorationCheckpoint( firstTestProfile, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) + ) + + monitorFactory.waitForNextSuccessfulResult(deleteCheckpointProvider) + } + + @Test + fun testCheckpointController_saveCheckpoint_deleteSavedCheckpoint_checkpointWasDeleted() { + explorationCheckpointTestHelper.saveCheckpointForFractionsStory0Exploration0( + profileId = firstTestProfile, + version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION + ) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + monitorFactory.ensureDataProviderExecutes(deleteCheckpointProvider) // Verify that the checkpoint was deleted. - val retrieveCheckpointLiveData = + val retrieveCheckpointProvider = explorationCheckpointController.retrieveExplorationCheckpoint( firstTestProfile, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + ) - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test fun testController_saveCheckpoint_deleteUnsavedCheckpoint_isFailure() { saveCheckpoint(firstTestProfile, index = 0) - val deleteCheckpointLiveData = explorationCheckpointController.deleteSavedExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsFailure(mockResultObserver, resultCaptor) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - assertThat(resultCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) - assertThat(resultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("No saved checkpoint with explorationId") + val error = monitorFactory.waitForNextFailureResult(deleteCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) + assertThat(error).hasMessageThat().contains("No saved checkpoint with explorationId") } @Test fun testController_saveCheckpoint_deleteSavedCheckpointFromDifferentProfile_isFailure() { saveCheckpoint(firstTestProfile, index = 0) - val deleteCheckpointLiveData = explorationCheckpointController.deleteSavedExplorationCheckpoint( - secondTestProfile, - createExplorationIdForIndex(0) - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsFailure(mockResultObserver, resultCaptor) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + secondTestProfile, + createExplorationIdForIndex(0) + ) - assertThat(resultCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) - assertThat(resultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("No saved checkpoint with explorationId") + val error = monitorFactory.waitForNextFailureResult(deleteCheckpointProvider) + + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) + assertThat(error).hasMessageThat().contains("No saved checkpoint with explorationId") } @Test @@ -340,11 +299,14 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + + val checkpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + + monitorFactory.waitForNextSuccessfulResult(checkpointProvider) } @Test @@ -353,67 +315,24 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_OLD_VERSION ) - explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.OutdatedExplorationCheckpointException::class.java - ) - } - private fun verifyMockObserverIsSuccessful( - mockObserver: Observer>, - captor: ArgumentCaptor> - ) { - testCoroutineDispatchers.runCurrent() - verify(mockObserver, atLeastOnce()).onChanged(captor.capture()) - assertThat(captor.value.isSuccess()).isTrue() - } + val checkpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - private fun verifyMockObserverIsFailure( - mockObserver: Observer>, - captor: ArgumentCaptor> - ) { - testCoroutineDispatchers.runCurrent() - verify(mockObserver, atLeastOnce()).onChanged(captor.capture()) - assertThat(captor.value.isFailure()).isTrue() + val error = monitorFactory.waitForNextFailureResult(checkpointProvider) + assertThat(error).isInstanceOf(OutdatedExplorationCheckpointException::class.java) } - private fun saveCheckpoint( - profileId: ProfileId, - index: Int - ) { - reset(mockResultObserver) - explorationCheckpointController.recordExplorationCheckpoint( + private fun saveCheckpoint(profileId: ProfileId, index: Int): Any? { + val recordProvider = explorationCheckpointController.recordExplorationCheckpoint( profileId = profileId, explorationId = createExplorationIdForIndex(index), explorationCheckpoint = createCheckpoint(index) - ).toLiveData().observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) - } - - /** - * Updates the saved checkpoint for the test exploration specified by the [index] supplied. - * - * For this function to work as intended, it has to be made sure that a checkpoint for the test - * exploration specified by the index already exists in the checkpoint database of that profile. - * - * This function can update the checkpoint of a particular test exploration only once. - */ - private fun saveUpdatedCheckpoint( - profileId: ProfileId, - index: Int - ) { - reset(mockResultObserver) - explorationCheckpointController.recordExplorationCheckpoint( - profileId = profileId, - explorationId = createExplorationIdForIndex(index), - explorationCheckpoint = createUpdatedCheckpoint(index) - ).toLiveData().observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) + ) + return monitorFactory.waitForNextSuccessfulResult(recordProvider) } private fun saveMultipleCheckpoints(profileId: ProfileId, numberOfCheckpoints: Int) { @@ -459,13 +378,6 @@ class ExplorationCheckpointControllerTest { .setStateIndex(0) .build() - private fun createUpdatedCheckpoint(index: Int): ExplorationCheckpoint = - ExplorationCheckpoint.newBuilder() - .setExplorationTitle(createExplorationTitleForIndex(index)) - .setPendingStateName("second_state") - .setStateIndex(1) - .build() - private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext() .inject(this) diff --git a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt index c283cd69adc..e1c23d44250 100644 --- a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt @@ -3,8 +3,6 @@ package org.oppia.android.domain.onboarding import android.app.Application import android.content.Context import android.os.Bundle -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.core.content.pm.ApplicationInfoBuilder import androidx.test.core.content.pm.PackageInfoBuilder @@ -14,18 +12,8 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.AppStartupState -import org.oppia.android.app.model.AppStartupState.StartupMode import org.oppia.android.app.model.AppStartupState.StartupMode.APP_IS_DEPRECATED import org.oppia.android.app.model.AppStartupState.StartupMode.USER_IS_ONBOARDED import org.oppia.android.app.model.AppStartupState.StartupMode.USER_NOT_YET_ONBOARDED @@ -33,11 +21,10 @@ import org.oppia.android.app.model.OnboardingState import org.oppia.android.data.persistence.PersistentCacheStore import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -58,66 +45,42 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [AppStartupStateController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @Config(application = AppStartupStateControllerTest.TestApplication::class) class AppStartupStateControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Rule - @JvmField - val executorRule = InstantTaskExecutorRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var appStartupStateController: AppStartupStateController - - @Inject - lateinit var cacheFactory: PersistentCacheStore.Factory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOnboardingObserver: Observer> - - @Captor - lateinit var appStartupStateCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var appStartupStateController: AppStartupStateController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory // TODO(#3792): Remove this usage of Locale (probably by introducing a test utility in the locale // package to generate these strings). private val expirationDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) } @Test - fun testController_providesInitialLiveData_indicatesUserHasNotOnboardedTheApp() { + fun testController_providesInitialState_indicatesUserHasNotOnboardedTheApp() { setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test - fun testControllerObserver_observedAfterSettingAppOnboarded_providesLiveData_userDidNotOnboardApp() { // ktlint-disable max-line-length + fun testControllerObserver_observedAfterSettingAppOnboarded_providesState_userDidNotOnboardApp() { setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() + val appStartupState = appStartupStateController.getAppStartupState() - appStartupState.observeForever(mockOnboardingObserver) appStartupStateController.markOnboardingFlowCompleted() testCoroutineDispatchers.runCurrent() // The result should not indicate that the user onboarded the app because markUserOnboardedApp // does not notify observers of the change. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -130,15 +93,12 @@ class AppStartupStateControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - // The app should be considered onboarded since a new LiveData instance was observed after + // The app should be considered onboarded since a new DataProvider instance was observed after // marking the app as onboarded. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_IS_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_IS_ONBOARDED) } @Test @@ -162,14 +122,11 @@ class AppStartupStateControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // The app should be considered not yet onboarded since the previous history was cleared. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -177,13 +134,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -191,13 +145,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringForToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -205,13 +156,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -219,13 +167,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = false, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -240,13 +185,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -261,13 +203,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -285,14 +224,11 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // The user should be considered onboarded, but the app is not yet deprecated. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_IS_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_IS_ONBOARDED) } @Test @@ -310,14 +246,11 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // Despite the user completing the onboarding flow, the app is still deprecated. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } private fun setUpTestApplicationComponent() { @@ -400,10 +333,6 @@ class AppStartupStateControllerTest { packageManager.installPackage(packageInfo) } - private fun ArgumentCaptor>.getStartupMode(): StartupMode { - return value.getOrThrow().startupMode - } - // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 12beb92be13..6c221881999 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.analytics import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,16 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_CONCEPT_CARD import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_INFO_TAB @@ -31,18 +22,15 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REV import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_REVISION_TAB import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_STORY_ACTIVITY import org.oppia.android.app.model.EventLog.Priority -import org.oppia.android.app.model.OppiaEventLogs import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -67,38 +55,18 @@ private const val TEST_SKILL_ID = "test_skillId" private const val TEST_SKILL_LIST_ID = "test_skillListId" private const val TEST_SUB_TOPIC_ID = 1 +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = AnalyticsControllerTest.TestApplication::class) class AnalyticsControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var analyticsController: AnalyticsController - - @Inject - lateinit var oppiaLogger: OppiaLogger - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var fakeEventLogger: FakeEventLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var dataProviders: DataProviders - - @Mock - lateinit var mockOppiaEventLogsObserver: Observer> - - @Captor - lateinit var oppiaEventLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var analyticsController: AnalyticsController + @Inject lateinit var oppiaLogger: OppiaLogger + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var fakeEventLogger: FakeEventLogger + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -375,7 +343,8 @@ class AnalyticsControllerTest { .isEqualTo(OPEN_CONCEPT_CARD) } - // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to the remote service. + // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to + // the remote service. @Test fun testController_logTransitionEvent_withNoNetwork_checkLogsEventToStore() { @@ -390,15 +359,9 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val eventLog = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider).getEventLog(0) // ESSENTIAL priority confirms that the event logged is a transition event. assertThat(eventLog.priority).isEqualTo(Priority.ESSENTIAL) assertThat(eventLog.context.activityContextCase).isEqualTo(OPEN_QUESTION_PLAYER) @@ -418,15 +381,9 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val eventLog = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider).getEventLog(0) // OPTIONAL priority confirms that the event logged is a click event. assertThat(eventLog.priority).isEqualTo(Priority.OPTIONAL) assertThat(eventLog.context.activityContextCase).isEqualTo(OPEN_QUESTION_PLAYER) @@ -438,16 +395,10 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLogStoreSize = oppiaEventLogsResultCaptor.value.getOrThrow().eventLogList.size - assertThat(eventLogStoreSize).isEqualTo(2) + val eventLogs = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider) + assertThat(eventLogs.eventLogList).hasSize(2) } @Test @@ -472,16 +423,11 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val firstEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) - val secondEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(1) + val eventLogs = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider) + val firstEventLog = eventLogs.getEventLog(0) + val secondEventLog = eventLogs.getEventLog(1) // OPTIONAL priority confirms that the event logged is a click event. assertThat(firstEventLog.priority).isEqualTo(Priority.OPTIONAL) @@ -511,16 +457,10 @@ class AnalyticsControllerTest { ) ) - val cachedEventLogs = analyticsController.getEventLogStore().toLiveData() - cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val logsProvider = analyticsController.getEventLogStore() val uploadedEventLog = fakeEventLogger.getMostRecentEvent() - val cachedEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val cachedEventLog = monitorFactory.waitForNextSuccessfulResult(logsProvider).getEventLog(0) // ESSENTIAL priority confirms that the event logged is a transition event. assertThat(uploadedEventLog.priority).isEqualTo(Priority.ESSENTIAL) @@ -538,25 +478,23 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val cachedEventLogs = analyticsController.getEventLogStore().toLiveData() - cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) - - val firstEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) - val secondEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(1) - val eventLogStoreSize = oppiaEventLogsResultCaptor.value.getOrThrow().eventLogList.size - assertThat(eventLogStoreSize).isEqualTo(2) - // In this case, 3 ESSENTIAL and 1 OPTIONAL event was logged. So while pruning, none of the retained logs should have OPTIONAL priority. + val logsProvider = analyticsController.getEventLogStore() + + val eventLogs = monitorFactory.waitForNextSuccessfulResult(logsProvider) + val firstEventLog = eventLogs.getEventLog(0) + val secondEventLog = eventLogs.getEventLog(1) + assertThat(eventLogs.eventLogList).hasSize(2) + // In this case, 3 ESSENTIAL and 1 OPTIONAL event was logged. So while pruning, none of the + // retained logs should have OPTIONAL priority. assertThat(firstEventLog.priority).isNotEqualTo(Priority.OPTIONAL) assertThat(secondEventLog.priority).isNotEqualTo(Priority.OPTIONAL) - // If we analyse the implementation of logMultipleEvents(), we can see that record pruning will begin from the logging of the third record. - // At first, the second event log will be removed as it has OPTIONAL priority and the event logged at the third place will become the event record at the second place in the store. - // When the forth event gets logged then the pruning will be purely based on timestamp of the event as both event logs have ESSENTIAL priority. - // As the third event's timestamp was lesser than that of the first event, it will be pruned from the store and the forth event will become the second event in the store. + // If we analyse the implementation of logMultipleEvents(), we can see that record pruning will + // begin from the logging of the third record. At first, the second event log will be removed as + // it has OPTIONAL priority and the event logged at the third place will become the event record + // at the second place in the store. When the forth event gets logged then the pruning will be + // purely based on timestamp of the event as both event logs have ESSENTIAL priority. As the + // third event's timestamp was lesser than that of the first event, it will be pruned from the + // store and the forth event will become the second event in the store. assertThat(firstEventLog.timestamp).isEqualTo(1556094120000) assertThat(secondEventLog.timestamp).isEqualTo(1556094100000) } diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt index 656263a82a0..7e4bc2e6614 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.exceptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,28 +10,18 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ExceptionLog.ExceptionType -import org.oppia.android.app.model.OppiaExceptionLogs import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -53,35 +42,18 @@ private const val TEST_TIMESTAMP_IN_MILLIS_TWO = 1556094110000 private const val TEST_TIMESTAMP_IN_MILLIS_THREE = 1556094100000 private const val TEST_TIMESTAMP_IN_MILLIS_FOUR = 1556094000000 +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExceptionsControllerTest.TestApplication::class) class ExceptionsControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var exceptionsController: ExceptionsController - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOppiaExceptionLogsObserver: Observer> - - @Captor - lateinit var oppiaExceptionLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var exceptionsController: ExceptionsController + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -108,7 +80,8 @@ class ExceptionsControllerTest { assertThat(exceptionLogged).isEqualTo(exceptionThrown) } - // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to the remote service. + // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to + // the remote service. @Test fun testController_logException_nonFatal_withNoNetwork_logsToCacheStore() { @@ -116,17 +89,10 @@ class ExceptionsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify( - mockOppiaExceptionLogsObserver, - atLeastOnce() - ).onChanged(oppiaExceptionLogsResultCaptor.capture()) + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionLog = exceptionsLog.getExceptionLog(0) val exception = exceptionLog.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -148,15 +114,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception("TEST MESSAGE", Throwable("TEST")) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionLog = exceptionsLog.getExceptionLog(0) val exception = exceptionLog.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -193,25 +154,25 @@ class ExceptionsControllerTest { TEST_TIMESTAMP_IN_MILLIS_FOUR ) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - val exceptionOne = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exceptionTwo = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(1) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionOne = exceptionsLog.getExceptionLog(0) + val exceptionTwo = exceptionsLog.getExceptionLog(1) - // In this case, 3 fatal and 1 non-fatal exceptions were logged. The order of logging was fatal->non-fatal->fatal->fatal. - // So after pruning, none of the retained logs should have non-fatal exception type. + // In this case, 3 fatal and 1 non-fatal exceptions were logged. The order of logging was + // fatal->non-fatal->fatal->fatal. So after pruning, none of the retained logs should have + // non-fatal exception type. assertThat(exceptionOne.exceptionType).isNotEqualTo(ExceptionType.NON_FATAL) assertThat(exceptionTwo.exceptionType).isNotEqualTo(ExceptionType.NON_FATAL) - // If we analyse the order of logging of exceptions, we can see that record pruning will begin from the logging of the third record. - // At first, the second exception log will be removed as it has non-fatal exception type and the exception logged at the third place will become the exception record at the second place in the store. - // When the forth exception gets logged then the pruning will be purely based on timestamp of the exception as both exception logs have fatal exception type. - // As the third exceptions's timestamp was lesser than that of the first event, it will be pruned from the store and the forth exception will become the second exception in the store. + // If we analyse the order of logging of exceptions, we can see that record pruning will begin + // from the logging of the third record. At first, the second exception log will be removed as + // it has non-fatal exception type and the exception logged at the third place will become the + // exception record at the second place in the store. When the forth exception gets logged then + // the pruning will be purely based on timestamp of the exception as both exception logs have + // fatal exception type. As the third exceptions's timestamp was lesser than that of the first + // event, it will be pruned from the store and the forth exception will become the second + // exception in the store. assertThat(exceptionOne.timestampInMillis).isEqualTo(TEST_TIMESTAMP_IN_MILLIS_ONE) assertThat(exceptionTwo.timestampInMillis).isEqualTo(TEST_TIMESTAMP_IN_MILLIS_FOUR) assertThat(exceptionOne.message).isEqualTo("TEST1") @@ -235,16 +196,10 @@ class ExceptionsControllerTest { TEST_TIMESTAMP_IN_MILLIS_THREE ) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val cacheStoreSize = oppiaExceptionLogsResultCaptor.value.getOrThrow().exceptionLogList.size - - assertThat(cacheStoreSize).isEqualTo(2) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + assertThat(exceptionsLog.exceptionLogList).hasSize(2) } @Test @@ -254,16 +209,11 @@ class ExceptionsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) val exceptionFromRemoteService = fakeExceptionLogger.getMostRecentException() - val exceptionFromCacheStorage = - oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionFromCacheStorage = exceptionsLog.getExceptionLog(0) val exception = exceptionFromCacheStorage.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -287,15 +237,11 @@ class ExceptionsControllerTest { exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionOne = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exceptionTwo = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(1) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionOne = exceptionsLog.getExceptionLog(0) + val exceptionTwo = exceptionsLog.getExceptionLog(1) assertThat(exceptionOne.exceptionType).isEqualTo(ExceptionType.NON_FATAL) assertThat(exceptionTwo.exceptionType).isEqualTo(ExceptionType.FATAL) } @@ -306,15 +252,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception() exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exception = exceptionsLog.getExceptionLog(0).toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val expectedExceptionStackTraceElems = exceptionThrown.stackTrace.extractRelevantDetails() assertThat(exception.message).isEqualTo(null) @@ -330,15 +271,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception("TEST") exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exception = exceptionsLog.getExceptionLog(0).toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val expectedExceptionStackTraceElems = exceptionThrown.stackTrace.extractRelevantDetails() assertThat(exception.message).isEqualTo("TEST") diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt index a160aa5562c..b0702bf7430 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.exceptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,28 +10,17 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.OppiaExceptionLogs import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -46,38 +34,18 @@ import javax.inject.Inject import javax.inject.Qualifier import javax.inject.Singleton +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = UncaughtExceptionLoggerStartupListenerTest.TestApplication::class) class UncaughtExceptionLoggerStartupListenerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var uncaughtExceptionLoggerStartupListener: UncaughtExceptionLoggerStartupListener - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var exceptionsController: ExceptionsController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOppiaExceptionLogsObserver: Observer> - - @Captor - lateinit var oppiaExceptionLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var uncaughtExceptionStartupListener: UncaughtExceptionLoggerStartupListener + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var exceptionsController: ExceptionsController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -88,28 +56,22 @@ class UncaughtExceptionLoggerStartupListenerTest { fun testHandler_throwException_withNoNetwork_verifyLogInCache() { networkConnectionUtil.setCurrentConnectionStatus(NONE) val exceptionThrown = Exception("TEST") - uncaughtExceptionLoggerStartupListener.uncaughtException( + uncaughtExceptionStartupListener.uncaughtException( Thread.currentThread(), exceptionThrown ) val cachedExceptions = exceptionsController.getExceptionLogStore() - cachedExceptions.toLiveData().observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionLogs = monitorFactory.waitForNextSuccessfulResult(cachedExceptions) + val exception = exceptionLogs.getExceptionLog(0).toException() assertThat(exception.message).matches("java.lang.Exception: TEST") } @Test fun testHandler_throwException_withNetwork_verifyLogToRemoteService() { val exceptionThrown = Exception("TEST") - uncaughtExceptionLoggerStartupListener.uncaughtException( + uncaughtExceptionStartupListener.uncaughtException( Thread.currentThread(), exceptionThrown ) diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt index b18153a13a2..6bbb4c3366b 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.platformparameter import android.app.Application import android.content.Context -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,24 +9,15 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.PlatformParameter import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.logging.EnableConsoleLog @@ -52,39 +41,16 @@ private const val BOOLEAN_PLATFORM_PARAMETER_NAME = "boolean_platform_parameter_ private const val BOOLEAN_PLATFORM_PARAMETER_VALUE = true /** Tests for [PlatformParameterController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = PlatformParameterControllerTest.TestApplication::class) class PlatformParameterControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Rule - @JvmField - val executorRule = InstantTaskExecutorRule() - - @Inject - lateinit var platformParameterController: PlatformParameterController - - @Inject - lateinit var platformParameterSingleton: PlatformParameterSingleton - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockUnitObserver: Observer> - - @Captor - lateinit var unitCaptor: ArgumentCaptor> - - @Mock - lateinit var mockObserverForAny: Observer > - - @Captor - lateinit var captorForAny: ArgumentCaptor> + @Inject lateinit var platformParameterController: PlatformParameterController + @Inject lateinit var platformParameterSingleton: PlatformParameterSingleton + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val mockPlatformParameterList by lazy { listOf( @@ -100,12 +66,10 @@ class PlatformParameterControllerTest { @Test fun testController_noPreviousDatabase_readPlatformParameters_platformParameterMapIsEmpty() { setUpTestApplicationComponent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must be empty as there was no previously cached data. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isEmpty() } @@ -121,12 +85,10 @@ class PlatformParameterControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpTestApplicationComponent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must have values as application had cached platform parameter data. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isNotEmpty() verifyEntriesInsidePlatformParameterMap(platformParameterSingleton.getPlatformParameterMap()) } @@ -136,12 +98,10 @@ class PlatformParameterControllerTest { setUpTestApplicationComponent() platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) testCoroutineDispatchers.runCurrent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must have values as we updated the database with dummy list. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isNotEmpty() verifyEntriesInsidePlatformParameterMap(platformParameterSingleton.getPlatformParameterMap()) } @@ -160,25 +120,21 @@ class PlatformParameterControllerTest { setUpTestApplicationComponent() platformParameterController.updatePlatformParameterDatabase(listOf()) testCoroutineDispatchers.runCurrent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The new set of values must be empty as we updated the database with an empty list. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isEmpty() } @Test fun testController_noPreviousDatabase_performUpdateOperation_returnsSuccess() { setUpTestApplicationComponent() - platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - .toLiveData().observeForever(mockObserverForAny) - testCoroutineDispatchers.runCurrent() + val updateProvider = + platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - // After a successful update operation we should receive a async result for success - verify(mockObserverForAny, atLeastOnce()).onChanged(captorForAny.capture()) - assertThat(captorForAny.value.isSuccess()).isTrue() + // After a successful update operation we should receive a success result. + monitorFactory.waitForNextSuccessfulResult(updateProvider) } @Test @@ -193,13 +149,11 @@ class PlatformParameterControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpTestApplicationComponent() - platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - .toLiveData().observeForever(mockObserverForAny) - testCoroutineDispatchers.runCurrent() + val updateProvider = + platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - // After a successful update operation we should receive a async result for success - verify(mockObserverForAny, atLeastOnce()).onChanged(captorForAny.capture()) - assertThat(captorForAny.value.isSuccess()).isTrue() + // After a successful update operation we should receive a success result. + monitorFactory.waitForNextSuccessfulResult(updateProvider) } /** @@ -274,7 +228,7 @@ class PlatformParameterControllerTest { interface Builder { @BindsInstance fun setApplication(application: Application): Builder - fun build(): PlatformParameterControllerTest.TestApplicationComponent + fun build(): TestApplicationComponent } fun inject(platformParameterControllerTest: PlatformParameterControllerTest) @@ -285,7 +239,7 @@ class PlatformParameterControllerTest { } class TestApplication : Application(), DataProvidersInjectorProvider { - private val component: PlatformParameterControllerTest.TestApplicationComponent by lazy { + private val component: TestApplicationComponent by lazy { DaggerPlatformParameterControllerTest_TestApplicationComponent.builder() .setApplication(this) .build() diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt index 70ccd1e8c08..799b615842c 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.platformparameter.syncup import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.core.content.pm.ApplicationInfoBuilder import androidx.test.core.content.pm.PackageInfoBuilder @@ -23,12 +22,8 @@ import dagger.Module import dagger.Provides import okhttp3.OkHttpClient import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.PlatformParameter import org.oppia.android.data.backends.gae.BaseUrl import org.oppia.android.data.backends.gae.JsonPrefixNetworkInterceptor @@ -42,6 +37,7 @@ import org.oppia.android.domain.platformparameter.PlatformParameterController import org.oppia.android.domain.platformparameter.PlatformParameterSingletonImpl import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.network.MockPlatformParameterService import org.oppia.android.testing.network.RetrofitTestModule import org.oppia.android.testing.platformparameter.TEST_BOOLEAN_PARAM_NAME @@ -55,8 +51,6 @@ import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -77,6 +71,8 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [PlatformParameterSyncUpWorker]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config( @@ -84,31 +80,13 @@ import javax.inject.Singleton manifest = Config.NONE ) class PlatformParameterSyncUpWorkerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Mock - lateinit var mockUnitObserver: Observer> - - @Inject - lateinit var platformParameterSingleton: PlatformParameterSingleton - - @Inject - lateinit var platformParameterController: PlatformParameterController - - @Inject - lateinit var platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var platformParameterSingleton: PlatformParameterSingleton + @Inject lateinit var platformParameterController: PlatformParameterController + @Inject lateinit var platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var context: Context + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val expectedTestStringParameter = PlatformParameter.newBuilder() .setName(TEST_STRING_PARAM_NAME) @@ -173,8 +151,7 @@ class PlatformParameterSyncUpWorkerTest { assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. @@ -242,8 +219,7 @@ class PlatformParameterSyncUpWorkerTest { assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. @@ -327,8 +303,7 @@ class PlatformParameterSyncUpWorkerTest { .isEqualTo(PlatformParameterSyncUpWorker.EMPTY_RESPONSE_EXCEPTION_MSG) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt index 752699cee61..036b41444e6 100644 --- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.profile import android.app.Application import android.content.Context -import androidx.lifecycle.Observer +import android.net.Uri import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,32 +11,25 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.AppLanguage +import org.oppia.android.app.model.AppLanguage.CHINESE_APP_LANGUAGE import org.oppia.android.app.model.AudioLanguage -import org.oppia.android.app.model.DeviceSettings +import org.oppia.android.app.model.AudioLanguage.FRENCH_AUDIO_LANGUAGE import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileDatabase import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.ReadingTextSize +import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -53,63 +46,38 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [ProfileManagementControllerTest]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileManagementControllerTest.TestApplication::class) class ProfileManagementControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var profileTestHelper: ProfileTestHelper - - @Inject - lateinit var profileManagementController: ProfileManagementController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockProfilesObserver: Observer>> - - @Captor - lateinit var profilesResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockProfileObserver: Observer> - - @Captor - lateinit var profileResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockUpdateResultObserver: Observer> - - @Captor - lateinit var updateResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockWasProfileAddedResultObserver: Observer> - - @Captor - lateinit var wasProfileAddedResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockDeviceSettingsObserver: Observer> - - @Captor - lateinit var deviceSettingsResultCaptor: ArgumentCaptor> - - private val PROFILES_LIST = listOf( - Profile.newBuilder().setName("James").setPin("123").setAllowDownloadAccess(true).build(), - Profile.newBuilder().setName("Sean").setPin("234").setAllowDownloadAccess(false).build(), - Profile.newBuilder().setName("Ben").setPin("345").setAllowDownloadAccess(true).build(), - Profile.newBuilder().setName("Rajat").setPin("456").setAllowDownloadAccess(false).build(), - Profile.newBuilder().setName("Veena").setPin("567").setAllowDownloadAccess(true).build() - ) + @Inject lateinit var context: Context + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var profileManagementController: ProfileManagementController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + + private companion object { + private val PROFILES_LIST = listOf( + Profile.newBuilder().setName("James").setPin("123").setAllowDownloadAccess(true).build(), + Profile.newBuilder().setName("Sean").setPin("234").setAllowDownloadAccess(false).build(), + Profile.newBuilder().setName("Ben").setPin("345").setAllowDownloadAccess(true).build(), + Profile.newBuilder().setName("Rajat").setPin("456").setAllowDownloadAccess(false).build(), + Profile.newBuilder().setName("Veena").setPin("567").setAllowDownloadAccess(true).build() + ) + + private val ADMIN_PROFILE_ID_0 = ProfileId.newBuilder().setInternalId(0).build() + private val PROFILE_ID_1 = ProfileId.newBuilder().setInternalId(1).build() + private val PROFILE_ID_2 = ProfileId.newBuilder().setInternalId(2).build() + private val PROFILE_ID_3 = ProfileId.newBuilder().setInternalId(3).build() + private val PROFILE_ID_4 = ProfileId.newBuilder().setInternalId(4).build() + private val PROFILE_ID_6 = ProfileId.newBuilder().setInternalId(6).build() + + private const val DEFAULT_PIN = "12345" + private const val DEFAULT_ALLOW_DOWNLOAD_ACCESS = true + private const val DEFAULT_AVATAR_COLOR_RGB = -10710042 + } @Before fun setUp() { @@ -122,25 +90,17 @@ class ProfileManagementControllerTest { @Test fun testAddProfile_addProfile_checkProfileIsAdded() { - profileManagementController.addProfile( - name = "James", - pin = "123", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "James", pin = "123") - val profileDatabase = readProfileDatabase() - verifyUpdateSucceeded() + monitorFactory.waitForNextSuccessfulResult(dataProvider) + val profileDatabase = readProfileDatabase() val profile = profileDatabase.profilesMap[0]!! assertThat(profile.name).isEqualTo("James") assertThat(profile.pin).isEqualTo("123") assertThat(profile.allowDownloadAccess).isEqualTo(true) assertThat(profile.id.internalId).isEqualTo(0) - assertThat(profile.readingTextSize).isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) assertThat(profile.appLanguage).isEqualTo(AppLanguage.ENGLISH_APP_LANGUAGE) assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue() @@ -149,60 +109,35 @@ class ProfileManagementControllerTest { @Test fun testAddProfile_addProfileWithNotUniqueName_checkResultIsFailure() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.addProfile( - name = "JAMES", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "JAMES", pin = "321") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("JAMES is not unique to other profiles") + val failure = monitorFactory.waitForNextFailureResult(dataProvider) + assertThat(failure).hasMessageThat().contains("JAMES is not unique to other profiles") } @Test fun testAddProfile_addProfileWithNumberInName_checkResultIsFailure() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.addProfile( - name = "James034", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "James034", pin = "321") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("James034 does not contain only letters") + val failure = monitorFactory.waitForNextFailureResult(dataProvider) + assertThat(failure).hasMessageThat().contains("James034 does not contain only letters") } @Test fun testGetProfile_addManyProfiles_checkGetProfileIsCorrect() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.getProfile(ProfileId.newBuilder().setInternalId(3).build()) - .toLiveData() - .observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = profileManagementController.getProfile(PROFILE_ID_3) - verifyGetProfileSucceeded() - val profile = profileResultCaptor.value.getOrThrow() + val profile = monitorFactory.waitForNextSuccessfulResult(dataProvider) assertThat(profile.name).isEqualTo("Rajat") assertThat(profile.pin).isEqualTo("456") assertThat(profile.allowDownloadAccess).isEqualTo(false) assertThat(profile.id.internalId).isEqualTo(3) - assertThat(profile.readingTextSize).isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) assertThat(profile.appLanguage).isEqualTo(AppLanguage.ENGLISH_APP_LANGUAGE) assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) } @@ -210,13 +145,10 @@ class ProfileManagementControllerTest { @Test fun testGetProfiles_addManyProfiles_checkAllProfilesAreAdded() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = profileManagementController.getProfiles() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profiles = monitorFactory.waitForNextSuccessfulResult(dataProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(PROFILES_LIST.size) @@ -226,22 +158,12 @@ class ProfileManagementControllerTest { @Test fun testGetProfiles_addManyProfiles_restartApplication_addProfile_checkAllProfilesAreAdded() { addTestProfiles() - testCoroutineDispatchers.runCurrent() setUpTestApplicationComponent() - profileManagementController.addProfile( - name = "Nikita", - pin = "678", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = false - ).toLiveData() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + addNonAdminProfileAndWait(name = "Nikita", pin = "678", allowDownloadAccess = false) + val dataProvider = profileManagementController.getProfiles() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profiles = monitorFactory.waitForNextSuccessfulResult(dataProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(PROFILES_LIST.size + 1) @@ -251,244 +173,159 @@ class ProfileManagementControllerTest { @Test fun testUpdateName_addProfiles_updateWithUniqueName_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateName(profileId, "John").toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_2, "John") + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().name).isEqualTo("John") + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.name).isEqualTo("John") } @Test fun testUpdateName_addProfiles_updateWithNotUniqueName_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateName(profileId, "James").toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_2, "James") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("James is not unique to other profiles") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("James is not unique to other profiles") } @Test fun testUpdateName_addProfiles_updateWithBadProfileId_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updateName(profileId, "John").toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_6, "John") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateName_addProfiles_updateProfileAvatar_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController + val updateProvider = profileManagementController .updateProfileAvatar( - profileId, + PROFILE_ID_2, /* avatarImagePath = */ null, - colorRgb = -10710042 - ).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + colorRgb = DEFAULT_AVATAR_COLOR_RGB + ) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().avatar.avatarColorRgb) - .isEqualTo(-10710042) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.avatar.avatarColorRgb).isEqualTo(DEFAULT_AVATAR_COLOR_RGB) } @Test fun testUpdatePin_addProfiles_updatePin_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updatePin(profileId, "321").toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updatePin(PROFILE_ID_2, "321") + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().pin).isEqualTo("321") + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.pin).isEqualTo("321") } @Test fun testUpdatePin_addProfiles_updateWithBadProfileId_checkUpdateFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updatePin(profileId, "321").toLiveData() - .observeForever(mockUpdateResultObserver) + val updateProvider = profileManagementController.updatePin(PROFILE_ID_6, "321") testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateAllowDownloadAccess_addProfiles_updateDownloadAccess_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateAllowDownloadAccess(profileId, false).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateAllowDownloadAccess(PROFILE_ID_2, false) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().allowDownloadAccess) - .isEqualTo(false) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.allowDownloadAccess).isEqualTo(false) } @Test fun testUpdateAllowDownloadAccess_addProfiles_updateWithBadProfileId_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updateAllowDownloadAccess(profileId, false).toLiveData() - .observeForever(mockUpdateResultObserver) + val updateProvider = profileManagementController.updateAllowDownloadAccess(PROFILE_ID_6, false) testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateReadingTextSize_addProfiles_updateWithFontSize18_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateReadingTextSize(profileId, ReadingTextSize.MEDIUM_TEXT_SIZE) - .toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateReadingTextSize(PROFILE_ID_2, MEDIUM_TEXT_SIZE) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().readingTextSize) - .isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) } @Test fun testUpdateAppLanguage_addProfiles_updateWithChineseLanguage_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateAppLanguage(profileId, AppLanguage.CHINESE_APP_LANGUAGE) - .toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateAppLanguage(PROFILE_ID_2, CHINESE_APP_LANGUAGE) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().appLanguage) - .isEqualTo(AppLanguage.CHINESE_APP_LANGUAGE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.appLanguage).isEqualTo(CHINESE_APP_LANGUAGE) } @Test fun testUpdateAudioLanguage_addProfiles_updateWithFrenchLanguage_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController - .updateAudioLanguage(profileId, AudioLanguage.FRENCH_AUDIO_LANGUAGE).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, FRENCH_AUDIO_LANGUAGE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().audioLanguage) - .isEqualTo(AudioLanguage.FRENCH_AUDIO_LANGUAGE) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.audioLanguage).isEqualTo(FRENCH_AUDIO_LANGUAGE) } @Test fun testDeleteProfile_addProfiles_deleteProfile_checkDeletionIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.deleteProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val deleteProvider = profileManagementController.deleteProfile(PROFILE_ID_2) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) - assertThat(profileResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextSuccessfulResult(deleteProvider) + monitorFactory.waitForNextFailureResult(profileProvider) assertThat(File(getAbsoluteDirPath("2")).isDirectory).isFalse() } @Test fun testDeleteProfile_addProfiles_deleteProfiles_addProfile_checkIdIsNotReused() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId3 = ProfileId.newBuilder().setInternalId(3).build() - val profileId4 = ProfileId.newBuilder().setInternalId(4).build() - profileManagementController.deleteProfile(profileId3).toLiveData() - profileManagementController.deleteProfile(profileId4).toLiveData() - profileManagementController.addProfile( - name = "John", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + profileManagementController.deleteProfile(PROFILE_ID_3) + profileManagementController.deleteProfile(PROFILE_ID_4) + addAdminProfileAndWait(name = "John", pin = "321") - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profilesProvider = profileManagementController.getProfiles() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(4) @@ -502,21 +339,15 @@ class ProfileManagementControllerTest { @Test fun testDeleteProfile_addProfiles_deleteProfiles_restartApplication_checkDeletionIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - val profileId2 = ProfileId.newBuilder().setInternalId(2).build() - val profileId3 = ProfileId.newBuilder().setInternalId(3).build() - profileManagementController.deleteProfile(profileId1).toLiveData() - profileManagementController.deleteProfile(profileId2).toLiveData() - profileManagementController.deleteProfile(profileId3).toLiveData() + profileManagementController.deleteProfile(PROFILE_ID_1) + profileManagementController.deleteProfile(PROFILE_ID_2) + profileManagementController.deleteProfile(PROFILE_ID_3) testCoroutineDispatchers.runCurrent() setUpTestApplicationComponent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow() + val profilesProvider = profileManagementController.getProfiles() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles.size).isEqualTo(2) assertThat(profiles.first().name).isEqualTo("James") assertThat(profiles.last().name).isEqualTo("Veena") @@ -528,38 +359,25 @@ class ProfileManagementControllerTest { @Test fun testLoginToProfile_addProfiles_loginToProfile_checkGetProfileIdAndLoginTimestampIsCorrect() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.loginToProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val loginProvider = profileManagementController.loginToProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileManagementController.getCurrentProfileId().internalId) - .isEqualTo(2) - assertThat(profileResultCaptor.value.getOrThrow().lastLoggedInTimestampMs) - .isNotEqualTo(0) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(loginProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(2) + assertThat(profile.lastLoggedInTimestampMs).isNotEqualTo(0) } @Test fun testLoginToProfile_addProfiles_loginToProfileWithBadProfileId_checkLoginFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.loginToProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val loginProvider = profileManagementController.loginToProfile(PROFILE_ID_6) - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() + val error = monitorFactory.waitForNextFailureResult(loginProvider) + assertThat(error) + .hasMessageThat() .contains( "org.oppia.android.domain.profile.ProfileManagementController\$ProfileNotFoundException: " + "ProfileId 6 is not associated with an existing profile" @@ -568,439 +386,178 @@ class ProfileManagementControllerTest { @Test fun testWasProfileEverAdded_addAdminProfile_checkIfProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "123", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val addProvider = addAdminProfile(name = "James", pin = "123") - val profileDatabase = readProfileDatabase() + monitorFactory.waitForNextSuccessfulResult(addProvider) - verifyUpdateSucceeded() + val profileDatabase = readProfileDatabase() assertThat(profileDatabase.wasProfileEverAdded).isEqualTo(false) } @Test fun testWasProfileEverAdded_addAdminProfile_getWasProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) - testCoroutineDispatchers.runCurrent() + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() - verifyWasProfileEverAddedSucceeded() - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isFalse() } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_checkIfProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") val profileDatabase = readProfileDatabase() - verifyUpdateSucceeded() assertThat(profileDatabase.wasProfileEverAdded).isEqualTo(true) assertThat(profileDatabase.profilesMap.size).isEqualTo(2) } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_getWasProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() testCoroutineDispatchers.runCurrent() - verifyWasProfileEverAddedSucceeded() - - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isTrue() } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_deleteUserProfile_profileIsAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.deleteProfile(profileId1).toLiveData() + profileManagementController.deleteProfile(PROFILE_ID_1) testCoroutineDispatchers.runCurrent() val profileDatabase = readProfileDatabase() - - verifyUpdateSucceeded() assertThat(profileDatabase.profilesMap.size).isEqualTo(1) } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_deleteUserProfile_profileWasAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.deleteProfile(profileId1).toLiveData() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") + profileManagementController.deleteProfile(PROFILE_ID_1) testCoroutineDispatchers.runCurrent() - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() testCoroutineDispatchers.runCurrent() - verifyWasProfileEverAddedSucceeded() - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isTrue() } @Test fun testAddAdminProfile_addAnotherAdminProfile_checkSecondAdminProfileWasNotAdded() { - profileManagementController.addProfile( - name = "Rohit", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "Rohit") - profileManagementController.addProfile( - name = "Ben", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val addProfile2 = addAdminProfile(name = "Ben") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Profile cannot be an admin") + val error = monitorFactory.waitForNextFailureResult(addProfile2) + assertThat(error).hasMessageThat().contains("Profile cannot be an admin") } @Test fun testDeviceSettings_addAdminProfile_getDefaultDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - verifyGetDeviceSettingsSucceeded() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isFalse() assertThat(deviceSettings.automaticallyUpdateTopics).isFalse() } @Test fun testDeviceSettings_addAdminProfile_updateDeviceWifiSettings_getDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController.updateWifiPermissionDeviceSettings( - adminProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - verifyUpdateSucceeded() + val updateProvider = profileManagementController.updateWifiPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, + downloadAndUpdateOnWifiOnly = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider) - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - - verifyGetDeviceSettingsSucceeded() - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isTrue() assertThat(deviceSettings.automaticallyUpdateTopics).isFalse() } @Test fun testDeviceSettings_addAdminProfile_updateTopicsAutoDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController - .updateTopicAutomaticallyPermissionDeviceSettings( - adminProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - verifyUpdateSucceeded() - - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - verify( - mockDeviceSettingsObserver, - atLeastOnce() - ).onChanged(deviceSettingsResultCaptor.capture()) - assertThat(deviceSettingsResultCaptor.value.isSuccess()).isTrue() + val updateProvider = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, automaticallyUpdateTopics = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider) - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isFalse() assertThat(deviceSettings.automaticallyUpdateTopics).isTrue() } @Test fun testDeviceSettings_addAdminProfile_updateDeviceWifiSettings_andTopicDevSettings_succeeds() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController.updateWifiPermissionDeviceSettings( - adminProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateSucceeded() + addAdminProfileAndWait(name = "James") - profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( - adminProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateSucceeded() - - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - verifyGetDeviceSettingsSucceeded() + val updateProvider1 = + profileManagementController.updateWifiPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, downloadAndUpdateOnWifiOnly = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider1) + val updateProvider2 = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, automaticallyUpdateTopics = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider2) - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isTrue() assertThat(deviceSettings.automaticallyUpdateTopics).isTrue() } @Test fun testDeviceSettings_updateDeviceWifiSettings_fromUserProfile_isFailure() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateWifiPermissionDeviceSettings( + PROFILE_ID_1, downloadAndUpdateOnWifiOnly = true + ) - val userProfileId = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.updateWifiPermissionDeviceSettings( - userProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() + monitorFactory.waitForNextFailureResult(updateProvider) } @Test fun testDeviceSettings_updateTopicsAutomaticallyDeviceSettings_fromUserProfile_isFailure() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - val userProfileId = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( - userProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - } - - private fun verifyGetDeviceSettingsSucceeded() { - verify( - mockDeviceSettingsObserver, - atLeastOnce() - ).onChanged(deviceSettingsResultCaptor.capture()) - assertThat(deviceSettingsResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetProfileSucceeded() { - verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) - assertThat(profileResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetMultipleProfilesSucceeded() { - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyUpdateSucceeded() { - verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyUpdateFailed() { - verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isFailure()).isTrue() - } + val updateProvider = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + PROFILE_ID_1, automaticallyUpdateTopics = true + ) - private fun verifyWasProfileEverAddedSucceeded() { - verify( - mockWasProfileAddedResultObserver, - atLeastOnce() - ).onChanged(wasProfileAddedResultCaptor.capture()) - assertThat(wasProfileAddedResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextFailureResult(updateProvider) } private fun addTestProfiles() { - PROFILES_LIST.forEach { - profileManagementController.addProfile( - name = it.name, - pin = it.pin, - avatarImagePath = null, - allowDownloadAccess = it.allowDownloadAccess, - colorRgb = -10710042, - isAdmin = false - ).toLiveData() + val profileAdditionProviders = PROFILES_LIST.map { + addNonAdminProfile(it.name, pin = it.pin, allowDownloadAccess = it.allowDownloadAccess) } + profileAdditionProviders.forEach(monitorFactory::ensureDataProviderExecutes) } private fun checkTestProfilesArePresent(resultList: List) { @@ -1023,13 +580,52 @@ class ProfileManagementControllerTest { private fun readProfileDatabase(): ProfileDatabase { return FileInputStream( - File( - context.filesDir, - "profile_database.cache" - ) + File(context.filesDir, "profile_database.cache") ).use(ProfileDatabase::parseFrom) } + private fun addAdminProfile(name: String, pin: String = DEFAULT_PIN): DataProvider = + addProfile(name, pin, isAdmin = true) + + private fun addAdminProfileAndWait(name: String, pin: String = DEFAULT_PIN) { + monitorFactory.ensureDataProviderExecutes(addAdminProfile(name, pin)) + } + + private fun addNonAdminProfile( + name: String, + pin: String = DEFAULT_PIN, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB + ): DataProvider { + return addProfile( + name, pin, avatarImagePath = null, allowDownloadAccess, colorRgb, isAdmin = false + ) + } + + private fun addNonAdminProfileAndWait( + name: String, + pin: String = DEFAULT_PIN, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB + ) { + monitorFactory.ensureDataProviderExecutes( + addNonAdminProfile(name, pin, allowDownloadAccess, colorRgb) + ) + } + + private fun addProfile( + name: String, + pin: String = DEFAULT_PIN, + avatarImagePath: Uri? = null, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB, + isAdmin: Boolean + ): DataProvider { + return profileManagementController.addProfile( + name, pin, avatarImagePath, allowDownloadAccess, colorRgb, isAdmin + ) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index 5539f784c4f..b906664be8f 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.question import android.app.Application import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -16,15 +14,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.AnsweredQuestionOutcome import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.EphemeralState.StateTypeCase.COMPLETED_STATE @@ -73,8 +62,6 @@ import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -100,59 +87,15 @@ private const val TOLERANCE = 1e-5 @LooperMode(LooperMode.Mode.PAUSED) @Config(application = QuestionAssessmentProgressControllerTest.TestApplication::class) class QuestionAssessmentProgressControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() + @get:Rule val oppiaTestRule = OppiaTestRule() - @get:Rule - val oppiaTestRule = OppiaTestRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var questionTrainingController: QuestionTrainingController - - @Inject - lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockScoreAndMasteryLiveDataObserver: - Observer> - - @Mock - lateinit var mockAsyncNullableResultLiveDataObserver: Observer> - - @Mock - lateinit var mockAsyncAnswerOutcomeObserver: Observer> - - @Mock - lateinit var mockAsyncResultLiveDataObserver: Observer> - - @Captor - lateinit var asyncResultCaptor: ArgumentCaptor> - - @Captor - lateinit var performanceCalculationCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncNullableResultCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncAnswerOutcomeCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var questionTrainingController: QuestionTrainingController + @Inject lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var translationController: TranslationController private lateinit var profileId1: ProfileId @@ -294,18 +237,13 @@ class QuestionAssessmentProgressControllerTest { @Test fun testSubmitAnswer_beforePlaying_failsWithError() { setUpTestApplicationWithSeed(questionSeed = 0) - val result = + + val submitAnswerProvider = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) + val failure = monitorFactory.waitForNextFailureResult(submitAnswerProvider) + assertThat(failure) .hasMessageThat() .contains("Cannot submit an answer if a training session has not yet begun.") } @@ -316,17 +254,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -335,17 +266,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.feedback.html).contains("That's correct!") assertThat(answerOutcome.isCorrectAnswer).isTrue() } @@ -356,17 +280,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -375,17 +292,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.feedback.html).contains("Incorrect. Try again.") assertThat(answerOutcome.isCorrectAnswer).isFalse() } @@ -451,15 +361,10 @@ class QuestionAssessmentProgressControllerTest { fun testMoveToNext_beforePlaying_failsWithError() { setUpTestApplicationWithSeed(questionSeed = 0) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a next question if a training session has not begun.") } @@ -470,17 +375,11 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify that we can't move ahead since the current state isn't yet completed. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -492,15 +391,9 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitMultipleChoiceAnswer(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToQuestionResult) } @Test @@ -524,17 +417,11 @@ class QuestionAssessmentProgressControllerTest { moveToNextQuestion() // Try skipping past the current state. - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify we can't move ahead since the new state isn't yet completed. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -545,17 +432,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_01) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createTextInputAnswer("1/4")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createTextInputAnswer("1/4")) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isTrue() assertThat(answerOutcome.feedback.html).contains("That's correct!") } @@ -566,18 +446,11 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_01) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createTextInputAnswer("2/4")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createTextInputAnswer("2/4")) // Verify that the answer was wrong, and that there's no handler for it so the default outcome // is returned. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() } @@ -589,9 +462,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitNumericInputAnswerAndMoveToNextQuestion(3.0) - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(5.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(5.0)) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should stay pending, and the wrong answer should be @@ -613,9 +484,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitNumericInputAnswerAndMoveToNextQuestion(3.0) - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(4.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(4.0)) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should now be completed with the correct answer. @@ -635,17 +504,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(3.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(3.0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isTrue() assertThat(answerOutcome.feedback.html).contains("That's correct!") } @@ -656,17 +518,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() } @@ -696,17 +551,11 @@ class QuestionAssessmentProgressControllerTest { submitNumericInputAnswerAndMoveToNextQuestion(5.0) submitMultipleChoiceAnswerAndMoveToNextQuestion(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify we can't navigate past the last state of the training session. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -753,12 +602,10 @@ class QuestionAssessmentProgressControllerTest { submitNumericInputAnswerAndMoveToNextQuestion(5.0) submitMultipleChoiceAnswerAndMoveToNextQuestion(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) + questionAssessmentProgressController.moveToNextQuestion() testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") @@ -768,12 +615,10 @@ class QuestionAssessmentProgressControllerTest { fun testSubmitAnswer_beforePlaying_failsWithError_logsException() { setUpTestApplicationWithSeed(questionSeed = 0) - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception) .hasMessageThat() @@ -786,17 +631,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() @@ -831,7 +669,7 @@ class QuestionAssessmentProgressControllerTest { .isEqualTo(2) val hintAndSolution = currentQuestion.ephemeralState.state.interaction.getHint(0) assertThat(hintAndSolution.hintContent.html).contains("Hint text will appear here") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) @@ -849,7 +687,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) @@ -862,7 +700,9 @@ class QuestionAssessmentProgressControllerTest { val hintAndSolution = currentQuestion.ephemeralState.state.interaction.solution assertThat(hintAndSolution.correctAnswer.correctAnswer).contains("1/4") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) // Verify that the current state updates. Hint revealed is true. val updatedState = waitForGetCurrentQuestionSuccessfulLoad() @@ -1015,7 +855,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1078,7 +918,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1223,7 +1063,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1290,7 +1130,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1529,11 +1369,6 @@ class QuestionAssessmentProgressControllerTest { ) } - private fun subscribeToScoreAndMasteryCalculations(skillIdList: List) { - questionAssessmentProgressController.calculateScores(skillIdList).toLiveData() - .observeForever(mockScoreAndMasteryLiveDataObserver) - } - private fun startSuccessfulTrainingSession(skillIdList: List) { startSuccessfulTrainingSession(profileId1, skillIdList) } @@ -1648,7 +1483,7 @@ class QuestionAssessmentProgressControllerTest { } else if (index == 1) { assertThat(hint.hintContent.html).contains("

Second hint text will appear here

") } - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = index) ) } @@ -1664,7 +1499,7 @@ class QuestionAssessmentProgressControllerTest { private fun viewHintForQuestion2(ephemeralQuestion: EphemeralQuestion) { val hint = ephemeralQuestion.ephemeralState.state.interaction.getHint(0) assertThat(hint.hintContent.html).contains("

Hint text will appear here

") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) } @@ -1672,7 +1507,9 @@ class QuestionAssessmentProgressControllerTest { private fun viewSolutionForQuestion2(ephemeralQuestion: EphemeralQuestion) { val solution = ephemeralQuestion.ephemeralState.state.interaction.solution assertThat(solution.correctAnswer.correctAnswer).isEqualTo("3.0") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) } private fun submitCorrectAnswerForQuestion3(): EphemeralQuestion { @@ -1686,7 +1523,9 @@ class QuestionAssessmentProgressControllerTest { private fun viewSolutionForQuestion3(ephemeralQuestion: EphemeralQuestion) { val solution = ephemeralQuestion.ephemeralState.state.interaction.solution assertThat(solution.correctAnswer.correctAnswer).isEqualTo("1/2") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) } private fun submitCorrectAnswerForQuestion4(): EphemeralQuestion { @@ -1720,32 +1559,9 @@ class QuestionAssessmentProgressControllerTest { pendingState.helpIndex.isSolutionRevealed() private fun getExpectedGrade(skillIdList: List): UserAssessmentPerformance { - subscribeToScoreAndMasteryCalculations(skillIdList) - testCoroutineDispatchers.runCurrent() - verify( - mockScoreAndMasteryLiveDataObserver, - atLeastOnce() - ).onChanged(performanceCalculationCaptor.capture()) - return performanceCalculationCaptor.value.getOrThrow() - } - - /** - * Verifies that the specified live data provides at least one successful operation. This will - * change test-wide mock state, and synchronizes background execution. - */ - private fun verifyOperationSucceeds(liveData: LiveData>) { - reset(mockAsyncResultLiveDataObserver) - liveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - asyncResultCaptor.value.apply { - // This bit of conditional logic is used to add better error reporting when failures occur. - if (isFailure()) { - throw AssertionError("Operation failed", getErrorOrNull()) - } - assertThat(isSuccess()).isTrue() - } - reset(mockAsyncResultLiveDataObserver) + return monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.calculateScores(skillIdList) + ) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index f116d87232d..5b181a11894 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.question import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,16 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.classify.InteractionsModule import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule @@ -54,8 +45,6 @@ import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -70,35 +59,17 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [QuestionTrainingController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = QuestionTrainingControllerTest.TestApplication::class) class QuestionTrainingControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var questionTrainingController: QuestionTrainingController - - @Inject - lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCurrentQuestionLiveDataObserver: Observer> - - @Captor - lateinit var currentQuestionResultCaptor: ArgumentCaptor> - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var questionTrainingController: QuestionTrainingController + @Inject lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId1: ProfileId @@ -127,16 +98,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_1 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_1) } @Test @@ -159,16 +124,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_0 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_0) } @Test @@ -191,16 +150,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_3 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_3) } @Test diff --git a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt index 39f554a9034..08faa224e3b 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,30 +10,19 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.CacheAssetsLocally -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -49,41 +37,18 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [StoryProgressController]. */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = StoryProgressControllerTest.TestApplication::class) class StoryProgressControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressController: StoryProgressController - - @Inject - lateinit var profileTestHelper: ProfileTestHelper - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockRecordProgressObserver: Observer> - - @Captor - lateinit var recordProgressResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockRetrieveChapterPlayStateObserver: Observer> - - @Captor - lateinit var retrieveChapterPlayStateCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var storyProgressController: StoryProgressController + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId: ProfileId @@ -99,244 +64,228 @@ class StoryProgressControllerTest { @Test fun testStoryProgressController_recordCompletedChapter_isSuccessful() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordProvider = + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordProvider) } @Test fun testStoryProgressController_recordChapterAsInProgressSaved_isSuccessful() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordProvider) } @Test fun testStoryProgressController_chapterCompleted_markChapterAsSaved_playStateIsCompleted() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + ) - storyProgressController.recordChapterAsInProgressSaved( + val recordInProgressProvider = storyProgressController.recordChapterAsInProgressSaved( profileId, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0, fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.COMPLETED ) + + monitorFactory.waitForNextSuccessfulResult(recordInProgressProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.COMPLETED) } @Test fun testStoryProgressController_chapterNotStarted_markChapterAsSaved_playStateIsSaved() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_SAVED - ) + val recordCompletionProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordCompletionProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_SAVED) } @Test fun testStoryProgressController_markChapterAsNotSaved_markChapterAsSaved_playStateIsSaved() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_SAVED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) + + val progressSavedProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_SAVED) } @Test fun testStoryProgressController_recordChapterAsNotSaved_isSuccessful() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordNotSavedProvider) } @Test fun testStoryProgressController_chapterCompleted_markChapterAsNotSaved_playStateIsCompleted() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.COMPLETED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) + + val recordNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.COMPLETED) } @Test fun testStoryProgressController_chapterNotStarted_markChapterAsSaved_playStateIsNotSaved() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_NOT_SAVED - ) + val progressNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_NOT_SAVED) } @Test fun testStoryProgressController_markChapterAsSaved_markChapterAsNotSaved_playStateIsNotSaved() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_NOT_SAVED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) + + val progressNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_NOT_SAVED) } - private fun verifyChapterPlayStateIsCorrect( + private fun retrieveChapterPlayState( profileId: ProfileId, topicId: String, storyId: String, - explorationId: String, - chapterPlayState: ChapterPlayState - ) { - storyProgressController.retrieveChapterPlayStateByExplorationId( - profileId, - topicId, - storyId, - explorationId - ).toLiveData().observeForever(mockRetrieveChapterPlayStateObserver) - - testCoroutineDispatchers.runCurrent() - - verify(mockRetrieveChapterPlayStateObserver, atLeastOnce()) - .onChanged(retrieveChapterPlayStateCaptor.capture()) - - assertThat(retrieveChapterPlayStateCaptor.value.isSuccess()).isTrue() - assertThat(retrieveChapterPlayStateCaptor.value.getOrThrow()).isEqualTo(chapterPlayState) - } - - private fun verifyRecordProgressSucceeded() { - verify(mockRecordProgressObserver, atLeastOnce()) - .onChanged(recordProgressResultCaptor.capture()) - assertThat(recordProgressResultCaptor.value.isSuccess()).isTrue() - reset(mockRecordProgressObserver) + explorationId: String + ): ChapterPlayState { + val playStateProvider = + storyProgressController.retrieveChapterPlayStateByExplorationId( + profileId, topicId, storyId, explorationId + ) + return monitorFactory.waitForNextSuccessfulResult(playStateProvider) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt index 72329652946..4fa110258e9 100755 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -16,22 +15,11 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterSummary -import org.oppia.android.app.model.CompletedStoryList -import org.oppia.android.app.model.OngoingTopicList import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Question import org.oppia.android.app.model.StorySummary -import org.oppia.android.app.model.Topic import org.oppia.android.app.model.TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_IN_FUTURE import org.oppia.android.app.model.TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_NOW import org.oppia.android.app.model.WrittenTranslationLanguageSelection @@ -54,8 +42,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -80,73 +66,16 @@ private const val INVALID_TOPIC_ID_1 = "INVALID_TOPIC_ID_1" @LooperMode(LooperMode.Mode.PAUSED) @Config(application = TopicControllerTest.TestApplication::class) class TopicControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() + @get:Rule val oppiaTestRule = OppiaTestRule() - @get:Rule - val oppiaTestRule = OppiaTestRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var topicController: TopicController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockCompletedStoryListObserver: Observer> - - @Captor - lateinit var completedStoryListResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockOngoingTopicListObserver: Observer> - - @Captor - lateinit var ongoingTopicListResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockQuestionListObserver: Observer>> - - @Captor - lateinit var questionListResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockStorySummaryObserver: Observer> - - @Captor - lateinit var storySummaryResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockChapterSummaryObserver: Observer> - - @Captor - lateinit var chapterSummaryResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockTopicObserver: Observer> - - @Captor - lateinit var topicResultCaptor: ArgumentCaptor> - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var context: Context + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var topicController: TopicController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var translationController: TranslationController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId1: ProfileId private lateinit var profileId2: ProfileId @@ -161,76 +90,52 @@ class TopicControllerTest { @Test fun testRetrieveTopic_validSecondTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_1) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(TEST_TOPIC_ID_1) } @Test fun testRetrieveTopic_validSecondTopic_returnsTopicWithThumbnail() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_1) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test fun testRetrieveTopic_fractionsTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyCount).isEqualTo(1) } @Test fun testRetrieveTopic_fractionsTopic_hasCorrectDescription() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.description).contains("You'll often need to talk about") } @Test fun testRetrieveTopic_ratiosTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, RATIOS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, RATIOS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(topic.storyCount).isEqualTo(2) } @Test fun testRetrieveTopic_ratiosTopic_hasCorrectDescription() { - topicController.getTopic( - profileId1, RATIOS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, RATIOS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(topic.description).contains( "Many everyday problems involve thinking about proportions" @@ -239,227 +144,165 @@ class TopicControllerTest { @Test fun testRetrieveTopic_invalidTopic_returnsFailure() { - topicController.getTopic( - profileId1, INVALID_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, INVALID_TOPIC_ID_1) - verifyGetTopicFailed() + monitorFactory.waitForNextFailureResult(topicProvider) } @Test fun testRetrieveTopic_testTopic_published_returnsAsAvailable() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_0 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_0) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicPlayAvailability.availabilityCase).isEqualTo(AVAILABLE_TO_PLAY_NOW) } @Test fun testRetrieveTopic_testTopic_unpublished_returnsAsAvailableInFuture() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_2 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_2) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicPlayAvailability.availabilityCase).isEqualTo(AVAILABLE_TO_PLAY_IN_FUTURE) } @Test fun testRetrieveStory_validStory_isSuccessful() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val storyResult = storySummaryResultCaptor.value - assertThat(storyResult).isNotNull() - assertThat(storyResult!!.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(storyProvider) } @Test fun testRetrieveStory_validStory_returnsCorrectStory() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(TEST_STORY_ID_2) } @Test fun testRetrieveStory_validStory_returnsStoryWithName() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyName).isEqualTo("Other Interesting Story") } @Test fun testRetrieveStory_fractionsStory_returnsCorrectStory() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(FRACTIONS_STORY_ID_0) } @Test fun testRetrieveStory_fractionsStory_returnsStoryWithName() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyName).isEqualTo("Matthew Goes to the Bakery") } @Test fun testRetrieveStory_ratiosFirstStory_returnsCorrectStory() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(story.storyName).isEqualTo("Ratios: Part 1") } @Test fun testRetrieveStory_ratiosFirstStory_returnsStoryWithMultipleChapters() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() - assertThat(getExplorationIds(story)).containsExactly( - RATIOS_EXPLORATION_ID_0, - RATIOS_EXPLORATION_ID_1 - ).inOrder() + val expIds = getExplorationIds(monitorFactory.waitForNextSuccessfulResult(storyProvider)) + assertThat(expIds).containsExactly(RATIOS_EXPLORATION_ID_0, RATIOS_EXPLORATION_ID_1).inOrder() } @Test fun testRetrieveStory_ratiosSecondStory_returnsCorrectStory() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(RATIOS_STORY_ID_1) assertThat(story.storyName).isEqualTo("Ratios: Part 2") } @Test fun testRetrieveStory_ratiosSecondStory_returnsStoryWithMultipleChapters() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() - assertThat(getExplorationIds(story)).containsExactly( - RATIOS_EXPLORATION_ID_2, - RATIOS_EXPLORATION_ID_3 - ).inOrder() + val expIds = getExplorationIds(monitorFactory.waitForNextSuccessfulResult(storyProvider)) + assertThat(expIds).containsExactly(RATIOS_EXPLORATION_ID_2, RATIOS_EXPLORATION_ID_3).inOrder() } @Test fun testRetrieveStory_validStory_returnsStoryWithChapter() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(getExplorationIds(story)).containsExactly(TEST_EXPLORATION_ID_4) } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterName() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.getChapter(0).name).isEqualTo("Fifth Exploration") } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterSummary() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.getChapter(0).summary) .isEqualTo("Matthew learns about fractions.") } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterThumbnail() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) val chapter = story.getChapter(0) assertThat(chapter.chapterThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test fun testRetrieveStory_invalidStory_returnsFailure() { - topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1) - verifyGetStoryFailed() - assertThat(storySummaryResultCaptor.value!!.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(storyProvider) } @Test fun testRetrieveChapter_validChapter_returnsCorrectChapterSummary() { - topicController.retrieveChapter( - FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockChapterSummaryObserver) - testCoroutineDispatchers.runCurrent() + val chapterProvider = + topicController.retrieveChapter( + FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0 + ) - verifyRetrieveChapterSucceeded() - val chapterSummary = chapterSummaryResultCaptor.value.getOrThrow() + val chapterSummary = monitorFactory.waitForNextSuccessfulResult(chapterProvider) assertThat(chapterSummary.name).isEqualTo("What is a Fraction?") - assertThat(chapterSummary.summary) - .isEqualTo("Matthew learns about fractions.") + assertThat(chapterSummary.summary).isEqualTo("Matthew learns about fractions.") } @Test fun testRetrieveChapter_invalidChapter_returnsFailure() { - topicController.retrieveChapter( - FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, RATIOS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockChapterSummaryObserver) - testCoroutineDispatchers.runCurrent() - - verifyRetrieveChapterFailed() - assertThat(chapterSummaryResultCaptor.value.getErrorOrNull()).isInstanceOf( - TopicController.ChapterNotFoundException::class.java - ) + val chapterProvider = + topicController.retrieveChapter( + FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, RATIOS_EXPLORATION_ID_0 + ) + + val error = monitorFactory.waitForNextFailureResult(chapterProvider) + assertThat(error).isInstanceOf(TopicController.ChapterNotFoundException::class.java) } @Test @@ -710,29 +553,21 @@ class TopicControllerTest { @Test fun testRetrieveSubtopicTopic_validSubtopic_returnsSubtopicWithThumbnail() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.subtopicList[0].subtopicThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForSkillIds_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds( listOf(TEST_SKILL_ID_0, TEST_SKILL_ID_1) ) - questionsListProvider.toLiveData().observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(5) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -746,17 +581,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId0_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_0) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_0)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(4) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -770,17 +598,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId1_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_1) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_1)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(3) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -793,17 +614,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId2_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_2) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_2)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(4) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -817,17 +631,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForRatiosSkillId0_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(RATIOS_SKILL_ID_0) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(RATIOS_SKILL_ID_0)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(1) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -840,39 +647,27 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForInvalidSkillIds_returnsResultForValidSkillsOnly() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds( listOf(TEST_SKILL_ID_0, TEST_SKILL_ID_1, "NON_EXISTENT_SKILL_ID") ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(5) } @Test fun testGetTopic_invalidTopicId_getTopic_noResultFound() { - topicController.getTopic( - profileId1, INVALID_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, INVALID_TOPIC_ID_1) - verifyGetTopicFailed() + monitorFactory.waitForNextFailureResult(topicProvider) } @Test fun testGetTopic_validTopicId_withoutAnyProgress_getTopicSucceedsWithCorrectProgress() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.NOT_STARTED) @@ -886,13 +681,9 @@ class TopicControllerTest { fun testGetTopic_recordProgress_getTopic_correctProgressFound() { markFractionsStory0Chapter0AsCompleted() - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.COMPLETED) @@ -902,21 +693,17 @@ class TopicControllerTest { @Test fun testGetStory_invalidData_getStory_noResultFound() { - topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1) - verifyGetStoryFailed() + monitorFactory.waitForNextFailureResult(storyProvider) } @Test fun testGetStory_validData_withoutAnyProgress_getStorySucceedsWithCorrectProgress() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val storySummary = storySummaryResultCaptor.value.getOrThrow() + val storySummary = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(storySummary.storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(storySummary.chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.NOT_STARTED) @@ -930,13 +717,9 @@ class TopicControllerTest { fun testGetStory_recordProgress_getTopic_correctProgressFound() { markFractionsStory0Chapter0AsCompleted() - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.COMPLETED) @@ -946,13 +729,9 @@ class TopicControllerTest { @Test fun testOngoingTopicList_validData_withoutAnyProgress_ongoingTopicListIsEmpty() { - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -960,13 +739,9 @@ class TopicControllerTest { fun testOngoingTopicList_recordOneChapterCompleted_correctOngoingList() { markFractionsStory0Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(FRACTIONS_TOPIC_ID) } @@ -976,13 +751,9 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -993,13 +764,9 @@ class TopicControllerTest { markFractionsStory0Chapter1AsCompleted() markRatiosStory0Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) } @@ -1009,13 +776,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -1025,25 +788,18 @@ class TopicControllerTest { markRatiosStory0Chapter1AsCompleted() markRatiosStory1Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) } @Test fun testCompletedStoryList_validData_withoutAnyProgress_completedStoryListIsEmpty() { - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(0) } @@ -1051,12 +807,9 @@ class TopicControllerTest { fun testCompletedStoryList_recordOneChapterProgress_completedStoryListIsEmpty() { markFractionsStory0Chapter0AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(0) } @@ -1065,12 +818,9 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(1) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[0].topicId).isEqualTo(FRACTIONS_TOPIC_ID) @@ -1081,12 +831,10 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val storySummary = storySummaryResultCaptor.value.getOrThrow() + val storySummary = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(storySummary.chapterCount).isEqualTo(2) assertThat(storySummary.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) assertThat(storySummary.chapterList[1].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) @@ -1098,12 +846,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(1) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) @@ -1116,12 +861,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(2) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[1].storyId).isEqualTo(RATIOS_STORY_ID_0) @@ -1339,54 +1081,6 @@ class TopicControllerTest { monitorFactory.waitForNextSuccessfulResult(updateProvider) } - private fun verifyGetTopicSucceeded() { - verify(mockTopicObserver, atLeastOnce()).onChanged(topicResultCaptor.capture()) - assertThat(topicResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetTopicFailed() { - verify(mockTopicObserver, atLeastOnce()).onChanged(topicResultCaptor.capture()) - assertThat(topicResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyGetStorySucceeded() { - verify(mockStorySummaryObserver, atLeastOnce()).onChanged(storySummaryResultCaptor.capture()) - assertThat(storySummaryResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetStoryFailed() { - verify(mockStorySummaryObserver, atLeastOnce()).onChanged(storySummaryResultCaptor.capture()) - assertThat(storySummaryResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyRetrieveChapterSucceeded() { - verify(mockChapterSummaryObserver, atLeastOnce()) - .onChanged(chapterSummaryResultCaptor.capture()) - assertThat(chapterSummaryResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyRetrieveChapterFailed() { - verify(mockChapterSummaryObserver, atLeastOnce()) - .onChanged(chapterSummaryResultCaptor.capture()) - assertThat(chapterSummaryResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyGetOngoingTopicListSucceeded() { - verify( - mockOngoingTopicListObserver, - atLeastOnce() - ).onChanged(ongoingTopicListResultCaptor.capture()) - assertThat(ongoingTopicListResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetCompletedStoryListSucceeded() { - verify( - mockCompletedStoryListObserver, - atLeastOnce() - ).onChanged(completedStoryListResultCaptor.capture()) - assertThat(completedStoryListResultCaptor.value.isSuccess()).isTrue() - } - private fun getExplorationIds(story: StorySummary): List { return story.chapterList.map(ChapterSummary::getExplorationId) } diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt index 6e8ec92869b..6e7c0e81340 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,24 +10,16 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.PromotedActivityList import org.oppia.android.app.model.PromotedStory -import org.oppia.android.app.model.TopicList import org.oppia.android.app.model.TopicSummary import org.oppia.android.app.model.UpcomingTopic import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.story.StoryProgressTestHelper @@ -40,8 +31,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.gcsresource.DefaultResourceBucketName @@ -59,42 +48,18 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [TopicListController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = TopicListControllerTest.TestApplication::class) class TopicListControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var topicListController: TopicListController - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockTopicListObserver: Observer> - - @Mock - lateinit var mockPromotedActivityListObserver: Observer> - - @Captor - lateinit var topicListResultCaptor: ArgumentCaptor> - - @Captor - lateinit var promotedActivityListResultCaptor: - ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var topicListController: TopicListController + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId0: ProfileId @@ -108,31 +73,24 @@ class TopicListControllerTest { fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) } - private fun setUpTestApplicationComponent() { - ApplicationProvider.getApplicationContext().inject(this) - } - @Test fun testRetrieveTopicList_isSuccessful() { - val topicListLiveData = topicListController.getTopicList().toLiveData() - - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicListController.getTopicList() - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - val topicListResult = topicListResultCaptor.value - assertThat(topicListResult!!.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(topicListProvider) } @Test fun testRetrieveTopicList_providesListOfMultipleTopics() { val topicList = retrieveTopicList() + assertThat(topicList.topicSummaryCount).isGreaterThan(1) } @Test fun testRetrieveTopicList_firstTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val firstTopic = topicList.getTopicSummary(0) assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.name).isEqualTo("First Test Topic") @@ -141,6 +99,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_firstTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val firstTopic = topicList.getTopicSummary(0) assertThat(firstTopic.totalChapterCount).isEqualTo(2) } @@ -148,6 +107,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_secondTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val secondTopic = topicList.getTopicSummary(1) assertThat(secondTopic.topicId).isEqualTo(TEST_TOPIC_ID_1) assertThat(secondTopic.name).isEqualTo("Second Test Topic") @@ -156,6 +116,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_secondTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val secondTopic = topicList.getTopicSummary(1) assertThat(secondTopic.totalChapterCount).isEqualTo(1) } @@ -163,6 +124,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_fractionsTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val fractionsTopic = topicList.getTopicSummary(2) assertThat(fractionsTopic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(fractionsTopic.name).isEqualTo("Fractions") @@ -171,6 +133,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_fractionsTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val fractionsTopic = topicList.getTopicSummary(2) assertThat(fractionsTopic.totalChapterCount).isEqualTo(2) } @@ -178,6 +141,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_ratiosTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val ratiosTopic = topicList.getTopicSummary(3) assertThat(ratiosTopic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(ratiosTopic.name).isEqualTo("Ratios and Proportional Reasoning") @@ -186,32 +150,26 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_ratiosTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val ratiosTopic = topicList.getTopicSummary(3) assertThat(ratiosTopic.totalChapterCount).isEqualTo(4) } @Test fun testRetrieveTopicList_doesNotContainUnavailableTopic() { - val topicListLiveData = topicListController.getTopicList().toLiveData() - - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicList = retrieveTopicList() // Verify that the topic list does not contain a not-yet published topic (since it can't be // played by the user). - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - val topicList = topicListResultCaptor.value.getOrThrow() val topicIds = topicList.topicSummaryList.map(TopicSummary::getTopicId) assertThat(topicIds).doesNotContain(TEST_TOPIC_ID_2) } @Test fun testRetrievePromotedActivityList_defaultLesson_hasCorrectInfo() { - topicListController.getPromotedActivityList(profileId0).toLiveData() - .observeForever(mockPromotedActivityListObserver) - testCoroutineDispatchers.runCurrent() + val promotedActivityProvider = topicListController.getPromotedActivityList(profileId0) - verifyGetPromotedActivityListSucceeded() + monitorFactory.waitForNextSuccessfulResult(promotedActivityProvider) } @Test @@ -659,14 +617,6 @@ class TopicListControllerTest { ) } - private fun verifyGetPromotedActivityListSucceeded() { - verify( - mockPromotedActivityListObserver, - atLeastOnce() - ).onChanged(promotedActivityListResultCaptor.capture()) - assertThat(promotedActivityListResultCaptor.value.isSuccess()).isTrue() - } - private fun verifyPromotedStoryAsFirstTestTopicStory0Exploration0(promotedStory: PromotedStory) { assertThat(promotedStory.explorationId).isEqualTo(TEST_EXPLORATION_ID_2) assertThat(promotedStory.storyId).isEqualTo(TEST_STORY_ID_0) @@ -795,21 +745,17 @@ class TopicListControllerTest { assertThat(promotedStory.totalChapterCount).isEqualTo(2) } - private fun retrieveTopicList(): TopicList { - val topicListLiveData = topicListController.getTopicList().toLiveData() - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - return topicListResultCaptor.value.getOrThrow() - } + private fun retrieveTopicList() = + monitorFactory.waitForNextSuccessfulResult(topicListController.getTopicList()) private fun retrievePromotedActivityList(): PromotedActivityList { - testCoroutineDispatchers.runCurrent() - topicListController.getPromotedActivityList(profileId0).toLiveData() - .observeForever(mockPromotedActivityListObserver) - testCoroutineDispatchers.runCurrent() - verifyGetPromotedActivityListSucceeded() - return promotedActivityListResultCaptor.value.getOrThrow() + return monitorFactory.waitForNextSuccessfulResult( + topicListController.getPromotedActivityList(profileId0) + ) + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) } // TODO(#89): Move this to a common test application component. diff --git a/model/src/main/proto/exploration.proto b/model/src/main/proto/exploration.proto index 25294ddcaf9..463af6f4688 100644 --- a/model/src/main/proto/exploration.proto +++ b/model/src/main/proto/exploration.proto @@ -310,6 +310,9 @@ message AnswerAndResponse { // Oppia's response to the answer the learner submitted. SubtitledHtml feedback = 2; + + // Whether the answer was labelled by the creator as correct. + bool is_correct_answer = 3; } message AnswerOutcome { diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index adb016b4ac0..f8357268559 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -358,7 +358,6 @@ exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/L exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgress.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionConstantsProvider.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingConstantsProvider.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 469bf3439e5..b36528c4160 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -643,6 +643,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/RichTextVie exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TestImageLoaderModule.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TestLogReportingModule.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TextInputActionTestActivity.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/environment/TestEnvironmentConfig.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/GenericViewMatchers.kt" diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 8c05e6683c6..7eea5ba99e3 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -37,6 +37,7 @@ kt_android_library( "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", "//testing/src/main/java/org/oppia/android/testing/time:fake_oppia_clock", "//third_party:androidx_core_core-ktx", @@ -76,6 +77,8 @@ TEST_DEPS = [ "//domain", "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:retriever_test_module", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt new file mode 100644 index 00000000000..a4f3eeb8155 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt @@ -0,0 +1,222 @@ +package org.oppia.android.testing.data + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.ComparableSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.FloatSubject +import com.google.common.truth.IntegerSubject +import com.google.common.truth.IterableSubject +import com.google.common.truth.LongSubject +import com.google.common.truth.MapSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.ThrowableSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat +import com.google.protobuf.MessageLite +import org.oppia.android.util.data.AsyncResult + +// TODO(#4236): Add tests for this class. + +/** + * Truth subject for verifying properties of [AsyncResult]s. + * + * Call [assertThat] to create the subject. + */ +class AsyncResultSubject( + failureMetadata: FailureMetadata?, + @PublishedApi internal val actual: AsyncResult +) : Subject(failureMetadata, actual) { + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Pending]. */ + fun isPending() { + ensureActualIsType>() + } + + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Pending]. */ + fun isNotPending() { + ensureActualIsNotType>() + } + + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Success]. */ + fun isSuccess() { + ensureActualIsType>() + } + + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Success]. */ + fun isNotSuccess() { + ensureActualIsNotType>() + } + + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Failure]. */ + fun isFailure() { + ensureActualIsType>() + } + + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Failure]. */ + fun isNotFailure() { + ensureActualIsNotType>() + } + + /** + * Verifies that the [AsyncResult] under test is of type [AsyncResult.Success] and then calls + * [block] with the [AsyncResult.Success.value] result. + * + * Note that this does not perform type checking, so it's up to the caller to ensure that the [T] + * type used by the [AsyncResult] is correct. + */ + fun hasSuccessValueWhere(block: T.() -> Unit) = + ensureActualIsType>().value.block() + + /** + * Returns a [Subject] that can be used to perform additional assertions about the + * [AsyncResult.Success.value] of the result under test (this verifies that the result is a + * success, similar to [isSuccess]). + */ + fun isSuccessThat(): Subject = assertThat(ensureActualIsType>().value) + + /* NOTE TO DEVELOPERS: Add more subject types below, as needed. */ + + /** + * Returns a [ComparableSubject] of type [C] using the same considerations as [isSuccessThat], + * except this also verifies that the success value is a [Comparable] (though it can't verify + * [C] due to type erasure). + */ + inline fun > isComparableSuccessThat(): ComparableSubject = + assertThat(extractSuccessValue()) + + /** + * Returns a [StringSubject] using the same considerations as [isSuccessThat], except this also + * verifies that the successful value is a [String]. + */ + fun isStringSuccessThat(): StringSubject = assertThat(extractSuccessValue()) + + /** + * Returns a [BooleanSubject] for the success value (as a [Boolean] version of + * [isStringSuccessThat]). + */ + fun isBooleanSuccessThat(): BooleanSubject = assertThat(extractSuccessValue()) + + /** + * Returns an [IntegerSubject] for the success value (as an [Int] version of + * [isStringSuccessThat]). + */ + fun isIntSuccessThat(): IntegerSubject = assertThat(extractSuccessValue()) + + /** + * Returns a [LongSubject] for the success value (as a [Long] version of [isStringSuccessThat]). + */ + fun isLongSuccessThat(): LongSubject = assertThat(extractSuccessValue()) + + /** + * Returns a [FloatSubject] for the success value (as a [Float] version of [isStringSuccessThat]). + */ + fun isFloatSuccessThat(): FloatSubject = assertThat(extractSuccessValue()) + + /** + * Returns a [DoubleSubject] for the success value (as a [Double] version of + * [isStringSuccessThat]). + */ + fun isDoubleSuccessThat(): DoubleSubject = assertThat(extractSuccessValue()) + + /** + * Returns a [LiteProtoSubject] for the success value (as a [MessageLite] version of + * [isStringSuccessThat]). + */ + fun isProtoSuccessThat(): LiteProtoSubject = assertThat(extractSuccessValue()) + + /** + * Returns an [IterableSubject] for the success value (as an [Iterator] version of + * [isComparableSuccessThat], including the inability to verify [E]). + */ + fun isIterableSuccessThat(): IterableSubject = assertThat(extractSuccessValue>()) + + /** + * Returns a [MapSubject] for the success value (as a [Map] version of [isComparableSuccessThat], + * including the inability to verify [K] and [V]). + */ + fun asMapSuccessThat(): MapSubject = assertThat(extractSuccessValue>()) + + /** + * Returns a [ThrowableSubject] for the success value (as a [MessageLite] version of + * [isStringSuccessThat]). + */ + fun asThrowableSuccessThat(): ThrowableSubject = assertThat(extractSuccessValue()) + + /** + * Verifies that the result under test is a failure (similar to [isFailure]) and returns a + * [ThrowableSubject] to verify details about the [AsyncResult.Failure.error]. + */ + fun isFailureThat(): ThrowableSubject = + assertThat(ensureActualIsType>().error) + + /** + * Verifies that the result under test is newer or the same age as [other] (per + * [AsyncResult.isNewerThanOrSameAgeAs]). + */ + fun isNewerOrSameAgeAs(other: AsyncResult) { + assertThat(actual.isNewerThanOrSameAgeAs(other)).isTrue() + } + + /** + * Verifies that the result under test is older than [other] (per + * [AsyncResult.isNewerThanOrSameAgeAs]). + */ + fun isOlderThan(other: AsyncResult) { + assertThat(actual.isNewerThanOrSameAgeAs(other)).isFalse() + } + + /** + * Returns a [BooleanSubject] for verifying whether the [AsyncResult] under test and [other] + * effectively have the same value per [AsyncResult.hasSameEffectiveValueAs]. + */ + fun hasSameEffectiveValueAs(other: AsyncResult): BooleanSubject = + assertThat(actual.hasSameEffectiveValueAs(other)) + + /** + * Verifies the result under test is successful (per [ensureActualIsType]) and returns its + * [AsyncResult.Success.value] as type [T] (this method will fail if the conversion can't happen). + * + * Note that this is a [PublishedApi] method since it's referenced in functions inlined above, and + * should never be called outside this class. + */ + @PublishedApi // See: https://stackoverflow.com/a/41905907/3689782. + internal inline fun extractSuccessValue(): T { + return ensureActualIsType>().value.also { + assertThat(it).isInstanceOf(T::class.java) + } + } + + /** + * Verifies that the result under test is of type [T] (which can be useful when generally checking + * for pending, failure, or success results), failing if it isn't. + * + * Note that this is a [PublishedApi] method since it's referenced in functions inlined above, and + * should never be called outside this class. + */ + @PublishedApi + internal inline fun ensureActualIsType(): T { + assertThat(actual).isInstanceOf(T::class.java) + // This extra check is just to ensure Kotlin knows 'actual' is of type 'T'. + check(actual is T) { "Error: Truth didn't correctly catch mis-typing." } + return actual + } + + private inline fun ensureActualIsNotType() { + assertThat(actual).isNotInstanceOf(T::class.java) + } + + companion object { + /** + * Returns a new [AsyncResultSubject] to verify aspects of the specified [AsyncResult] value. + */ + fun assertThat(actual: AsyncResult): AsyncResultSubject { + return assertAbout( + Factory, AsyncResult>(::AsyncResultSubject) + ).that(actual) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel index d7f87e9869d..67f93f5d11b 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel @@ -6,6 +6,21 @@ Package for common test utilities corresponding to data processing & data provid load("@dagger//:workspace_defs.bzl", "dagger_rules") load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +kt_android_library( + name = "async_result_subject", + testonly = True, + srcs = [ + "AsyncResultSubject.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + "//third_party:com_google_protobuf_protobuf-javalite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/data:async_result", + ], +) + kt_android_library( name = "data_provider_test_monitor", testonly = True, @@ -15,6 +30,7 @@ kt_android_library( visibility = ["//:oppia_testing_visibility"], deps = [ ":dagger", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", "//third_party:androidx_test_runner", diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt index d2cecc3b1b1..c9dec47324f 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt @@ -9,15 +9,16 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.data.DataProviderTestMonitor.Factory import org.oppia.android.testing.mockito.anyOrNull import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import java.lang.IllegalStateException import javax.inject.Inject -// TODO(#3813): Migrate all data provider tests over to using this utility. /** * A test monitor for [DataProvider]s that provides operations to simplify waiting for the * provider's results, or to verify that notifications actually change the data provider when @@ -118,17 +119,24 @@ class DataProviderTestMonitor private constructor( } private fun retrieveSuccess(operation: () -> AsyncResult): T { - return operation().also { - // Sanity check. - check(it.isSuccess()) { "Expected next result to be a success, not: $it" } - }.getOrThrow() + return when (val result = operation()) { + // Sanity check. Ensure that the full failure stack trace is thrown. + is AsyncResult.Failure -> { + throw IllegalStateException( + "Expected next result to be a success, not: $result", result.error + ) + } + is AsyncResult.Pending -> error("Expected next result to be a success, not: $result") + is AsyncResult.Success -> result.value + } } private fun retrieveFailing(operation: () -> AsyncResult): Throwable { - return operation().also { - // Sanity check. - check(it.isFailure()) { "Expected next result to be a failure, not: $it" } - }.getErrorOrNull() ?: error("Expect result to have a failure error") + return when (val result = operation()) { + is AsyncResult.Failure -> result.error + is AsyncResult.Pending, is AsyncResult.Success -> + error("Expected next result to be a failure, not: $result") + } } /** @@ -148,6 +156,32 @@ class DataProviderTestMonitor private constructor( } } + /** + * Convenience method for verifying that [dataProvider] has at least one result (whether it be + * successful or an error), waiting if needed for the result (see [waitForNextResult]). + * + * This method ought to be used when data providers need to be processed mid-test since using + * [waitForNextSuccessfulResult] or [waitForNextFailureResult] have the disadvantages that they + * are also verifying pass/fail state (which is usually not desired mid-test during the + * arrangement and act portions). While this method is also verifying something (execution), it + * can be considered more of a sanity check than an actual check for correctness (i.e. "this + * data provider must have executed for the test to proceed"). + * + * Note that this will fail if the result of the data provider is pending (it must provide at + * least one success or failure). + */ + fun ensureDataProviderExecutes(dataProvider: DataProvider) { + // Waiting for a result is the same as ensuring the conditions are right for the provider to + // execute (since it must return a result if it's executed, even if it's pending). + val monitor = createMonitor(dataProvider) + monitor.waitForNextResult().also { + monitor.stopObservingDataProvider() + }.also { + // There must be an actual result for the provider to be successful. + assertThat(it).isNotPending() + } + } + /** * Convenience function for monitoring the specified data provider & waiting for its next result * (expected to be a success). See [waitForNextSuccessResult] for specifics. diff --git a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt index 1186150a18e..3fa1b289e88 100644 --- a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt +++ b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt @@ -13,9 +13,11 @@ import org.mockito.MockitoAnnotations import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.domain.topic.RATIOS_EXPLORATION_ID_0 +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.util.data.AsyncResult @@ -240,7 +242,7 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockExplorationCheckpointObserver, atLeastOnce()) .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() + assertThat(explorationCheckpointCaptor.value is AsyncResult.Success).isTrue() } } @@ -270,11 +272,9 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockExplorationCheckpointObserver, atLeastOnce()) .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isFailure()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + assertThat(explorationCheckpointCaptor.value) + .isFailureThat() + .isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } } @@ -322,7 +322,7 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockLiveDataObserver, atLeastOnce()) .onChanged(liveDataResultCaptor.capture()) - assertThat(liveDataResultCaptor.value.isSuccess()).isTrue() + assertThat(liveDataResultCaptor.value is AsyncResult.Success).isTrue() } } diff --git a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt index a5b1635949b..03db80e5ff7 100644 --- a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt +++ b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt @@ -929,7 +929,7 @@ class StoryProgressTestHelper @Inject constructor( // Verify that the observer was called, and that the result was successful. InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockLiveDataObserver, atLeastOnce()).onChanged(liveDataResultCaptor.capture()) - assertThat(liveDataResultCaptor.value.isSuccess()).isTrue() + assertThat(liveDataResultCaptor.value is AsyncResult.Success).isTrue() } } diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index bfc5a1ab0ac..2152aed1832 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -18,6 +18,7 @@ oppia_android_test( "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", "//model/src/main/proto:languages_java_proto_lite", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", diff --git a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt index 32984b7ef24..6335e207172 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt @@ -9,6 +9,7 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import kotlinx.coroutines.delay import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -16,6 +17,7 @@ import org.mockito.exceptions.verification.NeverWantedButInvoked import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule @@ -43,17 +45,10 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(application = DataProviderTestMonitorTest.TestApplication::class) class DataProviderTestMonitorTest { - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var asyncDataSubscriptionManager: AsyncDataSubscriptionManager - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var asyncDataSubscriptionManager: AsyncDataSubscriptionManager + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @Before fun setUp() { @@ -94,46 +89,44 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextResult_pendingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isPending()).isTrue() + assertThat(result).isPending() } @Test fun testWaitForNextResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isFailure()).isTrue() - assertThat(result.getErrorOrNull()).hasMessageThat().contains("Failure") + assertThat(result).isFailureThat().hasMessageThat().contains("Failure") } @Test fun testWaitForNextResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("str value") + assertThat(result).isStringSuccessThat().isEqualTo("str value") } @Test fun testWaitForNextResult_failureThenSuccess_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -142,15 +135,14 @@ class DataProviderTestMonitorTest { asyncDataSubscriptionManager.notifyChangeAsync("test") val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("str value") + assertThat(result).isStringSuccessThat().isEqualTo("str value") } @Test fun testWaitForNextResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() @@ -158,8 +150,7 @@ class DataProviderTestMonitorTest { asyncDataSubscriptionManager.notifyChangeAsync("test") val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("second") + assertThat(result).isStringSuccessThat().isEqualTo("second") } /* Tests for waitForNextSuccessResult */ @@ -167,7 +158,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -179,7 +170,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -191,7 +182,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -204,7 +195,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_failureThenSuccess_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -219,7 +210,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -234,7 +225,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -250,7 +241,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -261,7 +252,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_successfulDataProvider_wait_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -274,7 +265,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_pendingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -287,7 +278,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_failingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -301,7 +292,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -316,7 +307,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -332,7 +323,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_successThenFailure_notified_wait_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -348,7 +339,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_differentValues_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -365,7 +356,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -377,7 +368,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -389,7 +380,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -402,7 +393,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_successThenFailure_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -417,7 +408,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -432,7 +423,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -448,7 +439,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -459,7 +450,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_failingDataProvider_wait_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -472,7 +463,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_pendingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -485,7 +476,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_successfulDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -499,7 +490,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -513,7 +504,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_successThenFailure_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -529,7 +520,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_failureThenSuccess_notified_wait_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -545,7 +536,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_differentValues_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -562,7 +553,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -573,7 +564,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -584,7 +575,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -596,7 +587,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -610,7 +601,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -626,7 +617,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_differentValues_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -641,7 +632,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_waitForSuccess_noChanges_doesNotThrowException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextSuccessResult() @@ -652,12 +643,125 @@ class DataProviderTestMonitorTest { // retrieved. } + /* Tests for ensureDataProviderExecutes */ + + @Test + fun testEnsureDataProviderExecutes_pendingDataProvider_throwsException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Pending() + } + + val failure = + assertThrows(AssertionError::class) { + monitorFactory.ensureDataProviderExecutes(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("not to be an instance of") + assertThat(failure).hasMessageThat().contains("Pending") + } + + @Test + fun testEnsureDataProviderExecutes_unfinishedDataProvider_throwsException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + delay(1000L) + AsyncResult.Success("str value") + } + + val failure = + assertThrows(AssertionError::class) { + monitorFactory.ensureDataProviderExecutes(dataProvider) + } + + // The result will fail since the provider never even provided a result (since it required + // advancing the test clock before a result would be available). + assertThat(failure).hasMessageThat().contains("Wanted but not invoked") + } + + @Test + fun testEnsureDataProviderExecutes_failingDataProvider_doesNotThrowException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Failure(Exception("Failure")) + } + + val failure = assertThrows(IllegalStateException::class) { + monitorFactory.waitForNextSuccessfulResult(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("Expected next result to be a success") + } + + @Test + fun testEnsureDataProviderExecutes_successfulDataProvider_doesNotThrowException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Success("str value") + } + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("str value") + } + + @Test + fun testEnsureDataProviderExecutes_failureThenSuccess_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") + ) + monitorFactory.waitForNextFailureResult(dataProvider) + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("str value") + } + + @Test + fun testEnsureDataProviderExecutes_successThenFailure_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) + ) + monitorFactory.waitForNextSuccessfulResult(dataProvider) + + val failure = assertThrows(IllegalStateException::class) { + monitorFactory.waitForNextSuccessfulResult(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("Expected next result to be a success") + } + + @Test + fun testEnsureDataProviderExecutes_differentValues_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("first"), AsyncResult.Success("second") + ) + monitorFactory.waitForNextSuccessfulResult(dataProvider) + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("second") + } + + @Test + fun testEnsureDataProviderExecutes_twiceForChangedProvider_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("first"), AsyncResult.Success("second") + ) + + val firstResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) + val secondResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(firstResult).isEqualTo("first") + assertThat(secondResult).isEqualTo("second") + } + /* Tests for waitForNextSuccessfulResult */ @Test fun testWaitForNextSuccessfulResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val failure = @@ -671,7 +775,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessfulResult_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val failure = assertThrows(IllegalStateException::class) { @@ -684,7 +788,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessfulResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -696,7 +800,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_failureThenSuccess_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -709,7 +813,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_successThenFailure_consumed_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -724,7 +828,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_differentValues_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -737,7 +841,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_twiceForChangedProvider_returnsCorrectValues() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val firstResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -752,7 +856,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val failure = assertThrows(IllegalStateException::class) { @@ -765,7 +869,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val result = monitorFactory.waitForNextFailureResult(dataProvider) @@ -776,7 +880,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val failure = assertThrows(IllegalStateException::class) { @@ -790,7 +894,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_successThenFailure_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -803,7 +907,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_failureThenSuccess_consumed_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -818,7 +922,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_differentValues_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -831,7 +935,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_twiceForChangedProvider_returnsCorrectValues() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val firstResult = monitorFactory.waitForNextFailureResult(dataProvider) diff --git a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt index 603194058ba..799be06163f 100644 --- a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.testing.lightweightcheckpointing import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,17 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController @@ -31,6 +21,7 @@ import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.domain.topic.RATIOS_EXPLORATION_ID_0 import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -41,8 +32,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -57,34 +46,18 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [ExplorationCheckpointTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationCheckpointTestHelperTest.TestApplication::class) class ExplorationCheckpointTestHelperTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val profileId = ProfileId.newBuilder().setInternalId(0).build() @@ -105,12 +78,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) } @Test @@ -120,24 +92,22 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) explorationCheckpointTestHelper.updateCheckpointForFractionsStory0Exploration0( profileId = profileId, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint1.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } @Test @@ -147,12 +117,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME) } @Test @@ -162,24 +131,24 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo( + FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME + ) explorationCheckpointTestHelper.updateCheckpointForFractionsStory0Exploration1( profileId = profileId, version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION, ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint1.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_SECOND_STATE_NAME) } @Test @@ -189,12 +158,10 @@ class ExplorationCheckpointTestHelperTest { version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName).isEqualTo(RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) } @Test @@ -204,49 +171,30 @@ class ExplorationCheckpointTestHelperTest { version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName).isEqualTo(RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) explorationCheckpointTestHelper.updateCheckpointForRatiosStory0Exploration0( profileId = profileId, version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint1.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(RATIOS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } - private fun verifySavedCheckpointHasCorrectExplorationDetails( + private fun retrieveCheckpoint( profileId: ProfileId, - explorationId: String, - explorationTitle: String, - pendingStateName: String - ) { - reset(mockExplorationCheckpointObserver) - val retrieveFakeCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - retrieveFakeCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - // Verify saved checkpoint has correct exploration title and pending state name. - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - assertThat(explorationCheckpointCaptor.value.getOrThrow().explorationTitle) - .isEqualTo(explorationTitle) - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingStateName) - .isEqualTo(pendingStateName) + explorationId: String + ): ExplorationCheckpoint { + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) + return monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) } // TODO(#89): Move this to a common test application component. diff --git a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt index e831ced54df..163b3bf3909 100644 --- a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt @@ -21,16 +21,16 @@ import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.Profile import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -44,38 +44,24 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -/** Tests for [ProfileTestHelperTest]. */ +/** Tests for [ProfileTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileTestHelperTest.TestApplication::class) class ProfileTestHelperTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() + @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule() - @Inject - lateinit var context: Context + @Inject lateinit var context: Context + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var profileManagementController: ProfileManagementController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject - lateinit var profileTestHelper: ProfileTestHelper + @Mock lateinit var mockUpdateResultObserver: Observer> - @Inject - lateinit var profileManagementController: ProfileManagementController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockProfilesObserver: Observer>> - - @Captor - lateinit var profilesResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockUpdateResultObserver: Observer> - - @Captor - lateinit var updateResultCaptor: ArgumentCaptor> + @Captor lateinit var updateResultCaptor: ArgumentCaptor> @Before fun setUp() { @@ -89,14 +75,12 @@ class ProfileTestHelperTest { @Test fun testInitializeProfiles_initializeProfiles_checkProfilesAreAddedAndCurrentIsSet() { profileTestHelper.initializeProfiles().observeForever(mockUpdateResultObserver) - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - val profiles = profilesResultCaptor.value.getOrThrow() + assertThat(updateResultCaptor.value).isSuccess() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles[0].name).isEqualTo("Admin") assertThat(profiles[0].isAdmin).isTrue() assertThat(profiles[1].name).isEqualTo("Ben") @@ -107,14 +91,12 @@ class ProfileTestHelperTest { @Test fun testInitializeProfiles_addOnlyAdminProfile_checkProfileIsAddedAndCurrentIsSet() { profileTestHelper.addOnlyAdminProfile().observeForever(mockUpdateResultObserver) - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - val profiles = profilesResultCaptor.value.getOrThrow() + assertThat(updateResultCaptor.value).isSuccess() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles.size).isEqualTo(1) assertThat(profiles[0].name).isEqualTo("Admin") assertThat(profiles[0].isAdmin).isTrue() @@ -125,12 +107,11 @@ class ProfileTestHelperTest { fun testAddMoreProfiles_addMoreProfiles_checkProfilesAreAdded() { profileTestHelper.addMoreProfiles(10) testCoroutineDispatchers.runCurrent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(profilesResultCaptor.value.getOrThrow().size).isEqualTo(10) + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) + assertThat(profiles).hasSize(10) } @Test @@ -141,7 +122,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(0) } @@ -153,7 +134,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(1) } @@ -165,7 +146,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(2) } diff --git a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt index 10464c26f96..6831d7b2254 100644 --- a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.testing.story import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,17 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterProgress import org.oppia.android.app.model.ChapterSummary @@ -53,6 +43,7 @@ import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_1 import org.oppia.android.domain.topic.TopicController import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -61,9 +52,6 @@ import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.LoadLessonProtosFromAssets -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProvider -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -77,44 +65,19 @@ import javax.inject.Inject import javax.inject.Singleton /** Tests for [StoryProgressTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = StoryProgressTestHelperTest.TestApplication::class) class StoryProgressTestHelperTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var topicController: TopicController - - @Inject - lateinit var persistentCacheStoreFactory: PersistentCacheStore.Factory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockTopicObserver: Observer> - - @Captor - lateinit var topicResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockTopicProgressDatabaseObserver: Observer> - - @Captor - lateinit var topicProgressDatabaseResultCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var topicController: TopicController + @Inject lateinit var persistentCacheStoreFactory: PersistentCacheStore.Factory + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val profileId0: ProfileId by lazy { ProfileId.newBuilder().setInternalId(0).build() } private val profileId1: ProfileId by lazy { ProfileId.newBuilder().setInternalId(1).build() } @@ -1652,11 +1615,8 @@ class StoryProgressTestHelperTest { assertThat(exp2.isStartedNotCompleted()).isFalse() } - private fun getTopic(profileId: ProfileId, topicId: String): Topic { - return retrieveSuccessfulResult( - topicController.getTopic(profileId, topicId), mockTopicObserver, topicResultCaptor - ) - } + private fun getTopic(profileId: ProfileId, topicId: String): Topic = + monitorFactory.waitForNextSuccessfulResult(topicController.getTopic(profileId, topicId)) private fun Topic.getStory(storyId: String): StorySummary { return storyList.find { it.storyId == storyId } ?: error("Failed to find story: $storyId") @@ -1664,8 +1624,6 @@ class StoryProgressTestHelperTest { private fun Topic.isNotStarted(): Boolean = storyList.all { it.isNotStarted() } - private fun Topic.isStartedNotCompleted(): Boolean = storyList.any { it.isStartedNotCompleted() } - private fun Topic.isInProgressSaved(): Boolean = storyList.any { it.isInProgressSaved() } private fun Topic.isInProgressNotSaved(): Boolean = storyList.any { it.isInProgressNotSaved() } @@ -1680,9 +1638,6 @@ class StoryProgressTestHelperTest { private fun StorySummary.isNotStarted(): Boolean = chapterList.all { it.isNotStarted() } - private fun StorySummary.isStartedNotCompleted(): Boolean = - chapterList.any { it.isStartedNotCompleted() } - private fun StorySummary.isInProgressSaved(): Boolean = chapterList.any { it.isInProgressSaved() } private fun StorySummary.isInProgressNotSaved(): Boolean = @@ -1714,9 +1669,7 @@ class StoryProgressTestHelperTest { TopicProgressDatabase.getDefaultInstance(), profileId ) - return retrieveSuccessfulResult( - persistentCacheStore, mockTopicProgressDatabaseObserver, topicProgressDatabaseResultCaptor - ) + return monitorFactory.waitForNextSuccessfulResult(persistentCacheStore) } private fun TopicProgressDatabase.getTopicProgress(topicId: String): TopicProgress { @@ -1731,25 +1684,6 @@ class StoryProgressTestHelperTest { return chapterProgressMap[expId] ?: error("Failed to get progress for chapter: $expId") } - private fun retrieveSuccessfulResult( - dataProvider: DataProvider, - mockObserver: Observer>, - mockResultCaptor: ArgumentCaptor> - ): T { - val requestLiveData = dataProvider.toLiveData() - reset(mockObserver) - requestLiveData.observeForever(mockObserver) - - // Provide time for the topic retrieval to complete. - testCoroutineDispatchers.runCurrent() - - verify(mockObserver, atLeastOnce()).onChanged(mockResultCaptor.capture()) - requestLiveData.removeObserver(mockObserver) - val result = mockResultCaptor.value - assertThat(result.isSuccess()).isTrue() - return result.getOrThrow() - } - // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt index 68e6f0b6188..2e158ee93a1 100644 --- a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt @@ -30,6 +30,7 @@ import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.testing.time.FakeSystemClock @@ -339,20 +340,19 @@ class CoroutineExecutorServiceTest { val getResult = testDispatcherScope.async { try { - AsyncResult.success(callableFuture.get(/* timeout= */ 1, TimeUnit.SECONDS)) + AsyncResult.Success(callableFuture.get(/* timeout= */ 1, TimeUnit.SECONDS)) } catch (e: ExecutionException) { - AsyncResult.failed(e) + AsyncResult.Failure(e) } } testDispatcher.runUntilIdle() // The getter should return since the task has finished. assertThat(getResult.isCompleted).isTrue() - assertThat(getResult.getCompleted().isFailure()).isTrue() - assertThat(getResult.getCompleted().getErrorOrNull()) - .isInstanceOf(ExecutionException::class.java) - assertThat(getResult.getCompleted().getErrorOrNull()?.cause) - .isInstanceOf(TimeoutException::class.java) + assertThat(getResult.getCompleted()).isFailureThat().apply { + isInstanceOf(ExecutionException::class.java) + hasCauseThat().isInstanceOf(TimeoutException::class.java) + } } @Test diff --git a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt index 7889986d6a1..1cdc6230dd5 100644 --- a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt +++ b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt @@ -2,88 +2,105 @@ package org.oppia.android.util.data import android.os.SystemClock -/** Represents the result from a single asynchronous function. */ -class AsyncResult private constructor( - private val status: Status, - private val resultTimeMillis: Long, - private val value: T? = null, - private val error: Throwable? = null -) { - /** Represents the status of an asynchronous result. */ - enum class Status { - /** Indicates that the asynchronous operation is not yet completed. */ - PENDING, - - /** Indicates that the asynchronous operation completed successfully and has a result. */ - SUCCEEDED, - - /** Indicates that the asynchronous operation failed and has an error. */ - FAILED - } - - /** Returns whether this result is still pending. */ - fun isPending(): Boolean { - return status == Status.PENDING - } - - /** Returns whether this result has completed successfully. */ - fun isSuccess(): Boolean { - return status == Status.SUCCEEDED - } - - /** Returns whether this result has completed unsuccessfully. */ - fun isFailure(): Boolean { - return status == Status.FAILED - } - - /** Returns whether this result has completed (successfully or unsuccessfully). */ - fun isCompleted(): Boolean { - return isSuccess() || isFailure() - } +/** + * Represents the result from a single asynchronous function. + * + * [AsyncResult]s exist in one of three states: + * - [Pending] to indicate an operation that hasn't yet completed + * - [Success] to indicate an operation that finished as expected + * - [Failure] to indicate an operation that finished in an unexpected way + * + * Since this is a sealed class, each type of result can be created by constructing one of the types + * listed above. Results can also leverage Kotlin's exhaustive ``when`` statements when checking for + * the type of results received: + * + * ```kotlin + * when (result) { + * is AsyncResult.Pending -> { /* Show that the operation is pending. */ } + * is AsyncResult.Success -> { /* Do something with result.value. */ } + * is AsyncResult.Failure -> { /* Do something with result.error. */ } + * } + * ``` + * + * Note that the above is the suggested way to always check for a result type since it ensures that + * all possibilities are always considered, and can help minimize bugs. + * + * **A note on immutability:** This class is inherently immutable, though it may contain types which + * are not (based on [T]). It's **strongly** suggested to only ever use this class with immutable + * [T] types since the app's entire multithreading environment assumes this class to be safe to pass + * between threads and coroutines. + * + * **A note on pending:** While [AsyncResult.Pending] exists, some [DataProvider]s may instead elect + * to simply not provide a result until one is available. Both are acceptable results so long as the + * UI knows how to react (i.e. it's the difference between the UI showing a loading indicator upon + * initiating the operation and stopping it when a success/error is received, versus initiating the + * loading indicator only after a pending result is received). It's generally recommended that data + * providers always provide a pending result by default, but it may lead to a better user experience + * to utilize it as a signal that a long operation is underway). This API may be changed in the + * future to make these design choices more clear-cut and deliberate when implementing data + * providers. + * + * **A note on equality:** Two [AsyncResult]s may not be equal or have the same hash code if they + * are different ages as indicated by [resultTimeMillis]. For this reason, care must be taken when + * storing results in hash or equivalence based data structures. When comparing two results where + * the results' ages should be ignored, one can use [hasSameEffectiveValueAs] instead of direct + * equality checking. + */ +sealed class AsyncResult { + /** + * The timestamp (in millis) of when this result was created. + * + * This value should only be used to compared results created in the same process, and should + * never be persisted or transferred across process boundaries. This value should be stable across + * changes to the user device's system clock or calendar settings. + */ + protected abstract val resultTimeMillis: Long - /** Returns whether this result is newer than, or the same age as, the specified result of the same type. */ + /** + * Returns whether this result is newer than, or the same age as, the specified result of the same + * type. + */ fun isNewerThanOrSameAgeAs(otherResult: AsyncResult): Boolean { return resultTimeMillis >= otherResult.resultTimeMillis } - /** Returns the value of the result if it succeeded, otherwise the specified default value. */ - fun getOrDefault(defaultValue: T): T { - return if (isSuccess()) value!! else defaultValue - } - /** - * Returns the value of the result if it succeeded, otherwise throws the underlying exception. Throws if this result - * is not yet completed. + * Returns whether this result has the same effective value as [other], that is, that the two can + * be considered equal in value but not necessarily age. + * + * This function is useful for cases when the end effect of processing an [AsyncResult] is not + * dependent on the age of the result (indicated by [resultTimeMillis]) which may affect functions + * like [equals] and [hashCode]. */ - fun getOrThrow(): T { - check(isCompleted()) { "Result is not yet completed." } - if (isSuccess()) return value!! else throw error!! - } - - /** Returns the underlying exception if this result failed, otherwise null. */ - fun getErrorOrNull(): Throwable? { - return if (isFailure()) error else null + fun hasSameEffectiveValueAs(other: AsyncResult): Boolean { + return when (val thisResult = this) { + is Pending -> other is Pending // Two pending results are effectively equal. + is Success -> if (other is Success) thisResult.isEffectivelyEqualTo(other) else false + is Failure -> if (other is Failure) thisResult.isEffectivelyEqualTo(other) else false + } } /** - * Returns a version of this result that retains its pending and failed states, but transforms its success state - * according to the specified transformation function. + * Returns a version of this result that retains its pending and failed states, but transforms its + * success state according to the specified transformation function. * - * Note that if the current result is a failure, the transformed result's failure will be a chained exception with - * this result's failure as the root cause to preserve this transformation in the exception's stacktrace. + * Note that if the current result is a failure, the transformed result's failure will be a + * chained exception with this result's failure as the root cause to preserve this transformation + * in the exception's stacktrace. * - * Note also that the specified transformation function should have no side effects, and be non-blocking. + * Note also that the specified transformation function should have no side effects, and be + * non-blocking. */ fun transform(transformFunction: (T) -> O): AsyncResult { - return transformWithResult { value -> - success(transformFunction(value)) - } + return transformWithResult { value -> Success(transformFunction(value)) } } /** - * Returns a transformed version of this result in the same way as [transform] except it supports using a blocking - * transformation function instead of a non-blocking one. Note that the transform function is only used if the current - * result is a success, at which case the function's result becomes the new, transformed result. + * Returns a transformed version of this result in the same way as [transform] except it supports + * using a blocking transformation function instead of a non-blocking one. + * + * Note that the transform function is only used if the current result is a success, at which case + * the function's result becomes the new, transformed result. */ suspend fun transformAsync(transformFunction: suspend (T) -> AsyncResult): AsyncResult { return transformWithResultAsync { value -> @@ -92,14 +109,17 @@ class AsyncResult private constructor( } /** - * Returns a version of this result that retains its pending and failed states, but combines its success state with - * the success state of another result, according to the specified combine function. + * Returns a version of this result that retains its pending and failed states, but combines its + * success state with the success state of another result, according to the specified combine + * function. * - * Note that if the other result is either pending or failed, that pending or failed state will be propagated to the - * returned result rather than attempting to combine the two states. Only successful states are combined. + * Note that if the other result is either pending or failed, that pending or failed state will be + * propagated to the returned result rather than attempting to combine the two states. Only + * successful states are combined. * - * Note that if the current result is a failure, the transformed result's failure will be a chained exception with - * this result's failure as the root cause to preserve this combination in the exception's stacktrace. + * Note that if the current result is a failure, the transformed result's failure will be a + * chained exception with this result's failure as the root cause to preserve this combination in + * the exception's stacktrace. * * Note also that the specified combine function should have no side effects, and be non-blocking. */ @@ -108,16 +128,17 @@ class AsyncResult private constructor( combineFunction: (T, T2) -> O ): AsyncResult { return transformWithResult { value1 -> - otherResult.transformWithResult { value2 -> - success(combineFunction(value1, value2)) - } + otherResult.transformWithResult { value2 -> Success(combineFunction(value1, value2)) } } } /** - * Returns a version of this result that is combined with another result in the same way as [combineWith], except it - * supports using a blocking combine function instead of a non-blocking one. Note that the combine function is only - * used if both results are a success, at which case the function's result becomes the new, combined result. + * Returns a version of this result that is combined with another result in the same way as + * [combineWith], except it supports using a blocking combine function instead of a non-blocking + * one. + * + * Note that the combine function is only used if both results are a success, at which case the + * function's result becomes the new, combined result. */ suspend fun combineWithAsync( otherResult: AsyncResult, @@ -131,75 +152,54 @@ class AsyncResult private constructor( } private fun transformWithResult(transformFunction: (T) -> AsyncResult): AsyncResult { - return when (status) { - Status.PENDING -> pending() - Status.FAILED -> failed(ChainedFailureException(error!!)) - Status.SUCCEEDED -> transformFunction(value!!) + return when (this) { + is Pending -> Pending() + is Success -> transformFunction(value) + is Failure -> Failure(ChainedFailureException(error)) } } private suspend fun transformWithResultAsync( transformFunction: suspend (T) -> AsyncResult ): AsyncResult { - return when (status) { - Status.PENDING -> pending() - Status.FAILED -> failed(ChainedFailureException(error!!)) - Status.SUCCEEDED -> transformFunction(value!!) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (other == null || other.javaClass != javaClass) { - return false - } - val otherResult = other as AsyncResult<*> - return otherResult.status == status && otherResult.error == error && otherResult.value == value - } - - override fun hashCode(): Int { - // Automatically generated hashCode() function that has parity with equals(). - var result = status.hashCode() - result = 31 * result + (value?.hashCode() ?: 0) - result = 31 * result + (error?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return when (status) { - Status.PENDING -> "AsyncResult[status=PENDING]" - Status.FAILED -> "AsyncResult[status=FAILED, error=$error]" - Status.SUCCEEDED -> "AsyncResult[status=SUCCESS, value=$value]" - } - } - - companion object { - /** Returns a pending result. */ - fun pending(): AsyncResult { - return AsyncResult(status = Status.PENDING, resultTimeMillis = SystemClock.uptimeMillis()) - } - - /** Returns a successful result with the specified payload. */ - fun success(value: T): AsyncResult { - return AsyncResult( - status = Status.SUCCEEDED, - resultTimeMillis = SystemClock.uptimeMillis(), - value = value - ) - } - - /** Returns a failed result with the specified error. */ - fun failed(error: Throwable): AsyncResult { - return AsyncResult( - status = Status.FAILED, - resultTimeMillis = SystemClock.uptimeMillis(), - error = error - ) + return when (this) { + is Pending -> Pending() + is Success -> transformFunction(value) + is Failure -> Failure(ChainedFailureException(error)) } } /** A chained exception to preserve failure stacktraces for [transform] and [transformAsync]. */ class ChainedFailureException(cause: Throwable) : Exception(cause) + + /** [AsyncResult] representing an operation that may be completed in the future. */ + data class Pending( + override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() + + /** [AsyncResult] representing an operation that succeeded with a specific [value]. */ + data class Success( + val value: T, + override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() { + /** + * Returns whether this [Success] is effectively equal to [otherResult] (i.e., has the same + * [value], but not necessarily the same [resultTimeMillis]). + */ + internal fun isEffectivelyEqualTo(otherResult: Success): Boolean = + value == otherResult.value + } + + /** [AsyncResult] representing an operation that failed with a specific [error]. */ + data class Failure( + val error: Throwable, + override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() { + /** + * Returns whether this [Failure] is effectively equal to [otherResult] (i.e., has the same + * [error], but not necessarily the same [resultTimeMillis]). + */ + internal fun isEffectivelyEqualTo(otherResult: Failure): Boolean = + error == otherResult.error + } } diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt index b812a442013..26a4c8b2fd9 100644 --- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt +++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt @@ -6,6 +6,7 @@ import dagger.Reusable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import org.oppia.android.util.logging.ExceptionLogger import org.oppia.android.util.threading.BackgroundDispatcher @@ -50,7 +51,7 @@ class DataProviders @Inject constructor( this@transform.retrieveData().transform(function) } catch (e: Exception) { dataProviders.exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -119,7 +120,7 @@ class DataProviders @Inject constructor( this@combineWith.retrieveData().combineWith(dataProvider.retrieveData(), function) } catch (e: Exception) { dataProviders.exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -187,10 +188,10 @@ class DataProviders @Inject constructor( override suspend fun retrieveData(): AsyncResult { return try { - AsyncResult.success(loadFromMemory()) + AsyncResult.Success(loadFromMemory()) } catch (e: Exception) { exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -329,7 +330,9 @@ class DataProviders @Inject constructor( checkNotNull(value) { "Null values should not be posted to NotifiableAsyncLiveData." } val currentCache = cache // This is safe because cache can only be changed on the main thread. if (currentCache != null) { - if (value.isNewerThanOrSameAgeAs(currentCache) && currentCache != value) { + if (value.isNewerThanOrSameAgeAs(currentCache) && + !currentCache.hasSameEffectiveValueAs(value) + ) { // Only propagate the value if it's changed and is newer since it's possible for observer // callbacks to happen out-of-order. cache = value diff --git a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt index 8dcb09ec100..64c8ed47ed3 100644 --- a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.async import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.BackgroundTestDispatcher import org.oppia.android.testing.threading.TestCoroutineDispatcher @@ -18,12 +19,14 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.testing.time.FakeSystemClock +import org.oppia.android.util.data.AsyncResult.ChainedFailureException import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import kotlin.test.assertFailsWith /** Tests for [AsyncResult]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AsyncResultTest { @@ -47,184 +50,184 @@ class AsyncResultTest { /* Pending tests. */ - @Test - fun testPendingAsyncResult_isPending() { - val result = AsyncResult.pending() - - assertThat(result.isPending()).isTrue() - } - - @Test - fun testPendingAsyncResult_isNotSuccess() { - val result = AsyncResult.pending() - - assertThat(result.isSuccess()).isFalse() - } - - @Test - fun testPendingAsyncResult_isNotFailure() { - val result = AsyncResult.pending() - - assertThat(result.isFailure()).isFalse() - } - - @Test - fun testPendingAsyncResult_isNotCompleted() { - val result = AsyncResult.pending() - - assertThat(result.isCompleted()).isFalse() - } - - @Test - fun testPendingAsyncResult_getOrDefault_returnsDefault() { - val result = AsyncResult.pending() - - assertThat(result.getOrDefault("default")).isEqualTo("default") - } - - @Test - fun testPendingAsyncResult_getOrThrow_throwsIllegalStateExceptionDueToIncompletion() { - val result = AsyncResult.pending() - - assertFailsWith { result.getOrThrow() } - } - - @Test - fun testPendingAsyncResult_getErrorOrNull_returnsNull() { - val result = AsyncResult.pending() - - assertThat(result.getErrorOrNull()).isNull() - } - @Test fun testPendingAsyncResult_transformed_isStillPending() { - val original = AsyncResult.pending() + val original = AsyncResult.Pending() val transformed = original.transform { 0 } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_transformedAsync_isStillPending() { - val original = AsyncResult.pending() + val original = AsyncResult.Pending() - val transformed = original.blockingTransformAsync { AsyncResult.success(0) } + val transformed = original.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithPending_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithFailure_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithSuccess_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success(1.0f) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithPending_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithFailure_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithSuccess_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success(1.0f) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingResult_isEqualToAnotherPendingResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() // Two pending results are the same regardless of their types. - assertThat(result).isEqualTo(AsyncResult.pending()) + assertThat(result).isEqualTo(AsyncResult.Pending()) } @Test fun testPendingResult_isNotEqualToFailedResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() - assertThat(result).isNotEqualTo(AsyncResult.failed(UnsupportedOperationException())) + assertThat(result).isNotEqualTo(AsyncResult.Failure(UnsupportedOperationException())) } @Test fun testPendingResult_isNotEqualToSucceededResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() - assertThat(result).isNotEqualTo(AsyncResult.success("Success")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Success")) + } + + @Test + fun testPendingResult_andPendingResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testPendingResult_andPendingResult_sameExceptAge_areEffectivelyEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + // Two pending results are always effectively equal. + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testPendingResult_andSuccessResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success("Success") + + // A pending result is never equivalent to a successful one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testPendingResult_andFailureResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + // A pending result is never equivalent to a failing one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() } @Test fun testPendingResult_hashCode_isEqualToAnotherPendingResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testPendingResult_hashCode_isNotEqualToSucceededResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testPendingResult_hashCode_isNotEqualToFailedResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() assertThat(resultHash).isNotEqualTo( - AsyncResult.failed( + AsyncResult.Failure( UnsupportedOperationException() ).hashCode() ) } + @Test + fun testPendingResult_andPendingResult_sameExceptAge_hashCodes_areNotEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testPendingResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -233,8 +236,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithOtherPendingResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -243,8 +246,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithSucceededResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val success = AsyncResult.success("value") + val pendingResult = AsyncResult.Pending() + val success = AsyncResult.Success("value") val areSameAge = pendingResult.isNewerThanOrSameAgeAs(success) @@ -253,8 +256,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithFailedResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val failure = AsyncResult.failed(RuntimeException()) + val pendingResult = AsyncResult.Pending() + val failure = AsyncResult.Failure(RuntimeException()) val areSameAge = pendingResult.isNewerThanOrSameAgeAs(failure) @@ -263,9 +266,9 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithOlderPendingResult_isNewer() { - val olderResult = AsyncResult.pending() + val olderResult = AsyncResult.Pending() fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.pending() + val newerResult = AsyncResult.Pending() val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -274,9 +277,9 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithNewerPendingResult_isNotNewer() { - val olderResult = AsyncResult.pending() + val olderResult = AsyncResult.Pending() fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.pending() + val newerResult = AsyncResult.Pending() val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) @@ -286,267 +289,277 @@ class AsyncResultTest { /* Success tests. */ @Test - fun testSucceededAsyncResult_isNotPending() { - val result = AsyncResult.success("value") - - assertThat(result.isPending()).isFalse() - } - - @Test - fun testSucceededAsyncResult_isSuccess() { - val result = AsyncResult.success("value") - - assertThat(result.isSuccess()).isTrue() - } - - @Test - fun testSucceededAsyncResult_isNotFailure() { - val result = AsyncResult.success("value") - - assertThat(result.isFailure()).isFalse() - } - - @Test - fun testSucceededAsyncResult_isCompleted() { - val result = AsyncResult.success("value") - - assertThat(result.isCompleted()).isTrue() - } - - @Test - fun testSucceededAsyncResult_getOrDefault_returnsValue() { - val result = AsyncResult.success("value") + fun testSucceededAsyncResult_hasCorrectValue() { + val result = AsyncResult.Success("value") - assertThat(result.getOrDefault("default")).isEqualTo("value") - } - - @Test - fun testSucceededAsyncResult_getOrThrow_returnsValue() { - val result = AsyncResult.success("value") - - assertThat(result.getOrThrow()).isEqualTo("value") - } - - @Test - fun testSucceededAsyncResult_getErrorOrNull_returnsNull() { - val result = AsyncResult.success("value") - - assertThat(result.getErrorOrNull()).isNull() + assertThat(result.value).isEqualTo("value") } @Test fun testSucceededAsyncResult_transformed_hasTransformedValue() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") val transformed = original.transform { 0 } - assertThat(transformed.getOrThrow()).isEqualTo(0) + assertThat(transformed).isIntSuccessThat().isEqualTo(0) } @Test fun testSucceededAsyncResult_transformedAsyncPending_isPending() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") - val transformed = original.blockingTransformAsync { AsyncResult.pending() } + val transformed = original.blockingTransformAsync { AsyncResult.Pending() } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_transformedAsyncSuccess_hasTransformedValue() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") - val transformed = original.blockingTransformAsync { AsyncResult.success(0) } + val transformed = original.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.getOrThrow()).isEqualTo(0) + assertThat(transformed).isIntSuccessThat().isEqualTo(0) } @Test fun testSucceededAsyncResult_transformedAsyncFailed_isFailure() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") val transformed = original.blockingTransformAsync { - AsyncResult.failed(UnsupportedOperationException()) + AsyncResult.Failure(UnsupportedOperationException()) } - // Note that the failure is not chained since the transform function was responsible for 'throwing' it. - assertThat(transformed.getErrorOrNull()).isInstanceOf(UnsupportedOperationException::class.java) + // Note that the failure is not chained since the transform function was responsible for + // 'throwing' it. + assertThat(transformed).isFailureThat().isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testSucceededAsyncResult_combinedWithPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedWithFailure_isFailedWithCorrectChainedFailure() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf(RuntimeException::class.java) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat().hasCauseThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedWithSuccess_hasCombinedSuccessValue() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.combineWith(result2) { v1, v2 -> v1 + v2 } - assertThat(combined.getOrThrow()).contains("value") - assertThat(combined.getOrThrow()).contains("1.0") + assertThat(combined).isStringSuccessThat().contains("value") + assertThat(combined).isStringSuccessThat().contains("1.0") } @Test fun testSucceededAsyncResult_combinedAsyncWithPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithFailure_isFailedWithCorrectChainedFailure() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - RuntimeException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat().hasCauseThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.pending() } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> + AsyncResult.Pending() + } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultFailure_isFailed() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.blockingCombineWithAsync(result2) { _, _ -> - AsyncResult.failed(RuntimeException()) + AsyncResult.Failure(RuntimeException()) } - // Note that the failure is not chained since the transform function was responsible for 'throwing' it. - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf(RuntimeException::class.java) + // Note that the failure is not chained since the transform function was responsible for + // 'throwing' it. + assertThat(combined).isFailureThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultSuccess_hasCombinedSuccessValue() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.blockingCombineWithAsync(result2) { v1, v2 -> - AsyncResult.success(v1 + v2) + AsyncResult.Success(v1 + v2) } - assertThat(combined.getOrThrow()).contains("value") - assertThat(combined.getOrThrow()).contains("1.0") + assertThat(combined).isStringSuccessThat().contains("value") + assertThat(combined).isStringSuccessThat().contains("1.0") } @Test fun testSucceededResult_isNotEqualToPendingResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isNotEqualTo(AsyncResult.pending()) + assertThat(result).isNotEqualTo(AsyncResult.Pending()) } @Test fun testSucceededResult_isEqualToSameSucceededResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isEqualTo(AsyncResult.success("Success")) + assertThat(result).isEqualTo(AsyncResult.Success("Success")) } @Test fun testSucceededResult_isNotEqualToDifferentSucceededResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isNotEqualTo(AsyncResult.success("Other value")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Other value")) } @Test fun testSucceededResult_isNotEqualToDifferentTypedSucceededResult() { - val result = AsyncResult.success("0") + val result = AsyncResult.Success("0") - assertThat(result).isNotEqualTo(AsyncResult.success(0)) + assertThat(result).isNotEqualTo(AsyncResult.Success(0)) } @Test fun testSucceededResult_isNotEqualToFailedResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") + + assertThat(result).isNotEqualTo(AsyncResult.Failure(UnsupportedOperationException())) + } - assertThat(result).isNotEqualTo(AsyncResult.failed(UnsupportedOperationException())) + @Test + fun testSucceededResult_andSuccessfulResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testSucceededResult_andPendingResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success") + val result2 = AsyncResult.Pending() + + // A successful result is never equivalent to a pending one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testSucceededResult_andSucceededResult_sameValue_differentAges_areEffectivelyEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testSucceededResult_andSucceededResult_differentValues_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success1") + val result2 = AsyncResult.Success("Success2") + + // The two results have different effective values. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testSucceededResult_andFailureResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success1") + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + // A successful result is never equivalent to a failing one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() } @Test fun testSucceededResult_hashCode_isNotEqualToPendingResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isNotEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testSucceededResult_hashCode_isEqualToSameSucceededResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() - assertThat(resultHash).isEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testSucceededResult_hashCode_isNotEqualToDifferentSucceededResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Other value").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Other value").hashCode()) } @Test fun testSucceededResult_hashCode_isNotEqualToDifferentTypedSucceededResult() { - val resultHash = AsyncResult.success("0").hashCode() + val resultHash = AsyncResult.Success("0").hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success(0)) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success(0)) } @Test fun testSucceededResult_hashCode_isNotEqualToFailedResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() assertThat(resultHash).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException()).hashCode() + AsyncResult.Failure(UnsupportedOperationException()).hashCode() ) } + @Test + fun testSucceededResult_andSucceededResult_sameExceptAge_hashCodes_areNotEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testSucceededResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.success("value") + val result = AsyncResult.Success("value") val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -555,8 +568,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithPendingResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val success = AsyncResult.success("value") + val pendingResult = AsyncResult.Pending() + val success = AsyncResult.Success("value") val areSameAge = success.isNewerThanOrSameAgeAs(pendingResult) @@ -565,8 +578,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithOtherSucceededResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success("value") + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success("value") val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -575,8 +588,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithFailedResult_createdAtTheSameTime_areTheSameAge() { - val success = AsyncResult.success("value") - val failure = AsyncResult.failed(RuntimeException()) + val success = AsyncResult.Success("value") + val failure = AsyncResult.Failure(RuntimeException()) val areSameAge = success.isNewerThanOrSameAgeAs(failure) @@ -585,9 +598,9 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithOlderSucceededResult_isNewer() { - val olderResult = AsyncResult.success("value") + val olderResult = AsyncResult.Success("value") fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.success("value") + val newerResult = AsyncResult.Success("value") val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -596,254 +609,292 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithNewerSucceededResult_isNotNewer() { - val olderResult = AsyncResult.success("value") + val olderResult = AsyncResult.Success("value") fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.success("value") + val newerResult = AsyncResult.Success("value") val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) assertThat(isNewer).isFalse() } - /* Failure tests. */ - @Test - fun testFailedAsyncResult_isNotPending() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_nullValue_canRetrieveNullValue() { + val result = AsyncResult.Success(null) - assertThat(result.isPending()).isFalse() + assertThat(result.value).isNull() } @Test - fun testFailedAsyncResult_isNotSuccess() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_nullValue_transformIntoString_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success(null) - assertThat(result.isSuccess()).isFalse() - } - - @Test - fun testFailedAsyncResult_isFailure() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result2 = result1.transform { "string" } - assertThat(result.isFailure()).isTrue() + assertThat(result2).isStringSuccessThat().isEqualTo("string") } @Test - fun testFailedAsyncResult_isCompleted() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_stringValue_transformIntoNull_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success("string") - assertThat(result.isCompleted()).isTrue() - } - - @Test - fun testFailedAsyncResult_getOrDefault_returnsDefault() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result2: AsyncResult = result1.transform { null } - assertThat(result.getOrDefault("default")).isEqualTo("default") + assertThat(result2).isSuccessThat().isNull() } @Test - fun testFailedAsyncResult_getOrThrow_throwsFailureException() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_combineStringAndNullResult_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success("string") + val result2 = AsyncResult.Success(null) - assertFailsWith { result.getOrThrow() } + val combined = result1.combineWith(result2) { _, _ -> "combined" } + + assertThat(combined).isStringSuccessThat().isEqualTo("combined") } + /* Failure tests. */ + @Test - fun testFailedAsyncResult_getErrorOrNull_returnsFailureException() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testFailedAsyncResult_containsFailureException() { + val result = AsyncResult.Failure(UnsupportedOperationException()) - assertThat(result.getErrorOrNull()).isInstanceOf(UnsupportedOperationException::class.java) + assertThat(result.error).isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_transformed_throwsChainedFailureException_withCorrectRootCause() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result = AsyncResult.Failure(UnsupportedOperationException()) val transformed = result.transform { 0 } - assertThat(transformed.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(transformed.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(transformed).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(transformed).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_transformedAsync_throwsChainedFailureException_withCorrectRootCause() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result = AsyncResult.Failure(UnsupportedOperationException()) - val transformed = result.blockingTransformAsync { AsyncResult.success(0) } + val transformed = result.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(transformed.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(transformed).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(transformed).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithPending_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithFailure_hasFirstFailureChained() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithSuccess_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success(1.0f) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithPending_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithFailure_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithSuccess_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success(1.0f) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedResult_isNotEqualToPendingResult() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - assertThat(result).isNotEqualTo(AsyncResult.pending()) + assertThat(result).isNotEqualTo(AsyncResult.Pending()) } @Test fun testFailedResult_isNotEqualToSucceededResult() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - assertThat(result).isNotEqualTo(AsyncResult.success("Success")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Success")) } @Test fun testFailedResult_isEqualToFailedResultWithSameExceptionObject() { val failure = UnsupportedOperationException("Reason") - val result = AsyncResult.failed(failure) + val result = AsyncResult.Failure(failure) - assertThat(result).isEqualTo(AsyncResult.failed(failure)) + assertThat(result).isEqualTo(AsyncResult.Failure(failure)) } @Test fun testFailedResult_isNotEqualToFailedResultWithDifferentInstanceOfSameExceptionType() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - // Different exceptions have different stack traces, so they can't be equal despite similar constructions. + // Different exceptions have different stack traces, so they can't be equal despite similar + // constructions. assertThat(result).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException("Reason")) + AsyncResult.Failure(UnsupportedOperationException("Reason")) ) } + @Test + fun testFailedResult_andFailedResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testFailedResult_andPendingResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() + + // A failing result is never equivalent to a pending one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testFailedResult_andSucceededResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success("Success1") + + // A failing result is never equivalent to a successful one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testFailedResult_andFailedResult_sameException_differentAges_areEffectivelyEqual() { + val exception = UnsupportedOperationException("Reason") + val result1 = AsyncResult.Failure(exception) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(exception) + + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testFailedResult_andFailedResult_differentValues_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException("Reason 1")) + val result2 = AsyncResult.Failure(UnsupportedOperationException("Reason 2")) + + // The two results have different effective values. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + @Test fun testFailedResult_hashCode_isNotEqualToPendingResult() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isNotEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testFailedResult_hashCode_isNotEqualToSucceededResult() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testFailedResult_hashCode_isEqualToFailedResultWithSameExceptionObject() { val failure = UnsupportedOperationException("Reason") - val resultHash = AsyncResult.failed(failure).hashCode() + val resultHash = AsyncResult.Failure(failure).hashCode() - assertThat(resultHash).isEqualTo(AsyncResult.failed(failure).hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Failure(failure).hashCode()) } @Test fun testFailedResult_hashCode_isNotEqualToFailedResultWithDifferentInstanceOfSameExceptionType() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() - // Different exceptions have different stack traces, so they can't be equal despite similar constructions. + // Different exceptions have different stack traces, so they can't be equal despite similar + // constructions. assertThat(resultHash).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() ) } + @Test + fun testFailedResult_andFailedResult_sameExceptAge_hashCodes_areNotEqual() { + val exception = UnsupportedOperationException("Reason") + val result1 = AsyncResult.Failure(exception) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(exception) + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testFailedResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.failed(RuntimeException()) + val result = AsyncResult.Failure(RuntimeException()) val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -852,8 +903,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithPendingResult_createdAtTheSameTime_areTheSameAge() { - val failure = AsyncResult.failed(RuntimeException()) - val pendingResult = AsyncResult.pending() + val failure = AsyncResult.Failure(RuntimeException()) + val pendingResult = AsyncResult.Pending() val areSameAge = failure.isNewerThanOrSameAgeAs(pendingResult) @@ -862,8 +913,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithSucceededResult_createdAtTheSameTime_areTheSameAge() { - val failure = AsyncResult.failed(RuntimeException()) - val success = AsyncResult.success("value") + val failure = AsyncResult.Failure(RuntimeException()) + val success = AsyncResult.Success("value") val areSameAge = failure.isNewerThanOrSameAgeAs(success) @@ -872,8 +923,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithOtherFailedResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.failed(RuntimeException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(RuntimeException()) + val result2 = AsyncResult.Failure(RuntimeException()) val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -882,9 +933,9 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithOlderFailedResult_isNewer() { - val olderResult = AsyncResult.failed(RuntimeException()) + val olderResult = AsyncResult.Failure(RuntimeException()) fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.failed(RuntimeException()) + val newerResult = AsyncResult.Failure(RuntimeException()) val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -893,9 +944,9 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithNewerFailedResult_isNotNewer() { - val olderResult = AsyncResult.failed(RuntimeException()) + val olderResult = AsyncResult.Failure(RuntimeException()) fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.failed(RuntimeException()) + val newerResult = AsyncResult.Failure(RuntimeException()) val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) diff --git a/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel new file mode 100644 index 00000000000..ba600e1b24d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel @@ -0,0 +1,86 @@ +""" +Tests for lightweight exploration player domain components. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "AsyncDataSubscriptionManagerTest", + srcs = ["AsyncDataSubscriptionManagerTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.AsyncDataSubscriptionManagerTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_mockito_mockito-core", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "AsyncResultTest", + srcs = ["AsyncResultTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.AsyncResultTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/data:async_result", + ], +) + +oppia_android_test( + name = "DataProvidersTest", + srcs = ["DataProvidersTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.DataProvidersTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "InMemoryBlockingCacheTest", + srcs = ["InMemoryBlockingCacheTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.InMemoryBlockingCacheTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +dagger_rules() diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt index f15d1d20ad3..27085a29b38 100644 --- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt @@ -23,15 +23,17 @@ import org.mockito.Mock import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.testing.time.FakeSystemClock import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.combineWithAsync import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -61,6 +63,8 @@ private const val COMBINED_STR_VALUE_21 = "At least I thought I was. Now I'm not private const val COMBINED_STR_VALUE_02 = "I used to be indecisive. At least I thought I was." /** Tests for [DataProviders], [DataProvider]s, and [AsyncDataSubscriptionManager]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = DataProvidersTest.TestApplication::class) @@ -101,6 +105,9 @@ class DataProvidersTest { @Captor lateinit var intResultCaptor: ArgumentCaptor> + @Inject + lateinit var fakeSystemClock: FakeSystemClock + private var inMemoryCachedStr: String? = null private var inMemoryCachedStr2: String? = null @@ -126,7 +133,7 @@ class DataProvidersTest { override suspend fun retrieveData(): AsyncResult { hasRetrieveBeenCalled = true - return AsyncResult.pending() + return AsyncResult.Pending() } } @@ -145,7 +152,7 @@ class DataProvidersTest { override suspend fun retrieveData(): AsyncResult { hasRetrieveBeenCalled = true - return AsyncResult.pending() + return AsyncResult.Pending() } } @@ -160,15 +167,14 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(123) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(123) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(123) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(123) } @Test @@ -177,7 +183,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(providerValue) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(providerValue) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -186,8 +192,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) } @Test @@ -196,7 +201,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(providerValue) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(providerValue) } providerValue = 456 asyncDataSubscriptionManager.notifyChangeAsync(simpleDataProvider.getId()) @@ -208,8 +213,7 @@ class DataProvidersTest { // The newer value should be observed. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) } @Test @@ -217,7 +221,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(123) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(123) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() @@ -228,14 +232,14 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() // The observer should have no interactions since the data hasn't changed. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test fun testConvertToLiveData_multipleUpdatesNoObserver_newObserver_observerReceivesLatest() { - val providerOldResult = AsyncResult.success(123) + val providerOldResult = AsyncResult.Success(123) testCoroutineDispatchers.advanceTimeBy(10) - val providerNewResult = AsyncResult.success(456) + val providerNewResult = AsyncResult.Success(456) val simpleDataProvider = object : DataProvider(context) { var callCount = 0 @@ -249,7 +253,7 @@ class DataProvidersTest { return when (++callCount) { 1 -> providerNewResult 2 -> providerOldResult - else -> AsyncResult.failed(AssertionError("Invalid test case")) + else -> AsyncResult.Failure(AssertionError("Invalid test case")) } } } @@ -262,11 +266,32 @@ class DataProvidersTest { // The more recent value should be observed despite it being retrieved first. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) assertThat(simpleDataProvider.callCount).isEqualTo(2) // Sanity check for the test logic itself. } + @Test + fun testConvertToLiveData_dataProvider_providesPendingResultTwice_doesNotRedeliver() { + val simpleDataProvider = object : DataProvider(context) { + override fun getId(): Any = "simple_data_provider" + + // Return a new pending result for each call. + override suspend fun retrieveData(): AsyncResult = AsyncResult.Pending() + } + // Ensure the initial value is retrieved. + simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) + testCoroutineDispatchers.advanceUntilIdle() + reset(mockIntLiveDataObserver) + + testCoroutineDispatchers.advanceTimeBy(10) + asyncDataSubscriptionManager.notifyChangeAsync(simpleDataProvider.getId()) + testCoroutineDispatchers.advanceUntilIdle() + + // Despite there being a notification, it shouldn't redeliver the result since the two values + // are effectively equal. + verifyNoMoreInteractions(mockIntLiveDataObserver) + } + @Test fun testInMemoryDataProvider_toLiveData_deliversInMemoryValue() { val dataProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) @@ -275,8 +300,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -290,7 +314,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() // The observer should not be notified again since the value hasn't changed. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -304,8 +328,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -322,8 +345,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -338,8 +360,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -358,8 +379,7 @@ class DataProvidersTest { // The first value should be observed since a completely different provider was notified. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -407,31 +427,29 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test fun testAsyncInMemoryDataProvider_toLiveData_deliversInMemoryValue() { val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testAsyncInMemoryDataProvider_toLiveData_withChangedValue_beforeReg_deliversSecondValue() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } inMemoryCachedStr = STR_VALUE_1 @@ -439,15 +457,14 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test fun testAsyncInMemoryDataProvider_toLiveData_withChangedValue_afterReg_deliversFirstValue() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } // Ensure the initial state is sent before changing the cache. @@ -458,15 +475,14 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testAsyncInMemoryDataProvider_changedValueAfterReg_notified_deliversValueTwo() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -475,8 +491,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -484,13 +499,13 @@ class DataProvidersTest { // Ensure the suspend operation is initially blocked. val blockingOperation = backgroundCoroutineScope.async { STR_VALUE_0 } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(blockingOperation.await()) + AsyncResult.Success(blockingOperation.await()) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The observer should never be called since the underlying async function hasn't yet completed. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -498,7 +513,7 @@ class DataProvidersTest { // Ensure the suspend operation is initially blocked. val blockingOperation = backgroundCoroutineScope.async { STR_VALUE_0 } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(blockingOperation.await()) + AsyncResult.Success(blockingOperation.await()) } // Start observing the provider, then complete its suspend function. @@ -509,8 +524,7 @@ class DataProvidersTest { // The provider will deliver a value immediately when the suspend function completes (no // additional notification is needed). verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -518,7 +532,7 @@ class DataProvidersTest { var fakeLoadMemoryCallbackCalled = false val fakeLoadMemoryCallback: suspend () -> AsyncResult = { fakeLoadMemoryCallbackCalled = true - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0, fakeLoadMemoryCallback) @@ -535,7 +549,7 @@ class DataProvidersTest { var fakeLoadMemoryCallbackCalled = false val fakeLoadMemoryCallback: suspend () -> AsyncResult = { fakeLoadMemoryCallbackCalled = true - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0, fakeLoadMemoryCallback) @@ -555,7 +569,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -567,10 +581,9 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -582,8 +595,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -600,8 +612,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -618,8 +629,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -638,8 +648,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's notification // behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -661,8 +670,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -674,7 +682,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -687,12 +695,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -778,12 +784,10 @@ class DataProvidersTest { // Note that the exception type here is not chained since the failure occurred in the transform // function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -796,14 +800,11 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat().hasMessageThat() - .contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -817,8 +818,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -837,8 +837,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -857,8 +856,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -879,8 +877,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's notification // behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -904,8 +901,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -919,7 +915,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) // No value should be delivered since the async function is blocked. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test @@ -935,8 +931,7 @@ class DataProvidersTest { // The value should now be delivered since the async function was unblocked. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -955,15 +950,14 @@ class DataProvidersTest { // Verify that even though the transformed provider is blocked, the base can still properly // publish changes. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testTransformAsync_toLiveData_transformedPending_deliversPending() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) { - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -971,14 +965,14 @@ class DataProvidersTest { // The transformation result yields a pending delivered result. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test fun testTransformAsync_toLiveData_transformedFailure_deliversFailure() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) { - AsyncResult.failed(IllegalStateException("Transform failure")) + AsyncResult.Failure(IllegalStateException("Transform failure")) } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -987,12 +981,10 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -1007,7 +999,7 @@ class DataProvidersTest { // Since the base provider is pending, so is the transformed provider. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -1024,14 +1016,11 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat().hasMessageThat() - .contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -1115,8 +1104,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -1137,8 +1125,7 @@ class DataProvidersTest { // Notifying the first base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1159,8 +1146,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1180,8 +1166,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1205,8 +1190,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -1227,8 +1211,7 @@ class DataProvidersTest { // Notifying the second base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1249,8 +1232,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1270,8 +1252,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1295,8 +1276,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -1311,7 +1291,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1326,7 +1306,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1341,7 +1321,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1357,12 +1337,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1378,12 +1356,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1400,12 +1376,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1567,12 +1541,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1590,12 +1562,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1616,12 +1586,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1639,10 +1607,9 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -1658,8 +1625,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -1681,8 +1647,7 @@ class DataProvidersTest { // Notifying the first base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1704,8 +1669,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1725,8 +1689,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1750,8 +1713,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -1773,8 +1735,7 @@ class DataProvidersTest { // Notifying the second base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1796,8 +1757,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1817,8 +1777,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1842,8 +1801,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -1859,7 +1817,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1875,7 +1833,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1891,7 +1849,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1908,12 +1866,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1930,12 +1886,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1953,12 +1907,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2121,12 +2073,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2145,12 +2095,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2172,12 +2120,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2189,13 +2135,13 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered since the first provider is blocked. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2207,7 +2153,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2216,8 +2162,7 @@ class DataProvidersTest { // The value should now be delivered since the provider was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2229,13 +2174,13 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered since the first provider is blocked. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2247,7 +2192,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2256,8 +2201,7 @@ class DataProvidersTest { // The value should now be delivered since the provider was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2273,7 +2217,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2292,8 +2236,7 @@ class DataProvidersTest { // The value should be delivered since the async function was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2304,14 +2247,14 @@ class DataProvidersTest { baseProvider2, COMBINED_PROVIDER_ID ) { _: String, _: String -> - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -2322,17 +2265,16 @@ class DataProvidersTest { baseProvider2, COMBINED_PROVIDER_ID ) { _: String, _: String -> - AsyncResult.failed(IllegalStateException("Combine failure")) + AsyncResult.Failure(IllegalStateException("Combine failure")) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -2346,8 +2288,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -2366,8 +2307,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2386,8 +2326,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2408,8 +2347,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's // notification behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -2433,8 +2371,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -2448,7 +2385,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) // No value should be delivered since the async function is blocked. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test @@ -2464,8 +2401,7 @@ class DataProvidersTest { // The value should now be delivered since the async function was unblocked. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -2484,15 +2420,14 @@ class DataProvidersTest { // Verify that even though the transformed provider is blocked, the base can still properly // publish changes. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testNestedXformedProvider_toLiveData_transformedPending_deliversPending() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformNested(TRANSFORMED_PROVIDER_ID) { - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -2500,14 +2435,14 @@ class DataProvidersTest { // The transformation result yields a pending delivered result. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test fun testNestedXformedProvider_toLiveData_transformedFailure_deliversFailure() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformNested(TRANSFORMED_PROVIDER_ID) { - AsyncResult.failed(IllegalStateException("Transform failure")) + AsyncResult.Failure(IllegalStateException("Transform failure")) } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -2516,11 +2451,10 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()) - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasMessageThat().contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -2535,7 +2469,7 @@ class DataProvidersTest { // Since the base provider is pending, so is the transformed provider. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -2552,13 +2486,11 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()) - .isInstanceOf(AsyncResult.ChainedFailureException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasCauseThat().isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasCauseThat().hasMessageThat().contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -2645,8 +2577,7 @@ class DataProvidersTest { // The observer should get the newest value immediately. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2664,8 +2595,7 @@ class DataProvidersTest { // The observer should get the newest value immediately. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0_DOUBLED) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0_DOUBLED) } @Test @@ -2683,8 +2613,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2707,8 +2636,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_2) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_2) } @Test @@ -2731,8 +2659,7 @@ class DataProvidersTest { // Since the base provider was replaced, it shouldn't result in any observed change. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_2) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_2) } @Test @@ -2758,8 +2685,7 @@ class DataProvidersTest { // Since the base provider was replaced, the old notification should not trigger a newly // change even though the new base technically did change (but it wasn't notified yet). verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2823,7 +2749,7 @@ class DataProvidersTest { private suspend fun transformStringAsync(str: String): AsyncResult { val deferred = backgroundCoroutineScope.async { transformString(str) } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } /** @@ -2833,7 +2759,7 @@ class DataProvidersTest { private suspend fun transformStringDoubledAsync(str: String): AsyncResult { val deferred = backgroundCoroutineScope.async { transformString(str) * 2 } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } private fun combineStrings(str1: String, str2: String): String { @@ -2847,7 +2773,7 @@ class DataProvidersTest { private suspend fun combineStringsAsync(str1: String, str2: String): AsyncResult { val deferred = backgroundCoroutineScope.async { combineStrings(str1, str2) } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } private fun createSuccessfulDataProvider(id: Any, value: T): DataProvider { @@ -2858,7 +2784,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { // Android Studio incorrectly suggests to remove the explicit argument. @Suppress("RemoveExplicitTypeArguments") - AsyncResult.pending() + (AsyncResult.Pending()) } } @@ -2866,7 +2792,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { // Android Studio incorrectly suggests to remove the explicit argument. @Suppress("RemoveExplicitTypeArguments") - AsyncResult.failed(failure) + (AsyncResult.Failure(failure)) } } @@ -2879,7 +2805,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { val deferred = backgroundCoroutineScope.async { value } deferred.await() - AsyncResult.success(deferred.getCompleted()) + AsyncResult.Success(deferred.getCompleted()) } } diff --git a/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt b/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt index 3f355805ac9..e5e2a69b59b 100644 --- a/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt @@ -35,6 +35,8 @@ private const val CREATED_ASYNC_VALUE = "created async value" private const val UPDATED_ASYNC_VALUE = "updated async value" /** Tests for [InMemoryBlockingCache]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE)