diff --git a/convention-plugin/build.gradle.kts b/convention-plugin/build.gradle.kts index edd3bf4..05989d4 100644 --- a/convention-plugin/build.gradle.kts +++ b/convention-plugin/build.gradle.kts @@ -48,6 +48,10 @@ gradlePlugin { id = "org.metaborg.convention.java" implementationClass = "org.metaborg.convention.JavaConventionPlugin" } + create("convention.junit") { + id = "org.metaborg.convention.junit" + implementationClass = "org.metaborg.convention.JUnitConventionPlugin" + } create("convention.maven-publish") { id = "org.metaborg.convention.maven-publish" implementationClass = "org.metaborg.convention.MavenPublishConventionPlugin" diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionExtension.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionExtension.kt new file mode 100644 index 0000000..8aad4fc --- /dev/null +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionExtension.kt @@ -0,0 +1,21 @@ +package org.metaborg.convention + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import javax.inject.Inject + +/** Configuration for the JUnit convention. */ +open class JUnitConventionExtension @Inject constructor( + /** The Gradle object factory. */ + objects: ObjectFactory, +) { + /** The name of the version catalog to use for the JUnit dependency. */ + val versionCatalogName: Property = objects.property(String::class.java) + .convention("libs") + /** The alias of the JUnit dependency in the version catalog. */ + val jUnitAlias: Property = objects.property(String::class.java) + .convention("junit") + /** The default JUnit dependency to use if the version catalog dependency cannot be found. */ + val jUnitDependency: Property = objects.property(Any::class.java) + .convention("org.junit.jupiter:junit-jupiter:5.10.3") +} diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionPlugin.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionPlugin.kt new file mode 100644 index 0000000..ace0931 --- /dev/null +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/JUnitConventionPlugin.kt @@ -0,0 +1,89 @@ +package org.metaborg.convention + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType + +/** + * Configures a Gradle project to use JUnit 5 for testing. + * + * Also applies the `java-base` plugin to the project. Apply the `java` or `java-library` plugin manually. + */ +class JUnitConventionPlugin: Plugin { + + override fun apply(project: Project): Unit = with(project) { + // Add the configuration extension + val extension = extensions.create("junitConvention") + + // Apply the Java base plugin + plugins.apply("java-base") + + // Add the JUnit 5 dependency (org.junit.jupiter:junit-jupiter) + // - org.junit.jupiter:junit-jupiter-api + // - org.junit.jupiter:junit-jupiter-engine + // - org.junit.jupiter:junit-jupiter-params + // - org.junit.platform:junit-platform-commons + // - org.junit.platform:junit-platform-engine + dependencies { + versionCatalog() + add("testImplementation", + with(extension.versionCatalogName, extension.jUnitAlias, extension.jUnitDependency) { versionCatalogName, junitAlias, jUnitDependency -> + getLibrary(junitAlias, jUnitDependency, versionCatalogName) + } + ) + } + + plugins.withType { + tasks.withType { + // Support any JUnit 5 compatible test runner + useJUnitPlatform() + + // Configure the logging + testLogging { + // Default (lifecycle) logging level: only show test failures + lifecycle { + events(TestLogEvent.FAILED) + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + // Info logging level: show failed and skipped tests, and standard output/error + info { + events( + TestLogEvent.FAILED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR + ) + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + // Debug logging level: show all tests, and standard output/error + debug { + events( + TestLogEvent.STARTED, + TestLogEvent.PASSED, + TestLogEvent.FAILED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR + ) + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + } + } + } + } +} \ No newline at end of file diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/JavaConventionPlugin.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/JavaConventionPlugin.kt index a6bb8b9..84a5c69 100644 --- a/convention-plugin/src/main/kotlin/org/metaborg/convention/JavaConventionPlugin.kt +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/JavaConventionPlugin.kt @@ -26,11 +26,6 @@ class JavaConventionPlugin: Plugin { plugins.apply("java-base") plugins.withType { - tasks.withType { - // Support any JUnit 5 compatible test runner - useJUnitPlatform() - } - configure { // Compile to a specific Java version toolchain.languageVersion.set(extension.javaVersion) diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/Utils.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/Utils.kt index 35bcca9..7fd0c1d 100644 --- a/convention-plugin/src/main/kotlin/org/metaborg/convention/Utils.kt +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/Utils.kt @@ -1,6 +1,11 @@ package org.metaborg.convention +import org.gradle.api.Project +import org.gradle.api.UnknownDomainObjectException +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.getByType private fun zipId(p0: Provider, p1: Provider): Provider> = p0.zip(p1) { a0, a1 -> Pair(a0, a1) } @@ -8,26 +13,57 @@ private fun zipId(p0: Provider, p1: Provider): Provider Provider.with(p1: Provider, transformer: (A0, A1) -> R): Provider = +internal fun Provider.with(p1: Provider, transformer: (A0, A1) -> R): Provider = with(this, p1, transformer) @JvmName("alsoWith") -fun Provider.with(p1: Provider, p2: Provider, transformer: (A0, A1, A2) -> R): Provider = +internal fun Provider.with(p1: Provider, p2: Provider, transformer: (A0, A1, A2) -> R): Provider = with(this, p1, p2, transformer) @JvmName("alsoWith") -fun Provider.with(p1: Provider, p2: Provider, p3: Provider, transformer: (A0, A1, A2, A3) -> R): Provider = +internal fun Provider.with(p1: Provider, p2: Provider, p3: Provider, transformer: (A0, A1, A2, A3) -> R): Provider = with(this, p1, p2, p3, transformer) -fun with(p0: Provider, transformer: (A0) -> R): Provider = +internal fun with(p0: Provider, transformer: (A0) -> R): Provider = p0.map { a0 -> transformer(a0) } -fun with(p0: Provider, p1: Provider, transformer: (A0, A1) -> R): Provider = +internal fun with(p0: Provider, p1: Provider, transformer: (A0, A1) -> R): Provider = p0.zip(p1) { a0, a1 -> transformer(a0, a1) } -fun with(p0: Provider, p1: Provider, p2: Provider, transformer: (A0, A1, A2) -> R): Provider = +internal fun with(p0: Provider, p1: Provider, p2: Provider, transformer: (A0, A1, A2) -> R): Provider = p0.zip(zipId(p1, p2)) { a0, (a1, a2) -> transformer(a0, a1, a2) } -fun with(p0: Provider, p1: Provider, p2: Provider, p3: Provider, transformer: (A0, A1, A2, A3) -> R): Provider = +internal fun with(p0: Provider, p1: Provider, p2: Provider, p3: Provider, transformer: (A0, A1, A2, A3) -> R): Provider = p0.zip(zipId(p1, zipId(p2, p3))) { a0, (a1, a23) -> val (a2, a3) = a23; transformer(a0, a1, a2, a3) } + + +/** + * Gets the project's version catalog with the specified name, if it exists. + * + * @param catalogName The name of the version catalog. + * @return The [VersionCatalog]; or `null` if not found. + */ +internal fun Project.versionCatalog(catalogName: String = "libs"): VersionCatalog? = try { + extensions.getByType().named(catalogName) +} catch (e: UnknownDomainObjectException) { + null +} + +/** + * Gets a library with the specified alias from the specified version catalog of the project, + * or if not found, returns the provided dependency notation instead. + * + * For example, to get the JUnit dependency, call this function as: + * + * ```kotlin + * getLibrary("junit", "org.junit.jupiter:junit-jupiter-api:5.7.0") + * ``` + * + * Either this finds a dependency with alias `"junit"` in the version catalog, + * or returns the given dependency notation if not found. Note that a platform or + * explicit version restriction may still modify the dependency. + */ +internal fun Project.getLibrary(alias: String, dependencyNotation: Any, catalog: String = "libs"): Any { + return versionCatalog(catalog)?.findLibrary(alias)?.map { it.getOrNull() }?.orElse(null) ?: dependencyNotation +} \ No newline at end of file diff --git a/example/java-example/build.gradle.kts b/example/java-example/build.gradle.kts index e388d69..26a9322 100644 --- a/example/java-example/build.gradle.kts +++ b/example/java-example/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` id("org.metaborg.convention.java") + id("org.metaborg.convention.junit") id("org.metaborg.convention.maven-publish") }