Skip to content

Commit

Permalink
Reorganize and comment strcalc/build.gradle.kts
Browse files Browse the repository at this point in the history
The file was becoming a bit messy. There's probably still room for
further improvements, but hopefully this helps and demystifies any
lingering magic.
  • Loading branch information
mbland committed Nov 21, 2023
1 parent d31f74e commit 5973ae4
Showing 1 changed file with 84 additions and 44 deletions.
128 changes: 84 additions & 44 deletions strcalc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,61 +49,80 @@ jacoco {
toolVersion = "0.8.11"
}

// The frontend sources are under src/main/frontend. The Node plugin generates
// the pnpm_build and pnpm_test-ci task definitions from the "scripts" field of
// package.json.
node {
nodeProjectDir = file("src/main/frontend")
}

// The small/medium/large test schema is implemented via JUnit5 composite tags
// and custom Test tasks. See:
// Set inputs and outputs for the pnpm_build and pnpn_test-ci frontend tasks.
// This enables Gradle to cache the results instead of executing these tasks
// unconditionally on every build.
//
// - https://mike-bland.com/2023/08/31/the-test-pyramid-and-the-chain-reaction.html
// - https://junit.org/junit5/docs/current/user-guide/#writing-tests-meta-annotations
// - https://docs.gradle.org/current/userguide/java_testing.html#test_grouping
// The vite.config.js file in src/main/frontend specifies:
//
// This doesn't use the incubating JVM Test Suite plugin because it doesn't
// support includeTags():
// - The output directory, build/webapp
// - The default test results directory, build/test-results/test-frontend
// - The coverage directory, build/reports/frontend/coverage
//
// - https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html#jvm_test_suite_plugin
val setCommonTestOptions = { testTask: Test ->
testTask.reports { junitXml.apply { isOutputPerTestCase = true } }
testTask.testLogging {
showStandardStreams = project.hasProperty("testoutput")
}
}

val smallTests = tasks.named<Test>("test") {
description = "Runs small unit tests annotated with @SmallTest."
useJUnitPlatform { includeTags("small") }
setCommonTestOptions(this)
}
val testClasses = tasks.named("testClasses")

// The vite.config.ci-browser.js file, used by pnpm_test-ci, also specifies
// the build/test-results/test-frontend-browser directory.
val frontendDir = project.layout.projectDirectory.dir("src/main/frontend")
val frontendSources = fileTree(frontendDir) {
exclude("node_modules", ".*")
exclude("node_modules", ".*", "*.md")
}
val frontendOutputDir = project.layout.buildDirectory.dir("webapp").get()

val frontendBuild = tasks.named<Task>("pnpm_build") {
description = "Build src/main/frontend JavaScript into build/webapp"
inputs.files(frontendSources)
outputs.dir(frontendOutputDir)
}

val frontendTest = tasks.named<Task>("pnpm_test-ci") {
description = "Test frontend JavaScript in src/main/frontend"
dependsOn(frontendBuild)
inputs.files(frontendSources)

val buildDir = project.layout.buildDirectory
outputs.dir(buildDir.dir("test-results/test-frontend").get())
outputs.dir(buildDir.dir("test-results/test-frontend-browser").get())
val resultsDir = java.testResultsDir.get()
outputs.dir(resultsDir.dir("test-frontend"))
outputs.dir(resultsDir.dir("test-frontend-browser"))
}

// Configure the "war" task generated by the Gradle War plugin to depend upon
// the frontend build and to include its output files from build/webapp. This is
// in addition to the files within src/main/webapp, which the task includes by
// default.
//
// - https://docs.gradle.org/current/userguide/war_plugin.html
val war = tasks.named("war")
tasks.war {
dependsOn(frontendBuild)
from(frontendOutputDir)
}

// The small/medium/large test schema is implemented via JUnit5 composite tags
// and custom Test tasks. See:
//
// - https://mike-bland.com/2023/08/31/the-test-pyramid-and-the-chain-reaction.html
// - https://junit.org/junit5/docs/current/user-guide/#writing-tests-meta-annotations
// - https://docs.gradle.org/current/userguide/java_testing.html#test_grouping
//
// This doesn't use the incubating JVM Test Suite plugin because it doesn't
// support includeTags():
//
// - https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html#jvm_test_suite_plugin
val testClasses = tasks.named("testClasses")
val webappInputs = project.layout.projectDirectory.dir("src/main/webapp")

val setCommonTestOptions = { testTask: Test ->
testTask.reports { junitXml.apply { isOutputPerTestCase = true } }
testTask.testLogging {
showStandardStreams = project.hasProperty("testoutput")
}
}

val setLargerTestOptions = { testTask: Test ->
testTask.group = "verification"
testTask.dependsOn(testClasses)
Expand All @@ -124,16 +143,18 @@ val setLargerTestOptions = { testTask: Test ->
})
}

val addWebappInputs = { t: Task ->
t.inputs.dir(project.layout.projectDirectory.dir("src/main/webapp"))
val smallTests = tasks.named<Test>("test") {
description = "Runs small unit tests annotated with @SmallTest."
useJUnitPlatform { includeTags("small") }
setCommonTestOptions(this)
}

val mediumCoverageTests = tasks.register<Test>("test-medium-coverage") {
description = "Runs medium integration tests annotated with " +
"@MediumCoverageTest."
setLargerTestOptions(this)
dependsOn(frontendBuild)
addWebappInputs(this)
inputs.dir(webappInputs)
useJUnitPlatform { includeTags("medium & coverage") }
shouldRunAfter(smallTests, frontendTest)
}
Expand All @@ -142,7 +163,7 @@ val mediumTests = tasks.register<Test>("test-medium") {
description = "Runs medium integration tests annotated with @MediumTest."
setLargerTestOptions(this)
dependsOn(frontendBuild)
addWebappInputs(this)
inputs.dir(webappInputs)
useJUnitPlatform { includeTags("medium & !coverage") }
shouldRunAfter(smallTests, mediumCoverageTests, frontendTest)
extensions.configure(JacocoTaskExtension::class) {
Expand All @@ -166,20 +187,31 @@ val allTestSizes = arrayOf(
)

val allTests = tasks.register<Task>("test-all") {
description = "Runs the small, medium, and large test suites in order."
description = "Runs the frontend, small, medium, and large test suites, " +
"in that order."
group = "verification"
dependsOn(frontendTest, allTestSizes)
}

internal val testResultsDir = java.testResultsDir
internal val aggregatorClass = "org.apache.tools.ant.taskdefs.optional." +
"junit.XMLResultAggregator"
tasks.named("check") {
dependsOn(allTests)
}

// Used to emit paths of JUnit and coverage report files relative to the root
// directory of the project repository.
val relativeToRootDir = fun(absPath: java.nio.file.Path): java.nio.file.Path {
return rootDir.toPath().relativize(absPath)
}

// Based on
// JUnit emits a single test result file containing a single <testsuite>
// element for each test class. The dorny/test-reporter GitHub action from
// .github/workflows/publish-test-results.yaml requires a single, consolidated
// file containing a <testsuites> element.
//
// The "merge-test-reports" task, defined below, uses this mergeTestReports
// function to create the consolidated <testsuites> file. The implementation
// is based on:
//
// - https://blog.lehnerpat.com/post/2018-09-10/merging-per-suite-junit-reports-into-single-file-with-gradle-kotlin/
// - https://docs.gradle.org/current/userguide/ant.html
val mergeTestReports = fun(resultsDir: File) {
Expand All @@ -189,8 +221,9 @@ val mergeTestReports = fun(resultsDir: File) {
// results in an empty file, so skip it.
if (taskName.startsWith("test-frontend")) return

val relResultsDir = relativeToRootDir(resultsDir.toPath())
val reportTaskName = "merged-report-$taskName"
val aggregatorClass = "org.apache.tools.ant.taskdefs.optional." +
"junit.XMLResultAggregator"

ant.withGroovyBuilder {
"taskdef"(
Expand All @@ -204,7 +237,8 @@ val mergeTestReports = fun(resultsDir: File) {
"includes" to "TEST-*.xml")
}
}
logger.quiet("merged test reports: " + relResultsDir +
logger.quiet("merged test reports: " +
relativeToRootDir(resultsDir.toPath()) +
"/TESTS-TestSuites.xml")
}

Expand All @@ -215,7 +249,7 @@ task("merge-test-reports") {
shouldRunAfter(allTestSizes)

doLast {
val resultsDir = testResultsDir.asFile.get()
val resultsDir = java.testResultsDir.asFile.get()

if (resultsDir.exists()) {
resultsDir.listFiles().filter{ d: File -> d.isDirectory }.forEach {
Expand All @@ -225,13 +259,18 @@ task("merge-test-reports") {
}
}

// Emits the path to a JaCoCo coverage report file if the type of report
// (html, xml, or csv) is required. A report is "required" if the task's
// "report {}" configuration block sets, for example, `html.required = true`.
val emitReportLocation = fun(report: Report) {
if (report.required.get()) {
val location = report.outputLocation.get().asFile.toPath()
logger.quiet("coverage report: " + relativeToRootDir(location))
}
}

// Adds custom configuration to the jacocoTestReport task generated by the
// Gradle JaCoCo plugin.
tasks.named<JacocoReport>("jacocoTestReport") {
shouldRunAfter(smallTests, mediumCoverageTests)
executionData(mediumCoverageTests.get())
Expand All @@ -240,7 +279,9 @@ tasks.named<JacocoReport>("jacocoTestReport") {
}
}

// Generates:
// Generates an XML file suitable to upload to Coveralls.io from
// .github/workflows/run-tests.yaml:
//
// - strcalc/build/reports/jacoco/jacocoXmlTestReport/jacocoXmlTestReport.xml
tasks.register<JacocoReport>("jacocoXmlTestReport") {
shouldRunAfter(smallTests, mediumCoverageTests)
Expand All @@ -255,7 +296,10 @@ tasks.register<JacocoReport>("jacocoXmlTestReport") {
}
}

// https://github.com/ben-manes/gradle-versions-plugin#rejectversionsif-and-componentselection
// Utility function used by the ben-manes/gradle-versions-plugin tasks,
// configured below. See:
//
// - https://github.com/ben-manes/gradle-versions-plugin#rejectversionsif-and-componentselection
fun isNonStable(version: String): Boolean {
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any {
version.uppercase().contains(it)
Expand All @@ -272,7 +316,3 @@ tasks.withType<DependencyUpdatesTask> {
isNonStable(candidate.version) && !isNonStable(currentVersion)
}
}

tasks.named("check") {
dependsOn(allTests)
}

0 comments on commit 5973ae4

Please sign in to comment.