From 2d14066d165901f32d87bc6b9882fb4da9c1b006 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Sat, 5 Sep 2020 02:18:27 +0530 Subject: [PATCH] Fix #1106: Addition of Work Manager for uploading logs (#1680) * work manager start. * constraints + lint fixes. * work manager module + injection + onStart setup. * lint fixes. * approach change. * fixes. * app-module test fix. * inprogress * inprogress * tests in progress. * Worker Injection setup. * worker factory setup. * worker tests. * merging of request and worker class. * work request invocation on app creation setup. * tests. * app-module tests fix. * lint fix. * comments. * bazel building. * fixes. * fixes. * bazel. * lint fix. * more lint fix. * app module test + config impl change. * lint fix. * more lint fix. * nits. * Coroutine worker | no map. * changes. * work manager impl correction. * testing for initialiser. * Test fixes. * lint fixes. * changes. * lint fixes. * dependency changes. * lint fixes. * lint. * minor. * change 1. * child factory removal. * lint fix. * Add test applications to all tests which now rely on WorkManager to NOT be initialized (these tests shouldn't use OppiaApplication anymore). Includes some fixes for various tests that may have had flakiness/syncing issues before, and removed one test activity that wasn't actually used. One test was also ignored because it isn't testing the correct thing and it fails when the correct assertion is added, so follow-up work is needed. * comment addition. Co-authored-by: Ben Henning --- WORKSPACE | 1 + app/BUILD.bazel | 1 + app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 9 +- .../oppia/app/activity/ActivityComponent.kt | 2 - .../app/application/ApplicationComponent.kt | 9 +- .../oppia/app/application/OppiaApplication.kt | 10 +- .../app/testing/StoryFragmentTestActivity.kt | 50 ---- .../StoryFragmentTestActivityPresenter.kt | 42 --- .../layout/story_fragment_test_activity.xml | 7 - .../org/oppia/app/faq/FAQListFragmentTest.kt | 98 +++++++ .../oppia/app/faq/FAQSingleActivityTest.kt | 109 ++++++++ .../org/oppia/app/help/HelpFragmentTest.kt | 105 +++++++- .../mydownloads/MyDownloadsFragmentTest.kt | 131 ++++++--- .../org/oppia/app/parser/HtmlParserTest.kt | 158 +++++------ .../app/player/state/StateFragmentTest.kt | 7 +- .../ProfileProgressFragmentTest.kt | 173 +++++++----- .../app/recyclerview/BindableAdapterTest.kt | 93 +++++-- .../oppia/app/splash/SplashActivityTest.kt | 7 +- .../org/oppia/app/story/StoryActivityTest.kt | 102 +++++++ .../org/oppia/app/story/StoryFragmentTest.kt | 199 +++++++------- .../QuestionPlayerActivityTest.kt | 7 +- .../oppia/app/utility/RatioExtensionsTest.kt | 97 ++++++- .../oppia/app/home/HomeActivityLocalTest.kt | 7 +- .../app/parser/StringToRatioParserTest.kt | 96 ++++++- .../ExplorationActivityLocalTest.kt | 7 +- .../player/state/StateFragmentLocalTest.kt | 7 +- .../ProfileChooserFragmentLocalTest.kt | 7 +- .../oppia/app/story/StoryActivityLocalTest.kt | 7 +- .../options/AppLanguageFragmentTest.kt | 7 +- .../options/DefaultAudioFragmentTest.kt | 7 +- .../options/ReadingTextSizeFragmentTest.kt | 7 +- .../state/StateFragmentAccessibilityTest.kt | 7 +- .../topic/info/TopicInfoFragmentLocalTest.kt | 7 +- .../lessons/TopicLessonsFragmentLocalTest.kt | 7 +- .../QuestionPlayerActivityLocalTest.kt | 7 +- .../RevisionCardActivityLocalTest.kt | 7 +- domain/BUILD.bazel | 2 + domain/build.gradle | 2 + .../analytics/AnalyticsController.kt | 34 ++- .../exceptions/ExceptionsController.kt | 30 ++- .../LogUploadWorkManagerInitializer.kt | 74 +++++ .../loguploader/LogUploadWorker.kt | 105 ++++++++ .../loguploader/LogUploadWorkerFactory.kt | 22 ++ .../loguploader/LogUploadWorkerModule.kt | 17 ++ .../WorkManagerConfigurationModule.kt | 19 ++ .../analytics/AnalyticsControllerTest.kt | 16 +- .../LogUploadWorkManagerInitializerTest.kt | 255 ++++++++++++++++++ .../loguploader/LogUploadWorkerTest.kt | 236 ++++++++++++++++ utility/BUILD.bazel | 1 + utility/build.gradle | 1 + .../org/oppia/util/logging/LogUploader.kt | 14 + .../logging/firebase/FirebaseLogUploader.kt | 37 +++ .../firebase/FirebaseLogUploaderModule.kt | 12 + 54 files changed, 2044 insertions(+), 438 deletions(-) delete mode 100644 app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt delete mode 100644 app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt delete mode 100644 app/src/main/res/layout/story_fragment_test_activity.xml create mode 100644 domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt create mode 100644 domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt create mode 100644 domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt create mode 100644 domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt create mode 100644 domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt create mode 100644 domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt create mode 100644 domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt create mode 100644 utility/src/main/java/org/oppia/util/logging/LogUploader.kt create mode 100644 utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt create mode 100644 utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt diff --git a/WORKSPACE b/WORKSPACE index 06037a9572a..0347cbfacd6 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -137,6 +137,7 @@ maven_install( "androidx.test.ext:junit:1.1.1", "androidx.test:runner:1.2.0", "androidx.viewpager:viewpager:1.0.0", + "androidx.work:work-runtime-ktx:2.4.0", "com.android.support:support-annotations:28.0.0", "com.caverock:androidsvg-aar:1.4", "com.chaos.view:pinview:1.4.3", diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 567bdf9f3ab..b1f108627eb 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -578,6 +578,7 @@ kt_android_library( artifact("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"), artifact("androidx.multidex:multidex:2.0.1"), artifact("androidx.viewpager:viewpager:1.0.0"), + artifact("androidx.work:work-runtime-ktx:2.4.0"), artifact("com.caverock:androidsvg-aar"), artifact("javax.annotation:javax.annotation-api:jar"), ], diff --git a/app/build.gradle b/app/build.gradle index 280f313420f..e768b3fedd9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,6 +91,7 @@ dependencies { 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03', 'androidx.multidex:multidex:2.0.1', 'androidx.recyclerview:recyclerview:1.0.0', + 'androidx.work:work-runtime-ktx:2.4.0', 'com.chaos.view:pinview:1.4.3', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.android.material:material:1.2.0-alpha02', diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3701dcf2062..b67befe381f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -137,9 +138,6 @@ - @@ -159,5 +157,10 @@ + diff --git a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt index 3f619cd1496..3df35adb356 100644 --- a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt +++ b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt @@ -46,7 +46,6 @@ import org.oppia.app.testing.HtmlParserTestActivity import org.oppia.app.testing.ImageRegionSelectionTestActivity import org.oppia.app.testing.NavigationDrawerTestActivity import org.oppia.app.testing.ProfileChooserFragmentTestActivity -import org.oppia.app.testing.StoryFragmentTestActivity import org.oppia.app.testing.TestFontScaleConfigurationUtilActivity import org.oppia.app.testing.TopicRevisionTestActivity import org.oppia.app.testing.TopicTestActivity @@ -120,6 +119,5 @@ interface ActivityComponent { fun inject(topicRevisionTestActivity: TopicRevisionTestActivity) fun inject(topicTestActivity: TopicTestActivity) fun inject(topicTestActivityForStory: TopicTestActivityForStory) - fun inject(storyFragmentTestActivity: StoryFragmentTestActivity) fun inject(walkthroughActivity: WalkthroughActivity) } diff --git a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt index 6173c2863bf..f2fb1553ee8 100644 --- a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt @@ -2,6 +2,7 @@ package org.oppia.app.application // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. import android.app.Application +import androidx.work.Configuration import dagger.BindsInstance import dagger.Component import org.oppia.app.activity.ActivityComponent @@ -23,12 +24,15 @@ import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.ApplicationStartupListener import org.oppia.domain.oppialogger.LogStorageModule import org.oppia.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.util.accessibility.AccessibilityModule import org.oppia.util.caching.CachingModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.logging.firebase.LogReportingModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule @@ -59,7 +63,8 @@ import javax.inject.Singleton ViewBindingShimModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, RatioInputModule::class, UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class, - HintsAndSolutionConfigModule::class + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + HintsAndSolutionConfigModule::class, FirebaseLogUploaderModule::class ] ) @@ -74,4 +79,6 @@ interface ApplicationComponent : ApplicationInjector { fun getActivityComponentBuilderProvider(): Provider fun getApplicationStartupListeners(): Set + + fun getWorkManagerConfiguration(): Configuration } diff --git a/app/src/main/java/org/oppia/app/application/OppiaApplication.kt b/app/src/main/java/org/oppia/app/application/OppiaApplication.kt index 76859166e76..edf741846d7 100644 --- a/app/src/main/java/org/oppia/app/application/OppiaApplication.kt +++ b/app/src/main/java/org/oppia/app/application/OppiaApplication.kt @@ -3,6 +3,8 @@ package org.oppia.app.application import android.app.Application import androidx.appcompat.app.AppCompatActivity import androidx.multidex.MultiDexApplication +import androidx.work.Configuration +import androidx.work.WorkManager import com.google.firebase.FirebaseApp import org.oppia.app.activity.ActivityComponent import org.oppia.domain.oppialogger.ApplicationStartupListener @@ -11,7 +13,8 @@ import org.oppia.domain.oppialogger.ApplicationStartupListener class OppiaApplication : MultiDexApplication(), ActivityComponentFactory, - ApplicationInjectorProvider { + ApplicationInjectorProvider, + Configuration.Provider { /** The root [ApplicationComponent]. */ private val component: ApplicationComponent by lazy { DaggerApplicationComponent.builder() @@ -28,6 +31,11 @@ class OppiaApplication : override fun onCreate() { super.onCreate() FirebaseApp.initializeApp(applicationContext) + WorkManager.initialize(applicationContext, workManagerConfiguration) component.getApplicationStartupListeners().forEach(ApplicationStartupListener::onCreate) } + + override fun getWorkManagerConfiguration(): Configuration { + return component.getWorkManagerConfiguration() + } } diff --git a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt b/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt deleted file mode 100644 index e9af5c68e03..00000000000 --- a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.oppia.app.testing - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import org.oppia.app.activity.InjectableAppCompatActivity -import org.oppia.app.home.RouteToExplorationListener -import javax.inject.Inject - -const val INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.internalProfileId" -const val TOPIC_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.topic_id" -const val STORY_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.story_id" - -/** Test activity used for story fragment. */ -class StoryFragmentTestActivity : InjectableAppCompatActivity(), RouteToExplorationListener { - @Inject - lateinit var storyFragmentTestActivityPresenter: StoryFragmentTestActivityPresenter - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activityComponent.inject(this) - storyFragmentTestActivityPresenter.handleOnCreate() - } - - override fun routeToExploration( - internalProfileId: Int, - topicId: String, - storyId: String, - explorationId: String, - backflowScreen: Int? - ) { - // Do nothing since routing should be tested at the StoryActivity level. - } - - companion object { - /** Returns an [Intent] to create new [StoryFragmentTestActivity]s. */ - fun createTestActivityIntent( - context: Context, - internalProfileId: Int, - topicId: String, - storyId: String - ): Intent { - val intent = Intent(context, StoryFragmentTestActivity::class.java) - intent.putExtra(INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA, internalProfileId) - intent.putExtra(TOPIC_ID_TEST_INTENT_EXTRA, topicId) - intent.putExtra(STORY_ID_TEST_INTENT_EXTRA, storyId) - return intent - } - } -} diff --git a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt deleted file mode 100644 index 4fd2083b0eb..00000000000 --- a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.oppia.app.testing - -import androidx.appcompat.app.AppCompatActivity -import org.oppia.app.R -import org.oppia.app.activity.ActivityScope -import org.oppia.app.story.StoryFragment -import javax.inject.Inject - -/** The presenter for [StoryFragmentTestActivity]. */ -@ActivityScope -class StoryFragmentTestActivityPresenter @Inject constructor( - private val activity: AppCompatActivity -) { - fun handleOnCreate() { - activity.setContentView(R.layout.story_fragment_test_activity) - if (getStoryFragment() == null) { - val internalProfileId = activity.intent.getIntExtra(INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA, -1) - val topicId = checkNotNull( - activity.intent.getStringExtra(TOPIC_ID_TEST_INTENT_EXTRA) - ) { - "Expected non-null topic ID to be passed in using extra key: $TOPIC_ID_TEST_INTENT_EXTRA" - } - val storyId = checkNotNull( - activity.intent.getStringExtra(STORY_ID_TEST_INTENT_EXTRA) - ) { - "Expected non-null story ID to be passed in using extra key: $STORY_ID_TEST_INTENT_EXTRA" - } - activity.supportFragmentManager.beginTransaction().add( - R.id.story_fragment_placeholder, - StoryFragment.newInstance(internalProfileId, topicId, storyId) - ).commitNow() - } - } - - private fun getStoryFragment(): StoryFragment? { - return activity - .supportFragmentManager - .findFragmentById( - R.id.story_fragment_placeholder - ) as StoryFragment? - } -} diff --git a/app/src/main/res/layout/story_fragment_test_activity.xml b/app/src/main/res/layout/story_fragment_test_activity.xml deleted file mode 100644 index ed7d46bb202..00000000000 --- a/app/src/main/res/layout/story_fragment_test_activity.xml +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt index 0eaa43f2291..d423d029978 100644 --- a/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt @@ -1,7 +1,9 @@ package org.oppia.app.faq +import android.app.Application import android.content.Context import android.content.res.Resources +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider @@ -17,31 +19,78 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Component import org.hamcrest.Matchers.allOf import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.help.faq.FAQListActivity import org.oppia.app.help.faq.faqsingle.FAQSingleActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [FAQListFragment]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = FAQListFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi") class FAQListFragmentTest { + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @Before fun setUp() { Intents.init() + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() } @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } @@ -100,7 +149,56 @@ class FAQListFragmentTest { } } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + private fun getResources(): Resources { return ApplicationProvider.getApplicationContext().resources } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(faqListFragmentTest: FAQListFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerFAQListFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(faqListFragmentTest: FAQListFragmentTest) { + component.inject(faqListFragmentTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } } diff --git a/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt index 9b73ecc5953..28fcb625883 100644 --- a/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt @@ -1,8 +1,10 @@ package org.oppia.app.faq +import android.app.Application import android.content.Context import android.content.Intent import android.content.res.Resources +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView @@ -10,17 +12,75 @@ import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Component +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.help.faq.faqsingle.FAQSingleActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [FAQSingleActivity]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = FAQSingleActivityTest.TestApplication::class, qualifiers = "port-xxhdpi") class FAQSingleActivityTest { + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Before + fun setUp() { + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() + } + + @After + fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + } + @Test fun openFAQSingleActivity_checkQuestion_isDisplayed() { launch(createFAQSingleActivity()).use { @@ -35,6 +95,10 @@ class FAQSingleActivityTest { } } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + private fun createFAQSingleActivity(): Intent { return FAQSingleActivity.createFAQSingleActivityIntent( ApplicationProvider.getApplicationContext(), @@ -46,4 +110,49 @@ class FAQSingleActivityTest { private fun getResources(): Resources { return ApplicationProvider.getApplicationContext().resources } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(faqSingleActivityTest: FAQSingleActivityTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerFAQSingleActivityTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(faqSingleActivityTest: FAQSingleActivityTest) { + component.inject(faqSingleActivityTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } } diff --git a/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt index 0414817b590..3967c18f1cc 100644 --- a/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt @@ -1,6 +1,8 @@ package org.oppia.app.help +import android.app.Application import android.content.Intent +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider @@ -22,26 +24,79 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp +import dagger.Component import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.help.faq.FAQListActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = HelpFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi") class HelpFragmentTest { + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Before fun setUp() { + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() Intents.init() FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) } + @After + fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + Intents.release() + } + private fun createHelpActivityIntent( internalProfileId: Int, isFromNavigationDrawer: Boolean @@ -127,8 +182,52 @@ class HelpFragmentTest { } } - @After - fun tearDown() { - Intents.release() + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(helpFragmentTest: HelpFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerHelpFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(helpFragmentTest: HelpFragmentTest) { + component.inject(helpFragmentTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt index 29cfedc0a70..f1eb0a405f3 100644 --- a/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt @@ -1,9 +1,10 @@ package org.oppia.app.mydownloads import android.app.Application -import android.content.Context import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario.launch +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.swipeLeft @@ -14,26 +15,75 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 -import dagger.BindsInstance import dagger.Component -import dagger.Module -import dagger.Provides -import kotlinx.coroutines.CoroutineDispatcher import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.instanceOf +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.utility.EspressoTestsMatchers.matchCurrentTabTitle -import org.oppia.util.threading.BackgroundDispatcher -import org.oppia.util.threading.BlockingDispatcher +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject import javax.inject.Singleton /** Tests for [MyDownloadsFragment]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = MyDownloadsFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi") class MyDownloadsFragmentTest { + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Before + fun setUp() { + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() + } + + @After + fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + } @Test fun testMyDownloadsFragment_toolbarTitle_isDisplayedSuccessfully() { @@ -132,37 +182,52 @@ class MyDownloadsFragmentTest { } } - @Module - class TestModule { - @Provides - @Singleton - fun provideContext(application: Application): Context { - return application - } - - // TODO(#89): Introduce a proper IdlingResource for background dispatchers to ensure they all complete before - // proceeding in an Espresso test. This solution should also be interoperative with Robolectric contexts by using a - // test coroutine dispatcher. - - @Singleton - @Provides - @BackgroundDispatcher - fun provideBackgroundDispatcher( - @BlockingDispatcher blockingDispatcher: CoroutineDispatcher - ): CoroutineDispatcher { - return blockingDispatcher - } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton - @Component(modules = [TestModule::class]) - interface TestApplicationComponent { + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder + interface Builder : ApplicationComponent.Builder - fun build(): TestApplicationComponent + fun inject(myDownloadsFragmentTest: MyDownloadsFragmentTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerMyDownloadsFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(myDownloadsFragmentTest: MyDownloadsFragmentTest) { + component.inject(myDownloadsFragmentTest) } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt index f5cffce698f..8426fea61f7 100644 --- a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt @@ -2,11 +2,11 @@ package org.oppia.app.parser import android.app.Activity import android.app.Application -import android.content.Context import android.content.Intent import android.text.Spannable import android.text.style.ImageSpan import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches @@ -18,10 +18,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import com.google.common.truth.Truth.assertThat import dagger.Binds -import dagger.BindsInstance import dagger.Component import dagger.Module -import dagger.Provides import org.hamcrest.Matchers.not import org.junit.After import org.junit.Before @@ -29,20 +27,49 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.testing.HtmlParserTestActivity +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers import org.oppia.testing.TestDispatcherModule -import org.oppia.util.caching.CacheAssetsLocally +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.DefaultResourceBucketName -import org.oppia.util.logging.EnableConsoleLog -import org.oppia.util.logging.EnableFileLog -import org.oppia.util.logging.GlobalLogLevel -import org.oppia.util.logging.LogLevel +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.CustomBulletSpan -import org.oppia.util.parser.DefaultGcsPrefix import org.oppia.util.parser.GlideImageLoader import org.oppia.util.parser.HtmlParser -import org.oppia.util.parser.ImageDownloadUrlTemplate +import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageLoader +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton @@ -51,10 +78,14 @@ import javax.inject.Singleton /** Tests for [HtmlParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = HtmlParserTest.TestApplication::class, qualifiers = "port-xxhdpi") class HtmlParserTest { private lateinit var launchedActivity: Activity + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var htmlParserFactory: HtmlParser.Factory @@ -70,27 +101,18 @@ class HtmlParserTest { @Before fun setUp() { setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() Intents.init() val intent = Intent(Intent.ACTION_PICK) launchedActivity = activityTestRule.launchActivity(intent) - DaggerHtmlParserTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) } @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } - private fun setUpTestApplicationComponent() { - DaggerHtmlParserTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) - } - @Test fun testHtmlContent_handleCustomOppiaTags_parsedHtmlDisplaysStyledText() { val textView = activityTestRule.activity.findViewById( @@ -232,53 +254,8 @@ class HtmlParserTest { assertThat(htmlResult.toString()).doesNotContain(" ") } - // TODO(#89): Move this to a common test application component. - @Module - class TestModule { - @Provides - @Singleton - fun provideContext(application: Application): Context { - return application - } - - @Provides - @CacheAssetsLocally - fun provideCacheAssetsLocally(): Boolean = false - - @Provides - @DefaultGcsPrefix - @Singleton - fun provideDefaultGcsPrefix(): String { - return "https://storage.googleapis.com" - } - - @Provides - @DefaultResourceBucketName - @Singleton - fun provideDefaultGcsResource(): String { - return "oppiaserver-resources" - } - - @Provides - @ImageDownloadUrlTemplate - @Singleton - fun provideImageDownloadUrlTemplate(): String { - return "%s/%s/assets/image/%s" - } - - // TODO(#59): Either isolate these to their own shared test module, or use the real logging - // module in tests to avoid needing to specify these settings for tests. - @EnableConsoleLog - @Provides - fun provideEnableConsoleLog(): Boolean = true - - @EnableFileLog - @Provides - fun provideEnableFileLog(): Boolean = false - - @GlobalLogLevel - @Provides - fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) } @Module @@ -287,17 +264,48 @@ class HtmlParserTest { abstract fun provideGlideImageLoader(impl: GlideImageLoader): ImageLoader } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton - @Component(modules = [TestModule::class, ImageTestModule::class, TestDispatcherModule::class]) - interface TestApplicationComponent { + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, ImageTestModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder + interface Builder : ApplicationComponent.Builder - fun build(): TestApplicationComponent + fun inject(htmlParserTest: HtmlParserTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerHtmlParserTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent } - fun inject(htmlParserTest: HtmlParserTest) + fun inject(htmlParserTest: HtmlParserTest) { + component.inject(htmlParserTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt index f43e299469d..6b92edfb66c 100644 --- a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt @@ -103,6 +103,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.domain.topic.PrimeTopicAssetsControllerModule @@ -126,6 +128,7 @@ import org.oppia.testing.profile.ProfileTestHelper import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -1431,7 +1434,9 @@ class StateFragmentTest { TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigFastShowTestModule::class + ApplicationStartupListenerModule::class, HintsAndSolutionConfigFastShowTestModule::class, + WorkManagerConfigurationModule::class, LogUploadWorkerModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt index 46375a9952f..6050512cce8 100644 --- a/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt @@ -8,15 +8,13 @@ import android.content.Context import android.content.Intent import android.content.res.Resources import android.net.Uri -import android.os.Handler -import android.os.Looper import android.provider.MediaStore import android.view.View +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -24,7 +22,6 @@ import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition -import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intending @@ -42,7 +39,6 @@ import androidx.test.espresso.util.HumanReadables import androidx.test.espresso.util.TreeIterables import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp -import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides @@ -55,37 +51,70 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.completedstorylist.CompletedStoryListActivity import org.oppia.app.home.recentlyplayed.RecentlyPlayedActivity import org.oppia.app.model.ProfileId import org.oppia.app.ongoingtopiclist.OngoingTopicListActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.topic.TopicActivity import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.StoryProgressTestHelper import org.oppia.domain.topic.TEST_STORY_ID_0 import org.oppia.domain.topic.TEST_TOPIC_ID_0 +import org.oppia.testing.TestAccessibilityModule import org.oppia.testing.TestCoroutineDispatchers import org.oppia.testing.TestDispatcherModule import org.oppia.testing.TestLogReportingModule import org.oppia.testing.profile.ProfileTestHelper +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.EnableConsoleLog import org.oppia.util.logging.EnableFileLog import org.oppia.util.logging.GlobalLogLevel import org.oppia.util.logging.LogLevel +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.concurrent.AbstractExecutorService -import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.inject.Inject import javax.inject.Singleton /** Tests for [ProfileProgressFragment]. */ -@Config(qualifiers = "port-xxhdpi") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config( + application = ProfileProgressFragmentTest.TestApplication::class, + qualifiers = "port-xxhdpi" +) class ProfileProgressFragmentTest { @Inject @@ -108,7 +137,7 @@ class ProfileProgressFragmentTest { fun setUp() { Intents.init() setUpTestApplicationComponent() - IdlingRegistry.getInstance().register(MainThreadExecutor.countingResource) + testCoroutineDispatchers.registerIdlingResource() profileTestHelper.initializeProfiles() profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() FirebaseApp.initializeApp(context) @@ -116,17 +145,10 @@ class ProfileProgressFragmentTest { @After fun tearDown() { - IdlingRegistry.getInstance().unregister(MainThreadExecutor.countingResource) + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } - private fun setUpTestApplicationComponent() { - DaggerProfileProgressFragmentTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) - } - private fun createProfileProgressActivityIntent(profileId: Int): Intent { return ProfileProgressActivity.createProfileProgressActivityIntent( ApplicationProvider.getApplicationContext(), @@ -137,6 +159,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragment_checkProfileName_profileNameIsCorrect() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Admin")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.profile_name_text_view) @@ -149,6 +172,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragment_configurationChange_checkProfileName_profileNameIsCorrect() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) waitForTheView(withText("Admin")) onView( @@ -162,6 +186,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragment_openProfilePictureEditDialog() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Admin")) onView( atPositionOnView( @@ -170,6 +195,7 @@ class ProfileProgressFragmentTest { R.id.profile_edit_image ) ).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog()) .check(matches(isDisplayed())) } @@ -178,6 +204,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragment_openProfilePictureEditDialog_configurationChange_dialogIsStillOpen() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Admin")) onView( atPositionOnView( @@ -186,6 +213,7 @@ class ProfileProgressFragmentTest { R.id.profile_edit_image ) ).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog()) .check(matches(isDisplayed())) onView(isRoot()).perform(orientationLandscape()) @@ -202,6 +230,7 @@ class ProfileProgressFragmentTest { val activityResult = createGalleryPickActivityResultStub() intending(expectedIntent).respondWith(activityResult) launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Admin")) onView( atPositionOnView( @@ -210,6 +239,7 @@ class ProfileProgressFragmentTest { R.id.profile_edit_image ) ).perform(click()) + testCoroutineDispatchers.runCurrent() onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog()) .check(matches(isDisplayed())) onView(withText(R.string.profile_picture_edit_alert_dialog_choose_from_library)) @@ -221,6 +251,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkOngoingTopicsCount_countIsZero() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("0")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count) @@ -242,6 +273,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("2")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count) @@ -263,7 +295,9 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText("2")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count) @@ -276,6 +310,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkOngoingTopicsString_descriptionIsCorrect() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.topics_in_progress)) onView( atPositionOnView( @@ -300,6 +335,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.topics_in_progress)) onView( atPositionOnView( @@ -324,7 +360,9 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.topics_in_progress)) onView( atPositionOnView( @@ -340,6 +378,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkCompletedStoriesCount_countIsZero() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("0")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.completed_stories_count) @@ -361,6 +400,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("2")) onView( atPositionOnView(R.id.profile_progress_list, 0, R.id.completed_stories_count) @@ -373,6 +413,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkCompletedStoriesString_descriptionIsCorrect() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.stories_completed)) onView( atPositionOnView( @@ -398,6 +439,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.stories_completed)) onView( atPositionOnView( @@ -414,7 +456,9 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivity_changeConfiguration_recyclerViewItem1_storyNameIsCorrect() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.profile_progress_list)) .perform(scrollToPosition(1)) waitForTheView(withText("First Story")) @@ -429,6 +473,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivity_recyclerViewItem1_storyNameIsCorrect() { launch(createProfileProgressActivityIntent(0)).use { + testCoroutineDispatchers.runCurrent() onView(withId(R.id.profile_progress_list)).perform( scrollToPosition( 1 @@ -449,6 +494,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivity_recyclerViewItem1_topicNameIsCorrect() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(withId(R.id.profile_progress_list)).perform( scrollToPosition( 1 @@ -469,6 +515,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivity_clickRecyclerViewItem1_intentIsCorrect() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(withId(R.id.profile_progress_list)).perform( scrollToPosition( 1 @@ -481,6 +528,7 @@ class ProfileProgressFragmentTest { 1, R.id.topic_name_text_view ) ).perform(click()) + testCoroutineDispatchers.runCurrent() intended(hasComponent(TopicActivity::class.java.name)) intended(hasExtra(TopicActivity.getProfileIdKey(), internalProfileId)) intended(hasExtra(TopicActivity.getTopicIdKey(), TEST_TOPIC_ID_0)) @@ -491,12 +539,14 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivity_recyclerViewIndex0_clickViewAll_opensRecentlyPlayedActivity() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Admin")) onView(atPositionOnView(R.id.profile_progress_list, 0, R.id.view_all_text_view)) .check( matches(withText("View All")) ) .perform(click()) + testCoroutineDispatchers.runCurrent() intended(hasComponent(RecentlyPlayedActivity::class.java.name)) intended( hasExtra( @@ -510,6 +560,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivityNoProgress_recyclerViewIndex0_clickTopicCount_isNotClickable() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.topics_in_progress)) onView( atPositionOnView( @@ -525,6 +576,7 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivityNoProgress_recyclerViewIndex0_clickStoryCount_isNotClickable() { launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.stories_completed)) onView( atPositionOnView( @@ -540,7 +592,9 @@ class ProfileProgressFragmentTest { @Test fun testProfileProgressActivityNoProgress_recyclerViewIndex0_changeConfiguration_clickStoryCount_isNotClickable() { // ktlint-disable max-line-length launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.stories_completed)) onView( atPositionOnView( @@ -565,6 +619,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.topics_in_progress)) onView( atPositionOnView( @@ -573,6 +628,7 @@ class ProfileProgressFragmentTest { R.id.ongoing_topics_container ) ).perform(click()) + testCoroutineDispatchers.runCurrent() intended(hasComponent(OngoingTopicListActivity::class.java.name)) intended( hasExtra( @@ -595,6 +651,7 @@ class ProfileProgressFragmentTest { ) testCoroutineDispatchers.runCurrent() launch(createProfileProgressActivityIntent(internalProfileId)).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText(R.string.stories_completed)) onView( atPositionOnView( @@ -603,6 +660,7 @@ class ProfileProgressFragmentTest { R.id.completed_stories_container ) ).perform(click()) + testCoroutineDispatchers.runCurrent() intended(hasComponent(CompletedStoryListActivity::class.java.name)) intended( hasExtra( @@ -675,12 +733,6 @@ class ProfileProgressFragmentTest { @Module class TestModule { - @Provides - @Singleton - fun provideContext(application: Application): Context { - return application - } - // TODO(#59): Either isolate these to their own shared test module, or use the real logging // module in tests to avoid needing to specify these settings for tests. @EnableConsoleLog @@ -696,66 +748,51 @@ class ProfileProgressFragmentTest { fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton @Component( modules = [ - TestModule::class, TestLogReportingModule::class, LogStorageModule::class, - TestDispatcherModule::class + TestModule::class, TestDispatcherModule::class, ApplicationModule::class, + ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, + MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, + NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, + ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class, + GlideImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, + QuestionModule::class, TestLogReportingModule::class, TestAccessibilityModule::class, + LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, + ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, + RatioInputModule::class, ApplicationStartupListenerModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + HintsAndSolutionConfigModule::class, FirebaseLogUploaderModule::class ] ) - interface TestApplicationComponent { + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder - - fun build(): TestApplicationComponent - } + interface Builder : ApplicationComponent.Builder fun inject(optionsFragmentTest: ProfileProgressFragmentTest) } - /* ktlint-disable max-line-length */ - // TODO(#59): Move this to a general-purpose testing library that replaces all CoroutineExecutors with an - // Espresso-enabled executor service. This service should also allow for background threads to run in both Espresso - // and Robolectric to help catch potential race conditions, rather than forcing parallel execution to be sequential - // and immediate. - // NB: This also blocks on #59 to be able to actually create a test-only library. - /** - * An executor service that schedules all [Runnable]s to run asynchronously on the main thread. This is based on: - * https://android.googlesource.com/platform/packages/apps/TV/+/android-live-tv/src/com/android/tv/util/MainThreadExecutor.java. - */ - /* ktlint-enable max-line-length */ - private object MainThreadExecutor : AbstractExecutorService() { - override fun isTerminated(): Boolean = false - - private val handler = Handler(Looper.getMainLooper()) - val countingResource = - CountingIdlingResource("main_thread_executor_counting_idling_resource") - - override fun execute(command: Runnable?) { - countingResource.increment() - handler.post { - try { - command?.run() - } finally { - countingResource.decrement() - } - } + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerProfileProgressFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent } - override fun shutdown() { - throw UnsupportedOperationException() + fun inject(optionsFragmentTest: ProfileProgressFragmentTest) { + component.inject(optionsFragmentTest) } - override fun shutdownNow(): MutableList { - throw UnsupportedOperationException() + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() } - override fun isShutdown(): Boolean = false - - override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { - throw UnsupportedOperationException() - } + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt index af49be4fb98..137d0bd23b6 100644 --- a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt @@ -4,6 +4,7 @@ import android.app.Application import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario @@ -14,25 +15,60 @@ import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import dagger.BindsInstance import dagger.Component import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.databinding.TestTextViewForIntWithDataBindingBinding import org.oppia.app.databinding.TestTextViewForStringWithDataBindingBinding import org.oppia.app.model.TestModel import org.oppia.app.model.TestModel.ModelTypeCase +import org.oppia.app.parser.HtmlParserTest +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.testing.BINDABLE_TEST_FRAGMENT_TAG import org.oppia.app.testing.BindableAdapterTestActivity import org.oppia.app.testing.BindableAdapterTestFragment import org.oppia.app.testing.BindableAdapterTestFragmentPresenter import org.oppia.app.testing.BindableAdapterTestViewModel +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule import org.oppia.testing.TestCoroutineDispatchers import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton @@ -40,6 +76,7 @@ import javax.inject.Singleton /** Tests for [BindableAdapter]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = BindableAdapterTest.TestApplication::class, qualifiers = "port-xxhdpi") class BindableAdapterTest { companion object { private val STR_VALUE_0 = TestModel.newBuilder().setStrValue("Item 0").build() @@ -54,7 +91,8 @@ class BindableAdapterTest { @Before fun setUp() { - setUpTestApplication() + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() // Ensure that the bindable fragment's test state is properly reset each time. BindableAdapterTestFragmentPresenter.testBindableAdapter = null @@ -62,6 +100,8 @@ class BindableAdapterTest { @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + // Ensure that the bindable fragment's test state is properly cleaned up. BindableAdapterTestFragmentPresenter.testBindableAdapter = null } @@ -264,11 +304,8 @@ class BindableAdapterTest { } } - private fun setUpTestApplication() { - DaggerBindableAdapterTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) } private fun createSingleViewTypeNoDataBindingBindableAdapter(): BindableAdapter { @@ -361,22 +398,48 @@ class BindableAdapterTest { return activity.supportFragmentManager.findFragmentByTag(BINDABLE_TEST_FRAGMENT_TAG) as BindableAdapterTestFragment // ktlint-disable max-line-length } - // TODO(#89): Move this to a common test application component. + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton @Component( modules = [ - TestDispatcherModule::class + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, HtmlParserTest.ImageTestModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) - interface TestApplicationComponent { + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder + interface Builder : ApplicationComponent.Builder + + fun inject(bindableAdapterTest: BindableAdapterTest) + } - fun build(): TestApplicationComponent + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerBindableAdapterTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent } - fun inject(bindableAdapterTest: BindableAdapterTest) + fun inject(bindableAdapterTest: BindableAdapterTest) { + component.inject(bindableAdapterTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt index dd35e25fc0c..66bfee987dc 100644 --- a/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt @@ -53,6 +53,8 @@ import org.oppia.domain.onboarding.AppStartupStateController import org.oppia.domain.onboarding.testing.ExpirationMetaDataRetrieverTestModule import org.oppia.domain.onboarding.testing.FakeExpirationMetaDataRetriever import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.TestAccessibilityModule @@ -62,6 +64,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -281,7 +284,9 @@ class SplashActivityTest { TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverTestModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt index c7125def4d6..5dcb0c38ec8 100644 --- a/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt @@ -1,6 +1,8 @@ package org.oppia.app.story +import android.app.Application import android.content.Intent +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider @@ -15,33 +17,81 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp +import dagger.Component import org.hamcrest.CoreMatchers.allOf import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.player.exploration.ExplorationActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.TEST_EXPLORATION_ID_1 import org.oppia.domain.topic.TEST_STORY_ID_1 import org.oppia.domain.topic.TEST_TOPIC_ID_0 +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [StoryActivity]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = StoryActivityTest.TestApplication::class, qualifiers = "port-xxhdpi") class StoryActivityTest { + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + private val internalProfileId = 0 @Before fun setUp() { + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() Intents.init() FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) } @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } @@ -54,17 +104,20 @@ class StoryActivityTest { TEST_STORY_ID_1 ) ).use { + testCoroutineDispatchers.runCurrent() onView(withId(R.id.story_chapter_list)).perform( scrollToPosition( 1 ) ) + testCoroutineDispatchers.runCurrent() onView(withId(R.id.story_chapter_list)).perform( RecyclerViewActions.actionOnItemAtPosition( 1, click() ) ) + testCoroutineDispatchers.runCurrent() intended( allOf( @@ -78,6 +131,10 @@ class StoryActivityTest { } } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + private fun createStoryActivityIntent( internalProfileId: Int, topicId: String, @@ -90,4 +147,49 @@ class StoryActivityTest { storyId ) } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(storyActivityTest: StoryActivityTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerStoryActivityTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(storyActivityTest: StoryActivityTest) { + component.inject(storyActivityTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } } diff --git a/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt index fabf3b52447..09bcc64119b 100644 --- a/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt @@ -4,14 +4,12 @@ import android.app.Application import android.content.Context import android.content.Intent import android.content.res.Resources -import android.os.Handler -import android.os.Looper import android.view.View +import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -19,7 +17,6 @@ import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition -import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent @@ -29,39 +26,65 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.util.HumanReadables import androidx.test.espresso.util.TreeIterables import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp -import dagger.BindsInstance import dagger.Component -import dagger.Module -import dagger.Provides import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matcher import org.junit.After import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.model.ProfileId import org.oppia.app.player.exploration.ExplorationActivity +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.hasItemCount -import org.oppia.app.testing.StoryFragmentTestActivity +import org.oppia.app.shim.ViewBindingShimModule import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.FRACTIONS_STORY_ID_0 import org.oppia.domain.topic.FRACTIONS_TOPIC_ID +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.StoryProgressTestHelper +import org.oppia.testing.TestAccessibilityModule import org.oppia.testing.TestCoroutineDispatchers import org.oppia.testing.TestDispatcherModule import org.oppia.testing.TestLogReportingModule import org.oppia.testing.profile.ProfileTestHelper -import org.oppia.util.logging.EnableConsoleLog -import org.oppia.util.logging.EnableFileLog -import org.oppia.util.logging.GlobalLogLevel -import org.oppia.util.logging.LogLevel +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.concurrent.AbstractExecutorService -import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.inject.Inject import javax.inject.Singleton @@ -69,6 +92,7 @@ import javax.inject.Singleton /** Tests for [StoryFragment]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = StoryFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi") class StoryFragmentTest { @Inject @@ -91,7 +115,7 @@ class StoryFragmentTest { fun setUp() { Intents.init() setUpTestApplicationComponent() - IdlingRegistry.getInstance().register(MainThreadExecutor.countingResource) + testCoroutineDispatchers.registerIdlingResource() profileTestHelper.initializeProfiles() FirebaseApp.initializeApp(context) profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() @@ -104,17 +128,10 @@ class StoryFragmentTest { @After fun tearDown() { - IdlingRegistry.getInstance().unregister(MainThreadExecutor.countingResource) + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } - private fun setUpTestApplicationComponent() { - DaggerStoryFragmentTest_TestApplicationComponent.builder() - .setApplication(ApplicationProvider.getApplicationContext()) - .build() - .inject(this) - } - private fun createStoryActivityIntent(): Intent { return StoryActivity.createStoryActivityIntent( ApplicationProvider.getApplicationContext(), @@ -125,15 +142,22 @@ class StoryFragmentTest { } @Test + @Ignore("Test wasn't correct originally") // TODO(#1804): Fix this test & re-enable. fun testStoryFragment_clickOnToolbarNavigationButton_closeActivity() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() + onView(withId(R.id.story_toolbar)).perform(click()) + testCoroutineDispatchers.runCurrent() + + it.onActivity { activity -> assertThat(activity.isFinishing).isTrue() } } } @Test fun testStoryFragment_toolbarTitle_isDisplayedSuccessfully() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(withId(R.id.story_toolbar_title)) .check(matches(withText("Matthew Goes to the Bakery"))) @@ -142,7 +166,8 @@ class StoryFragmentTest { @Test fun testStoryFragment_correctStoryCountLoadedInHeader() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() val headerString: String = getResources().getQuantityString(R.plurals.story_total_chapters, 2, 1, 2) waitForTheView(withText("Chapter 1: What is a Fraction?")) @@ -167,7 +192,8 @@ class StoryFragmentTest { @Test fun testStoryFragment_correctNumberOfStoriesLoadedInRecyclerView() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(withId(R.id.story_chapter_list)).check(hasItemCount(3)) } @@ -175,8 +201,10 @@ class StoryFragmentTest { @Test fun testStoryFragment_changeConfiguration_textViewIsShownCorrectly() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(allOf(withId(R.id.story_chapter_list))).perform( scrollToPosition( @@ -193,8 +221,10 @@ class StoryFragmentTest { @Test fun testStoryFragment_chapterSummaryIsShownCorrectly() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) + testCoroutineDispatchers.runCurrent() onView(allOf(withId(R.id.story_chapter_list))).perform( scrollToPosition( 1 @@ -210,8 +240,10 @@ class StoryFragmentTest { @Test fun testStoryFragment_changeConfiguration_chapterSummaryIsShownCorrectly() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(allOf(withId(R.id.story_chapter_list))).perform( scrollToPosition( @@ -228,9 +260,11 @@ class StoryFragmentTest { @Test fun testStoryFragment_changeConfiguration_explorationStartCorrectly() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() waitForTheView(withText("Chapter 1: What is a Fraction?")) onView(allOf(withId(R.id.story_chapter_list))).perform( scrollToPosition( @@ -240,14 +274,17 @@ class StoryFragmentTest { onView( atPositionOnView(R.id.story_chapter_list, 1, R.id.story_chapter_card) ).perform(click()) + testCoroutineDispatchers.runCurrent() intended(hasComponent(ExplorationActivity::class.java.name)) } } @Test fun testStoryFragment_changeConfiguration_correctStoryCountInHeader() { - launch(createStoryActivityIntent()).use { + launch(createStoryActivityIntent()).use { + testCoroutineDispatchers.runCurrent() onView(isRoot()).perform(orientationLandscape()) + testCoroutineDispatchers.runCurrent() val headerString: String = getResources().getQuantityString(R.plurals.story_total_chapters, 2, 1, 2) onView(withId(R.id.story_chapter_list)).perform( @@ -275,6 +312,10 @@ class StoryFragmentTest { } } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + private fun getResources(): Resources { return ApplicationProvider.getApplicationContext().resources } @@ -327,88 +368,48 @@ class StoryFragmentTest { } } - @Module - class TestModule { - @Provides - @Singleton - fun provideContext(application: Application): Context { - return application - } - - // TODO(#59): Either isolate these to their own shared test module, or use the real logging - // module in tests to avoid needing to specify these settings for tests. - @EnableConsoleLog - @Provides - fun provideEnableConsoleLog(): Boolean = true - - @EnableFileLog - @Provides - fun provideEnableFileLog(): Boolean = false - - @GlobalLogLevel - @Provides - fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE - } - + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. @Singleton @Component( modules = [ - TestModule::class, TestLogReportingModule::class, LogStorageModule::class, - TestDispatcherModule::class + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) - interface TestApplicationComponent { + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { @Component.Builder - interface Builder { - @BindsInstance - fun setApplication(application: Application): Builder - - fun build(): TestApplicationComponent - } + interface Builder : ApplicationComponent.Builder fun inject(storyFragmentTest: StoryFragmentTest) } - // TODO(#59): Move this to a general-purpose testing library that replaces all CoroutineExecutors with an - // Espresso-enabled executor service. This service should also allow for background threads to run in both Espresso - // and Robolectric to help catch potential race conditions, rather than forcing parallel execution to be sequential - // and immediate. - // NB: This also blocks on #59 to be able to actually create a test-only library. - - /** - * An executor service that schedules all [Runnable]s to run asynchronously on the main thread. This is based on: - * https://android.googlesource.com/platform/packages/apps/TV/+/android-live-tv/src/com/android/tv/util/MainThreadExecutor.java. - */ - private object MainThreadExecutor : AbstractExecutorService() { - override fun isTerminated(): Boolean = false - - private val handler = Handler(Looper.getMainLooper()) - val countingResource = - CountingIdlingResource("main_thread_executor_counting_idling_resource") - - override fun execute(command: Runnable?) { - countingResource.increment() - handler.post { - try { - command?.run() - } finally { - countingResource.decrement() - } - } + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerStoryFragmentTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent } - override fun shutdown() { - throw UnsupportedOperationException() + fun inject(storyFragmentTest: StoryFragmentTest) { + component.inject(storyFragmentTest) } - override fun shutdownNow(): MutableList { - throw UnsupportedOperationException() + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() } - override fun isShutdown(): Boolean = false - - override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean { - throw UnsupportedOperationException() - } + override fun getApplicationInjector(): ApplicationInjector = component } } diff --git a/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt index 7141ee85e63..40ce8643692 100644 --- a/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt @@ -67,6 +67,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionCountPerTrainingSession import org.oppia.domain.question.QuestionTrainingSeed import org.oppia.domain.topic.FRACTIONS_SKILL_ID_0 @@ -80,6 +82,7 @@ import org.oppia.testing.profile.ProfileTestHelper import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -346,7 +349,9 @@ class QuestionPlayerActivityTest { TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, ApplicationStartupListenerModule::class, - RatioInputModule::class, HintsAndSolutionConfigFastShowTestModule::class + RatioInputModule::class, HintsAndSolutionConfigFastShowTestModule::class, + WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class, + LogUploadWorkerModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt index be3a9b15ccd..508700d46d1 100644 --- a/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt @@ -1,25 +1,69 @@ package org.oppia.app.utility +import android.app.Application import android.content.Context +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.model.RatioExpression +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.GlideImageLoaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [RatioExtensions]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = RatioExtensionsTest.TestApplication::class, qualifiers = "port-xxhdpi") class RatioExtensionsTest { - private lateinit var context: Context + @Inject + lateinit var context: Context @Before fun setUp() { - context = ApplicationProvider.getApplicationContext() + setUpTestApplicationComponent() } @Test @@ -42,7 +86,56 @@ class RatioExtensionsTest { ).isEqualTo("1 to 2") } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + private fun createRatio(element: List): RatioExpression { return RatioExpression.newBuilder().addAllRatioComponent(element).build() } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(ratioExtensionsTest: RatioExtensionsTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerRatioExtensionsTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(ratioExtensionsTest: RatioExtensionsTest) { + component.inject(ratioExtensionsTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } } diff --git a/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt index db734e56dec..97ff0f031ac 100644 --- a/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt @@ -36,6 +36,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.FakeEventLogger @@ -45,6 +47,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -111,7 +114,9 @@ class HomeActivityLocalTest { ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt index d7526d1fafb..834a8cd5d54 100644 --- a/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt +++ b/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt @@ -1,14 +1,55 @@ package org.oppia.app.parser +import android.app.Application import android.content.Context +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.app.activity.ActivityComponent +import org.oppia.app.application.ActivityComponentFactory +import org.oppia.app.application.ApplicationComponent +import org.oppia.app.application.ApplicationInjector +import org.oppia.app.application.ApplicationInjectorProvider +import org.oppia.app.application.ApplicationModule +import org.oppia.app.application.ApplicationStartupListenerModule import org.oppia.app.model.RatioExpression +import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.app.shim.ViewBindingShimModule +import org.oppia.domain.classify.InteractionsModule +import org.oppia.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.domain.question.QuestionModule +import org.oppia.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.testing.TestAccessibilityModule +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.caching.testing.CachingTestModule +import org.oppia.util.gcsresource.GcsResourceModule +import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.util.parser.HtmlParserEntityTypeModule +import org.oppia.util.parser.ImageParsingModule +import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton import kotlin.reflect.KClass import kotlin.reflect.full.cast import kotlin.test.fail @@ -16,14 +57,16 @@ import kotlin.test.fail /** Tests for [StringToRatioParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) +@Config(application = StringToRatioParserTest.TestApplication::class, qualifiers = "port-xxhdpi") class StringToRatioParserTest { + @Inject lateinit var context: Context + private lateinit var stringToRatioParser: StringToRatioParser - private lateinit var context: Context @Before fun setUp() { - context = ApplicationProvider.getApplicationContext() + setUpTestApplicationComponent() stringToRatioParser = StringToRatioParser() } @@ -131,6 +174,10 @@ class StringToRatioParserTest { .contains("Incorrectly formatted ratio: a:b:c") } + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + // TODO(#89): Move to a common test library. private fun assertThrows(type: KClass, operation: () -> Unit): T { try { @@ -148,4 +195,49 @@ class StringToRatioParserTest { private fun createRatio(element: List): RatioExpression { return RatioExpression.newBuilder().addAllRatioComponent(element).build() } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, HtmlParserTest.ImageTestModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(stringToRatioParserTest: StringToRatioParserTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerStringToRatioParserTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(stringToRatioParserTest: StringToRatioParserTest) { + component.inject(stringToRatioParserTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } } diff --git a/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt index 3d4332fc900..cb2a95ae0be 100644 --- a/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt @@ -36,6 +36,8 @@ import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.exploration.ExplorationDataController import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.TEST_EXPLORATION_ID_2 @@ -48,6 +50,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.networking.NetworkConnectionUtil import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule @@ -148,7 +151,9 @@ class ExplorationActivityLocalTest { ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt index 7434783724d..5b7885579b3 100644 --- a/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt @@ -77,6 +77,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.domain.topic.PrimeTopicAssetsControllerModule @@ -91,6 +93,7 @@ import org.oppia.testing.profile.ProfileTestHelper import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -1138,7 +1141,9 @@ class StateFragmentLocalTest { TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt index 8e73c5f09d7..26b97d70d23 100644 --- a/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt @@ -38,6 +38,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.FakeEventLogger @@ -47,6 +49,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -112,7 +115,9 @@ class ProfileChooserFragmentLocalTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt index 7986a7f765d..acd80d853a7 100644 --- a/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt @@ -37,6 +37,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.FakeEventLogger @@ -46,6 +48,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -128,7 +131,9 @@ class StoryActivityLocalTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt index a574039584a..924e4488eda 100644 --- a/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt +++ b/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt @@ -48,6 +48,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.TestAccessibilityModule @@ -59,6 +61,7 @@ import org.oppia.util.caching.CacheAssetsLocally import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -218,7 +221,9 @@ class AppLanguageFragmentTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, ApplicationStartupListenerModule::class, - RatioInputModule::class, HintsAndSolutionConfigModule::class + RatioInputModule::class, HintsAndSolutionConfigModule::class, + WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class, + LogUploadWorkerModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt index 4c95046fcec..7f7c35c0f1b 100644 --- a/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt +++ b/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt @@ -48,6 +48,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.TestAccessibilityModule @@ -59,6 +61,7 @@ import org.oppia.util.caching.CacheAssetsLocally import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -219,7 +222,9 @@ class DefaultAudioFragmentTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, ApplicationStartupListenerModule::class, - RatioInputModule::class, HintsAndSolutionConfigModule::class + RatioInputModule::class, HintsAndSolutionConfigModule::class, + WorkManagerConfigurationModule::class, LogUploadWorkerModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt index 715d1b4832c..100c5537a4d 100644 --- a/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt @@ -56,6 +56,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.TestAccessibilityModule @@ -67,6 +69,7 @@ import org.oppia.util.caching.CacheAssetsLocally import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -232,7 +235,9 @@ class ReadingTextSizeFragmentTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, ApplicationStartupListenerModule::class, - RatioInputModule::class, HintsAndSolutionConfigModule::class + RatioInputModule::class, HintsAndSolutionConfigModule::class, + WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class, + LogUploadWorkerModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt index 4393e87ec40..7b63cf96382 100644 --- a/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt +++ b/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt @@ -42,6 +42,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.TEST_EXPLORATION_ID_4 @@ -56,6 +58,7 @@ import org.oppia.util.accessibility.FakeAccessibilityManager import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -170,7 +173,9 @@ class StateFragmentAccessibilityTest { ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class, ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt index bb4f2998136..9657ac55aeb 100644 --- a/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt @@ -35,6 +35,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.FakeEventLogger @@ -44,6 +46,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -116,7 +119,9 @@ class TopicInfoFragmentLocalTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt index 5199c679aa3..14c7d10b147 100644 --- a/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt @@ -34,6 +34,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.testing.FakeEventLogger @@ -43,6 +45,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -118,7 +121,9 @@ class TopicLessonsFragmentLocalTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt index 4d827c6ba96..5c16d901250 100644 --- a/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt @@ -52,6 +52,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.domain.topic.TEST_SKILL_ID_1 @@ -63,6 +65,7 @@ import org.oppia.testing.profile.ProfileTestHelper import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -271,7 +274,9 @@ class QuestionPlayerActivityLocalTest { TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, ApplicationStartupListenerModule::class, - RatioInputModule::class, HintsAndSolutionConfigModule::class + RatioInputModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt index ed169c20dac..afc021b45a4 100644 --- a/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt @@ -34,6 +34,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.domain.classify.rules.textinput.TextInputRuleModule import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule import org.oppia.domain.oppialogger.LogStorageModule +import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule import org.oppia.domain.question.QuestionModule import org.oppia.domain.topic.FRACTIONS_TOPIC_ID import org.oppia.domain.topic.PrimeTopicAssetsControllerModule @@ -45,6 +47,7 @@ import org.oppia.testing.TestLogReportingModule import org.oppia.util.caching.testing.CachingTestModule import org.oppia.util.gcsresource.GcsResourceModule import org.oppia.util.logging.LoggerModule +import org.oppia.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.util.parser.GlideImageLoaderModule import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.util.parser.ImageParsingModule @@ -108,7 +111,9 @@ class RevisionCardActivityLocalTest { ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, - ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class + ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class ] ) interface TestApplicationComponent : ApplicationComponent, ApplicationInjector { diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 43b3ea816d3..8993b7f16c1 100644 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -18,6 +18,7 @@ kt_android_library( deps = [ ":dagger", "//data:persistent_cache_store", + artifact("androidx.work:work-runtime-ktx:2.4.0"), ], visibility = ["//visibility:public"], ) @@ -31,6 +32,7 @@ TEST_DEPS = [ "@robolectric//bazel:android-all", artifact("androidx.arch.core:core-testing"), artifact("androidx.test.ext:junit"), + artifact("androidx.work:work-testing:2.4.0"), artifact("com.google.truth:truth"), artifact("org.jetbrains.kotlin:kotlin-test-junit"), artifact("org.jetbrains.kotlin:kotlin-reflect"), diff --git a/domain/build.gradle b/domain/build.gradle index 6a75af33510..a2294c48bd3 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -48,6 +48,7 @@ dependencies { 'androidx.appcompat:appcompat:1.0.2', 'androidx.exifinterface:exifinterface:1.0.0-rc01', 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', + 'androidx.work:work-runtime-ktx:2.4.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', 'com.google.firebase:firebase-crashlytics:17.0.0', @@ -57,6 +58,7 @@ dependencies { 'android.arch.core:core-testing:1.1.1', 'androidx.test.espresso:espresso-core:3.2.0', 'androidx.test.ext:junit:1.1.1', + 'androidx.work:work-testing:2.4.0', 'com.google.dagger:dagger:2.24', 'com.google.truth:truth:0.43', 'junit:junit:4.12', diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt b/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt index 1c5f5b10b61..4ec6b713b80 100644 --- a/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt +++ b/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt @@ -1,14 +1,12 @@ package org.oppia.domain.oppialogger.analytics -import androidx.lifecycle.LiveData import org.oppia.app.model.EventLog import org.oppia.app.model.EventLog.EventAction import org.oppia.app.model.EventLog.Priority import org.oppia.app.model.OppiaEventLogs import org.oppia.data.persistence.PersistentCacheStore import org.oppia.domain.oppialogger.EventLogStorageCacheSize -import org.oppia.util.data.AsyncResult -import org.oppia.util.data.DataProviders +import org.oppia.util.data.DataProvider import org.oppia.util.logging.ConsoleLogger import org.oppia.util.logging.EventLogger import org.oppia.util.logging.ExceptionLogger @@ -23,7 +21,6 @@ import javax.inject.Inject class AnalyticsController @Inject constructor( private val eventLogger: EventLogger, cacheStoreFactory: PersistentCacheStore.Factory, - private val dataProviders: DataProviders, private val consoleLogger: ConsoleLogger, private val networkConnectionUtil: NetworkConnectionUtil, private val exceptionLogger: ExceptionLogger, @@ -154,11 +151,32 @@ class AnalyticsController @Inject constructor( oppiaEventLogs.eventLogList.withIndex() .minBy { it.value.timestamp }?.index + /** Returns a data provider for log reports that have been recorded for upload. */ + fun getEventLogStore(): DataProvider { + return eventLogStore + } + /** - * Returns a [LiveData] result which can be used to get [OppiaEventLogs] - * for the purpose of uploading in the presence of network connectivity. + * Returns a list of event log reports that have been recorded for upload. + * + * As we are using the await call on the deferred output of readDataAsync, the failure case would be caught and it'll throw an error. */ - fun getEventLogs(): LiveData> { - return dataProviders.convertToLiveData(eventLogStore) + suspend fun getEventLogStoreList(): MutableList { + return eventLogStore.readDataAsync().await().eventLogList + } + + /** Removes the first event log report that had been recorded for upload. */ + fun removeFirstEventLogFromStore() { + eventLogStore.storeDataAsync(updateInMemoryCache = true) { oppiaEventLogs -> + return@storeDataAsync oppiaEventLogs.toBuilder().removeEventLog(0).build() + }.invokeOnCompletion { + it?.let { + consoleLogger.e( + "Analytics Controller", + "Failed to remove event log", + it + ) + } + } } } diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt b/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt index 6d6a0068399..b4b3e4face1 100644 --- a/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt +++ b/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt @@ -1,6 +1,5 @@ package org.oppia.domain.oppialogger.exceptions -import androidx.lifecycle.LiveData import org.oppia.app.model.ExceptionLog import org.oppia.app.model.ExceptionLog.ExceptionType import org.oppia.app.model.OppiaExceptionLogs @@ -159,13 +158,34 @@ class ExceptionsController @Inject constructor( oppiaExceptionLogs.exceptionLogList.withIndex() .minBy { it.value.timestampInMillis }?.index - /** - * Returns a [LiveData] result which can be used to get [OppiaExceptionLogs] - * for the purpose of uploading in the presence of network connectivity. - */ + /** Returns a data provider for exception log reports that have been recorded for upload. */ fun getExceptionLogStore(): DataProvider { return exceptionLogStore } + + /** + * Returns a list of exception log reports which have been recorded for upload. + * + * As we are using the await call on the deferred output of readDataAsync, the failure case would be caught and it'll throw an error. + */ + suspend fun getExceptionLogStoreList(): MutableList { + return exceptionLogStore.readDataAsync().await().exceptionLogList + } + + /** Removes the first exception log report that had been recorded for upload. */ + fun removeFirstExceptionLogFromStore() { + exceptionLogStore.storeDataAsync(updateInMemoryCache = true) { oppiaExceptionLogs -> + return@storeDataAsync oppiaExceptionLogs.toBuilder().removeExceptionLog(0).build() + }.invokeOnCompletion { + it?.let { + consoleLogger.e( + "Analytics Controller", + "Failed to remove event log", + it + ) + } + } + } } /** Returns an [Exception] for an [ExceptionLog] object. */ diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt new file mode 100644 index 00000000000..604c96e713c --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt @@ -0,0 +1,74 @@ +package org.oppia.domain.oppialogger.loguploader + +import android.content.Context +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import org.oppia.domain.oppialogger.ApplicationStartupListener +import org.oppia.util.logging.LogUploader +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +/** Enqueues unique periodic work requests for uploading events and exceptions to the remote service on application creation. */ +@Singleton +class LogUploadWorkManagerInitializer @Inject constructor( + private val context: Context, + private val logUploader: LogUploader +) : ApplicationStartupListener { + + private val logUploadWorkerConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + private val workerCaseForUploadingEvents: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EVENT_WORKER + ) + .build() + + private val workerCaseForUploadingExceptions: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EXCEPTION_WORKER + ) + .build() + + private val workRequestForUploadingEvents: PeriodicWorkRequest = PeriodicWorkRequest + .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) + .setInputData(workerCaseForUploadingEvents) + .setConstraints(logUploadWorkerConstraints) + .build() + + private val workRequestForUploadingExceptions: PeriodicWorkRequest = PeriodicWorkRequest + .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS) + .setInputData(workerCaseForUploadingExceptions) + .setConstraints(logUploadWorkerConstraints) + .build() + + override fun onCreate() { + val workManager = WorkManager.getInstance(context) + logUploader.enqueueWorkRequestForEvents(workManager, workRequestForUploadingEvents) + logUploader.enqueueWorkRequestForExceptions(workManager, workRequestForUploadingExceptions) + } + + /** Returns the worker constraints set for the log uploading work requests. */ + fun getLogUploadWorkerConstraints(): Constraints = logUploadWorkerConstraints + + /** Returns the [UUID] of the work request that is enqueued for uploading event logs. */ + fun getWorkRequestForEventsId(): UUID = workRequestForUploadingEvents.id + + /** Returns the [UUID] of the work request that is enqueued for uploading exception logs. */ + fun getWorkRequestForExceptionsId(): UUID = workRequestForUploadingExceptions.id + + /** Returns the [Data] that goes into the work request that is enqueued for uploading event logs. */ + fun getWorkRequestDataForEvents(): Data = workerCaseForUploadingEvents + + /** Returns the [Data] that goes into the work request that is enqueued for uploading exception logs. */ + fun getWorkRequestDataForExceptions(): Data = workerCaseForUploadingExceptions +} diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt new file mode 100644 index 00000000000..d0c1754bd6f --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt @@ -0,0 +1,105 @@ +package org.oppia.domain.oppialogger.loguploader + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.oppia.domain.oppialogger.analytics.AnalyticsController +import org.oppia.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.domain.oppialogger.exceptions.toException +import org.oppia.util.logging.ConsoleLogger +import org.oppia.util.logging.EventLogger +import org.oppia.util.logging.ExceptionLogger +import org.oppia.util.threading.BackgroundDispatcher +import javax.inject.Inject + +/** Worker class that extracts log reports from the cache store and logs them to the remote service. */ +class LogUploadWorker private constructor( + context: Context, + params: WorkerParameters, + private val analyticsController: AnalyticsController, + private val exceptionsController: ExceptionsController, + private val exceptionLogger: ExceptionLogger, + private val eventLogger: EventLogger, + private val consoleLogger: ConsoleLogger, + @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher +) : CoroutineWorker(context, params) { + + companion object { + const val WORKER_CASE_KEY = "worker_case_key" + const val TAG = "LogUploadWorker.tag" + const val EVENT_WORKER = "event_worker" + const val EXCEPTION_WORKER = "exception_worker" + } + + override suspend fun doWork(): Result { + return when (inputData.getString(WORKER_CASE_KEY)) { + EVENT_WORKER -> { + withContext(backgroundDispatcher) { uploadEvents() } + } + EXCEPTION_WORKER -> { + withContext(backgroundDispatcher) { uploadExceptions() } + } + else -> Result.failure() + } + } + + /** Extracts exception logs from the cache store and logs them to the remote service. */ + private suspend fun uploadExceptions(): Result { + return try { + val exceptionLogs = exceptionsController.getExceptionLogStoreList() + exceptionLogs.let { + for (exceptionLog in it) { + exceptionLogger.logException(exceptionLog.toException()) + exceptionsController.removeFirstExceptionLogFromStore() + } + } + Result.success() + } catch (e: Exception) { + consoleLogger.e(TAG, e.toString(), e) + Result.failure() + } + } + + /** Extracts event logs from the cache store and logs them to the remote service. */ + private suspend fun uploadEvents(): Result { + return try { + val eventLogs = analyticsController.getEventLogStoreList() + eventLogs.let { + for (eventLog in it) { + eventLogger.logEvent(eventLog) + analyticsController.removeFirstEventLogFromStore() + } + } + Result.success() + } catch (e: Exception) { + consoleLogger.e(TAG, "Failed to upload events", e) + Result.failure() + } + } + + /** Creates an instance of [LogUploadWorker] by properly injecting dependencies. */ + class Factory @Inject constructor( + private val analyticsController: AnalyticsController, + private val exceptionsController: ExceptionsController, + private val exceptionLogger: ExceptionLogger, + private val eventLogger: EventLogger, + private val consoleLogger: ConsoleLogger, + @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher + ) { + + fun create(context: Context, params: WorkerParameters): CoroutineWorker { + return LogUploadWorker( + context, + params, + analyticsController, + exceptionsController, + exceptionLogger, + eventLogger, + consoleLogger, + backgroundDispatcher + ) + } + } +} diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt new file mode 100644 index 00000000000..caf751cb63d --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt @@ -0,0 +1,22 @@ +package org.oppia.domain.oppialogger.loguploader + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import javax.inject.Inject + +/** Custom [WorkerFactory] for the [LogUploadWorker]. */ +class LogUploadWorkerFactory @Inject constructor( + private val workerFactory: LogUploadWorker.Factory +) : WorkerFactory() { + + /** Returns a new [LogUploadWorker] for the given context and parameters. */ + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + return workerFactory.create(appContext, workerParameters) + } +} diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt new file mode 100644 index 00000000000..1f3fb412864 --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt @@ -0,0 +1,17 @@ +package org.oppia.domain.oppialogger.loguploader + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import org.oppia.domain.oppialogger.ApplicationStartupListener + +/** Provides [LogUploadWorker] related dependencies. */ +@Module +interface LogUploadWorkerModule { + + @Binds + @IntoSet + fun bindLogUploadWorkRequest( + logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer + ): ApplicationStartupListener +} diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt new file mode 100644 index 00000000000..7617554770b --- /dev/null +++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt @@ -0,0 +1,19 @@ +package org.oppia.domain.oppialogger.loguploader + +import androidx.work.Configuration +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +/** Provides [Configuration] for the work manager. */ +@Module +class WorkManagerConfigurationModule { + + @Singleton + @Provides + fun provideWorkManagerConfiguration( + logUploadWorkerFactory: LogUploadWorkerFactory + ): Configuration { + return Configuration.Builder().setWorkerFactory(logUploadWorkerFactory).build() + } +} diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 1a98b9875a5..90efa104d5f 100644 --- a/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -38,6 +38,7 @@ import org.oppia.testing.TestCoroutineDispatchers import org.oppia.testing.TestDispatcherModule import org.oppia.testing.TestLogReportingModule import org.oppia.util.data.AsyncResult +import org.oppia.util.data.DataProviders import org.oppia.util.logging.EnableConsoleLog import org.oppia.util.logging.EnableFileLog import org.oppia.util.logging.GlobalLogLevel @@ -82,6 +83,9 @@ class AnalyticsControllerTest { @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject + lateinit var dataProviders: DataProviders + @Mock lateinit var mockOppiaEventLogsObserver: Observer> @@ -366,7 +370,7 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogs() + val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) eventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( @@ -396,7 +400,7 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogs() + val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) eventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( @@ -417,7 +421,7 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val eventLogs = analyticsController.getEventLogs() + val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) eventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( @@ -453,7 +457,7 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogs() + val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) eventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( @@ -494,7 +498,7 @@ class AnalyticsControllerTest { ) ) - val cachedEventLogs = analyticsController.getEventLogs() + val cachedEventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( @@ -523,7 +527,7 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val cachedEventLogs = analyticsController.getEventLogs() + val cachedEventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore()) cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) testCoroutineDispatchers.advanceUntilIdle() verify( diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt new file mode 100644 index 00000000000..2e8df398651 --- /dev/null +++ b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt @@ -0,0 +1,255 @@ +package org.oppia.domain.oppialogger.loguploader + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Configuration +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import com.google.common.truth.Truth.assertThat +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.domain.oppialogger.EventLogStorageCacheSize +import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize +import org.oppia.domain.oppialogger.OppiaLogger +import org.oppia.domain.oppialogger.analytics.AnalyticsController +import org.oppia.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.testing.FakeEventLogger +import org.oppia.testing.FakeExceptionLogger +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.data.DataProviders +import org.oppia.util.logging.EnableConsoleLog +import org.oppia.util.logging.EnableFileLog +import org.oppia.util.logging.GlobalLogLevel +import org.oppia.util.logging.LogLevel +import org.oppia.util.logging.LogUploader +import org.oppia.util.networking.NetworkConnectionUtil +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.collections.last + +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class LogUploadWorkManagerInitializerTest { + + @Inject + lateinit var logUploadWorkerFactory: LogUploadWorkerFactory + + @Inject + lateinit var logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer + + @Inject + lateinit var analyticsController: AnalyticsController + + @Inject + lateinit var exceptionsController: ExceptionsController + + @Inject + lateinit var networkConnectionUtil: NetworkConnectionUtil + + @Inject + lateinit var fakeEventLogger: FakeEventLogger + + @Inject + lateinit var fakeExceptionLogger: FakeExceptionLogger + + @Inject + lateinit var dataProviders: DataProviders + + @Inject + lateinit var oppiaLogger: OppiaLogger + + @Inject + lateinit var fakeLogUploader: FakeLogUploader + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + private lateinit var context: Context + + @Before + fun setUp() { + networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext()) + setUpTestApplicationComponent() + context = InstrumentationRegistry.getInstrumentation().targetContext + val config = Configuration.Builder() + .setExecutor(SynchronousExecutor()) + .setWorkerFactory(logUploadWorkerFactory) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + } + + @Test + fun testWorkRequest_onCreate_enqueuesRequest_verifyRequestId() { + logUploadWorkManagerInitializer.onCreate() + testCoroutineDispatchers.runCurrent() + + val enqueuedEventWorkRequestId = logUploadWorkManagerInitializer.getWorkRequestForEventsId() + val enqueuedExceptionWorkRequestId = + logUploadWorkManagerInitializer.getWorkRequestForExceptionsId() + + assertThat(fakeLogUploader.getMostRecentEventRequestId()).isEqualTo(enqueuedEventWorkRequestId) + assertThat(fakeLogUploader.getMostRecentExceptionRequestId()).isEqualTo( + enqueuedExceptionWorkRequestId + ) + } + + @Test + fun testWorkRequest_verifyWorkerConstraints() { + val workerConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + val logUploadingWorkRequestConstraints = + logUploadWorkManagerInitializer.getLogUploadWorkerConstraints() + + assertThat(logUploadingWorkRequestConstraints).isEqualTo(workerConstraints) + } + + @Test + fun testWorkRequest_verifyWorkRequestDataForEvents() { + val workerCaseForUploadingEvents: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EVENT_WORKER + ) + .build() + + assertThat(logUploadWorkManagerInitializer.getWorkRequestDataForEvents()).isEqualTo( + workerCaseForUploadingEvents + ) + } + + @Test + fun testWorkRequest_verifyWorkRequestDataForExceptions() { + val workerCaseForUploadingExceptions: Data = Data.Builder() + .putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EXCEPTION_WORKER + ) + .build() + + assertThat(logUploadWorkManagerInitializer.getWorkRequestDataForExceptions()).isEqualTo( + workerCaseForUploadingExceptions + ) + } + + private fun setUpTestApplicationComponent() { + DaggerLogUploadWorkManagerInitializerTest_TestApplicationComponent.builder() + .setApplication(ApplicationProvider.getApplicationContext()) + .build() + .inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + @Module + class TestLogStorageModule { + + @Provides + @EventLogStorageCacheSize + fun provideEventLogStorageCacheSize(): Int = 2 + + @Provides + @ExceptionLogStorageCacheSize + fun provideExceptionLogStorageSize(): Int = 2 + } + + @Module + interface TestFirebaseLogUploaderModule { + + @Binds + fun bindsFakeLogUploader(fakeLogUploader: FakeLogUploader): LogUploader + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, TestLogReportingModule::class, + TestLogStorageModule::class, TestDispatcherModule::class, + LogUploadWorkerModule::class, TestFirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + fun build(): TestApplicationComponent + } + + fun inject(logUploadWorkRequestTest: LogUploadWorkManagerInitializerTest) + } +} + +/** A test specific fake for the log uploader. */ +@Singleton +class FakeLogUploader @Inject constructor() : + LogUploader { + + private val eventRequestIdList = mutableListOf() + private val exceptionRequestIdList = mutableListOf() + + override fun enqueueWorkRequestForEvents( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + eventRequestIdList.add(workRequest.id) + } + + override fun enqueueWorkRequestForExceptions( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + exceptionRequestIdList.add(workRequest.id) + } + + /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */ + fun getMostRecentEventRequestId() = eventRequestIdList.last() + + /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */ + fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last() +} diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt new file mode 100644 index 00000000000..86c1ad627e5 --- /dev/null +++ b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt @@ -0,0 +1,236 @@ +package org.oppia.domain.oppialogger.loguploader + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Configuration +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import com.google.common.truth.Truth.assertThat +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.app.model.EventLog +import org.oppia.domain.oppialogger.EventLogStorageCacheSize +import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize +import org.oppia.domain.oppialogger.OppiaLogger +import org.oppia.domain.oppialogger.analytics.AnalyticsController +import org.oppia.domain.oppialogger.analytics.TEST_TIMESTAMP +import org.oppia.domain.oppialogger.analytics.TEST_TOPIC_ID +import org.oppia.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.testing.FakeEventLogger +import org.oppia.testing.FakeExceptionLogger +import org.oppia.testing.TestCoroutineDispatchers +import org.oppia.testing.TestDispatcherModule +import org.oppia.testing.TestLogReportingModule +import org.oppia.util.data.DataProviders +import org.oppia.util.logging.EnableConsoleLog +import org.oppia.util.logging.EnableFileLog +import org.oppia.util.logging.GlobalLogLevel +import org.oppia.util.logging.LogLevel +import org.oppia.util.logging.LogUploader +import org.oppia.util.networking.NetworkConnectionUtil +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class LogUploadWorkerTest { + + @Inject + lateinit var networkConnectionUtil: NetworkConnectionUtil + + @Inject + lateinit var fakeEventLogger: FakeEventLogger + + @Inject + lateinit var fakeExceptionLogger: FakeExceptionLogger + + @Inject + lateinit var oppiaLogger: OppiaLogger + + @Inject + lateinit var analyticsController: AnalyticsController + + @Inject + lateinit var exceptionsController: ExceptionsController + + @Inject + lateinit var logUploadWorkerFactory: LogUploadWorkerFactory + + @Inject + lateinit var dataProviders: DataProviders + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + private lateinit var context: Context + + private val eventLogTopicContext = EventLog.newBuilder() + .setActionName(EventLog.EventAction.EVENT_ACTION_UNSPECIFIED) + .setContext( + EventLog.Context.newBuilder() + .setTopicContext( + EventLog.TopicContext.newBuilder() + .setTopicId(TEST_TOPIC_ID) + .build() + ) + .build() + ) + .setPriority(EventLog.Priority.ESSENTIAL) + .setTimestamp(TEST_TIMESTAMP) + .build() + + private val exception = Exception("TEST") + + @Before + fun setUp() { + networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext()) + setUpTestApplicationComponent() + context = InstrumentationRegistry.getInstrumentation().targetContext + val config = Configuration.Builder() + .setExecutor(SynchronousExecutor()) + .setWorkerFactory(logUploadWorkerFactory) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + } + + @Test + fun testWorker_logEvent_withoutNetwork_enqueueRequest_verifySuccess() { + networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE) + analyticsController.logTransitionEvent( + eventLogTopicContext.timestamp, + eventLogTopicContext.actionName, + oppiaLogger.createTopicContext(TEST_TOPIC_ID) + ) + + val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext()) + + val inputData = Data.Builder().putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EVENT_WORKER + ).build() + + val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + + workManager.enqueue(request) + testCoroutineDispatchers.runCurrent() + val workInfo = workManager.getWorkInfoById(request.id) + + assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) + assertThat(fakeEventLogger.getMostRecentEvent()).isEqualTo(eventLogTopicContext) + } + + @Test + fun testWorker_logException_withoutNetwork_enqueueRequest_verifySuccess() { + networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE) + exceptionsController.logNonFatalException(exception, TEST_TIMESTAMP) + + val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext()) + + val inputData = Data.Builder().putString( + LogUploadWorker.WORKER_CASE_KEY, + LogUploadWorker.EXCEPTION_WORKER + ).build() + + val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + workManager.enqueue(request) + testCoroutineDispatchers.runCurrent() + val workInfo = workManager.getWorkInfoById(request.id) + val exceptionGot = fakeExceptionLogger.getMostRecentException() + + assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) + assertThat(exceptionGot.message).isEqualTo("TEST") + assertThat(exceptionGot.stackTrace).isEqualTo(exception.stackTrace) + assertThat(exceptionGot.cause).isEqualTo(null) + } + + private fun setUpTestApplicationComponent() { + DaggerLogUploadWorkerTest_TestApplicationComponent.builder() + .setApplication(ApplicationProvider.getApplicationContext()) + .build() + .inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + @Module + class TestLogStorageModule { + + @Provides + @EventLogStorageCacheSize + fun provideEventLogStorageCacheSize(): Int = 2 + + @Provides + @ExceptionLogStorageCacheSize + fun provideExceptionLogStorageSize(): Int = 2 + } + + @Module + interface TestFirebaseLogUploaderModule { + + @Binds + fun bindsFakeLogUploader(fakeLogUploader: FakeLogUploader): LogUploader + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, TestLogReportingModule::class, + TestLogStorageModule::class, TestDispatcherModule::class, + LogUploadWorkerModule::class, TestFirebaseLogUploaderModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + fun build(): TestApplicationComponent + } + + fun inject(logUploadWorkerTest: LogUploadWorkerTest) + } +} diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index e60259fe61a..57c2bc72ae8 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -23,6 +23,7 @@ kt_android_library( "//app:crashlytics_deps", artifact("org.jetbrains.kotlinx:kotlinx-coroutines-core"), artifact("androidx.appcompat:appcompat"), + artifact("androidx.work:work-runtime-ktx"), artifact("com.github.bumptech.glide:glide"), artifact("com.caverock:androidsvg-aar"), ], diff --git a/utility/build.gradle b/utility/build.gradle index 3a594578e3a..e629c0ddc86 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -47,6 +47,7 @@ dependencies { implementation( 'androidx.appcompat:appcompat:1.0.2', 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', + 'androidx.work:work-runtime-ktx:2.4.0', 'com.caverock:androidsvg-aar:1.4', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', diff --git a/utility/src/main/java/org/oppia/util/logging/LogUploader.kt b/utility/src/main/java/org/oppia/util/logging/LogUploader.kt new file mode 100644 index 00000000000..f32dad802b9 --- /dev/null +++ b/utility/src/main/java/org/oppia/util/logging/LogUploader.kt @@ -0,0 +1,14 @@ +package org.oppia.util.logging + +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager + +/** Uploader for uploading events and exceptions to the remote service. */ +interface LogUploader { + + /** Enqueues a [workRequest] using the [workManager] for uploading event logs that are stored in the cache store. */ + fun enqueueWorkRequestForEvents(workManager: WorkManager, workRequest: PeriodicWorkRequest) + + /** Enqueues a [workRequest] using the [workManager] for uploading exception logs that are stored in the cache store. */ + fun enqueueWorkRequestForExceptions(workManager: WorkManager, workRequest: PeriodicWorkRequest) +} diff --git a/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt new file mode 100644 index 00000000000..8c5e2d56b96 --- /dev/null +++ b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt @@ -0,0 +1,37 @@ +package org.oppia.util.logging.firebase + +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import org.oppia.util.logging.LogUploader +import javax.inject.Inject + +private const val OPPIA_EVENT_WORK = "OPPIA_EVENT_WORK_REQUEST" +private const val OPPIA_EXCEPTION_WORK = "OPPIA_EXCEPTION_WORK_REQUEST" + +/** Enqueues work requests for uploading stored event/exception logs to the remote service. */ +class FirebaseLogUploader @Inject constructor() : + LogUploader { + + override fun enqueueWorkRequestForEvents( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + workManager.enqueueUniquePeriodicWork( + OPPIA_EVENT_WORK, + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } + + override fun enqueueWorkRequestForExceptions( + workManager: WorkManager, + workRequest: PeriodicWorkRequest + ) { + workManager.enqueueUniquePeriodicWork( + OPPIA_EXCEPTION_WORK, + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } +} diff --git a/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt new file mode 100644 index 00000000000..9d586a128b0 --- /dev/null +++ b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt @@ -0,0 +1,12 @@ +package org.oppia.util.logging.firebase + +import dagger.Binds +import dagger.Module +import org.oppia.util.logging.LogUploader + +/** Provides Log Uploader related dependencies. */ +@Module +interface FirebaseLogUploaderModule { + @Binds + fun bindFirebaseLogUploader(firebaseLogUploader: FirebaseLogUploader): LogUploader +}