Merge "Add suspendRunCatching to safely handle exceptions without a coroutine" into tm-qpr-dev am: 21f4c520b4 am: 70a8c58c9d

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21223361

Change-Id: I995a1c44694fc5cdb2bad2b89431a82e13f9ea09
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Marcello Galhardo
2023-02-06 19:56:50 +00:00
committed by Automerger Merge Worker
2 changed files with 115 additions and 0 deletions

View File

@@ -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) }

View File

@@ -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() }
}
}