Skip to content

Commit

Permalink
Add basic support for JUnit 5 (#62)
Browse files Browse the repository at this point in the history
* Add basic support for JUnit 5

#35

This is missing the ability to mix and match parameters
supported by Burst with parameters supported by JUnit 5.

* Support mixing and matching annotations

* Be more functional

* Spotless

* Update changelog

---------

Co-authored-by: Jesse Wilson <[email protected]>
  • Loading branch information
swankjesse and squarejesse authored Nov 5, 2024
1 parent 30ad9bc commit eb0be6b
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## [Unreleased]
[Unreleased]: https://github.com/cashapp/burst/compare/0.6.0...HEAD

**Added**

* Basic support for JUnit 5. Burst doesn't support JUnit 5 tests that populate parameters from extensions.

## [2.0.0] *(2024-10-30)*
[2.0.0]: https://github.com/cashapp/burst/releases/tag/2.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,31 @@ class BurstGradlePluginTest {
}
}

@Test
fun junit5() {
val projectDir = File("src/test/projects/junit5")

val taskName = ":lib:test"
val result = createRunner(projectDir, "clean", taskName).build()
assertThat(result.task(taskName)!!.outcome).isIn(*SUCCESS_OUTCOMES)

val testResults = projectDir.resolve("lib/build/test-results")

with(readTestSuite(testResults.resolve("test/TEST-CoffeeTest_Regular.xml"))) {
assertThat(testCases.map { it.name }).containsExactlyInAnyOrder(
"kotlinTestTest_Milk()",
"kotlinTestTest_None()",
"kotlinTestTest_Oat()",
"orgJunitJupiterApiTest_Milk()",
"orgJunitJupiterApiTest_None()",
"orgJunitJupiterApiTest_Oat()",
"orgJunitTest_Milk",
"orgJunitTest_None",
"orgJunitTest_Oat",
)
}
}

private fun createRunner(
projectDir: File,
vararg taskNames: String,
Expand Down
41 changes: 41 additions & 0 deletions burst-gradle-plugin/src/test/projects/junit5/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

buildscript {
repositories {
maven {
url = file("$rootDir/../../../../../build/testMaven").toURI()
}
mavenCentral()
google()
}
dependencies {
classpath("app.cash.burst:burst-gradle-plugin:${project.property("burstVersion")}")
classpath(libs.kotlin.gradlePlugin)
}
}

allprojects {
repositories {
maven {
url = file("$rootDir/../../../../../build/testMaven").toURI()
}
mavenCentral()
google()
}

tasks.withType(JavaCompile::class.java).configureEach {
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
}

tasks.withType(KotlinJvmCompile::class.java).configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
}
10 changes: 10 additions & 0 deletions burst-gradle-plugin/src/test/projects/junit5/lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
kotlin("jvm")
id("app.cash.burst")
}

dependencies {
testImplementation(kotlin("test"))
testImplementation(libs.junit)
testRuntimeOnly(libs.junit.vintage.engine)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import app.cash.burst.Burst
import kotlin.test.BeforeTest
import kotlin.test.Test

@Burst
class CoffeeTest(
private val espresso: Espresso,
) {
@BeforeTest
fun setUp() {
println("set up $espresso")
}

@kotlin.test.Test
fun kotlinTestTest(dairy: Dairy) {
println("@kotlin.test.Test running $espresso $dairy")
}

@org.junit.Test
fun orgJunitTest(dairy: Dairy) {
println("@org.junit.Test running $espresso $dairy")
}

@org.junit.jupiter.api.Test
fun orgJunitJupiterApiTest(dairy: Dairy) {
println("@org.junit.jupiter.api.Test running $espresso $dairy")
}
}

enum class Espresso { Decaf, Regular, Double }

enum class Dairy { None, Milk, Oat }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../../../../../gradle/libs.versions.toml"))
}
}
}

include(":lib")
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,46 @@ package app.cash.burst.kotlin

import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.hasAnnotation

/** Looks up APIs used by the code rewriters. */
internal class BurstApis private constructor(
pluginContext: IrPluginContext,
testPackage: FqPackageName,
private val testClassSymbols: List<IrClassSymbol>,
) {
val burstValues: IrFunctionSymbol = pluginContext.referenceFunctions(burstValuesId).single()

fun findTestAnnotation(function: IrSimpleFunction): IrClassSymbol? {
return function.annotations
.mapNotNull { it.type.classOrNull }
.firstOrNull { it in testClassSymbols }
}

companion object {
fun maybeCreate(pluginContext: IrPluginContext): BurstApis? {
// If we don't have @Burst, we don't have the runtime. Abort!
if (pluginContext.referenceClass(burstAnnotationId) == null) {
return null
}

if (pluginContext.referenceClass(junitTestClassId) != null) {
return BurstApis(pluginContext, junitPackage)
}
val testClassSymbols = listOfNotNull(
pluginContext.referenceClass(junit5TestClassId),
pluginContext.referenceClass(junitTestClassId),
pluginContext.referenceClass(kotlinTestClassId),
)

if (pluginContext.referenceClass(kotlinTestClassId) != null) {
return BurstApis(pluginContext, kotlinTestPackage)
// No @Test annotations? No Burst.
if (testClassSymbols.isEmpty()) {
return null
}

// No kotlin.test and no org.junit. No Burst for you.
return null
return BurstApis(pluginContext, testClassSymbols)
}
}

val testClassSymbol: IrClassSymbol = pluginContext.referenceClass(testPackage.classId("Test"))!!
val burstValues: IrFunctionSymbol = pluginContext.referenceFunctions(burstValuesId).single()
}

private val burstFqPackage = FqPackageName("app.cash.burst")
Expand All @@ -56,11 +65,10 @@ private val burstValuesId = burstFqPackage.callableId("burstValues")

private val junitPackage = FqPackageName("org.junit")
private val junitTestClassId = junitPackage.classId("Test")
private val junit5Package = FqPackageName("org.junit.jupiter.api")
private val junit5TestClassId = junit5Package.classId("Test")
private val kotlinTestPackage = FqPackageName("kotlin.test")
private val kotlinTestClassId = kotlinTestPackage.classId("Test")

internal val IrAnnotationContainer.hasAtTest: Boolean
get() = hasAnnotation(junitTestClassId) || hasAnnotation(kotlinTestClassId)

internal val IrAnnotationContainer.hasAtBurst: Boolean
get() = hasAnnotation(burstAnnotationId)
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class BurstIrGenerationExtension(
val originalFunctions = classDeclaration.functions.toList()

for (function in originalFunctions) {
if (!function.hasAtTest) continue
val testAnnotationClassSymbol = burstApis.findTestAnnotation(function) ?: continue
if (!classHasAtBurst && !function.hasAtBurst) continue

try {
Expand All @@ -70,6 +70,7 @@ class BurstIrGenerationExtension(
burstApis = burstApis,
originalParent = classDeclaration,
original = function,
testAnnotationClassSymbol = testAnnotationClassSymbol,
)
specializer.generateSpecializations()
} catch (e: BurstCompilationException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.starProjectedType
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.name.Name

Expand Down Expand Up @@ -63,6 +63,7 @@ internal class FunctionSpecializer(
private val burstApis: BurstApis,
private val originalParent: IrClass,
private val original: IrSimpleFunction,
private val testAnnotationClassSymbol: IrClassSymbol,
) {
fun generateSpecializations() {
val valueParameters = original.valueParameters
Expand All @@ -84,7 +85,7 @@ internal class FunctionSpecializer(

// Drop `@Test` from the original's annotations.
original.annotations = original.annotations.filter {
it.type.classFqName != burstApis.testClassSymbol.starProjectedType.classFqName
it.type.classOrNull != testAnnotationClassSymbol
}

// Add new declarations.
Expand Down Expand Up @@ -112,7 +113,7 @@ internal class FunctionSpecializer(
}
}

result.annotations += burstApis.testClassSymbol.asAnnotation()
result.annotations += testAnnotationClassSymbol.asAnnotation()

result.irFunctionBody(
context = pluginContext,
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ binary-compatibility-validator-gradle-plugin = { module = "org.jetbrains.kotlinx
dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" }
google-ksp = "com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.21-1.0.26"
junit = { module = "junit:junit", version = "4.13.2" }
junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version = "5.11.3" }
kotlin-compile-testing = { module = "dev.zacsweers.kctfork:core", version = "0.5.1" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" }
Expand Down

0 comments on commit eb0be6b

Please sign in to comment.