From d4f7eeba38447ba6629f607d6e0b397187db0c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 28 Oct 2024 18:32:11 +0100 Subject: [PATCH] 1.9.0: More plain Result features (#10) - add WasmJS target - add WasmWasi target (not or KmmResult-test, as Kotest does not support WASI yet) - Function Renames (old ones are still present, but deprecated) - rename `wrapping` -> `catchingAs` but keep the old names as deprecated alternative - add `catchingUnwrappedAs`, which works just like `catchingAs` but on a `Result` rather than a `KmmResult` to avoid instantiation overhead --- .github/workflows/build-jvm.yml | 4 +- CHANGELOG.md | 9 +- README.md | 8 +- gradle.properties | 2 +- kmmresult-test/build.gradle.kts | 15 +- kmmresult/build.gradle.kts | 14 +- .../kotlin/at/asitplus/KmmResult.kt | 89 -------- .../commonMain/kotlin/at/asitplus/NonFatal.kt | 37 ---- .../kotlin/at/asitplus/NonFatalCatching.kt | 202 ++++++++++++++++++ .../src/commonTest/kotlin/KmmResultTest.kt | 73 +++++-- 10 files changed, 285 insertions(+), 168 deletions(-) delete mode 100644 kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt create mode 100644 kmmresult/src/commonMain/kotlin/at/asitplus/NonFatalCatching.kt diff --git a/.github/workflows/build-jvm.yml b/.github/workflows/build-jvm.yml index cd9b378..3403f55 100644 --- a/.github/workflows/build-jvm.yml +++ b/.github/workflows/build-jvm.yml @@ -1,8 +1,8 @@ -name: Build JVM/JS/iOS/macOS/tvOS/MinGW/Linux +name: Build JVM/JS/iOS/macOS/tvOS/MinGW/wasmJS/wasmWasi/Linux on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd00d0..eb5a227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,4 +55,11 @@ - `result.shouldSucceed()` returning the contained value - remove Arrow dependency and import arrow's list of Fatal exceptions directly into our code - Introduce `Result.nonFatalOrThrow` to mimic KmmResult's non-fatal-only behaviour, but without the object instantiation overhead -- Introduce `carchingUnwrapped`, which mimics KmmResult's non-fatal-only behaviour, but without the object instantiation overhead \ No newline at end of file +- Introduce `carchingUnwrapped`, which mimics KmmResult's non-fatal-only behaviour, but without the object instantiation overhead + +## 1.9.0 +- add WasmJS target +- add WasmWasi target (not for KmmResult-test, as Kotest does not support WASI yet) +- Function Renames (old ones are still present, but deprecated) +- rename `wrapping` -> `catchingAs` but keep the old names as deprecated alternative + - add `catchingUnwrappedAs`, which works just like `catchingAs` but on a `Result` rather than a `KmmResult` to avoid instantiation overhead diff --git a/README.md b/README.md index 38a76b4..15ea668 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ is *not* a value class (a sealed `Either` type also does not interop well with S `KmmResult` comes to the rescue! → [Full documentation](https://a-sit-plus.github.io/KmmResult/). - ## Using in your Projects This library is available at maven central. @@ -88,12 +87,11 @@ KmmResult comes with `catching`. This is a non-fatal-only-catching version of st It re-throws any fatal exceptions, such as `OutOfMemoryError`. The underlying logic is borrowed from [Arrow's](https://arrow-kt.io)'s [`nonFatalOrThrow`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html). -The only downside of `catching` is that it incurs instatiation overhead, because it creates a `KmmResult` instance. +The only downside of `catching` is that it incurs instantiation overhead, because it creates a `KmmResult` instance. Internally, though, only the behaviour is important, not Swift interop. Hence, you don't care for a `KmmResult` and you certainly don't care for the cost of instantiating an object. Here, the `Result.nonFatalOrThrow()` extension shipped with KmmResult -comes to the rescue. It does exactly what the name suggest: It re-throws any fatal exception and leaved the `Result` object -untouched otherwise. - +comes to the rescue. It does exactly what the name suggest: It re-throws any fatal exception and leaves the `Result` object +untouched otherwise. As a convenience shorthand, there's `catchingUnwrapped` which directly returns an stdlib `Result`. Happy folding! diff --git a/gradle.properties b/gradle.properties index 9dd40b5..3f4a648 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.stability.nowarn=true kotlin.native.ignoreDisabledTargets=true -artifactVersion = 1.8.0 +artifactVersion = 1.9.0 org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled \ No newline at end of file diff --git a/kmmresult-test/build.gradle.kts b/kmmresult-test/build.gradle.kts index 871c252..345616f 100644 --- a/kmmresult-test/build.gradle.kts +++ b/kmmresult-test/build.gradle.kts @@ -1,6 +1,5 @@ import io.gitlab.arturbosch.detekt.Detekt -import org.gradle.kotlin.dsl.support.listFilesOrdered -import java.lang.management.ManagementFactory +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import java.net.URI plugins { @@ -86,10 +85,14 @@ kotlin { withJava() //for Java Interop tests } - js(IR) { - browser { testTask { enabled = false } } - nodejs() + listOf( + js(IR).apply { browser { testTask { enabled = false } } }, + @OptIn(ExperimentalWasmDsl::class) + wasmJs().apply { browser { testTask { enabled = false } } } + ).forEach { + it.nodejs() } + linuxX64() linuxArm64() mingwX64() @@ -97,7 +100,7 @@ kotlin { sourceSets { commonMain.dependencies { implementation(project(":kmmresult")) - api("io.kotest:kotest-assertions-core:5.9.1") + api("io.kotest:kotest-assertions-core:6.0.0.M1") } } diff --git a/kmmresult/build.gradle.kts b/kmmresult/build.gradle.kts index c139fd6..b20e9e2 100644 --- a/kmmresult/build.gradle.kts +++ b/kmmresult/build.gradle.kts @@ -1,4 +1,5 @@ import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import java.net.URI plugins { @@ -78,7 +79,6 @@ kotlin { it.binaries.framework { baseName = "KmmResult" binaryOption("bundleId", "at.asitplus.KmmResult") - embedBitcode("bitcode") xcf.add(this) isStatic = true } @@ -96,9 +96,15 @@ kotlin { withJava() //for Java Interop tests } - js(IR) { - browser { testTask { enabled = false } } - nodejs() + + listOf( + js(IR).apply { browser { testTask { enabled = false } } }, + @OptIn(ExperimentalWasmDsl::class) + wasmJs().apply { browser { testTask { enabled = false } } }, + @OptIn(ExperimentalWasmDsl::class) + wasmWasi() + ).forEach { + it.nodejs() } linuxX64() diff --git a/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt b/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt index 6c59566..9be8bb3 100644 --- a/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt +++ b/kmmresult/src/commonMain/kotlin/at/asitplus/KmmResult.kt @@ -264,92 +264,3 @@ inline fun KmmResult.recoverCatching(block: (error: Throwable) -> else -> catching { block(x) } } } - -/** - * Non-fatal-only-catching version of stdlib's [runCatching], directly returning a [KmmResult] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) - * logic to avoid a dependency on Arrow for a single function. - */ -@Suppress("TooGenericExceptionCaught") -inline fun catching(block: () -> T): KmmResult { - contract { - // not EXACTLY_ONCE, because inside a try block! - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return try { - KmmResult.success(block()) - } catch (e: Throwable) { - KmmResult.failure(e) - } -} - -/** - * Non-fatal-only-catching version of stdlib's [runCatching] (calling the specified function [block] with `this` value - * as its receiver), directly returning a [KmmResult] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) - * logic to avoid a dependency on Arrow for a single function. - */ -@Suppress("TooGenericExceptionCaught") -inline fun T.catching(block: T.() -> R): KmmResult { - contract { - // not EXACTLY_ONCE, because inside a try block! - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return try { - KmmResult.success(block()) - } catch (e: Throwable) { - KmmResult.failure(e) - } -} - -/** - * Runs the specified function [block], returning a [KmmResult]. - * Any non-fatal exception will be wrapped as the specified exception, unless it is already the specified type. - * - * Usage: `wrapping(asA = ::ThrowableType) { block }`. - */ -@Suppress("TooGenericExceptionCaught") -inline fun wrapping(asA: (String?, Throwable) -> E, block: () -> R): KmmResult { - contract { - callsInPlace(asA, InvocationKind.AT_MOST_ONCE) - // not EXACTLY_ONCE, because inside a try block! - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return try { - KmmResult.success(block()) - } catch (e: Throwable) { - KmmResult.failure( - when (e.nonFatalOrThrow()) { - is E -> e - else -> asA(e.message, e) - } - ) - } -} - -/** - * Runs the specified function [block] with `this` as its receiver, returning a [KmmResult]. - * Any non-fatal exception will be wrapped as the specified exception, unless it is already the specified type. - * - * Usage: `wrapping(asA = ::ThrowableType) { block }`. - */ -@Suppress("TooGenericExceptionCaught") -inline fun T.wrapping(asA: (String?, Throwable) -> E, block: T.() -> R): KmmResult { - contract { - callsInPlace(asA, InvocationKind.AT_MOST_ONCE) - // not EXACTLY_ONCE, because inside a try block! - callsInPlace(block, InvocationKind.AT_MOST_ONCE) - } - return try { - KmmResult.success(block()) - } catch (e: Throwable) { - KmmResult.failure( - when (e.nonFatalOrThrow()) { - is E -> e - else -> asA(e.message, e) - } - ) - } -} diff --git a/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt b/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt deleted file mode 100644 index 767bee6..0000000 --- a/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatal.kt +++ /dev/null @@ -1,37 +0,0 @@ -package at.asitplus - -/** - * Throws any fatal exceptions. This is a re-implementation taken from Arrow's - * [`nonFatalOrThrow`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) – - * to avoid a dependency on Arrow for a single function. - */ -@Suppress("NOTHING_TO_INLINE") -expect inline fun Throwable.nonFatalOrThrow(): Throwable - -/** - * Helper to effectively convert stdlib's [runCatching] to behave like KmmResult's Non-fatal-only [catching]. I.e. any - * fatal exceptions are thrown. - * The reason this exists is that [catching] incurs instantiation cost. - * This helper hence provides the best of both worlds. - */ -@Suppress("NOTHING_TO_INLINE") -inline fun Result.nonFatalOrThrow(): Result = this.onFailure { it.nonFatalOrThrow() } - -/** - * Non-fatal-only-catching version of stdlib's [runCatching], returning a [Result] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) - * logic to avoid a dependency on Arrow for a single function. - */ -@Suppress("NOTHING_TO_INLINE") -inline fun catchingUnwrapped(block: () -> T): Result = runCatching(block).nonFatalOrThrow() - -/** - * Non-fatal-only-catching version of stdlib's [runCatching] (calling the specified function [block] with `this` value - * as its receiver), directly returning a [Result] -- - * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s - * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) - * logic to avoid a dependency on Arrow for a single function. - */ -@Suppress("NOTHING_TO_INLINE") -inline fun T.catchingUnwrapped(block: T.() -> R): Result = runCatching(block).nonFatalOrThrow() diff --git a/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatalCatching.kt b/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatalCatching.kt new file mode 100644 index 0000000..029428e --- /dev/null +++ b/kmmresult/src/commonMain/kotlin/at/asitplus/NonFatalCatching.kt @@ -0,0 +1,202 @@ +@file:OptIn(kotlin.contracts.ExperimentalContracts::class) +@file:Suppress("TooManyFunctions", "TooGenericExceptionCaught", "MultiLineIfElse") + +package at.asitplus + +import at.asitplus.KmmResult.Companion.wrap +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Throws any fatal exceptions. This is a re-implementation taken from Arrow's + * [`nonFatalOrThrow`](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) – + * to avoid a dependency on Arrow for a single function. + */ +@Suppress("NOTHING_TO_INLINE") +expect inline fun Throwable.nonFatalOrThrow(): Throwable + +/** + * Helper to effectively convert stdlib's [runCatching] to behave like KmmResult's Non-fatal-only [catching]. I.e. any + * fatal exceptions are thrown. + * The reason this exists is that [catching] incurs instantiation cost. + * This helper hence provides the best of both worlds. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun Result.nonFatalOrThrow(): Result = this.onFailure { it.nonFatalOrThrow() } + +/** + * Non-fatal-only-catching version of stdlib's [runCatching], returning a [Result] -- + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. + */ +@Suppress("NOTHING_TO_INLINE") +inline fun catchingUnwrapped(block: () -> T): Result { + contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + return try { + Result.success(block()) + } catch (e: Throwable) { + Result.failure(e.nonFatalOrThrow()) + } +} + +/** @see catchingUnwrapped */ +@Suppress("NOTHING_TO_INLINE") +inline fun T.catchingUnwrapped(block: T.() -> R): Result { + contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + return try { + Result.success(block()) + } catch (e: Throwable) { + Result.failure(e.nonFatalOrThrow()) + } +} + +/** + * Non-fatal-only-catching version of stdlib's [runCatching], directly returning a [KmmResult] -- + * Re-throws any fatal exceptions, such as `OutOfMemoryError`. Re-implements [Arrow](https://arrow-kt.io)'s + * [nonFatalOrThrow](https://apidocs.arrow-kt.io/arrow-core/arrow.core/non-fatal-or-throw.html) + * logic to avoid a dependency on Arrow for a single function. + */ +inline fun catching(block: () -> T): KmmResult { + contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + return catchingUnwrapped(block).wrap() +} + +/** @see catching */ +inline fun R.catching(block: R.() -> T): KmmResult { + contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + return catchingUnwrapped(block).wrap() +} + +/** + * If the underlying [Result] is successful, returns it unchanged. + * If it failed, and the contained exception is of the specified type, returns it unchanged. + * Otherwise, wraps the contained exception in the specified type. + * + * Usage: `Result.wrapAs(a = ::ThrowableType)` + */ +inline fun Result.wrapAs(a: (String?, Throwable) -> E): Result { + contract { callsInPlace(a, InvocationKind.AT_MOST_ONCE) } + return exceptionOrNull().let { x -> + if ((x == null) || (x is E)) this@wrapAs + else Result.failure(a(x.message, x)) + } +} + +/** @see wrapAs */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +inline fun Result.wrapAs(a: (Throwable) -> E): Result { + contract { callsInPlace(a, InvocationKind.AT_MOST_ONCE) } + return wrapAs(a = { _, x -> a(x) }) +} + +/** + * Runs the specified function [block], returning a [Result]. + * Any non-fatal exception will be wrapped as the specified exception, unless it is already the specified type. + * + * Usage: `catchingUnwrappedAs(type = ::ThrowableType) { block }`. + */ +inline fun catchingUnwrappedAs(a: (String?, Throwable) -> E, block: () -> T): Result { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrapped(block).wrapAs(a) +} + +/** @see catchingUnwrappedAs */ +inline fun R.catchingUnwrappedAs( + a: (String?, Throwable) -> E, + block: R.() -> T +): Result { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrapped(block).wrapAs(a) +} + +/** @see catchingUnwrappedAs */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +inline fun catchingUnwrappedAs(a: (Throwable) -> E, block: () -> T): Result { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrapped(block).wrapAs(a) +} + +/** @see catchingUnwrappedAs */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +inline fun R.catchingUnwrappedAs(a: (Throwable) -> E, block: R.() -> T): Result { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return this.catchingUnwrapped(block).wrapAs(a) +} + +/** + * Runs the specified function [block], returning a [KmmResult]. + * Any non-fatal exception will be wrapped as the specified exception, unless it is already the specified type. + * + * Usage: `catchingAs(a = ::ThrowableType) { block }`. + */ +inline fun catchingAs(a: (String?, Throwable) -> E, block: () -> T): KmmResult { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrappedAs(a, block).wrap() +} + +/** @see catchingAs */ +inline fun R.catchingAs(a: (String?, Throwable) -> E, block: R.() -> T): KmmResult { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrappedAs(a, block).wrap() +} + +/** @see catchingAs */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +inline fun catchingAs(a: (Throwable) -> E, block: () -> T): KmmResult { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrappedAs(a, block).wrap() +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +@kotlin.internal.LowPriorityInOverloadResolution +inline fun R.catchingAs(a: (Throwable) -> E, block: R.() -> T): KmmResult { + contract { + callsInPlace(a, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingUnwrappedAs(a, block).wrap() +} + +@Deprecated("Function name was misleading", ReplaceWith("catchingAs(asA, block)")) +inline fun wrapping(asA: (String?, Throwable) -> E, block: () -> R): KmmResult { + contract { + callsInPlace(asA, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingAs(asA, block) +} + +@Deprecated("Function name was misleading", ReplaceWith("catchingAs(asA, block)")) +inline fun R.wrapping(asA: (String?, Throwable) -> E, block: R.() -> T): KmmResult { + contract { + callsInPlace(asA, InvocationKind.AT_MOST_ONCE) + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return catchingAs(asA, block) +} diff --git a/kmmresult/src/commonTest/kotlin/KmmResultTest.kt b/kmmresult/src/commonTest/kotlin/KmmResultTest.kt index 507c914..a749110 100644 --- a/kmmresult/src/commonTest/kotlin/KmmResultTest.kt +++ b/kmmresult/src/commonTest/kotlin/KmmResultTest.kt @@ -136,29 +136,6 @@ class KmmResultTest { assertEquals(f, f.wrap().unwrap()) } - @Test - fun testWrapping() { - class CustomException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) - wrapping(asA = ::CustomException) { - throw RuntimeException("foo") - }.let { - val ex = it.exceptionOrNull() - assertNotNull(ex) - assertIs(ex) - assertIs(ex.cause) - assertEquals(ex.message, "foo") - } - wrapping(asA = ::CustomException) { - throw CustomException("bar") - }.let { - val ex = it.exceptionOrNull() - assertNotNull(ex) - assertIs(ex) - assertNull(ex.cause) - assertEquals(ex.message, "bar") - } - } - @Test fun testToString() { assertEquals("KmmResult.success(null)", KmmResult.success(null).toString()) @@ -228,9 +205,59 @@ class KmmResultTest { assertFailsWith(CancellationException::class) { catching { throw CancellationException() } } + assertFailsWith(CancellationException::class) { + "Receiver".catching { throw CancellationException() } + } + + assertFailsWith(CancellationException::class) { + catchingUnwrapped { throw CancellationException() } + } + assertFailsWith(CancellationException::class) { + "Receiver".catchingUnwrapped { throw CancellationException() } + } runCatching { throw IndexOutOfBoundsException() }.nonFatalOrThrow() catching { throw IndexOutOfBoundsException() } + } + + @Test + fun testCatchingAs() { + class TestException(val usedTwoArgCtor: Boolean): Throwable() { + @Suppress("UNUSED") + constructor(a:String?,b:Throwable?): this(true) + @Suppress("UNUSED") + constructor(b:Throwable?): this(false) + } + assertIs( + catchingUnwrappedAs(a = ::TestException) { + throw NullPointerException() + }.exceptionOrNull() + ).usedTwoArgCtor.let(::assertTrue) + + assertIs( + catchingAs(a = ::IllegalStateException) { + throw NullPointerException() + }.exceptionOrNull() + ) + assertIs( + catchingUnwrappedAs(a = ::IllegalStateException) { + throw NullPointerException() + }.exceptionOrNull() + ) + + class NestedOnlyException(t: Throwable) : Throwable(t) + + assertIs( + at.asitplus.catchingAs(a = ::NestedOnlyException) { + throw NullPointerException() + }.exceptionOrNull() + ) + + assertIs( + at.asitplus.catchingUnwrappedAs(a = ::NestedOnlyException) { + throw NullPointerException() + }.exceptionOrNull() + ) } }