Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #2134: Fix concept card naming/linking #2980

Merged
merged 64 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5e33b02
Add support for loading proto versions of lessons.
BenHenning Mar 23, 2021
a3d5f4a
Upgrade Espresso versions to work around build issues.
BenHenning Mar 23, 2021
a017e09
Merge branch 'fix-protobuf-duplication-issue' into add-support-for-pr…
BenHenning Mar 23, 2021
1815e06
Lint fix post-copying from #2927.
BenHenning Mar 23, 2021
97b28ef
Fix Glide in Bazel.
BenHenning Mar 23, 2021
6aadb95
Fix concept card name escaping.
BenHenning Mar 23, 2021
156b01e
Copy over test fix from #2927.
BenHenning Mar 23, 2021
37547c9
Remove image loading annotation.
BenHenning Mar 23, 2021
b33ff7a
Add caching module to needed test suites.
BenHenning Mar 23, 2021
a6a2121
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Mar 23, 2021
e8b8d1f
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Mar 23, 2021
441e2ae
Add support for importing text protos as assets.
BenHenning Mar 24, 2021
927508b
Add gitignore for Android Studio Bazel plugin output.
BenHenning Mar 24, 2021
d39e926
Import textproto versions of existing dev assets.
BenHenning Mar 24, 2021
9079ab6
Undo Espresso version change since it breaks Bazel tests.
BenHenning Mar 24, 2021
be5b785
Fix tests.
BenHenning Mar 25, 2021
34333eb
Use correct Espresso core.
BenHenning Mar 25, 2021
a691bcf
Move coroutine dispatchers.
BenHenning Apr 3, 2021
8ab7437
Refactor testing utilities build graph.
BenHenning Apr 3, 2021
a78b59d
Introduce dedicated test for TestCoroutineDispatcher.
BenHenning Apr 6, 2021
1e138b9
Add tests for TestCoroutineDispatcher.
BenHenning Apr 8, 2021
797ff85
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 8, 2021
b45b212
Workaround ktlint semicolon issue.
BenHenning Apr 8, 2021
5db9e82
Lint fixes.
BenHenning Apr 8, 2021
236a4f0
Fix test post-merge.
BenHenning Apr 9, 2021
54dc9bc
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 9, 2021
dc8be26
Move CoroutineDispatcher to be part of threading.
BenHenning Apr 9, 2021
f2ef2b2
Lint fixes.
BenHenning Apr 9, 2021
12d42dc
Merge branch 'develop' into fix-protobuf-duplication-issue
BenHenning Apr 12, 2021
b0eb428
Undo version upgrade.
BenHenning Apr 12, 2021
138b83a
Merge branch 'fix-protobuf-duplication-issue' into add-support-for-pr…
BenHenning Apr 12, 2021
8601fde
Fix broken tests.
BenHenning Apr 12, 2021
7f6ba6d
Lint fixes.
BenHenning Apr 12, 2021
6aa8dbf
Add a bit more proto loading test coverage.
BenHenning Apr 12, 2021
b2a116b
Comment fix.
BenHenning Apr 12, 2021
c6fa23c
Lint fixes.
BenHenning Apr 12, 2021
3fae8d3
Address earlier TODO.
BenHenning Apr 13, 2021
caed7a7
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 13, 2021
467e90b
Post-merge restructure.
BenHenning Apr 13, 2021
522c9b6
Merge branch 'develop' into add-support-for-proto-lesson-loading-from…
BenHenning Apr 13, 2021
6d16f2a
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 13, 2021
adab013
Post-merge fixes.
BenHenning Apr 13, 2021
38d797e
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 13, 2021
537ec36
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 13, 2021
5b48ada
Add tests for new tag handlers.
BenHenning Apr 14, 2021
4ab42a2
Fix broken test build.
BenHenning Apr 14, 2021
661da02
Merge branch 'add-coroutine-dispatcher-tests' into fix-glide-image-lo…
BenHenning Apr 14, 2021
4d8d5be
Post-merge lint fixes.
BenHenning Apr 14, 2021
5ae75e4
Merge branch 'add-coroutine-dispatcher-tests' into add-support-for-pr…
BenHenning Apr 14, 2021
76270d3
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 14, 2021
a5d8d17
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 14, 2021
5d40f2b
Lint fixes.
BenHenning Apr 14, 2021
c5ee89d
Address reviewer comment.
BenHenning Apr 14, 2021
39ab486
Post-merge fix.
BenHenning Apr 14, 2021
770252a
Fix typo.
BenHenning Apr 14, 2021
9e99d27
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 15, 2021
31f4f9b
Merge branch 'add-coroutine-dispatcher-tests' into add-support-for-pr…
BenHenning Apr 15, 2021
fe0cd33
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 15, 2021
bf1b6a6
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 15, 2021
3fddc33
Merge branch 'develop' into add-support-for-proto-lesson-loading-from…
BenHenning Apr 15, 2021
e6b41a9
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 15, 2021
e5369b5
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 15, 2021
92b4cc3
Merge branch 'develop' into fix-glide-image-loading
BenHenning Apr 23, 2021
19ea66c
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add tests for TestCoroutineDispatcher.
This required a bunch of refactoring, adding the first examples of
dedicated Bazel test build files, and introducing a new test pattern for
sharing code between different implementations. Further, it refined the
API for TesCoroutineDispatcher & fixed some issues in the
implementations (especially Robolectric).

Tests verified as non-flaky in Robolectric across 1000 runs, and 0.2%
flaky across 1000 runs for Espresso (though that may be fixed; waiting
on a follow-up run to confirm stability).
  • Loading branch information
BenHenning committed Apr 8, 2021
commit 1e138b95fa27d626d4105d859226f6971652eda8
5 changes: 3 additions & 2 deletions testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ TEST_DEPS = [
# Qualified file paths for test classes that have been migrated over to their own packages &
# shouldn't be defined as module-level tests.
# keep sorted
MIGRATED_TESTS = [
]
MIGRATED_TESTS = glob([
"src/test/java/org/oppia/android/testing/threading/*.kt",
])

[testing_test(
name = test_file_path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ kt_android_library(
],
)

# Note that this library has restricted visibility since tests should only use the dependency
# directly in special circumstances. In general, you don't need this. Use TestCoroutineDispatchers
# instead.
kt_android_library(
name = "test_coroutine_dispatcher",
testonly = True,
srcs = [
"TestCoroutineDispatcher.kt",
],
visibility = [
"//testing/src/test/java/org/oppia/android/testing/threading:__pkg__",
],
deps = [
"//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@ abstract class TestCoroutineDispatcher : CoroutineDispatcher() {
* Returns whether there are any tasks known to the dispatcher that have not yet been started.
*
* Note that some of these tasks may be scheduled for the future. This is meant to be used in
* conjunction with [org.oppia.android.testing.FakeSystemClock.advanceTime] since that along with
* [runCurrent] will execute all tasks up to the new time. If the time returned by
* conjunction with [org.oppia.android.testing.time.FakeSystemClock.advanceTime] since that along
* with [runCurrent] will execute all tasks up to the new time. If the time returned by
* [getNextFutureTaskCompletionTimeMillis] plus the current time is passed to
* [org.oppia.android.testing.FakeSystemClock.advanceTime], this dispatcher guarantees that
* [org.oppia.android.testing.time.FakeSystemClock.advanceTime], this dispatcher guarantees that
* [hasPendingTasks] will return false after a call to [runCurrent] returns.
*
* This function makes no guarantees about idleness with respect to other dispatchers (e.g. even
* if all tasks are executed, another dispatcher could schedule another task on this dispatcher in
* response to a task from this dispatcher being executed). Cross-thread communication should be
* managed using [TestCoroutineDispatchers], instead.
*
* Note that it's up to the implementation to define what constitutes as 'pending' as this may
* differ for different testing environments.
*/
abstract fun hasPendingTasks(): Boolean

Expand All @@ -58,6 +61,9 @@ abstract class TestCoroutineDispatcher : CoroutineDispatcher() {
* If [runCurrent] is used, this function is guaranteed to return false after that function
* returns. Note that the same threading caveats mentioned for [hasPendingTasks] also pertains to
* this function.
*
* Note that it's up to the implementation to define what constitutes as 'completable' as this may
* differ for different testing environments.
*/
abstract fun hasPendingCompletableTasks(): Boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlinx.coroutines.delay as delayInScope // Needed to avoid conflict with Delay.delay().

/**
Expand Down Expand Up @@ -44,26 +43,23 @@ class TestCoroutineDispatcherEspressoImpl private constructor(
// Tasks immediately will start running, so track the task immediately.
executingTaskCount.incrementAndGet()
notifyIfRunning()
realCoroutineDispatcher.dispatch(
context,
kotlinx.coroutines.Runnable {
try {
block.run()
} finally {
executingTaskCount.decrementAndGet()
taskCompletionTimes.remove(taskId)
}
notifyIfIdle()
realCoroutineDispatcher.dispatch(context) {
try {
block.run()
} finally {
executingTaskCount.decrementAndGet()
taskCompletionTimes.remove(taskId)
}
)
notifyIfIdle()
}
}

override fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>
) {
val taskId = totalTaskCount.incrementAndGet()
taskCompletionTimes[taskId] = timeMillis
taskCompletionTimes[taskId] = System.currentTimeMillis() + timeMillis
val block: CancellableContinuation<Unit>.() -> Unit = {
realCoroutineDispatcher.resumeUndispatched(Unit)
}
Expand Down Expand Up @@ -93,14 +89,8 @@ class TestCoroutineDispatcherEspressoImpl private constructor(
override fun hasPendingTasks(): Boolean = executingTaskCount.get() != 0

override fun getNextFutureTaskCompletionTimeMillis(timeMillis: Long): Long? {
var nextFutureTaskTime: Long? = null
// Find the next most recent task completion time that's after the specified time.
for (entry in taskCompletionTimes.entries) {
if (entry.value > timeMillis) {
nextFutureTaskTime = nextFutureTaskTime?.let { min(it, entry.value) } ?: entry.value
}
}
return nextFutureTaskTime
return taskCompletionTimes.values.filter { it > timeMillis }.minOrNull()
}

override fun hasPendingCompletableTasks(): Boolean {
Expand Down
10 changes: 10 additions & 0 deletions testing/src/test/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# TODO(#1532): Rename file to 'BUILD' post-Gradle.
"""
Top-level testing module files & targets.
"""

filegroup(
name = "test_manifest",
srcs = ["AndroidManifest.xml"],
visibility = ["//:oppia_testing_visibility"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# TODO(#1532): Rename file to 'BUILD' post-Gradle.
"""
Tests for threading-specific test utilities.
"""

load("@dagger//:workspace_defs.bzl", "dagger_rules")
load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")
load("//:oppia_android_test.bzl", "oppia_android_test")

kt_android_library(
name = "test_coroutine_dispatcher_test_base",
testonly = True,
srcs = ["TestCoroutineDispatcherTestBase.kt"],
deps = [
"//testing/src/main/java/org/oppia/android/testing/threading:annotations",
"//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatcher",
"//third_party:com_google_truth_truth",
"//third_party:org_mockito_mockito-core",
],
)

oppia_android_test(
name = "TestCoroutineDispatcherEspressoImplTest",
srcs = ["TestCoroutineDispatcherEspressoImplTest.kt"],
custom_package = "org.oppia.android.data.testing",
test_class = "org.oppia.android.testing.threading.TestCoroutineDispatcherEspressoImplTest",
test_manifest = "//testing/src/test:test_manifest",
deps = [
":dagger",
":test_coroutine_dispatcher_test_base",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//third_party:robolectric_android-all",
"//utility",
],
)

oppia_android_test(
name = "TestCoroutineDispatcherRobolectricImplTest",
srcs = ["TestCoroutineDispatcherRobolectricImplTest.kt"],
custom_package = "org.oppia.android.data.testing",
test_class = "org.oppia.android.testing.threading.TestCoroutineDispatcherRobolectricImplTest",
test_manifest = "//testing/src/test:test_manifest",
deps = [
":dagger",
":test_coroutine_dispatcher_test_base",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//third_party:robolectric_android-all",
"//utility",
],
)

oppia_android_test(
name = "TestCoroutineDispatcherTest",
srcs = ["TestCoroutineDispatcherTest.kt"],
custom_package = "org.oppia.android.data.testing",
test_class = "org.oppia.android.testing.threading.TestCoroutineDispatcherTest",
test_manifest = "//testing/src/test:test_manifest",
deps = [
":dagger",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//third_party:robolectric_android-all",
"//utility",
],
)

dagger_rules()
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package org.oppia.android.testing.threading

import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
import org.oppia.android.testing.TestLogReportingModule
import org.oppia.android.testing.robolectric.IsOnRobolectric
import org.oppia.android.util.data.DataProvidersInjector
import org.oppia.android.util.data.DataProvidersInjectorProvider
import org.oppia.android.util.logging.EnableConsoleLog
import org.oppia.android.util.logging.EnableFileLog
import org.oppia.android.util.logging.GlobalLogLevel
import org.oppia.android.util.logging.LogLevel
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Singleton

/**
* Tests for [TestCoroutineDispatcherEspressoImpl].
*
* Note that this is testing the Espresso implementation of the test coroutine dispatcher using
* Robolectric which means certain time constructs need to be used to try and control threading
* since the tests can't rely on synchronization barriers (like the dispatcher itself).
*/
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
@Config(
application = TestCoroutineDispatcherEspressoImplTest.TestApplication::class,
qualifiers = "port-xxhdpi"
)
class TestCoroutineDispatcherEspressoImplTest: TestCoroutineDispatcherTestBase(
shortTaskDelayMillis = 5L,
longTaskDelayMillis = 15000L,
longTaskDelayDeltaCheckMillis = 1000L
) {
@Before
@InternalCoroutinesApi
@ExperimentalCoroutinesApi
override fun setUp() {
setUpTestApplicationComponent()
verifyDispatcherImplementation<TestCoroutineDispatcherEspressoImpl>()
}

override fun getCurrentTimeMillis(): Long = System.currentTimeMillis()

override fun advanceTimeBy(timeMillis: Long) {
// While an antipattern, the tests are configured per the task delay passed in the constructor
// to use small delays so that different test scenarios can be tested in a real thread
// environment.
Thread.sleep(timeMillis)
}

@Suppress("ControlFlowWithEmptyBody")
override fun stabilizeAfterDispatcherFlush() {
// Spin the test thread until the dispatcher has finished.
while (backgroundTestDispatcher.hasPendingTasks());
}

override fun ensureFutureTasksAreScheduled() {
val mockRunnable = mock(Runnable::class.java)
scheduleFutureTask(delayMs = 1L, mockRunnable)

// Wait up to 500ms for the delayed runnable to be called. This should be sufficiently less than
// the long time delay for tests to complete.
verify(mockRunnable, timeout(/* millis= */ 500L).atLeastOnce()).run()

// Wait a bit longer in case the quick execution introduces an ordering issue.
advanceTimeBy(10L)
}

// Note that this test intentionally differs from Robolectric: the Espresso implementation chooses
// to consider a future task as in-progress even during the delay since it needs to communicate
// back to Espresso (via an IdlingResource) that a task needs to be run. This intentionally
// differs from the Robolectric version which aims to allow idleness between calls to
// advanceTime().
@Test
fun testDispatcher_hasPendingCompletableTasks_advanceBeforeTask_returnsTrue() {
// Use a longer task time to avoid inadvertently running the task.
scheduleFutureTask(longTaskDelayMillis, mockRunnable1)
ensureFutureTasksAreScheduled()

advanceTimeBy(shortTaskDelayMillis - 1)
val hasCompletableTasks = backgroundTestDispatcher.hasPendingCompletableTasks()

// Any scheduled task is considered executing.
assertThat(hasCompletableTasks).isTrue()
}

// For similar reasons, testing the 'true' case requires some deviation here. Tasks are considered
// pending & completable immediately after being scheduled in a real threaded environment, unlike
// the Robolectric variant of TestCoroutineDispatcher.
@Test
fun testDispatcher_hasPendingCompletableTasks_futureTask_returnsTrue() {
// Use a longer delay to provide time for the pending tasks check.
scheduleFutureTask(longTaskDelayMillis, mockRunnable1)
ensureFutureTasksAreScheduled()

val hasCompletableTasks = backgroundTestDispatcher.hasPendingCompletableTasks()

// The immediate task is completable now.
assertThat(hasCompletableTasks).isTrue()
}

private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext<TestApplication>().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

@Provides
@IsOnRobolectric
fun provideIsOnRobolectric(): Boolean = false
}

// TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
@Singleton
@Component(
modules = [
TestDispatcherModule::class,
TestModule::class,
TestLogReportingModule::class
]
)
interface TestApplicationComponent : DataProvidersInjector {
@Component.Builder
interface Builder {
@BindsInstance
fun setApplication(application: Application): Builder
fun build(): TestApplicationComponent
}

fun inject(test: TestCoroutineDispatcherEspressoImplTest)
}

class TestApplication : Application(), DataProvidersInjectorProvider {
private val component: TestApplicationComponent by lazy {
DaggerTestCoroutineDispatcherEspressoImplTest_TestApplicationComponent.builder()
.setApplication(this)
.build()
}

fun inject(test: TestCoroutineDispatcherEspressoImplTest) {
component.inject(test)
}

override fun getDataProvidersInjector(): DataProvidersInjector = component
}
}
Loading