Merge pull request #25 from bnorm/soft-assertion-sample

Create soft assertion sample and fix bugs it exposed
This commit is contained in:
Brian Norman
2020-09-27 20:38:22 +00:00
committed by GitHub
4 changed files with 94 additions and 25 deletions
@@ -17,8 +17,7 @@
package com.bnorm.power package com.bnorm.power
import com.bnorm.power.internal.ReturnableBlockTransformer import com.bnorm.power.internal.ReturnableBlockTransformer
import org.jetbrains.kotlin.backend.common.FileLoweringPass import org.jetbrains.kotlin.backend.common.*
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.ir.asSimpleLambda import org.jetbrains.kotlin.backend.common.ir.asSimpleLambda
import org.jetbrains.kotlin.backend.common.ir.inline 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.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.path import org.jetbrains.kotlin.ir.declarations.path
import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.*
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.impl.IrFunctionExpressionImpl import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.util.*
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.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid
@@ -138,7 +126,7 @@ class PowerAssertCallTransformer(
val title = when { val title = when {
messageArgument is IrConst<*> -> messageArgument messageArgument is IrConst<*> -> messageArgument
messageArgument is IrStringConcatenation -> 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 -> { messageArgument != null -> {
val invoke = messageArgument.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INVOKE } val invoke = messageArgument.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INVOKE }
irCallOp(invoke.symbol, invoke.returnType, messageArgument) irCallOp(invoke.symbol, invoke.returnType, messageArgument)
@@ -147,20 +135,20 @@ class PowerAssertCallTransformer(
else -> irString("Assertion failed") 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()) // println(tree.dump())
return generator.buildAssert(this, root) return generator.buildAssert(this, root)
// .also { println(it.dump())} // .also { println(it.dump()) }
} }
} }
private interface FunctionDelegate { 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? { private fun findDelegate(fqName: FqName): FunctionDelegate? {
@@ -174,8 +162,13 @@ class PowerAssertCallTransformer(
return@mapNotNull when { return@mapNotNull when {
isStringSupertype(parameters[1].type) -> { isStringSupertype(parameters[1].type) -> {
object : FunctionDelegate { 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 { 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(0, irFalse())
putValueArgument(1, message) putValueArgument(1, message)
} }
@@ -184,7 +177,7 @@ class PowerAssertCallTransformer(
} }
isStringFunction(parameters[1].type) -> { isStringFunction(parameters[1].type) -> {
object : FunctionDelegate { 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 scope = this
val lambda = buildFun { val lambda = buildFun {
name = Name.special("<anonymous>") name = Name.special("<anonymous>")
@@ -198,8 +191,13 @@ class PowerAssertCallTransformer(
} }
parent = scope.parent 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 { 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(0, irFalse())
putValueArgument(1, expression) putValueArgument(1, expression)
} }
+7 -1
View File
@@ -57,5 +57,11 @@ kotlin {
} }
configure<com.bnorm.power.PowerAssertGradleExtension> { configure<com.bnorm.power.PowerAssertGradleExtension> {
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"
)
} }
@@ -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<Throwable>()
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 <R> 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()
}
@@ -29,4 +29,22 @@ class PowerAssertTest {
fun require() { fun require() {
require(Person.UNKNOWN.size == 1) require(Person.UNKNOWN.size == 1)
} }
@Test
fun softAssert() {
val unknown: List<Person>? = 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" }
}
}
} }