diff options
| author | 2023-02-06 14:49:18 +0000 | |
|---|---|---|
| committer | 2023-02-06 14:49:18 +0000 | |
| commit | 21f4c520b461efde3b612cbd19828e16ad302f7b (patch) | |
| tree | f9629ea89b080fcb781982ca573d4fcaead1b09c | |
| parent | d57a294101baeda2ef49c36bcbdbf6e564722280 (diff) | |
| parent | b91328aba6c34d0590ee0fd66641e0161c95f056 (diff) | |
Merge "Add `suspendRunCatching` to safely handle exceptions without a coroutine" into tm-qpr-dev
| -rw-r--r-- | packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt | 63 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt | 52 |
2 files changed, 115 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt new file mode 100644 index 000000000000..b9736671b5ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.common.coroutine + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withTimeout + +/** + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the block function execution + * and encapsulating it as a failure. + * + * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing + * any [CancellationException]. + * + * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does + * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use + * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully. + * + * @see <a href="https://github.com/Kotlin/kotlinx.coroutines/issues/1814">link</a> + */ +suspend inline fun <T> suspendRunCatching(crossinline block: suspend () -> T): Result<T> = + try { + Result.success(block()) + } catch (e: Throwable) { + // Ensures the try-catch block will not break structured concurrency. + currentCoroutineContext().ensureActive() + Result.failure(e) + } + +/** + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the block function execution + * and encapsulating it as a failure. + * + * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing + * any [CancellationException]. + * + * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does + * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use + * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully. + * + * @see <a href="https://github.com/Kotlin/kotlinx.coroutines/issues/1814">link</a> + */ +suspend inline fun <T, R> T.suspendRunCatching(crossinline block: suspend T.() -> R): Result<R> = + // Overload with a `this` receiver, matches with `kotlin.runCatching` functions. + // Qualified name needs to be used to avoid a recursive call. + com.android.systemui.common.coroutine.suspendRunCatching { block(this) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt new file mode 100644 index 000000000000..d552c9d922ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.common.coroutine + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +/** atest SystemUITests:CoroutineResultTest */ +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CoroutineResultTest : SysuiTestCase() { + + @Test + fun suspendRunCatching_shouldReturnSuccess() = runTest { + val actual = suspendRunCatching { "Placeholder" } + assertThat(actual.isSuccess).isTrue() + assertThat(actual.getOrNull()).isEqualTo("Placeholder") + } + + @Test + fun suspendRunCatching_whenExceptionThrow_shouldResumeWithException() = runTest { + val actual = suspendRunCatching { throw Exception() } + assertThat(actual.isFailure).isTrue() + assertThat(actual.exceptionOrNull()).isInstanceOf(Exception::class.java) + } + + @Test(expected = CancellationException::class) + fun suspendRunCatching_whenCancelled_shouldResumeWithException() = runTest { + suspendRunCatching { cancel() } + } +} |