From 8c23de102270a2a6f68505fd7d4c52e68f8ea4b3 Mon Sep 17 00:00:00 2001 From: Tahsin Masrur Date: Thu, 8 Jun 2023 11:38:22 +0800 Subject: [PATCH] [CameraPipe] Update CapturePipelineTest to use TestScope instead of real time work The test is very slow for a unit test and takes around 3.5 mins due to using real time delays and awaitings. Instead, TestScope can be used which tracks virtual time making the tests significantly faster (20s) comparatively. Bug: 284492140 Bug: 286173948 Test: CapturePipelineTest Change-Id: Ic1a974af6e2cf56f319c484edb9cf68c97e18ed8 --- .../build.gradle | 1 + .../integration/impl/CapturePipelineTest.kt | 238 ++++++++++-------- 2 files changed, 134 insertions(+), 105 deletions(-) diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle index 76c669634db07..e1f8d7c6de093 100644 --- a/camera/camera-camera2-pipe-integration/build.gradle +++ b/camera/camera-camera2-pipe-integration/build.gradle @@ -52,6 +52,7 @@ dependencies { testImplementation(libs.mockitoKotlin4) testImplementation(libs.robolectric) testImplementation(libs.kotlinCoroutinesTest) + testImplementation(libs.kotlinTestJunit) testImplementation(project(":camera:camera-camera2-pipe-testing")) testImplementation(project(":camera:camera-testing")) testImplementation(project(":internal-testutils-ktx")) diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt index 236bd7d1fc37e..bc3ce60a86f95 100644 --- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt +++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt @@ -58,24 +58,29 @@ import androidx.camera.camera2.pipe.testing.FakeRequestMetadata import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.impl.utils.futures.Futures +import androidx.testutils.MainDispatcherRule import com.google.common.truth.Truth.assertThat import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit +import kotlin.test.assertFailsWith import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeoutOrNull import org.junit.After -import org.junit.AfterClass import org.junit.Assert import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -84,29 +89,23 @@ import org.robolectric.annotation.internal.DoNotInstrument import org.robolectric.shadows.StreamConfigurationMapBuilder import org.robolectric.util.ReflectionHelpers +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricCameraPipeTestRunner::class) @DoNotInstrument @Config(minSdk = Build.VERSION_CODES.LOLLIPOP) class CapturePipelineTest { + private val testScope = TestScope() + private val testDispatcher = StandardTestDispatcher(testScope.testScheduler) - companion object { - private val executor = Executors.newSingleThreadScheduledExecutor() - private val fakeUseCaseThreads by lazy { - val dispatcher = executor.asCoroutineDispatcher() - val cameraScope = CoroutineScope(Job() + dispatcher) + @get:Rule + val mainDispatcherRule = MainDispatcherRule(testDispatcher) - UseCaseThreads( - cameraScope, - executor, - dispatcher - ) - } - - @JvmStatic - @AfterClass - fun close() { - executor.shutdown() - } + private val fakeUseCaseThreads by lazy { + UseCaseThreads( + testScope, + testDispatcher.asExecutor(), + testDispatcher + ) } private val fakeRequestControl = object : FakeUseCaseCameraRequestControl() { @@ -192,9 +191,9 @@ class CapturePipelineTest { surfaceToStreamMap = emptyMap(), cameraStateAdapter = CameraStateAdapter(), ) - private var runningRepeatingStream: ScheduledFuture<*>? = null + private var runningRepeatingJob: Job? = null set(value) { - runningRepeatingStream?.cancel(false) + runningRepeatingJob?.cancel() field = value } @@ -233,7 +232,7 @@ class CapturePipelineTest { // Ensure the control is updated after the UseCaseCamera been set. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(testScope) ).isTrue() fakeRequestControl.torchUpdateEventList.clear() } @@ -256,20 +255,20 @@ class CapturePipelineTest { @After fun tearDown() { - runningRepeatingStream = null + runningRepeatingJob = null } @Test - fun miniLatency_flashOn_shouldTriggerAePreCapture(): Unit = runBlocking { + fun miniLatency_flashOn_shouldTriggerAePreCapture(): Unit = runTest { flashOn_shouldTriggerAePreCapture(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) } @Test - fun maxQuality_flashOn_shouldTriggerAePreCapture(): Unit = runBlocking { + fun maxQuality_flashOn_shouldTriggerAePreCapture(): Unit = runTest { flashOn_shouldTriggerAePreCapture(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) } - private suspend fun flashOn_shouldTriggerAePreCapture(imageCaptureMode: Int) { + private suspend fun TestScope.flashOn_shouldTriggerAePreCapture(imageCaptureMode: Int) { // Arrange. val requestList = mutableListOf() fakeCameraGraphSession.requestHandler = { requests -> @@ -286,30 +285,32 @@ class CapturePipelineTest { // Assert. assertThat( - fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(this) ).isTrue() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() // Assert 2, unlock3APostCapture should be called. assertThat( - fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(this) ).isTrue() } @Test - fun miniLatency_flashAutoFlashRequired_shouldTriggerAePreCapture(): Unit = runBlocking { + fun miniLatency_flashAutoFlashRequired_shouldTriggerAePreCapture(): Unit = runTest { flashAutoFlashRequired_shouldTriggerAePreCapture(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) } @Test - fun maxQuality_flashAutoFlashRequired_shouldTriggerAePreCapture(): Unit = runBlocking { + fun maxQuality_flashAutoFlashRequired_shouldTriggerAePreCapture(): Unit = runTest { flashAutoFlashRequired_shouldTriggerAePreCapture(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) } - private suspend fun flashAutoFlashRequired_shouldTriggerAePreCapture(imageCaptureMode: Int) { + private suspend fun TestScope.flashAutoFlashRequired_shouldTriggerAePreCapture( + imageCaptureMode: Int + ) { // Arrange. comboRequestListener.simulateRepeatingResult( resultParameters = mapOf( @@ -332,33 +333,33 @@ class CapturePipelineTest { // Assert 1, lock3AForCapture should be called, but not call unlock3APostCapture // (before capturing is finished). assertThat( - fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(this) ).isTrue() assertThat( - fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(2, TimeUnit.SECONDS) + fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(this) ).isFalse() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() // Assert 2, unlock3APostCapture should be called. assertThat( - fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.unlock3APostCaptureSemaphore.tryAcquire(this) ).isTrue() } @Test - fun miniLatency_withTorchAsFlashQuirk_shouldOpenTorch(): Unit = runBlocking { + fun miniLatency_withTorchAsFlashQuirk_shouldOpenTorch(): Unit = runTest { withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) } @Test - fun maxQuality_withTorchAsFlashQuirk_shouldOpenTorch(): Unit = runBlocking { + fun maxQuality_withTorchAsFlashQuirk_shouldOpenTorch(): Unit = runTest { withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) } - private suspend fun withTorchAsFlashQuirk_shouldOpenTorch(imageCaptureMode: Int) { + private suspend fun TestScope.withTorchAsFlashQuirk_shouldOpenTorch(imageCaptureMode: Int) { // Arrange. capturePipeline = CapturePipelineImpl( cameraProperties = fakeCameraProperties, @@ -385,34 +386,34 @@ class CapturePipelineTest { // Assert 1, torch should be turned on. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isTrue() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() // Assert 2, torch should be turned off. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isFalse() } @Test - fun miniLatency_withTemplateRecord_shouldOpenTorch(): Unit = runBlocking { + fun miniLatency_withTemplateRecord_shouldOpenTorch(): Unit = runTest { withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) } @Test - fun maxQuality_withTemplateRecord_shouldOpenTorch(): Unit = runBlocking { + fun maxQuality_withTemplateRecord_shouldOpenTorch(): Unit = runTest { withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) } - private suspend fun withTemplateRecord_shouldOpenTorch(imageCaptureMode: Int) { + private suspend fun TestScope.withTemplateRecord_shouldOpenTorch(imageCaptureMode: Int) { // Arrange. capturePipeline.template = CameraDevice.TEMPLATE_RECORD @@ -431,34 +432,34 @@ class CapturePipelineTest { // Assert 1, torch should be turned on. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isTrue() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() // Assert 2, torch should be turned off. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isFalse() } @Test - fun miniLatency_withFlashTypeTorch_shouldOpenTorch(): Unit = runBlocking { + fun miniLatency_withFlashTypeTorch_shouldOpenTorch(): Unit = runTest { withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) } @Test - fun maxQuality_withFlashTypeTorch_shouldOpenTorch(): Unit = runBlocking { + fun maxQuality_withFlashTypeTorch_shouldOpenTorch(): Unit = runTest { withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) } - private suspend fun withFlashTypeTorch_shouldOpenTorch(imageCaptureMode: Int) { + private suspend fun TestScope.withFlashTypeTorch_shouldOpenTorch(imageCaptureMode: Int) { // Arrange. val requestList = mutableListOf() fakeCameraGraphSession.requestHandler = { requests -> @@ -475,25 +476,25 @@ class CapturePipelineTest { // Assert 1, torch should be turned on. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isTrue() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() // Assert 2, torch should be turned off. assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(1) assertThat(fakeRequestControl.torchUpdateEventList.removeFirst()).isFalse() } @Test - fun miniLatency_flashRequired_withFlashTypeTorch_shouldLock3A(): Unit = runBlocking { + fun miniLatency_flashRequired_withFlashTypeTorch_shouldLock3A(): Unit = runTest { withFlashTypeTorch_shouldLock3A( ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY, ImageCapture.FLASH_MODE_ON @@ -501,14 +502,17 @@ class CapturePipelineTest { } @Test - fun maxQuality_withFlashTypeTorch_shouldLock3A(): Unit = runBlocking { + fun maxQuality_withFlashTypeTorch_shouldLock3A(): Unit = runTest { withFlashTypeTorch_shouldLock3A( ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_MODE_OFF ) } - private suspend fun withFlashTypeTorch_shouldLock3A(imageCaptureMode: Int, flashMode: Int) { + private suspend fun TestScope.withFlashTypeTorch_shouldLock3A( + imageCaptureMode: Int, + flashMode: Int + ) { // Arrange. val requestList = mutableListOf() fakeCameraGraphSession.requestHandler = { requests -> @@ -525,24 +529,25 @@ class CapturePipelineTest { // Assert 1, should call lock3A, but not call unlock3A (before capturing is finished). assertThat( - fakeCameraGraphSession.lock3ASemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.lock3ASemaphore.tryAcquire(this) ).isTrue() assertThat( - fakeCameraGraphSession.unlock3ASemaphore.tryAcquire(1, TimeUnit.SECONDS) + fakeCameraGraphSession.unlock3ASemaphore.tryAcquire(this) ).isFalse() // Complete the capture request. - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() requestList.complete() + advanceUntilIdle() // Assert 2, should call unlock3A. assertThat( - fakeCameraGraphSession.unlock3ASemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.unlock3ASemaphore.tryAcquire(this) ).isTrue() } @Test - fun miniLatency_withFlashTypeTorch_shouldNotLock3A(): Unit = runBlocking { + fun miniLatency_withFlashTypeTorch_shouldNotLock3A(): Unit = runTest { // Act. capturePipeline.submitStillCaptures( requests = listOf(singleRequest), @@ -553,17 +558,17 @@ class CapturePipelineTest { // Assert, there is no invocation on lock3A(). assertThat( - fakeCameraGraphSession.lock3ASemaphore.tryAcquire(1, TimeUnit.SECONDS) + fakeCameraGraphSession.lock3ASemaphore.tryAcquire(this) ).isFalse() } @Test - fun withFlashTypeTorch_torchAlreadyOn_skipTurnOnTorch(): Unit = runBlocking { + fun withFlashTypeTorch_torchAlreadyOn_skipTurnOnTorch(): Unit = runTest { // Arrange. // Ensure the torch is already turned on before capturing. torchControl.setTorchAsync(true) assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(2, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() // Act. @@ -576,12 +581,12 @@ class CapturePipelineTest { // Assert, there is no invocation on setTorch(). assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(1, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isFalse() } @Test - fun miniLatency_shouldNotAePreCapture(): Unit = runBlocking { + fun miniLatency_shouldNotAePreCapture(): Unit = runTest { // Act. capturePipeline.submitStillCaptures( requests = listOf(singleRequest), @@ -592,12 +597,12 @@ class CapturePipelineTest { // Assert, there is only 1 single capture request. assertThat( - fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(1, TimeUnit.SECONDS) + fakeCameraGraphSession.lock3AForCaptureSemaphore.tryAcquire(this) ).isFalse() } @Test - fun captureFailure_taskShouldFailure(): Unit = runBlocking { + fun captureFailure_taskShouldFailure(): Unit = runTest { // Arrange. fakeCameraGraphSession.requestHandler = { requests -> requests.forEach { request -> @@ -621,14 +626,15 @@ class CapturePipelineTest { ) // Assert. - val exception = Assert.assertThrows(ImageCaptureException::class.java) { - runBlocking { resultDeferredList.awaitAllWithTimeout() } + advanceUntilIdle() + val exception = assertFailsWith(ImageCaptureException::class) { + resultDeferredList.awaitAllWithTimeout() } assertThat(exception.imageCaptureError).isEqualTo(ImageCapture.ERROR_CAPTURE_FAILED) } @Test - fun captureCancel_taskShouldFailureWithCAMERA_CLOSED(): Unit = runBlocking { + fun captureCancel_taskShouldFailureWithCAMERA_CLOSED(): Unit = runTest { // Arrange. fakeCameraGraphSession.requestHandler = { requests -> requests.forEach { request -> @@ -650,6 +656,7 @@ class CapturePipelineTest { ) // Assert. + advanceUntilIdle() val exception = Assert.assertThrows(ExecutionException::class.java) { Futures.allAsList(resultDeferredList.map { it.asListenableFuture() @@ -662,7 +669,7 @@ class CapturePipelineTest { } @Test - fun stillCaptureWithFlashStopRepeatingQuirk_shouldStopRepeatingTemporarily() = runBlocking { + fun stillCaptureWithFlashStopRepeatingQuirk_shouldStopRepeatingTemporarily() = runTest { // Arrange ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", "SAMSUNG") ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-A716") @@ -687,23 +694,23 @@ class CapturePipelineTest { // Assert, stopRepeating -> submit -> startRepeating flow should be used. assertThat( - fakeCameraGraphSession.stopRepeatingSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.stopRepeatingSemaphore.tryAcquire(this) ).isTrue() assertThat( - fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.submitSemaphore.tryAcquire(this) ).isTrue() // Completing the submitted capture request. submittedRequestList.complete() assertThat( - fakeCameraGraphSession.repeatingRequestSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.repeatingRequestSemaphore.tryAcquire(this) ).isTrue() } @Test - fun stillCaptureWithFlashStopRepeatingQuirkNotEnabled_shouldNotStopRepeating() = runBlocking { + fun stillCaptureWithFlashStopRepeatingQuirkNotEnabled_shouldNotStopRepeating() = runTest { // Arrange val submittedRequestList = mutableListOf() fakeCameraGraphSession.requestHandler = { requests -> @@ -725,11 +732,11 @@ class CapturePipelineTest { // Assert, repeating should not be stopped when quirk not enabled. assertThat( - fakeCameraGraphSession.stopRepeatingSemaphore.tryAcquire(2, TimeUnit.SECONDS) + fakeCameraGraphSession.stopRepeatingSemaphore.tryAcquire(this) ).isFalse() assertThat( - fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeCameraGraphSession.submitSemaphore.tryAcquire(this) ).isTrue() // Resetting repeatingRequestSemaphore because startRepeating can be called before @@ -739,21 +746,21 @@ class CapturePipelineTest { submittedRequestList.complete() assertThat( - fakeCameraGraphSession.repeatingRequestSemaphore.tryAcquire(2, TimeUnit.SECONDS) + fakeCameraGraphSession.repeatingRequestSemaphore.tryAcquire(this) ).isFalse() } @Test - fun torchAsFlash_torchCorrection_shouldTurnsTorchOffOn(): Unit = runBlocking { + fun torchAsFlash_torchCorrection_shouldTurnsTorchOffOn(): Unit = runTest { torchStateCorrectionTest(ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH) } @Test - fun defaultCapture_torchCorrection_shouldTurnsTorchOffOn(): Unit = runBlocking { + fun defaultCapture_torchCorrection_shouldTurnsTorchOffOn(): Unit = runTest { torchStateCorrectionTest(ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH) } - private suspend fun torchStateCorrectionTest(flashType: Int) { + private suspend fun TestScope.torchStateCorrectionTest(flashType: Int) { // Arrange. torchControl.setTorchAsync(torch = true).join() verifyTorchState(true) @@ -776,8 +783,8 @@ class CapturePipelineTest { flashType = flashType, ) - assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() - assertThat(fakeRequestControl.setTorchSemaphore.tryAcquire(1, TimeUnit.SECONDS)).isFalse() + assertThat(fakeCameraGraphSession.submitSemaphore.tryAcquire(this)).isTrue() + assertThat(fakeRequestControl.setTorchSemaphore.tryAcquire(this)).isFalse() // Complete the capture request. requestList.complete() @@ -788,9 +795,9 @@ class CapturePipelineTest { assertThat(fakeRequestControl.torchUpdateEventList.size).isEqualTo(0) } - private fun verifyTorchState(state: Boolean) { + private fun TestScope.verifyTorchState(state: Boolean) { assertThat( - fakeRequestControl.setTorchSemaphore.tryAcquire(5, TimeUnit.SECONDS) + fakeRequestControl.setTorchSemaphore.tryAcquire(this) ).isTrue() assertThat(fakeRequestControl.torchUpdateEventList.removeFirst() == state).isTrue() } @@ -819,20 +826,30 @@ class CapturePipelineTest { requestParameters: Map, Any> = mutableMapOf(), resultParameters: Map, Any> = mutableMapOf(), ) { - executor.schedule({ - runningRepeatingStream = executor.scheduleAtFixedRate({ - val fakeRequestMetadata = FakeRequestMetadata(requestParameters = requestParameters) - val fakeFrameMetadata = FakeFrameMetadata(resultMetadata = resultParameters) - val fakeFrameInfo = FakeFrameInfo( - metadata = fakeFrameMetadata, requestMetadata = fakeRequestMetadata, - ) - this.onTotalCaptureResult( - requestMetadata = fakeRequestMetadata, - frameNumber = FrameNumber(101L), - totalCaptureResult = fakeFrameInfo, - ) - }, 0, period, TimeUnit.MILLISECONDS) - }, initialDelay, TimeUnit.MILLISECONDS) + let { listener -> + runningRepeatingJob = fakeUseCaseThreads.scope.launch { + delay(initialDelay) + + // the counter uses 1000 frames for repeating request instead of infinity so that + // coroutine can complete and lead to an idle state, should be sufficient for all + // our testing purposes here + var counter = 1000 + while (counter-- > 0) { + val fakeRequestMetadata = + FakeRequestMetadata(requestParameters = requestParameters) + val fakeFrameMetadata = FakeFrameMetadata(resultMetadata = resultParameters) + val fakeFrameInfo = FakeFrameInfo( + metadata = fakeFrameMetadata, requestMetadata = fakeRequestMetadata, + ) + listener.onTotalCaptureResult( + requestMetadata = fakeRequestMetadata, + frameNumber = FrameNumber(101L), + totalCaptureResult = fakeFrameInfo, + ) + delay(period) + } + } + } } private suspend fun Collection>.awaitAllWithTimeout( @@ -840,4 +857,15 @@ class CapturePipelineTest { ) = checkNotNull(withTimeoutOrNull(timeMillis) { awaitAll() }) { "Cannot complete the Deferred within $timeMillis" } + + /** + * Advances TestScope coroutine to idle state (i.e. all tasks completed) before trying to + * acquire semaphore immediately. + * + * This saves time by not having to explicitly wait for a semaphore status to be updated. + */ + private fun Semaphore.tryAcquire(testScope: TestScope): Boolean { + testScope.advanceUntilIdle() + return tryAcquire() + } }