Skip to content
This repository was archived by the owner on Jun 3, 2025. It is now read-only.

Commit d618452

Browse files
authored
Merge pull request #25 from bnorm/soft-assertion-sample
Create soft assertion sample and fix bugs it exposed
2 parents c91ca0d + 5dac80c commit d618452

File tree

4 files changed

+94
-25
lines changed

4 files changed

+94
-25
lines changed

kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
package com.bnorm.power
1818

1919
import com.bnorm.power.internal.ReturnableBlockTransformer
20-
import org.jetbrains.kotlin.backend.common.FileLoweringPass
21-
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
20+
import org.jetbrains.kotlin.backend.common.*
2221
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
2322
import org.jetbrains.kotlin.backend.common.ir.asSimpleLambda
2423
import org.jetbrains.kotlin.backend.common.ir.inline
@@ -39,22 +38,11 @@ import org.jetbrains.kotlin.ir.builders.parent
3938
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
4039
import org.jetbrains.kotlin.ir.declarations.IrFile
4140
import org.jetbrains.kotlin.ir.declarations.path
42-
import org.jetbrains.kotlin.ir.expressions.IrCall
43-
import org.jetbrains.kotlin.ir.expressions.IrConst
44-
import org.jetbrains.kotlin.ir.expressions.IrExpression
45-
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
46-
import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation
41+
import org.jetbrains.kotlin.ir.expressions.*
4742
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
4843
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
49-
import org.jetbrains.kotlin.ir.types.IrSimpleType
50-
import org.jetbrains.kotlin.ir.types.IrType
51-
import org.jetbrains.kotlin.ir.types.IrTypeArgument
52-
import org.jetbrains.kotlin.ir.types.IrTypeProjection
53-
import org.jetbrains.kotlin.ir.types.getClass
54-
import org.jetbrains.kotlin.ir.types.isBoolean
55-
import org.jetbrains.kotlin.ir.types.isSubtypeOf
56-
import org.jetbrains.kotlin.ir.util.functions
57-
import org.jetbrains.kotlin.ir.util.isFunctionOrKFunction
44+
import org.jetbrains.kotlin.ir.types.*
45+
import org.jetbrains.kotlin.ir.util.*
5846
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
5947
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
6048
import org.jetbrains.kotlin.ir.visitors.acceptVoid
@@ -138,7 +126,7 @@ class PowerAssertCallTransformer(
138126
val title = when {
139127
messageArgument is IrConst<*> -> messageArgument
140128
messageArgument is IrStringConcatenation -> messageArgument
141-
lambda != null -> lambda.inline(parent).transform(ReturnableBlockTransformer(context, symbol), null)
129+
lambda != null -> lambda.deepCopyWithSymbols(parent).inline(parent).transform(ReturnableBlockTransformer(context, symbol), null)
142130
messageArgument != null -> {
143131
val invoke = messageArgument.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INVOKE }
144132
irCallOp(invoke.symbol, invoke.returnType, messageArgument)
@@ -147,20 +135,20 @@ class PowerAssertCallTransformer(
147135
else -> irString("Assertion failed")
148136
}
149137

150-
return delegate.buildCall(this, buildMessage(file, fileSource, title, expression, subStack))
138+
return delegate.buildCall(this, expression, buildMessage(file, fileSource, title.deepCopyWithSymbols(parent), expression, subStack))
151139
}
152140
}
153141

154-
// println(assertionArgument.dump())
142+
// println(expression.dump())
155143
// println(tree.dump())
156144

157145
return generator.buildAssert(this, root)
158-
// .also { println(it.dump())}
146+
// .also { println(it.dump()) }
159147
}
160148
}
161149

162150
private interface FunctionDelegate {
163-
fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression
151+
fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression
164152
}
165153

166154
private fun findDelegate(fqName: FqName): FunctionDelegate? {
@@ -174,8 +162,13 @@ class PowerAssertCallTransformer(
174162
return@mapNotNull when {
175163
isStringSupertype(parameters[1].type) -> {
176164
object : FunctionDelegate {
177-
override fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression = with(builder) {
165+
override fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression = with(builder) {
178166
irCall(overload, type = overload.owner.returnType).apply {
167+
dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent)
168+
extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent)
169+
for (i in 0 until original.typeArgumentsCount) {
170+
putTypeArgument(i, original.getTypeArgument(i))
171+
}
179172
putValueArgument(0, irFalse())
180173
putValueArgument(1, message)
181174
}
@@ -184,7 +177,7 @@ class PowerAssertCallTransformer(
184177
}
185178
isStringFunction(parameters[1].type) -> {
186179
object : FunctionDelegate {
187-
override fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression = with(builder) {
180+
override fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression = with(builder) {
188181
val scope = this
189182
val lambda = buildFun {
190183
name = Name.special("<anonymous>")
@@ -198,8 +191,13 @@ class PowerAssertCallTransformer(
198191
}
199192
parent = scope.parent
200193
}
201-
val expression = IrFunctionExpressionImpl(-1, -1, context.irBuiltIns.stringType, lambda, IrStatementOrigin.LAMBDA)
194+
val expression = IrFunctionExpressionImpl(-1, -1, parameters[1].type, lambda, IrStatementOrigin.LAMBDA)
202195
irCall(overload, type = overload.owner.returnType).apply {
196+
dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent)
197+
extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent)
198+
for (i in 0 until original.typeArgumentsCount) {
199+
putTypeArgument(i, original.getTypeArgument(i))
200+
}
203201
putValueArgument(0, irFalse())
204202
putValueArgument(1, expression)
205203
}

sample/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,11 @@ kotlin {
5757
}
5858

5959
configure<com.bnorm.power.PowerAssertGradleExtension> {
60-
functions = listOf("kotlin.assert", "kotlin.test.assertTrue", "kotlin.require")
60+
functions = listOf(
61+
"kotlin.assert",
62+
"kotlin.test.assertTrue",
63+
"kotlin.require",
64+
"com.bnorm.power.AssertScope.assert",
65+
"com.bnorm.power.assert"
66+
)
6167
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.bnorm.power
2+
3+
import kotlin.contracts.*
4+
5+
typealias LazyMessage = () -> Any
6+
7+
interface AssertScope {
8+
fun assert(assertion: Boolean, lazyMessage: LazyMessage? = null)
9+
}
10+
11+
@OptIn(ExperimentalContracts::class)
12+
fun assert(assertion: Boolean, lazyMessage: LazyMessage? = null) {
13+
contract { returns() implies assertion }
14+
if (!assertion) {
15+
throw AssertionError(lazyMessage?.invoke()?.toString())
16+
}
17+
}
18+
19+
private class SoftAssertScope : AssertScope {
20+
private val assertions = mutableListOf<Throwable>()
21+
22+
override fun assert(assertion: Boolean, lazyMessage: LazyMessage?) {
23+
if (!assertion) {
24+
assertions.add(AssertionError(lazyMessage?.invoke()?.toString()))
25+
}
26+
}
27+
28+
fun close(exception: Throwable? = null) {
29+
if (assertions.isNotEmpty()) {
30+
val base = exception ?: AssertionError("Multiple failed assertions")
31+
for (assertion in assertions) {
32+
base.addSuppressed(assertion)
33+
}
34+
throw base
35+
}
36+
}
37+
}
38+
39+
@OptIn(ExperimentalContracts::class)
40+
fun <R> assertSoftly(block: AssertScope.() -> R): R {
41+
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
42+
43+
val scope = SoftAssertScope()
44+
val result = runCatching { scope.block() }
45+
scope.close(result.exceptionOrNull())
46+
return result.getOrThrow()
47+
}

sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,22 @@ class PowerAssertTest {
2929
fun require() {
3030
require(Person.UNKNOWN.size == 1)
3131
}
32+
33+
@Test
34+
fun softAssert() {
35+
val unknown: List<Person>? = Person.UNKNOWN
36+
assert(unknown != null)
37+
assert(unknown.size == 2)
38+
39+
val jane: Person
40+
val john: Person
41+
assertSoftly {
42+
jane = unknown[0]
43+
assert(jane.firstName == "Jane")
44+
assert(jane.lastName == "Doe") { "bad jane last name" }
45+
46+
john = unknown[1]
47+
assert(john.lastName == "Doe" && john.firstName == "John") { "bad john" }
48+
}
49+
}
3250
}

0 commit comments

Comments
 (0)