commit 7a6cd5b7a5fb1d7c5075a9ff2d129d2332fe0812
parent 63449f5d59ddb49f84498915b3ce8d41ce36f15b
Author: triesap <tyson@radroots.org>
Date: Mon, 10 Aug 2026 22:11:18 +0000
test: qualify the HarvestCircle build logic
- exercise every convention plugin in isolated fixtures without buildSrc coupling
- prove strict configuration-cache storage and reuse in fixtures and the application
- cover missing authority, stale generation, unsupported hosts, and empty test inventory
- reject incomplete package metadata, unsafe native payloads, and unknown provenance
Diffstat:
3 files changed, 127 insertions(+), 26 deletions(-)
diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt
@@ -4,6 +4,7 @@ import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.UnexpectedBuildFailure
import kotlin.io.path.createTempDirectory
import kotlin.io.path.createDirectories
+import kotlin.io.path.exists
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertTrue
@@ -71,16 +72,30 @@ class ConventionPluginSmokeTest {
val result = runner.build()
assertTrue(result.output.contains("BUILD SUCCESSFUL"), pluginId)
+ assertTrue(!fixture.resolve("buildSrc").exists(), "$pluginId fixture must not provide buildSrc classes")
if (pluginId == "org.harvestcircle.build.root") {
assertTrue(result.output.contains("verifyProductCoordinates"), result.output)
- assertTrue(runner.build().output.contains("Reusing configuration cache"))
- }
- if (pluginId == "org.harvestcircle.build.rust-ffi") {
- assertTrue(runner.build().output.contains("Reusing configuration cache"))
}
+ assertTrue(runner.build().output.contains("Reusing configuration cache"), pluginId)
}
}
+ @Test
+ fun rootPluginFailsClosedWhenTheProductManifestIsMissing() {
+ val fixture = createTempDirectory("harvestcircle-root-missing-manifest-")
+ fixture.resolve("settings.gradle.kts").writeText("rootProject.name = \"fixture\"\n")
+ fixture.resolve("build.gradle.kts").writeText("plugins { id(\"org.harvestcircle.build.root\") }\n")
+
+ val result =
+ GradleRunner.create()
+ .withProjectDir(fixture.toFile())
+ .withPluginClasspath()
+ .withArguments("verifyProductCoordinates", "--stacktrace")
+ .buildAndFail()
+
+ assertTrue(result.output.contains("harvestcircle-v1.properties"), result.output)
+ }
+
private fun prepareDesktopFixture(
fixture: java.nio.file.Path,
withUnitTest: Boolean = true,
@@ -330,6 +345,46 @@ class ConventionPluginSmokeTest {
}
}
+ @Test
+ fun packagingPluginAcceptsGovernedProvenanceAndRejectsUnknownInputs() {
+ val governed = createTempDirectory("harvestcircle-package-governed-provenance-")
+ preparePackagingBuild(governed, "exit 0")
+ val governedEnvironment =
+ System.getenv() +
+ mapOf(
+ "HARVESTCIRCLE_BUILD_SOURCE_COMMIT" to "a".repeat(40),
+ "HARVESTCIRCLE_BUILD_SOURCE_DIRTY" to "false",
+ "HARVESTCIRCLE_BUILD_RADROOTS_REVISION" to "b".repeat(40),
+ "SOURCE_DATE_EPOCH" to "1770000000",
+ )
+ val success =
+ GradleRunner.create()
+ .withProjectDir(governed.toFile())
+ .withPluginClasspath()
+ .withEnvironment(governedEnvironment)
+ .withArguments(":app:desktop:verifyReleaseBuildProvenance", "--stacktrace")
+ .build()
+ assertTrue(success.output.contains("BUILD SUCCESSFUL"), success.output)
+
+ val standalone = createTempDirectory("harvestcircle-package-unknown-provenance-")
+ preparePackagingBuild(standalone, "exit 0")
+ val failure =
+ GradleRunner.create()
+ .withProjectDir(standalone.toFile())
+ .withPluginClasspath()
+ .withEnvironment(
+ System.getenv() -
+ setOf(
+ "HARVESTCIRCLE_BUILD_SOURCE_COMMIT",
+ "HARVESTCIRCLE_BUILD_SOURCE_DIRTY",
+ "HARVESTCIRCLE_BUILD_RADROOTS_REVISION",
+ "SOURCE_DATE_EPOCH",
+ ),
+ ).withArguments(":app:desktop:verifyReleaseBuildProvenance", "--stacktrace")
+ .buildAndFail()
+ assertTrue(failure.output.contains("Release source commit provenance is unknown or malformed"), failure.output)
+ }
+
private fun prepareDesktopBuild(
fixture: java.nio.file.Path,
withUnitTest: Boolean,
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt
@@ -32,11 +32,7 @@ public abstract class VerifyDesktopBuildMetadataArtifact : DefaultTask() {
jar.getJarEntry("org/harvestcircle/application/generated/DesktopBuildMetadata.class")
?: throw GradleException("Desktop build metadata is missing from the application artifact")
val metadata = jar.getInputStream(entry).use { it.readBytes() }.toString(Charsets.ISO_8859_1)
- expectedBuildEvidence.get().forEach { evidence ->
- require(metadata.contains(evidence)) {
- "Desktop application artifact is missing generated build evidence"
- }
- }
+ requireBuildMetadataEvidence(metadata, expectedBuildEvidence.get())
}
}
}
@@ -315,26 +311,48 @@ public abstract class VerifyReleaseBuildProvenance : DefaultTask() {
private fun packagedNativeLibraries(
root: File,
expectedEntry: String,
-): List<ByteArray> =
- root.walkTopDown()
- .filter { it.isFile && it.extension == "jar" }
- .flatMap { jarFile ->
- JarFile(jarFile).use { jar ->
- val nativeEntries =
+): List<ByteArray> {
+ val entries =
+ root.walkTopDown()
+ .filter { it.isFile && it.extension == "jar" }
+ .flatMap { jarFile ->
+ JarFile(jarFile).use { jar ->
jar.entries().asSequence().filter { entry ->
!entry.isDirectory &&
entry.name.substringAfterLast('.').lowercase() in setOf("dylib", "so", "dll")
- }.toList()
- val productEntries =
- nativeEntries.filter { entry ->
- entry.name == expectedEntry || entry.name.lowercase().contains("harvestcircle")
- }
- require(productEntries.none { it.name != expectedEntry }) {
- "Package contains an unexpected or test native payload"
- }
- productEntries.map { entry -> jar.getInputStream(entry).use { it.readBytes() } }
- }.asSequence()
- }.toList()
+ }.map { entry -> entry.name to jar.getInputStream(entry).use { it.readBytes() } }.toList()
+ }.asSequence()
+ }.toList()
+ requireSingleCanonicalProductNativeEntry(entries.map { it.first }, expectedEntry)
+ return entries.filter { it.first == expectedEntry }.map { it.second }
+}
+
+internal fun requireBuildMetadataEvidence(
+ metadata: String,
+ expectedBuildEvidence: List<String>,
+) {
+ expectedBuildEvidence.forEach { evidence ->
+ require(metadata.contains(evidence)) {
+ "Desktop application artifact is missing generated build evidence"
+ }
+ }
+}
+
+internal fun requireSingleCanonicalProductNativeEntry(
+ nativeEntries: List<String>,
+ expectedEntry: String,
+) {
+ val productEntries =
+ nativeEntries.filter { entry ->
+ entry == expectedEntry || entry.lowercase().contains("harvestcircle")
+ }
+ require(productEntries.none { it != expectedEntry }) {
+ "Package contains an unexpected or test native payload"
+ }
+ require(productEntries.size == 1) {
+ "Package must contain exactly one production native library"
+ }
+}
private fun commandOutput(vararg command: String): String {
val process = ProcessBuilder(*command).redirectErrorStream(true).start()
diff --git a/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasksTest.kt b/build-logic/plugins/src/test/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasksTest.kt
@@ -0,0 +1,28 @@
+package org.harvestcircle.buildlogic.plugins.tasks
+
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class PackagingTasksTest {
+ @Test
+ fun metadataEvidenceMustBeComplete() {
+ requireBuildMetadataEvidence("version=1.0 toolchain=21", listOf("1.0", "21"))
+
+ assertFailsWith<IllegalArgumentException> {
+ requireBuildMetadataEvidence("version=1.0", listOf("1.0", "missing-toolchain"))
+ }
+ }
+
+ @Test
+ fun nativeInventoryRequiresOneCanonicalProductionPayload() {
+ val expected = "darwin-aarch64/libharvestcircle_ffi.dylib"
+ requireSingleCanonicalProductNativeEntry(listOf("com/sun/jna/darwin-aarch64/libjnidispatch.jnilib", expected), expected)
+
+ assertFailsWith<IllegalArgumentException> {
+ requireSingleCanonicalProductNativeEntry(listOf(expected, expected), expected)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ requireSingleCanonicalProductNativeEntry(listOf("darwin-aarch64/libharvestcircle_test_ffi.dylib"), expected)
+ }
+ }
+}