From 15b769961c6aeb8647a05de45a2714b69d892432 Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Fri, 22 Nov 2024 14:43:50 +0100 Subject: [PATCH] chore: add testing infrastructure and initial set of tests (#20) * chore: add testing infrastructure and initial set of tests * chore: remove more references to Hedera --- build.gradle.kts | 13 ++ ....hiero.gradle.base.jpms-modules.gradle.kts | 2 +- ...org.hiero.gradle.build.settings.gradle.kts | 4 +- .../org.hiero.gradle.feature.antlr.gradle.kts | 5 +- ...g.hiero.gradle.feature.java-doc.gradle.kts | 7 +- ...g.hiero.gradle.feature.protobuf.gradle.kts | 1 + ...eature.publish-maven-repository.gradle.kts | 4 +- ...ro.gradle.feature.test-fixtures.gradle.kts | 5 +- .../kotlin/org/hiero/gradle/tasks/GitClone.kt | 1 - .../hiero/gradle/test/ConventionPluginTest.kt | 66 ++++++++ .../org/hiero/gradle/test/QualityCheckTest.kt | 106 ++++++++++++ .../gradle/test/fixtures/GradleProject.kt | 155 ++++++++++++++++++ 12 files changed, 356 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/org/hiero/gradle/test/ConventionPluginTest.kt create mode 100644 src/test/kotlin/org/hiero/gradle/test/QualityCheckTest.kt create mode 100644 src/test/kotlin/org/hiero/gradle/test/fixtures/GradleProject.kt diff --git a/build.gradle.kts b/build.gradle.kts index f108f6f..2ffb919 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -111,6 +111,19 @@ if (publishSigningEnabled) { } } +testing { + @Suppress("UnstableApiUsage") + suites.named("test") { + useJUnitJupiter() + dependencies { + implementation("org.junit.jupiter:junit-jupiter-params") + implementation("org.assertj:assertj-core:3.26.3") + } + // If success, delete all test projects + targets.all { testTask { doLast { File("build/test-projects").deleteRecursively() } } } + } +} + spotless { val header = """ diff --git a/src/main/kotlin/org.hiero.gradle.base.jpms-modules.gradle.kts b/src/main/kotlin/org.hiero.gradle.base.jpms-modules.gradle.kts index 0dae400..2797e7c 100644 --- a/src/main/kotlin/org.hiero.gradle.base.jpms-modules.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.base.jpms-modules.gradle.kts @@ -313,7 +313,7 @@ jvmDependencyConflicts.consistentResolution { providesVersions(project.path) } else { providesVersions(":aggregation") - platform(":hedera-dependency-versions") + platform(":hiero-dependency-versions") } } diff --git a/src/main/kotlin/org.hiero.gradle.build.settings.gradle.kts b/src/main/kotlin/org.hiero.gradle.build.settings.gradle.kts index d338299..c585159 100644 --- a/src/main/kotlin/org.hiero.gradle.build.settings.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.build.settings.gradle.kts @@ -51,8 +51,8 @@ configure { plugin("org.hiero.gradle.base.jpms-modules") } } - if (layout.rootDirectory.dir("hedera-dependency-versions").asFile.isDirectory) { + if (layout.rootDirectory.dir("hiero-dependency-versions").asFile.isDirectory) { // "BOM" with versions of 3rd party dependencies - versions("hedera-dependency-versions") + versions("hiero-dependency-versions") } } diff --git a/src/main/kotlin/org.hiero.gradle.feature.antlr.gradle.kts b/src/main/kotlin/org.hiero.gradle.feature.antlr.gradle.kts index 0365dd8..bd82fe1 100644 --- a/src/main/kotlin/org.hiero.gradle.feature.antlr.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.feature.antlr.gradle.kts @@ -17,6 +17,7 @@ plugins { id("java") id("antlr") + id("org.hiero.gradle.base.jpms-modules") } configurations { @@ -24,13 +25,15 @@ configurations { // classpath of our runtime. // https://github.com/gradle/gradle/issues/820 api { setExtendsFrom(extendsFrom.filterNot { it == antlr.get() }) } - // Get ANTLR version from 'hedera-dependency-versions' + // Get ANTLR version from 'hiero-dependency-versions' antlr { extendsFrom(configurations["internal"]) } } dependencies { antlr("org.antlr:antlr4") } // See: https://github.com/gradle/gradle/issues/25885 +java { withSourcesJar() } + tasks.named("sourcesJar") { dependsOn(tasks.generateGrammarSource) } tasks.withType().configureEach { diff --git a/src/main/kotlin/org.hiero.gradle.feature.java-doc.gradle.kts b/src/main/kotlin/org.hiero.gradle.feature.java-doc.gradle.kts index e959a1a..71b2b32 100644 --- a/src/main/kotlin/org.hiero.gradle.feature.java-doc.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.feature.java-doc.gradle.kts @@ -22,12 +22,7 @@ plugins { tasks.withType().configureEach { options { this as StandardJavadocDocletOptions - tags( - "apiNote:a:API Note:", - "implSpec:a:Implementation Requirements:", - "implNote:a:Implementation Note:" - ) - options.windowTitle = "Hedera Consensus Node" + options.windowTitle = "Hiero" options.memberLevel = JavadocMemberLevel.PACKAGE addStringOption("Xdoclint:all,-missing", "-Xwerror") } diff --git a/src/main/kotlin/org.hiero.gradle.feature.protobuf.gradle.kts b/src/main/kotlin/org.hiero.gradle.feature.protobuf.gradle.kts index 418a9db..f93d6db 100644 --- a/src/main/kotlin/org.hiero.gradle.feature.protobuf.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.feature.protobuf.gradle.kts @@ -17,6 +17,7 @@ plugins { id("java") id("com.google.protobuf") + id("org.hiero.gradle.base.jpms-modules") } // Configure Protobuf Plugin to download protoc executable rather than using local installed version diff --git a/src/main/kotlin/org.hiero.gradle.feature.publish-maven-repository.gradle.kts b/src/main/kotlin/org.hiero.gradle.feature.publish-maven-repository.gradle.kts index 5762834..b9f2bcf 100644 --- a/src/main/kotlin/org.hiero.gradle.feature.publish-maven-repository.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.feature.publish-maven-repository.gradle.kts @@ -21,6 +21,7 @@ plugins { id("maven-publish") id("signing") id("io.freefair.maven-central.validate-poms") + id("org.hiero.gradle.base.lifecycle") } java { @@ -41,7 +42,7 @@ if (publishSigningEnabled) { publishing.publications.withType().configureEach { versionMapping { // Everything published takes the versions from the resolution result. - // These are the versions we define in 'hedera-dependency-versions' + // These are the versions we define in 'hiero-dependency-versions' // and use consistently in all modules. allVariants { fromResolutionResult() } } @@ -104,6 +105,7 @@ publishing.publications.withType().configureEach { developers { devGroups.forEach { mail, team -> developer { + id = team as String name = team as String email = mail as String organization = "Hiero - a Linux Foundation Decentralized Trust project" diff --git a/src/main/kotlin/org.hiero.gradle.feature.test-fixtures.gradle.kts b/src/main/kotlin/org.hiero.gradle.feature.test-fixtures.gradle.kts index efaaf9f..0b8947c 100644 --- a/src/main/kotlin/org.hiero.gradle.feature.test-fixtures.gradle.kts +++ b/src/main/kotlin/org.hiero.gradle.feature.test-fixtures.gradle.kts @@ -16,7 +16,10 @@ import org.gradle.api.component.AdhocComponentWithVariants -plugins { id("java-test-fixtures") } +plugins { + id("java") + id("java-test-fixtures") +} // Disable publishing of test fixture if 'java-test-fixtures' plugin is used // https://docs.gradle.org/current/userguide/java_testing.html#ex-disable-publishing-of-test-fixtures-variants diff --git a/src/main/kotlin/org/hiero/gradle/tasks/GitClone.kt b/src/main/kotlin/org/hiero/gradle/tasks/GitClone.kt index efc9ed0..4dac536 100644 --- a/src/main/kotlin/org/hiero/gradle/tasks/GitClone.kt +++ b/src/main/kotlin/org/hiero/gradle/tasks/GitClone.kt @@ -49,7 +49,6 @@ abstract class GitClone : DefaultTask() { init { offline.set(startParameter.isOffline) - localCloneDirectory.set(layout.buildDirectory.dir("hedera-protobufs")) // If a 'branch' is configured, the task is never up-to-date as it may change outputs.upToDateWhen { !branch.isPresent } } diff --git a/src/test/kotlin/org/hiero/gradle/test/ConventionPluginTest.kt b/src/test/kotlin/org/hiero/gradle/test/ConventionPluginTest.kt new file mode 100644 index 0000000..f3bf085 --- /dev/null +++ b/src/test/kotlin/org/hiero/gradle/test/ConventionPluginTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 Hiero a Series of LF Projects, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.hiero.gradle.test + +import java.io.File +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.hiero.gradle.test.fixtures.GradleProject +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** + * This tests makes sure that each plugin can be used individually of the other plugins. When + * applied in a project context, a plugin sometimes works without declaring the plugins it depends + * on in its own plugins {} block. This can happen if the required plugins are coincidentally + * applied before in the project. This test makes sure that each plugin goes through the Gradle + * configuration phase if it is applied on its own. + */ +class ConventionPluginTest { + + @ParameterizedTest + @MethodSource("pluginIds") + fun `each plugin can be applied individually without error`(pluginId: String) { + val p = GradleProject() + when { + pluginId.endsWith(".settings") -> + p.settingsFile("""plugins { id("${pluginId.substringBeforeLast(".settings")}") }""") + pluginId.endsWith(".root") -> + p.file( + "build.gradle.kts", + """plugins { id("${pluginId.substringBeforeLast(".settings")}") }""" + ) + else -> p.withMinimalStructure().moduleBuildFile("""plugins { id("$pluginId") }""") + } + + val result = p.help() + + assertThat(result.task(":help")!!.outcome).isEqualTo(SUCCESS) + } + + companion object { + @JvmStatic + fun pluginIds(): Array { + val pluginList = + File("src/main/kotlin") + .listFiles()!! + .filter { it.isFile && it.name.endsWith(".gradle.kts") } + .map { it.name.substringBeforeLast(".gradle.kts") } + return pluginList.toTypedArray() + } + } +} diff --git a/src/test/kotlin/org/hiero/gradle/test/QualityCheckTest.kt b/src/test/kotlin/org/hiero/gradle/test/QualityCheckTest.kt new file mode 100644 index 0000000..7b0d67c --- /dev/null +++ b/src/test/kotlin/org/hiero/gradle/test/QualityCheckTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 Hiero a Series of LF Projects, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.hiero.gradle.test + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome +import org.hiero.gradle.test.fixtures.GradleProject +import org.junit.jupiter.api.Test + +class QualityCheckTest { + + @Test + fun `qualityCheck passes for a well-defined minimal project`() { + val p = GradleProject().withMinimalStructure() + p.moduleBuildFile("""plugins { id("org.hiero.gradle.module.library") }""") + + val result = p.qualityCheck() + + assertThat(result.task(":module-a:qualityCheck")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + } + + @Test + fun `qualityCheck fails for invalid pom xml`() { + val p = GradleProject().withMinimalStructure() + p.moduleBuildFile("""plugins { id("org.hiero.gradle.module.library") }""") + p.descriptionTxt.delete() + + val result = p.failQualityCheck() + + assertThat(result.task(":module-a:validatePomFileForMavenPublication")?.outcome) + .isEqualTo(TaskOutcome.FAILED) + assertThat(p.problemsReport).content().contains("Missing Element in Maven Pom") + assertThat(p.problemsReport).content().contains("No description found") + } + + @Test + fun `qualityCheck fails for wrong dependency scopes`() { + val p = GradleProject().withMinimalStructure() + p.dependencyVersionsFile( + """ + plugins { + id("org.hiero.gradle.base.lifecycle") + id("org.hiero.gradle.base.jpms-modules") + } + dependencies.constraints { + api("com.fasterxml.jackson.core:jackson-databind:2.16.0") { + because("com.fasterxml.jackson.databind") + } + api("org.apache.commons:commons-lang3:3.14.0") { + because("org.apache.commons.lang3") + } + }""" + .trimIndent() + ) + p.moduleBuildFile("""plugins { id("org.hiero.gradle.module.library") }""") + p.moduleInfoFile( + """ + module org.hiero.product.module.a { + requires transitive com.fasterxml.jackson.databind; + requires org.apache.commons.lang3; + }""" + .trimIndent() + ) + p.javaSourceFile( + """ + package foo; + + import com.fasterxml.jackson.databind.ObjectMapper; + + public class ModuleA { + private ObjectMapper om; + }""" + .trimIndent() + ) + + val result = p.failQualityCheck() + + assertThat(result.task(":module-a:checkModuleDirectivesScope")?.outcome) + .isEqualTo(TaskOutcome.FAILED) + assertThat(result.output) + .contains( + """ + Please add the following requires directives: + requires com.fasterxml.jackson.databind; + + Please remove the following requires directives (or change to runtimeOnly): + requires org.apache.commons.lang3; + requires transitive com.fasterxml.jackson.databind;""" + .trimIndent() + ) + } +} diff --git a/src/test/kotlin/org/hiero/gradle/test/fixtures/GradleProject.kt b/src/test/kotlin/org/hiero/gradle/test/fixtures/GradleProject.kt new file mode 100644 index 0000000..8cc4000 --- /dev/null +++ b/src/test/kotlin/org/hiero/gradle/test/fixtures/GradleProject.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 Hiero a Series of LF Projects, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.hiero.gradle.test.fixtures + +import java.io.File +import java.lang.management.ManagementFactory +import java.util.UUID +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner + +/** + * Access to a minimal project inside a temporary folder. The project contain files that are + * expected to exist in our setup. + */ +class GradleProject { + + private val projectDir = File("build/test-projects/${UUID.randomUUID()}") + + val problemsReport = file("build/reports/problems/problems-report.html") + private val gradlePropertiesFile = file("gradle.properties") + private val settingsFile = file("settings.gradle.kts") + private val dependencyVersions = file("hiero-dependency-versions/build.gradle.kts") + private val aggregation = file("gradle/aggregation/build.gradle.kts") + private val versionFile = file("version.txt") + private val jdkVersionFile = file("gradle/jdk-version.txt") + + private val developersProperties = file("product/developers.properties") + val descriptionTxt = file("product/description.txt") + private val moduleBuildFile = file("product/module-a/build.gradle.kts") + private val moduleInfo = file("product/module-a/src/main/java/module-info.java") + private val javaSourceFile = + file("product/module-a/src/main/java/org/hiero/product/module/a/ModuleA.java") + + private val expectedHeader = + """ + /* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + """ + .trimIndent() + + fun withMinimalStructure(): GradleProject { + gradlePropertiesFile.writeText( + """ + org.gradle.configuration-cache=true + # org.gradle.unsafe.isolated-projects=true + # org.gradle.caching=true + """ + .trimIndent() + ) + settingsFile( + """ + plugins { id("org.hiero.gradle.build") } + + rootProject.name = "test-project" + + javaModules { directory("product") } + """ + .trimIndent() + ) + dependencyVersionsFile( + """plugins { + id("org.hiero.gradle.base.lifecycle") + id("org.hiero.gradle.base.jpms-modules") + }""" + .trimIndent() + ) + aggregation.writeText("") + versionFile.writeText("1.0") + jdkVersionFile.writeText("17.0.12") + developersProperties.writeText("test=test@hiero.org") + descriptionTxt.writeText("A module to test hiero-gradle-conventions") + moduleInfoFile("module org.hiero.product.module.a {}") + javaSourceFile( + """ + package org.hiero.product.module.a; + + class ModuleA {} + """ + .trimIndent() + ) + + return this + } + + fun settingsFile(content: String) = settingsFile.also { it.writeFormatted(content) } + + fun moduleBuildFile(content: String) = moduleBuildFile.also { it.writeFormatted(content) } + + fun moduleInfoFile(content: String) = moduleInfo.also { it.writeFormatted(content) } + + fun javaSourceFile(content: String) = javaSourceFile.also { it.writeFormatted(content) } + + fun dependencyVersionsFile(content: String) = + dependencyVersions.also { it.writeFormatted(content) } + + fun file(path: String, content: String? = null) = + File(projectDir, path).also { + it.parentFile.mkdirs() + if (content != null) { + it.writeText(content) + } + } + + fun help(): BuildResult = runner(listOf("help")).build() + + fun build(): BuildResult = runner(listOf("build")).build() + + fun qualityCheck(): BuildResult = runner(listOf("qualityCheck")).build() + + fun failQualityCheck(): BuildResult = runner(listOf("qualityCheck")).buildAndFail() + + private fun File.writeFormatted(content: String) { + writeText("$expectedHeader\n\n$content\n") + } + + private fun runner(args: List) = + GradleRunner.create() + .forwardOutput() + .withPluginClasspath() + .withProjectDir(projectDir) + .withArguments(args + listOf("-s", "--warning-mode=all")) + .withDebug( + ManagementFactory.getRuntimeMXBean() + .inputArguments + .toString() + .contains("-agentlib:jdwp") + ) +}