diff --git a/plugin/CHANGELOG.md b/plugin/CHANGELOG.md index b1a5c9aa..8b1f4d89 100644 --- a/plugin/CHANGELOG.md +++ b/plugin/CHANGELOG.md @@ -3,6 +3,9 @@ Change Log ## Unreleased - JUnit 5.10.3 +- Updates to the `jacocoOptions` DSL + - Change the return type of each report type to match Jacoco expectations (html -> Directory; csv & xml -> File) + - Turn off generation of csv & xml reports by default, matching Jacoco default configuration ## 1.10.2.0 (2024-07-25) - JUnit 5.10.2 diff --git a/plugin/android-junit5/api/android-junit5.api b/plugin/android-junit5/api/android-junit5.api index bf5d20e0..21bc70bf 100644 --- a/plugin/android-junit5/api/android-junit5.api +++ b/plugin/android-junit5/api/android-junit5.api @@ -5,7 +5,7 @@ public final class de/mannodermaus/gradle/plugins/junit5/AndroidJUnitPlatformPlu } public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/AndroidJUnitPlatformExtension : groovy/lang/GroovyObjectSupport { - public fun (Lorg/gradle/api/model/ObjectFactory;)V + public fun (Lorg/gradle/api/Project;Lorg/gradle/api/model/ObjectFactory;)V public final fun configurationParameter (Ljava/lang/String;Ljava/lang/String;)V public final fun configurationParameters (Ljava/util/Map;)V public final fun filters (Ljava/lang/String;Lorg/gradle/api/Action;)V @@ -40,21 +40,31 @@ public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/InstrumentationT public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions { public fun (Lorg/gradle/api/model/ObjectFactory;)V - public final fun getCsv ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report; + public final fun getCsv ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$FileReport; public abstract fun getExcludedClasses ()Lorg/gradle/api/provider/ListProperty; - public final fun getHtml ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report; + public final fun getHtml ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$DirectoryReport; public abstract fun getOnlyGenerateTasksForVariants ()Lorg/gradle/api/provider/SetProperty; public abstract fun getTaskGenerationEnabled ()Lorg/gradle/api/provider/Property; - public final fun getXml ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report; + public final fun getXml ()Lde/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$FileReport; } -public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report { +public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$DirectoryReport : de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report { + public fun ()V + public abstract fun getDestination ()Lorg/gradle/api/file/DirectoryProperty; + public final fun invoke (Lkotlin/jvm/functions/Function1;)V +} + +public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$FileReport : de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report { public fun ()V public abstract fun getDestination ()Lorg/gradle/api/file/RegularFileProperty; - public abstract fun getEnabled ()Lorg/gradle/api/provider/Property; public final fun invoke (Lkotlin/jvm/functions/Function1;)V } +public abstract class de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions$Report { + public abstract fun getDestination ()Lorg/gradle/api/file/FileSystemLocationProperty; + public abstract fun getEnabled ()Lorg/gradle/api/provider/Property; +} + public abstract class de/mannodermaus/gradle/plugins/junit5/tasks/AndroidJUnit5JacocoReport : org/gradle/testing/jacoco/tasks/JacocoReport { public fun ()V } diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/AndroidJUnitPlatformExtension.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/AndroidJUnitPlatformExtension.kt index 1411ca7d..d0731457 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/AndroidJUnitPlatformExtension.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/AndroidJUnitPlatformExtension.kt @@ -14,6 +14,7 @@ import java.io.File import javax.inject.Inject public abstract class AndroidJUnitPlatformExtension @Inject constructor( + project: Project, private val objects: ObjectFactory ) : GroovyObjectSupport() { @@ -110,18 +111,23 @@ public abstract class AndroidJUnitPlatformExtension @Inject constructor( /** * Options for controlling Jacoco reporting */ - @Suppress("CAST_NEVER_SUCCEEDS") public val jacocoOptions: JacocoOptions = objects.newInstance(JacocoOptions::class.java).apply { taskGenerationEnabled.convention(true) onlyGenerateTasksForVariants.convention(emptySet()) excludedClasses.set(listOf("**/R.class", "**/R$*.class", "**/BuildConfig.*")) + + // Just like Jacoco itself, enable only HTML by default. + // We have to supply an output location for all reports though, + // as keeping this unset would lead to issues + // (ref. https://github.com/mannodermaus/android-junit5/issues/346) + val defaultReportDir = project.layout.buildDirectory.dir("reports/jacoco") html.enabled.convention(true) - html.destination.set(null as? File) - csv.enabled.convention(true) - csv.destination.set(null as? File) - xml.enabled.convention(true) - xml.destination.set(null as? File) + html.destination.convention(defaultReportDir.map { it.dir("html") }) + csv.enabled.convention(false) + csv.destination.convention(defaultReportDir.map { it.file("jacoco.csv") }) + xml.enabled.convention(false) + xml.destination.convention(defaultReportDir.map { it.file("jacoco.xml") }) } public fun jacocoOptions(action: Action) { diff --git a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions.kt b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions.kt index 6b9395fd..d0287228 100644 --- a/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions.kt +++ b/plugin/android-junit5/src/main/kotlin/de/mannodermaus/gradle/plugins/junit5/dsl/JacocoOptions.kt @@ -1,10 +1,14 @@ package de.mannodermaus.gradle.plugins.junit5.dsl +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.FileSystemLocationProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty +import org.gradle.api.reporting.Report.OutputType import org.gradle.api.tasks.Input import org.gradle.internal.enterprise.test.FileProperty import javax.inject.Inject @@ -35,17 +39,17 @@ public abstract class JacocoOptions @Inject constructor( /** * Options for controlling the HTML Report generated by Jacoco */ - public val html: Report = objects.newInstance(Report::class.java) + public val html: DirectoryReport = objects.newInstance(DirectoryReport::class.java) /** * Options for controlling the CSV Report generated by Jacoco */ - public val csv: Report = objects.newInstance(Report::class.java) + public val csv: FileReport = objects.newInstance(FileReport::class.java) /** * Options for controlling the XML Report generated by Jacoco */ - public val xml: Report = objects.newInstance(Report::class.java) + public val xml: FileReport = objects.newInstance(FileReport::class.java) /** * List of class name patterns that should be excluded from being processed by Jacoco. @@ -54,12 +58,7 @@ public abstract class JacocoOptions @Inject constructor( @get:Input public abstract val excludedClasses: ListProperty - public abstract class Report { - - public operator fun invoke(config: Report.() -> Unit) { - this.config() - } - + public sealed class Report { /** * Whether this report should be generated */ @@ -67,11 +66,28 @@ public abstract class JacocoOptions @Inject constructor( public abstract val enabled: Property /** - * Name of the file to be generated; note that + * Name of the file/directory to be generated; note that * due to the variant-aware nature of the plugin, * each variant will be assigned a distinct folder if necessary */ + public abstract val destination: FileSystemLocationProperty + } + + public abstract class DirectoryReport : Report() { @get:Input - public abstract val destination: RegularFileProperty + public abstract override val destination: DirectoryProperty + + public operator fun invoke(config: DirectoryReport.() -> Unit) { + this.config() + } + } + + public abstract class FileReport : Report() { + @get:Input + public abstract override val destination: RegularFileProperty + + public operator fun invoke(config: FileReport.() -> Unit) { + this.config() + } } } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt index d027f277..8285aa89 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/FunctionalTests.kt @@ -12,6 +12,7 @@ import org.gradle.configurationcache.extensions.capitalized import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.DynamicContainer.dynamicContainer import org.junit.jupiter.api.DynamicNode @@ -25,32 +26,43 @@ import java.io.File @TestInstance(PER_CLASS) @DisabledOnCI class FunctionalTests { + private val environment = TestEnvironment() + private lateinit var folder: File + + // Test permutations for AGP (default: empty set, which will exercise all) + private val testedAgpVersions: Set = setOf( + ) + + // Test permutations for projects (default: empty set, which will exercise all) + private val testedProjects: Set = setOf( + ) + + // Whether to pass "-i" to the Gradle runners, increasing insight into their output + private val verboseOutput = false + + // Whether to delete all virtual project root folders after executing these tests + private val cleanOutputFolderAfterTests = true + + @BeforeAll + fun beforeAll() { + // The "project provider" is responsible for the construction + // of all virtual Gradle projects, using a template file located in + // the project's test resources. + folder = File("build/tmp/virtualProjectsRoot").also { it.mkdirs() } + } + + @AfterAll + fun afterAll() { + if (cleanOutputFolderAfterTests) { + folder.deleteRecursively() + } + } - private val environment = TestEnvironment() - private lateinit var folder: File - - // Test permutations for AGP (default: empty set, which will exercise all) - private val testedAgpVersions: Set = setOf( - ) - - // Test permutations for projects (default: empty set, which will exercise all) - private val testedProjects: Set = setOf( - ) - - @BeforeAll - fun beforeAll() { - // The "project provider" is responsible for the construction - // of all virtual Gradle projects, using a template file located in - // the project's test resources. - folder = File("build/tmp/virtualProjectsRoot").also { it.mkdirs() } - } - - @TestFactory - fun execute(): List = - // Create a matrix of permutations between the AGP versions to test - // and the language of the project's build script - environment.supportedAgpVersions.filterAgpVersions() - .map { agp -> + @TestFactory + fun execute(): List = environment.supportedAgpVersions.filterAgpVersions() + .map { agp -> + // Create a matrix of permutations between the AGP versions to test + // and the language of the project's build script val projectCreator = FunctionalTestProjectCreator(folder, environment) // Generate a container for all tests with this specific AGP/Language combination @@ -58,110 +70,119 @@ class FunctionalTests { // Exercise each test project within the given environment projectCreator.allSpecs.filterSpecs().map { spec -> - dynamicTest(spec.name) { - // Required for visibility inside IJ's logging console (display names are still bugged in the IDE) - println("AGP: ${agp.version}, Project: ${spec.name}, Forced Gradle: ${agp.requiresGradle ?: "no"}") - - // Create a virtual project with the given settings & AGP version. - // This call will throw a TestAbortedException if the spec is not eligible for this version, - // marking the test as ignored in the process - val project = projectCreator.createProject(spec, agp) - - // Execute the tests of the virtual project with Gradle - val taskName = spec.task ?: "test" - val result = runGradle(agp, taskName) - .withProjectDir(project) - .build() - - // Check that the task execution was successful in general - when (val outcome = result.task(":$taskName")?.outcome) { - TaskOutcome.UP_TO_DATE -> { - // Nothing to do, a previous build already checked this - println("Test task up-to-date; skipping assertions.") - } - - TaskOutcome.SUCCESS -> { - // Based on the spec's configuration in the test project, - // assert that all test classes have been executed as expected - for (expectation in spec.expectedTests) { - result.assertAgpTests( - buildType = expectation.buildType, - productFlavor = expectation.productFlavor, - tests = expectation.testsList - ) + dynamicTest(spec.name) { + // Required for visibility inside IJ's logging console (display names are still bugged in the IDE) + println("AGP: ${agp.version}, Project: ${spec.name}, Forced Gradle: ${agp.requiresGradle ?: "no"}") + + // Create a virtual project with the given settings & AGP version. + // This call will throw a TestAbortedException if the spec is not eligible for this version, + // marking the test as ignored in the process + val project = projectCreator.createProject(spec, agp) + + // Execute the tests of the virtual project with Gradle + val taskName = spec.task ?: "test" + val result = runGradle(agp, taskName) + .withProjectDir(project) + .build() + + // Check that the task execution was successful in general + when (val outcome = result.task(":$taskName")?.outcome) { + TaskOutcome.UP_TO_DATE -> { + // Nothing to do, a previous build already checked this + println("Test task up-to-date; skipping assertions.") + } + + TaskOutcome.SUCCESS -> { + // Based on the spec's configuration in the test project, + // assert that all test classes have been executed as expected + for (expectation in spec.expectedTests) { + result.assertAgpTests( + buildType = expectation.buildType, + productFlavor = expectation.productFlavor, + tests = expectation.testsList + ) + } + } + + else -> { + // Unexpected result; fail + fail { + "Unexpected task outcome: $outcome\n\nRaw output:\n\n${result.output}" + } + } } - } - - else -> { - // Unexpected result; fail - fail { "Unexpected task outcome: $outcome" } - } } - } } ) - } + } - /* Private */ + /* Private */ - private fun List.filterAgpVersions(): List = - if (testedAgpVersions.isEmpty()) { - // Nothing to do, exercise functional tests on all AGP versions - this - } else { - filter { agp -> - testedAgpVersions.any { it == agp.shortVersion } - } - } - - private fun List.filterSpecs(): List = - if (testedProjects.isEmpty()) { - // Nothing to do, exercise all different projects - this - } else { - filter { spec -> - testedProjects.any { it == spec.name } + private fun List.filterAgpVersions(): List = + if (testedAgpVersions.isEmpty()) { + // Nothing to do, exercise functional tests on all AGP versions + this + } else { + filter { agp -> + testedAgpVersions.any { it == agp.shortVersion } + } } - } - private fun runGradle(agpVersion: TestedAgp, task: String) = - GradleRunner.create() - .apply { - if (agpVersion.requiresGradle != null) { - withGradleVersion(agpVersion.requiresGradle) + private fun List.filterSpecs(): List = + if (testedProjects.isEmpty()) { + // Nothing to do, exercise all different projects + this + } else { + filter { spec -> + testedProjects.any { it == spec.name } } - } - .withArguments(task, "--stacktrace") - .withPrunedPluginClasspath(agpVersion) - - // Helper DSL to assert AGP-specific results of the virtual Gradle executions. - // This asserts the output of the build against the given criteria - private fun BuildResult.assertAgpTests( - buildType: String, - productFlavor: String? = null, - tests: List - ) { - this.prettyPrint() - - // Construct task name from given build type and/or product flavor - // Examples: - // - buildType="debug", productFlavor=null --> ":testDebugUnitTest" - // - buildType="debug", productFlavor="free" --> ":testFreeDebugUnitTest" - val taskName = ":test${productFlavor?.capitalized() ?: ""}${buildType.capitalized()}UnitTest" - - // Perform assertions - assertWithMessage("AGP Tests for '$taskName' did not match expectations") - .about(::BuildResultSubject) - .that(this) - .output() - .ofTask(taskName) - .apply { - tests.forEach { expectedClass -> - val line = "$expectedClass > test() PASSED" - contains(line) - println(line) - } - executedTestCount().isEqualTo(tests.size) } - } + + private fun runGradle(agpVersion: TestedAgp, task: String): GradleRunner { + val arguments = buildList { + add(task) + add("--stacktrace") + if (verboseOutput) add("-i") + } + + return GradleRunner.create() + .apply { + if (agpVersion.requiresGradle != null) { + withGradleVersion(agpVersion.requiresGradle) + } + } + .withArguments(arguments) + .withPrunedPluginClasspath(agpVersion) + } + + // Helper DSL to assert AGP-specific results of the virtual Gradle executions. + // This asserts the output of the build against the given criteria + private fun BuildResult.assertAgpTests( + buildType: String, + productFlavor: String? = null, + tests: List + ) { + this.prettyPrint() + + // Construct task name from given build type and/or product flavor + // Examples: + // - buildType="debug", productFlavor=null --> ":testDebugUnitTest" + // - buildType="debug", productFlavor="free" --> ":testFreeDebugUnitTest" + val taskName = ":test${productFlavor?.capitalized() ?: ""}${buildType.capitalized()}UnitTest" + + // Perform assertions + assertWithMessage("AGP Tests for '$taskName' did not match expectations") + .about(::BuildResultSubject) + .that(this) + .output() + .ofTask(taskName) + .apply { + tests.forEach { expectedClass -> + val line = "$expectedClass > test() PASSED" + contains(line) + println(line) + } + executedTestCount().isEqualTo(tests.size) + } + } } diff --git a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt index 82936ef5..62792c2f 100644 --- a/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt +++ b/plugin/android-junit5/src/test/kotlin/de/mannodermaus/gradle/plugins/junit5/plugin/AgpJacocoVariantTests.kt @@ -212,4 +212,14 @@ interface AgpJacocoVariantTests : AgpVariantAwareTests { } } } + + @Test + fun `can override destination directory of jacoco report`() { + val project = createProject().applyJacocoPlugin().build() + + project.junitPlatform.jacocoOptions.html { + destination.set(project.layout.buildDirectory.dir("customReports")) + } + project.evaluate() + } } diff --git a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template index ae33e50c..fe4aa81e 100644 --- a/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template +++ b/plugin/android-junit5/src/test/resources/test-projects/build.gradle.kts.template @@ -120,7 +120,7 @@ junitPlatform { jacocoOptions { html { enabled.set(true) - destination.set(layout.buildDirectory.file("reports/jacoco/${name}.html")) + destination.set(layout.buildDirectory.dir("reports/jacocoCustom")) } } {% endif %} diff --git a/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml b/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml index 9eaacd07..98ea62f5 100644 --- a/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml +++ b/plugin/android-junit5/src/test/resources/test-projects/jacoco/config.toml @@ -1,3 +1,3 @@ [settings] useJacoco = true -task = "tasks" +task = "jacocoTestReportDebug" diff --git a/plugin/android-junit5/src/test/resources/test-projects/jacoco/src/test/java/de/mannodermaus/app/AdderTest.java b/plugin/android-junit5/src/test/resources/test-projects/jacoco/src/test/java/de/mannodermaus/app/AdderTest.java new file mode 100644 index 00000000..609bde4e --- /dev/null +++ b/plugin/android-junit5/src/test/resources/test-projects/jacoco/src/test/java/de/mannodermaus/app/AdderTest.java @@ -0,0 +1,13 @@ +package de.mannodermaus.app; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class JavaTest { + @Test + void test() { + Adder adder = new Adder(); + assertEquals(4, adder.add(2, 2), "This should succeed!"); + } +}