diff --git a/README.md b/README.md index 4ee78d7..eb15bd8 100644 --- a/README.md +++ b/README.md @@ -39,29 +39,29 @@ fun drinkSoda( } ``` -### Enum Parameters - -If your parameter is an enum type, you don't need to call `burstValues()`. Burst will test each -value of that enum. +### Boolean Parameters -```kotlin -enum class Distribution { - Fountain, Can, Bottle -} -``` +If your parameter is a boolean, you don't need to call `burstValues()`. Burst will test both values. +Specify a default value to use that in the IDE. ```kotlin @Burst class DrinkSodaTest( - val distribution: Distribution, + val ice: Boolean = true, ) { ... } ``` -If you specify a default value for the enum, Burst will use that when running in the IDE. +### Enum Parameters + +Burst will test each value of enum types. Specify a default value to use that in the IDE. ```kotlin +enum class Distribution { + Fountain, Can, Bottle +} + @Burst class DrinkSodaTest( val distribution: Distribution = Distribution.Can, @@ -78,7 +78,8 @@ Use multiple parameters to test all variations. @Test fun drinkSoda( soda: String = burstValues("Pepsi", "Coke"), - distribution: Distribution, + ice: Boolean = true, + distribution: Distribution = Distribution.Can, ) { ... } @@ -86,12 +87,18 @@ fun drinkSoda( The test will be specialized for each combination of arguments. - * `drinkSoda("Pepsi", Distribution.Fountain)` - * `drinkSoda("Pepsi", Distribution.Can)` - * `drinkSoda("Pepsi", Distribution.Bottle)` - * `drinkSoda("Coke", Distribution.Fountain)` - * `drinkSoda("Coke", Distribution.Can)` - * `drinkSoda("Coke", Distribution.Bottle)` + * `drinkSoda("Pepsi", true, Distribution.Fountain)` + * `drinkSoda("Pepsi", true, Distribution.Can)` + * `drinkSoda("Pepsi", true, Distribution.Bottle)` + * `drinkSoda("Pepsi", false, Distribution.Fountain)` + * `drinkSoda("Pepsi", false, Distribution.Can)` + * `drinkSoda("Pepsi", false, Distribution.Bottle)` + * `drinkSoda("Coke", true, Distribution.Fountain)` + * `drinkSoda("Coke", true, Distribution.Can)` + * `drinkSoda("Coke", true, Distribution.Bottle)` + * `drinkSoda("Coke", false, Distribution.Fountain)` + * `drinkSoda("Coke", false, Distribution.Can)` + * `drinkSoda("Coke", false, Distribution.Bottle)` Gradle Setup ------------ diff --git a/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt b/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt index 91862a8..04485ce 100644 --- a/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt +++ b/burst-kotlin-plugin-tests/src/test/kotlin/app/cash/burst/kotlin/BurstKotlinPluginTest.kt @@ -105,7 +105,7 @@ class BurstKotlinPluginTest { assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode, result.messages) assertThat(result.messages).contains( "CoffeeTest.kt:7:12 " + - "@Burst parameter must be an enum or have a burstValues() default value", + "@Burst parameter must be a boolean, enum, or have a burstValues() default value", ) } @@ -134,7 +134,7 @@ class BurstKotlinPluginTest { assertEquals(KotlinCompilation.ExitCode.COMPILATION_ERROR, result.exitCode, result.messages) assertThat(result.messages).contains( "CoffeeTest.kt:9:12 " + - "@Burst parameter default value must be burstValues(), an enum constant, or absent", + "@Burst parameter default value must be burstValues(), a constant, or absent", ) } @@ -573,6 +573,88 @@ class BurstKotlinPluginTest { ) } + @Test + fun booleanParameters() { + val result = compile( + sourceFile = SourceFile.kotlin( + "CoffeeTest.kt", + """ + import app.cash.burst.Burst + import app.cash.burst.burstValues + import kotlin.test.Test + + @Burst + class CoffeeTest { + val log = mutableListOf() + + @Test + fun test(iced: Boolean) { + log += "running iced=${'$'}iced" + } + } + """, + ), + ) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages) + + val baseClass = result.classLoader.loadClass("CoffeeTest") + val baseInstance = baseClass.constructors.single().newInstance() + val baseLog = baseClass.getMethod("getLog").invoke(baseInstance) as MutableList<*> + + baseClass.getMethod("test_false").invoke(baseInstance) + assertThat(baseLog).containsExactly("running iced=false") + baseLog.clear() + + baseClass.getMethod("test_true").invoke(baseInstance) + assertThat(baseLog).containsExactly("running iced=true") + baseLog.clear() + } + + @Test + fun booleanDefaultValues() { + val result = compile( + sourceFile = SourceFile.kotlin( + "CoffeeTest.kt", + """ + import app.cash.burst.Burst + import app.cash.burst.burstValues + import kotlin.test.Test + + @Burst + class CoffeeTest { + val log = mutableListOf() + + @Test + fun testDefaultTrue(iced: Boolean = true) { + log += "running testDefaultTrue iced=${'$'}iced" + } + + @Test + fun testDefaultFalse(iced: Boolean = false) { + log += "running testDefaultFalse iced=${'$'}iced" + } + } + """, + ), + ) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages) + + val baseClass = result.classLoader.loadClass("CoffeeTest") + val baseInstance = baseClass.constructors.single().newInstance() + val baseLog = baseClass.getMethod("getLog").invoke(baseInstance) as MutableList<*> + + baseClass.getMethod("testDefaultTrue").invoke(baseInstance) + baseClass.getMethod("testDefaultTrue_false").invoke(baseInstance) + baseClass.getMethod("testDefaultFalse").invoke(baseInstance) + baseClass.getMethod("testDefaultFalse_true").invoke(baseInstance) + assertThat(baseLog).containsExactly( + "running testDefaultTrue iced=true", + "running testDefaultTrue iced=false", + "running testDefaultFalse iced=false", + "running testDefaultFalse iced=true", + ) + } + private val Class<*>.testSuffixes: List get() = methods.mapNotNull { when { diff --git a/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt b/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt index 4e3f7fa..ad23f45 100644 --- a/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt +++ b/burst-kotlin-plugin/src/main/kotlin/app/cash/burst/kotlin/Argument.kt @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrGetEnumValue import org.jetbrains.kotlin.ir.expressions.IrVararg +import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.types.IrType @@ -35,6 +36,7 @@ import org.jetbrains.kotlin.ir.types.classFqName import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.util.classId import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.util.defaultType import org.jetbrains.kotlin.ir.util.isEnumClass import org.jetbrains.kotlin.ir.visitors.IrElementVisitor import org.jetbrains.kotlin.name.NameUtils @@ -69,6 +71,22 @@ private class EnumValueArgument( } } +private class BooleanArgument( + private val original: IrElement, + private val booleanType: IrType, + override val isDefault: Boolean, + private val value: Boolean, +) : Argument { + override val name = value.toString() + + override fun expression() = + IrConstImpl.boolean(original.startOffset, original.endOffset, booleanType, value) + + override fun accept(visitor: IrElementVisitor, data: D): R { + return original.accept(visitor, data) + } +} + private class BurstValuesArgument( private val declarationParent: IrDeclarationParent, override val isDefault: Boolean, @@ -104,8 +122,9 @@ internal fun IrPluginContext.allPossibleArguments( val classId = parameter.type.getClass()?.classId ?: unexpectedParameter(parameter) val referenceClass = referenceClass(classId)?.owner ?: unexpectedParameter(parameter) - if (referenceClass.isEnumClass) { - return enumValueArguments(referenceClass, parameter) + when { + referenceClass.isEnumClass -> return enumValueArguments(referenceClass, parameter) + referenceClass.defaultType == irBuiltIns.booleanType -> return booleanArguments(parameter) } unexpectedParameter(parameter) @@ -190,16 +209,33 @@ private fun enumValueArguments( } } +private fun IrPluginContext.booleanArguments( + parameter: IrValueParameter, +): List { + val defaultValue = parameter.defaultValue?.let { defaultValue -> + (defaultValue.expression as? IrConst<*>)?.value ?: unexpectedDefaultValue(parameter) + } + + return listOf(false, true).map { + BooleanArgument( + original = parameter, + booleanType = irBuiltIns.booleanType, + isDefault = defaultValue == it, + value = it, + ) + } +} + private fun unexpectedParameter(parameter: IrValueParameter): Nothing { throw BurstCompilationException( - "@Burst parameter must be an enum or have a burstValues() default value", + "@Burst parameter must be a boolean, enum, or have a burstValues() default value", parameter, ) } private fun unexpectedDefaultValue(parameter: IrValueParameter): Nothing { throw BurstCompilationException( - "@Burst parameter default value must be burstValues(), an enum constant, or absent", + "@Burst parameter default value must be burstValues(), a constant, or absent", parameter, ) }