Loading packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/SuspendUtilTests.kt +97 −14 Original line number Diff line number Diff line Loading @@ -18,11 +18,15 @@ package com.android.systemui.util.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import java.lang.System.currentTimeMillis import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith Loading @@ -33,24 +37,103 @@ class RaceSuspendTest : SysuiTestCase() { @Test fun raceSimple() = runBlocking { val winner = CompletableDeferred<Int>() val result = async { race( { winner.await() }, { awaitCancellation() }, ) } val result = async { race({ winner.await() }, { awaitCancellation() }) } winner.complete(1) assertThat(result.await()).isEqualTo(1) } @Test fun raceImmediate() = runBlocking { assertThat(race<Int>({ 1 }, { 2 })).isEqualTo(1) } } @SmallTest @RunWith(AndroidJUnit4::class) class TimeoutSuspendTest : SysuiTestCase() { @Test fun launchWithTimeout() = runBlocking { val result = launchWithTimeout(500.milliseconds) { 1 } assertThat(result.isSuccess).isTrue() } @Test fun raceImmediate() = runBlocking { assertThat( race<Int>( { 1 }, { 2 }, ) ) .isEqualTo(1) fun launchWithTimeout_timesOutWhenDelay() = runBlocking { val launchStart = currentTimeMillis() val result = launchWithTimeout(100.milliseconds) { println("Starting delay") delay(1000.milliseconds) println("Finished delay") } val launchDurationMs = currentTimeMillis() - launchStart // VERIFY that block timed out assertThat(result.isSuccess).isFalse() // VERIFY that the result was returned faster than the delay's minimum duration assertThat(launchDurationMs).isAtMost(999) } @Test fun awaitAndCancelOnTimeout_coroutine() = runBlocking { val deferred = CompletableDeferred<Int>() launch { delay(100.milliseconds) deferred.complete(1) } val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isSuccess).isTrue() assertThat(result.getOrNull()).isEqualTo(1) } @Test fun awaitAndCancelOnTimeout_thread_sleep() = runBlocking { val deferred = CompletableDeferred<Int>() Thread { Thread.sleep(100) deferred.complete(1) } .start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isSuccess).isTrue() assertThat(result.getOrNull()).isEqualTo(1) } @Test fun awaitAndCancelOnTimeout_thread_sleepForeverUntilCancelled() = runBlocking { val deferred = CompletableDeferred<Int>() val thread = Thread { while (!deferred.isCancelled) { Thread.sleep(100) } } thread.start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isFailure).isTrue() delay(200.milliseconds) assertThat(thread.isAlive).isFalse() } @Test fun awaitAndCancelOnTimeout_thread_busyLoop() = runBlocking { val deferred = CompletableDeferred<Int>() val thread = Thread { val busyLoopStart = currentTimeMillis() println("Starting busy loop") while (currentTimeMillis() - busyLoopStart < 1000) { // busy loop } println("Finished busy loop") } val operationStart = currentTimeMillis() thread.start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) val operationDurationMs = currentTimeMillis() - operationStart // VERIFY that the thread is still running assertThat(thread.isAlive).isTrue() // VERIFY that block timed out assertThat(result.isSuccess).isFalse() // VERIFY that the result was returned faster than the busy loop's minimum duration assertThat(operationDurationMs).isAtMost(999) // CLEANUP: let the busy loop end delay(1000.milliseconds) // VERIFY that the thread is stopped assertThat(thread.isAlive).isFalse() } } packages/SystemUI/src/com/android/systemui/util/kotlin/Suspend.kt +50 −1 Original line number Diff line number Diff line Loading @@ -16,9 +16,14 @@ package com.android.systemui.util.kotlin import com.android.app.tracing.coroutines.launchTraced as launch import java.util.concurrent.CancellationException import java.util.concurrent.TimeoutException import kotlin.time.Duration import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import kotlinx.coroutines.coroutineScope import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.delay /** * Runs the given [blocks] in parallel, returning the result of the first one to complete, and Loading @@ -33,3 +38,47 @@ suspend fun <R> race(vararg blocks: suspend () -> R): R = coroutineScope { } completion.await().also { raceJob.cancel() } } /** * Runs the given [block] in parallel with a timeout, returning failure if the timeout completes * first. */ suspend fun <R> launchWithTimeout(timeout: Duration, block: suspend () -> R): Result<R> = race( { Result.success(block()) }, { delay(timeout) Result.failure(TimeoutException("$block timed out after $timeout")) }, ) /** * Awaits this [Deferred] until the given [timeout] has elapsed. * * If the timeout is reached before the deferred completes, the returned [Result] will contain * [TimeoutException], and the deferred will be cancelled. * * If the deferred completes normally and in time, the returned Result will contain the value. * * If the deferred completes exceptionally, the returned Result will contain the exception. * * This is designed to solve situations that arise when the deferred work is not cooperative, for * example when it is loading bitmap data from another process and we want to have a limit on how * long we will wait, even if we cannot cancel that operation. */ suspend fun <R> Deferred<R>.awaitAndCancelOnTimeout(timeout: Duration): Result<R> { val deferred = this return race( { try { Result.success(deferred.await()) } catch (ex: CancellationException) { deferred.cancel() throw ex } catch (ex: Exception) { Result.failure(ex) } }, { delay(timeout) Result.failure(TimeoutException("$deferred timed out after $timeout")) }, ) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/SuspendUtilTests.kt +97 −14 Original line number Diff line number Diff line Loading @@ -18,11 +18,15 @@ package com.android.systemui.util.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import java.lang.System.currentTimeMillis import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith Loading @@ -33,24 +37,103 @@ class RaceSuspendTest : SysuiTestCase() { @Test fun raceSimple() = runBlocking { val winner = CompletableDeferred<Int>() val result = async { race( { winner.await() }, { awaitCancellation() }, ) } val result = async { race({ winner.await() }, { awaitCancellation() }) } winner.complete(1) assertThat(result.await()).isEqualTo(1) } @Test fun raceImmediate() = runBlocking { assertThat(race<Int>({ 1 }, { 2 })).isEqualTo(1) } } @SmallTest @RunWith(AndroidJUnit4::class) class TimeoutSuspendTest : SysuiTestCase() { @Test fun launchWithTimeout() = runBlocking { val result = launchWithTimeout(500.milliseconds) { 1 } assertThat(result.isSuccess).isTrue() } @Test fun raceImmediate() = runBlocking { assertThat( race<Int>( { 1 }, { 2 }, ) ) .isEqualTo(1) fun launchWithTimeout_timesOutWhenDelay() = runBlocking { val launchStart = currentTimeMillis() val result = launchWithTimeout(100.milliseconds) { println("Starting delay") delay(1000.milliseconds) println("Finished delay") } val launchDurationMs = currentTimeMillis() - launchStart // VERIFY that block timed out assertThat(result.isSuccess).isFalse() // VERIFY that the result was returned faster than the delay's minimum duration assertThat(launchDurationMs).isAtMost(999) } @Test fun awaitAndCancelOnTimeout_coroutine() = runBlocking { val deferred = CompletableDeferred<Int>() launch { delay(100.milliseconds) deferred.complete(1) } val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isSuccess).isTrue() assertThat(result.getOrNull()).isEqualTo(1) } @Test fun awaitAndCancelOnTimeout_thread_sleep() = runBlocking { val deferred = CompletableDeferred<Int>() Thread { Thread.sleep(100) deferred.complete(1) } .start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isSuccess).isTrue() assertThat(result.getOrNull()).isEqualTo(1) } @Test fun awaitAndCancelOnTimeout_thread_sleepForeverUntilCancelled() = runBlocking { val deferred = CompletableDeferred<Int>() val thread = Thread { while (!deferred.isCancelled) { Thread.sleep(100) } } thread.start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) assertThat(result.isFailure).isTrue() delay(200.milliseconds) assertThat(thread.isAlive).isFalse() } @Test fun awaitAndCancelOnTimeout_thread_busyLoop() = runBlocking { val deferred = CompletableDeferred<Int>() val thread = Thread { val busyLoopStart = currentTimeMillis() println("Starting busy loop") while (currentTimeMillis() - busyLoopStart < 1000) { // busy loop } println("Finished busy loop") } val operationStart = currentTimeMillis() thread.start() val result = deferred.awaitAndCancelOnTimeout(500.milliseconds) val operationDurationMs = currentTimeMillis() - operationStart // VERIFY that the thread is still running assertThat(thread.isAlive).isTrue() // VERIFY that block timed out assertThat(result.isSuccess).isFalse() // VERIFY that the result was returned faster than the busy loop's minimum duration assertThat(operationDurationMs).isAtMost(999) // CLEANUP: let the busy loop end delay(1000.milliseconds) // VERIFY that the thread is stopped assertThat(thread.isAlive).isFalse() } }
packages/SystemUI/src/com/android/systemui/util/kotlin/Suspend.kt +50 −1 Original line number Diff line number Diff line Loading @@ -16,9 +16,14 @@ package com.android.systemui.util.kotlin import com.android.app.tracing.coroutines.launchTraced as launch import java.util.concurrent.CancellationException import java.util.concurrent.TimeoutException import kotlin.time.Duration import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import kotlinx.coroutines.coroutineScope import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.delay /** * Runs the given [blocks] in parallel, returning the result of the first one to complete, and Loading @@ -33,3 +38,47 @@ suspend fun <R> race(vararg blocks: suspend () -> R): R = coroutineScope { } completion.await().also { raceJob.cancel() } } /** * Runs the given [block] in parallel with a timeout, returning failure if the timeout completes * first. */ suspend fun <R> launchWithTimeout(timeout: Duration, block: suspend () -> R): Result<R> = race( { Result.success(block()) }, { delay(timeout) Result.failure(TimeoutException("$block timed out after $timeout")) }, ) /** * Awaits this [Deferred] until the given [timeout] has elapsed. * * If the timeout is reached before the deferred completes, the returned [Result] will contain * [TimeoutException], and the deferred will be cancelled. * * If the deferred completes normally and in time, the returned Result will contain the value. * * If the deferred completes exceptionally, the returned Result will contain the exception. * * This is designed to solve situations that arise when the deferred work is not cooperative, for * example when it is loading bitmap data from another process and we want to have a limit on how * long we will wait, even if we cannot cancel that operation. */ suspend fun <R> Deferred<R>.awaitAndCancelOnTimeout(timeout: Duration): Result<R> { val deferred = this return race( { try { Result.success(deferred.await()) } catch (ex: CancellationException) { deferred.cancel() throw ex } catch (ex: Exception) { Result.failure(ex) } }, { delay(timeout) Result.failure(TimeoutException("$deferred timed out after $timeout")) }, ) }