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 +}