From 5dac80c5ddbf8c5c66aae0ac4c94748968783fe0 Mon Sep 17 00:00:00 2001 From: Brian Norman Date: Sun, 27 Sep 2020 15:14:22 -0500 Subject: [PATCH] Create soft assertion sample and fix bugs it exposed --- .../bnorm/power/PowerAssertCallTransformer.kt | 46 +++++++++--------- sample/build.gradle.kts | 8 +++- .../kotlin/com/bnorm/power/AssertScope.kt | 47 +++++++++++++++++++ .../kotlin/com/bnorm/power/PowerAssertTest.kt | 18 +++++++ 4 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 sample/src/commonMain/kotlin/com/bnorm/power/AssertScope.kt diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt index e5d06abe7f0..14bd6fd876d 100644 --- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt +++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt @@ -17,8 +17,7 @@ package com.bnorm.power import com.bnorm.power.internal.ReturnableBlockTransformer -import org.jetbrains.kotlin.backend.common.FileLoweringPass -import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.* import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.ir.asSimpleLambda import org.jetbrains.kotlin.backend.common.ir.inline @@ -39,22 +38,11 @@ import org.jetbrains.kotlin.ir.builders.parent import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.path -import org.jetbrains.kotlin.ir.expressions.IrCall -import org.jetbrains.kotlin.ir.expressions.IrConst -import org.jetbrains.kotlin.ir.expressions.IrExpression -import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin -import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation +import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol -import org.jetbrains.kotlin.ir.types.IrSimpleType -import org.jetbrains.kotlin.ir.types.IrType -import org.jetbrains.kotlin.ir.types.IrTypeArgument -import org.jetbrains.kotlin.ir.types.IrTypeProjection -import org.jetbrains.kotlin.ir.types.getClass -import org.jetbrains.kotlin.ir.types.isBoolean -import org.jetbrains.kotlin.ir.types.isSubtypeOf -import org.jetbrains.kotlin.ir.util.functions -import org.jetbrains.kotlin.ir.util.isFunctionOrKFunction +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid @@ -138,7 +126,7 @@ class PowerAssertCallTransformer( val title = when { messageArgument is IrConst<*> -> messageArgument messageArgument is IrStringConcatenation -> messageArgument - lambda != null -> lambda.inline(parent).transform(ReturnableBlockTransformer(context, symbol), null) + lambda != null -> lambda.deepCopyWithSymbols(parent).inline(parent).transform(ReturnableBlockTransformer(context, symbol), null) messageArgument != null -> { val invoke = messageArgument.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INVOKE } irCallOp(invoke.symbol, invoke.returnType, messageArgument) @@ -147,20 +135,20 @@ class PowerAssertCallTransformer( else -> irString("Assertion failed") } - return delegate.buildCall(this, buildMessage(file, fileSource, title, expression, subStack)) + return delegate.buildCall(this, expression, buildMessage(file, fileSource, title.deepCopyWithSymbols(parent), expression, subStack)) } } -// println(assertionArgument.dump()) +// println(expression.dump()) // println(tree.dump()) return generator.buildAssert(this, root) -// .also { println(it.dump())} +// .also { println(it.dump()) } } } private interface FunctionDelegate { - fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression + fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression } private fun findDelegate(fqName: FqName): FunctionDelegate? { @@ -174,8 +162,13 @@ class PowerAssertCallTransformer( return@mapNotNull when { isStringSupertype(parameters[1].type) -> { object : FunctionDelegate { - override fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression = with(builder) { + override fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression = with(builder) { irCall(overload, type = overload.owner.returnType).apply { + dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) + extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent) + for (i in 0 until original.typeArgumentsCount) { + putTypeArgument(i, original.getTypeArgument(i)) + } putValueArgument(0, irFalse()) putValueArgument(1, message) } @@ -184,7 +177,7 @@ class PowerAssertCallTransformer( } isStringFunction(parameters[1].type) -> { object : FunctionDelegate { - override fun buildCall(builder: IrBuilderWithScope, message: IrExpression): IrExpression = with(builder) { + override fun buildCall(builder: IrBuilderWithScope, original: IrCall, message: IrExpression): IrExpression = with(builder) { val scope = this val lambda = buildFun { name = Name.special("") @@ -198,8 +191,13 @@ class PowerAssertCallTransformer( } parent = scope.parent } - val expression = IrFunctionExpressionImpl(-1, -1, context.irBuiltIns.stringType, lambda, IrStatementOrigin.LAMBDA) + val expression = IrFunctionExpressionImpl(-1, -1, parameters[1].type, lambda, IrStatementOrigin.LAMBDA) irCall(overload, type = overload.owner.returnType).apply { + dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) + extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent) + for (i in 0 until original.typeArgumentsCount) { + putTypeArgument(i, original.getTypeArgument(i)) + } putValueArgument(0, irFalse()) putValueArgument(1, expression) } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index b89bfadfc1b..7b95f2eca87 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -57,5 +57,11 @@ kotlin { } configure { - functions = listOf("kotlin.assert", "kotlin.test.assertTrue", "kotlin.require") + functions = listOf( + "kotlin.assert", + "kotlin.test.assertTrue", + "kotlin.require", + "com.bnorm.power.AssertScope.assert", + "com.bnorm.power.assert" + ) } diff --git a/sample/src/commonMain/kotlin/com/bnorm/power/AssertScope.kt b/sample/src/commonMain/kotlin/com/bnorm/power/AssertScope.kt new file mode 100644 index 00000000000..8f966a979ea --- /dev/null +++ b/sample/src/commonMain/kotlin/com/bnorm/power/AssertScope.kt @@ -0,0 +1,47 @@ +package com.bnorm.power + +import kotlin.contracts.* + +typealias LazyMessage = () -> Any + +interface AssertScope { + fun assert(assertion: Boolean, lazyMessage: LazyMessage? = null) +} + +@OptIn(ExperimentalContracts::class) +fun assert(assertion: Boolean, lazyMessage: LazyMessage? = null) { + contract { returns() implies assertion } + if (!assertion) { + throw AssertionError(lazyMessage?.invoke()?.toString()) + } +} + +private class SoftAssertScope : AssertScope { + private val assertions = mutableListOf() + + override fun assert(assertion: Boolean, lazyMessage: LazyMessage?) { + if (!assertion) { + assertions.add(AssertionError(lazyMessage?.invoke()?.toString())) + } + } + + fun close(exception: Throwable? = null) { + if (assertions.isNotEmpty()) { + val base = exception ?: AssertionError("Multiple failed assertions") + for (assertion in assertions) { + base.addSuppressed(assertion) + } + throw base + } + } +} + +@OptIn(ExperimentalContracts::class) +fun assertSoftly(block: AssertScope.() -> R): R { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + + val scope = SoftAssertScope() + val result = runCatching { scope.block() } + scope.close(result.exceptionOrNull()) + return result.getOrThrow() +} diff --git a/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt b/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt index 5db665d2628..f0d83266453 100644 --- a/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt +++ b/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt @@ -29,4 +29,22 @@ class PowerAssertTest { fun require() { require(Person.UNKNOWN.size == 1) } + + @Test + fun softAssert() { + val unknown: List? = Person.UNKNOWN + assert(unknown != null) + assert(unknown.size == 2) + + val jane: Person + val john: Person + assertSoftly { + jane = unknown[0] + assert(jane.firstName == "Jane") + assert(jane.lastName == "Doe") { "bad jane last name" } + + john = unknown[1] + assert(john.lastName == "Doe" && john.firstName == "John") { "bad john" } + } + } }