Skip to content

Commit

Permalink
Add test for Gradle plugin and fix issues running tests for native ta…
Browse files Browse the repository at this point in the history
…rgets on Kotlin 1.7+ (#3107)

* Add (currently failing) test for Gradle plugin, and improve warnings generated by plugin when invalid configuration is detected.

* Fix issue where compilation for Kotlin/Native and Kotlin/JS projects would fail if the project uses Kotlin 1.7.

* Fix issue running Kotest tests under Kotlin 1.5.

The current version of Gradle uses Kotlin 1.5, so retaining backwards
compatibility with Kotlin 1.5 allows us to use Kotest to write tests
for Gradle plugins.

Resolves #3059.

* Fix issue where Gradle plugin tests could fail due to conflicting with other concurrent build tasks.

* Remove Gradle plugin test for now.

See #3075 (comment).

* Revert "Remove Gradle plugin test for now."

This reverts commit ec14c55.

* Disable JS browser test for now.

* Fix issue where no tests are run when targeting a native platform using Kotlin 1.7.

Kotlin 1.7 now uses the embeddable compiler JAR and corresponding
plugins (from "getPluginArtifact" rather than
"getPluginArtifactForNative" in KotestMultiplatformCompilerGradlePlugin)
by default for all platforms.

This means that the "JS" compiler plugin is used by all targets by
default in projects that use Kotlin 1.7+.

As a first step, I've brought the existing native plugin code over to
the "JS" plugin. Next steps are to rename the projects to make their
intention clearer and merge the transformers used by each platform.

* Rename JS compiler plugin project to better reflect its purpose.

* Update package name to match project name.

* Rename "native" compiler plugin project to better reflect its purpose.

* Refactor transformers to share more logic.

* Add tests to ensure Kotest behaves correctly with Kotlin/Native memory model.

* Extract further common functionality.

* Configure project config objects when running on Kotlin/Native.

* Reorder method calls in native transformer to match JS transformer.

* Further consolidate IR generation logic into base Transformer class.

* Fix issues running compiler plugin on modules that do not contain any Kotest tests.

* Add workaround for issues running Gradle plugin tests on GitHub Actions.

* Revert "Add workaround for issues running Gradle plugin tests on GitHub Actions."

This reverts commit 9df35f8.

* Set target JVM versions for all libraries.

* Add Yarn lock file.

* Run Gradle plugin tests in a separate GitHub Actions job.

Co-authored-by: Sam <sam@sksamuel.com>
  • Loading branch information
charleskorn and sksamuel committed Jul 29, 2022
1 parent b4dc446 commit 8101fdd
Show file tree
Hide file tree
Showing 33 changed files with 2,592 additions and 190 deletions.
12 changes: 10 additions & 2 deletions .github/workflows/PR.yml
Expand Up @@ -15,6 +15,14 @@ jobs:
test_linux:
runs-on: ubuntu-latest
if: github.repository == 'kotest/kotest'
strategy:
fail-fast: false
matrix:
target:
- jvmTest
- jsIrTest
- linuxX64Test
- :kotest-framework:kotest-framework-multiplatform-plugin-gradle:test
steps:
- name: Checkout the repo
uses: actions/checkout@v3
Expand All @@ -31,7 +39,7 @@ jobs:
- uses: gradle/gradle-build-action@v2

- name: Run tests
run: ./gradlew check --scan
run: ./gradlew ${{ matrix.target }} --scan

- name: Bundle the build report
if: failure()
Expand Down Expand Up @@ -128,4 +136,4 @@ jobs:
name: error-report
path: build-reports.zip
env:
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxMetaspaceSize=756m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Expand Up @@ -24,6 +24,7 @@ jobs:
- jvmTest
- jsIrTest
- linuxX64Test
- :kotest-framework:kotest-framework-multiplatform-plugin-gradle:test
steps:
- name: Checkout the repo
uses: actions/checkout@v3
Expand Down Expand Up @@ -131,4 +132,4 @@ jobs:


env:
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxMetaspaceSize=756m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
2 changes: 1 addition & 1 deletion .github/workflows/release_all.yml
Expand Up @@ -16,7 +16,7 @@ env:
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxMetaspaceSize=756m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release_multiplatform_plugin_gradle.yml
Expand Up @@ -9,7 +9,7 @@ on:

env:
RELEASE_VERSION: ${{ github.event.inputs.version }}
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxMetaspaceSize=756m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"

permissions:
contents: read
Expand Down
7 changes: 7 additions & 0 deletions buildSrc/src/main/kotlin/kotest-jvm-conventions.gradle.kts
Expand Up @@ -7,6 +7,8 @@ plugins {
kotlin {
targets {
jvm {
withJava()

compilations.all {
kotlinOptions {
jvmTarget = "1.8"
Expand All @@ -23,3 +25,8 @@ kotlin {
}
}
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
@@ -0,0 +1,12 @@
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.ir.declarations.IrFile
import java.io.File

// These extension properties are available in org.jetbrains.kotlin.ir.declarations, but were moved from one file to
// another in Kotlin 1.7. This breaks backwards compatibility with earlier versions of Kotlin.
// So instead of using the provided implementations, we've copied them here, so we can work with both Kotlin 1.7+ and earlier
// versions without issue.
// See https://github.com/kotest/kotest/issues/3060 and https://youtrack.jetbrains.com/issue/KT-52888 for more information.
internal val IrFile.path: String get() = fileEntry.name
internal val IrFile.name: String get() = File(path).name
@@ -0,0 +1,36 @@
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.builders.irBlockBody
import org.jetbrains.kotlin.ir.builders.irCall
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.util.getSimpleFunction
import org.jetbrains.kotlin.name.Name

class JsTransformer(messageCollector: MessageCollector, pluginContext: IrPluginContext) : Transformer(messageCollector, pluginContext) {
override fun generateLauncher(specs: Iterable<IrClass>, configs: Iterable<IrClass>, declarationParent: IrDeclarationParent): IrDeclaration {
val main = pluginContext.irFactory.buildFun {
name = Name.identifier("main")
returnType = pluginContext.irBuiltIns.unitType
}.also { func: IrSimpleFunction ->
func.body = DeclarationIrBuilder(pluginContext, func.symbol).irBlockBody {
+callLauncher(promiseFn, specs, configs) {
irCall(launcherConstructor)
}
}
}

return main
}

private val promiseFn by lazy {
launcherClass.getSimpleFunction(EntryPoint.PromiseMethodName)
?: error("Cannot find function ${EntryPoint.PromiseMethodName}")
}
}
@@ -1,4 +1,4 @@
package io.kotest.framework.multiplatform.js
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
Expand Down
@@ -0,0 +1,84 @@
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.IrSingleStatementBuilder
import org.jetbrains.kotlin.ir.builders.Scope
import org.jetbrains.kotlin.ir.builders.declarations.addGetter
import org.jetbrains.kotlin.ir.builders.declarations.buildField
import org.jetbrains.kotlin.ir.builders.declarations.buildProperty
import org.jetbrains.kotlin.ir.builders.irBlock
import org.jetbrains.kotlin.ir.builders.irBlockBody
import org.jetbrains.kotlin.ir.builders.irCall
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.getSimpleFunction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name

class NativeTransformer(messageCollector: MessageCollector, pluginContext: IrPluginContext) : Transformer(messageCollector, pluginContext) {
override fun generateLauncher(specs: Iterable<IrClass>, configs: Iterable<IrClass>, declarationParent: IrDeclarationParent): IrDeclaration {
val launcher = pluginContext.irFactory.buildProperty {
name = Name.identifier(EntryPoint.LauncherValName)
}.apply {
parent = declarationParent
annotations += IrSingleStatementBuilder(
pluginContext,
Scope(this.symbol),
UNDEFINED_OFFSET,
UNDEFINED_OFFSET
).build { irCall(eagerAnnotationConstructor) }

backingField = pluginContext.irFactory.buildField {
type = pluginContext.irBuiltIns.unitType
isFinal = true
isExternal = false
isStatic = true // top level vals must be static
name = Name.identifier(EntryPoint.LauncherValName)
}.also { field ->
field.correspondingPropertySymbol = this@apply.symbol
field.initializer = pluginContext.irFactory.createExpressionBody(startOffset, endOffset) {
this.expression = DeclarationIrBuilder(pluginContext, field.symbol).irBlock {
+callLauncher(launchFn, specs, configs) {
irCall(withTeamCityListenerMethodNameFn).also { withTeamCity ->
withTeamCity.dispatchReceiver = irCall(launcherConstructor)
}
}
}
}
}

addGetter {
returnType = pluginContext.irBuiltIns.unitType
}.also { func ->
func.body = DeclarationIrBuilder(pluginContext, func.symbol).irBlockBody {
}
}
}

return launcher
}

private val launchFn by lazy {
launcherClass.getSimpleFunction(EntryPoint.LaunchMethodName)
?: error("Cannot find function ${EntryPoint.LaunchMethodName}")
}

private val withTeamCityListenerMethodNameFn by lazy {
launcherClass.getSimpleFunction(EntryPoint.WithTeamCityListenerMethodName)
?: error("Cannot find function ${EntryPoint.WithTeamCityListenerMethodName}")
}

private val eagerAnnotationConstructor by lazy {
val annotationName = FqName("kotlin.native.EagerInitialization")

val annotation = pluginContext.referenceClass(annotationName)
?: error("Cannot find eager initialisation annotation class $annotationName")

annotation.constructors.single()
}
}
@@ -0,0 +1,23 @@
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.platform.js.isJs
import org.jetbrains.kotlin.platform.konan.isNative

class SpecIrGenerationExtension(private val messageCollector: MessageCollector) : IrGenerationExtension {

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
val platform = pluginContext.platform

val transformer = when {
platform.isJs() -> JsTransformer(messageCollector, pluginContext)
platform.isNative() -> NativeTransformer(messageCollector, pluginContext)
else -> throw UnsupportedOperationException("Cannot use Kotest compiler plugin with platform: $platform")
}

moduleFragment.transform(transformer, null)
}
}
@@ -0,0 +1,115 @@
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.ir.addChild
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.toLogger
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope
import org.jetbrains.kotlin.ir.builders.irCall
import org.jetbrains.kotlin.ir.builders.irVararg
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.getSimpleFunction
import org.jetbrains.kotlin.ir.util.kotlinFqName
import org.jetbrains.kotlin.name.FqName
import java.util.concurrent.CopyOnWriteArrayList

abstract class Transformer(protected val messageCollector: MessageCollector, protected val pluginContext: IrPluginContext) : IrElementTransformerVoidWithContext() {
private val specs = CopyOnWriteArrayList<IrClass>()
private var configs = CopyOnWriteArrayList<IrClass>()

override fun visitClassNew(declaration: IrClass): IrStatement {
super.visitClassNew(declaration)
if (declaration.isProjectConfig()) configs.add(declaration)
return declaration
}

override fun visitFileNew(declaration: IrFile): IrFile {
super.visitFileNew(declaration)
val specs = declaration.specs()
messageCollector.toLogger().log("${declaration.name} contains ${specs.size} spec(s): ${specs.joinToString(", ") { it.kotlinFqName.asString() }}")
this.specs.addAll(specs)
return declaration
}

override fun visitModuleFragment(declaration: IrModuleFragment): IrModuleFragment {
val fragment = super.visitModuleFragment(declaration)

messageCollector.toLogger().log("Detected ${configs.size} configs:")
configs.forEach {
messageCollector.toLogger().log(it.kotlinFqName.asString())
}

messageCollector.toLogger().log("Detected ${specs.size} JS specs:")
specs.forEach {
messageCollector.toLogger().log(it.kotlinFqName.asString())
}

if (specs.isEmpty()) {
return fragment
}

val file = declaration.files.first()
val launcher = generateLauncher(specs, configs, file)
file.addChild(launcher)

return fragment
}

abstract fun generateLauncher(specs: Iterable<IrClass>, configs: Iterable<IrClass>, declarationParent: IrDeclarationParent): IrDeclaration

protected fun IrBuilderWithScope.callLauncher(
launchFunction: IrSimpleFunctionSymbol,
specs: Iterable<IrClass>,
configs: Iterable<IrClass>,
constructorGenerator: IrBuilderWithScope.() -> IrExpression
): IrCall {
return irCall(launchFunction).also { promise: IrCall ->
promise.dispatchReceiver = irCall(withSpecsFn).also { withSpecs ->
withSpecs.putValueArgument(
0,
irVararg(
pluginContext.irBuiltIns.stringType,
specs.map { irCall(it.constructors.first()) }
)
)
withSpecs.dispatchReceiver = irCall(withConfigFn).also { withConfig ->
withConfig.putValueArgument(
0,
irVararg(
pluginContext.irBuiltIns.stringType,
configs.map { irCall(it.constructors.first()) }
)
)
withConfig.dispatchReceiver = constructorGenerator()
}
}
}
}

protected val launcherClass by lazy {
pluginContext.referenceClass(FqName(EntryPoint.TestEngineClassName))
?: error("Cannot find ${EntryPoint.TestEngineClassName} class reference")
}

protected val launcherConstructor by lazy { launcherClass.constructors.first { it.owner.valueParameters.isEmpty() } }

protected val withSpecsFn by lazy {
launcherClass.getSimpleFunction(EntryPoint.WithSpecsMethodName)
?: error("Cannot find function ${EntryPoint.WithSpecsMethodName}")
}

protected val withConfigFn by lazy {
launcherClass.getSimpleFunction(EntryPoint.WithConfigMethodName)
?: error("Cannot find function ${EntryPoint.WithConfigMethodName}")
}
}
@@ -1,9 +1,11 @@
package io.kotest.framework.multiplatform.js
package io.kotest.framework.multiplatform.embeddablecompiler

object EntryPoint {

// we use a public val to register each spec
// const val LauncherValName = "kotest_launcher"
const val LauncherValName = "launcher"

// the method invoked to start the tests, must exist on TestEngineLauncher
const val LaunchMethodName = "launch"

// the method invoked on TestEngineLauncher to start the tests
// in JS we use promise() which ultimately calls into GlobalScope.promise on JS platforms
Expand All @@ -17,4 +19,7 @@ object EntryPoint {

// the method invoked to add configs on the launcher, must exist on TestEngineLauncher
const val WithConfigMethodName = "withProjectConfig"

// the method invoked to set the team city listener, must exist on TestEngineLauncher
const val WithTeamCityListenerMethodName = "withTeamCityListener"
}
@@ -1,4 +1,4 @@
package io.kotest.framework.multiplatform.js
package io.kotest.framework.multiplatform.embeddablecompiler

import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrFile
Expand Down Expand Up @@ -27,7 +27,7 @@ val abstractProjectConfigFqName = FqName("io.kotest.core.config.AbstractProjectC
fun IrFile.specs() = declarations.filterIsInstance<IrClass>().filter { it.isSpecClass() }

/**
* Returns true fi this IrClass is a project config
* Returns true if this IrClass is a project config
*/
fun IrClass.isProjectConfig() = superTypes().any { it.classFqName == abstractProjectConfigFqName }

Expand Down

0 comments on commit 8101fdd

Please sign in to comment.