From 6e9dbd418b7a236fc5583c30a83f5d49780bb08f Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Tue, 12 May 2026 09:14:31 -0700 Subject: [PATCH] test: add timeout and progress logging to integration test Make MavenSyncIntegrationTest distinguish slow from hung: configurable test timeout via MAVEN_SYNC_IT_TIMEOUT_MINUTES (default 15m), per-artifact INFO progress log, asset-listing heartbeat every 25 versions, and elapsed time in the final summary. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../mavensync/MavenSyncIntegrationTest.kt | 138 ++++++++++-------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt b/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt index 2ea4fa5..b230e21 100644 --- a/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt +++ b/src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt @@ -6,6 +6,7 @@ import com.sksamuel.hoplite.ConfigLoaderBuilder import com.sksamuel.hoplite.ExperimentalHoplite import com.sksamuel.hoplite.addFileSource import com.sksamuel.hoplite.addResourceSource +import io.github.oshai.kotlinlogging.KotlinLogging import io.kotest.assertions.withClue import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldBeEmpty @@ -15,12 +16,16 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path import java.util.TreeMap +import kotlin.time.Duration.Companion.minutes +import kotlin.time.TimeSource + +private val logger = KotlinLogging.logger {} /** * Source-side integration test. Drives a real `MavenHttpRepository` against a user-supplied repo - * URL (and optional credentials) and runs everything the sync engine does *up to but excluding* - * the upload to the target: crawl, parse, target-diff (against an empty fake), and per-version - * asset listing. + * URL (and optional credentials) and runs everything the sync engine does *up to but excluding* the + * upload to the target: crawl, parse, target-diff (against an empty fake), and per-version asset + * listing. * * Gated on the `MAVEN_SYNC_IT_CONFIG` env var (path to a JSON file using the standard [SyncConfig] * schema). When unset, the test is reported as disabled and no network calls are made. @@ -32,80 +37,96 @@ class MavenSyncIntegrationTest : FunSpec({ val configPath = System.getenv("MAVEN_SYNC_IT_CONFIG") val enabled = !configPath.isNullOrBlank() - - test("discovers artifacts, versions, and assets from a real source repo") - .config(enabled = enabled) { - val config = loadIntegrationConfig(File(configPath!!)) - val options = config.toSyncOptions() - - config.source.toMavenHttpRepository().use { source -> - val target = FakeMavenHttpRepository("integration-test-target") - val report = DiscoveryReport() - - source.crawl(options.paths, options.crawlDelay).collect { metadata -> - val targetMetadata = - target.queryArtifactMetadata(metadata.group, metadata.artifact) - val missingVersions = - metadata.artifactVersions.toSet() - - targetMetadata.artifactVersions.toSet() - missingVersions.forEach { version -> - val coordinates = Coordinates(metadata.group, metadata.artifact, version) - val assets = - source.listArtifactVersionAssets( - coordinates, - options.transferChecksums, - options.transferSignatures, - ) - report.add(coordinates, assets) + val timeoutMinutes = System.getenv("MAVEN_SYNC_IT_TIMEOUT_MINUTES")?.toLongOrNull() ?: 15L + + test("discovers artifacts, versions, and assets from a real source repo").config( + enabled = enabled, + timeout = timeoutMinutes.minutes, + ) { + val start = TimeSource.Monotonic.markNow() + val config = loadIntegrationConfig(File(configPath!!)) + val options = config.toSyncOptions() + + config.source.toMavenHttpRepository().use { source -> + val target = FakeMavenHttpRepository("integration-test-target") + val report = DiscoveryReport() + var artifactsSeen = 0 + var versionsListed = 0 + + source.crawl(options.paths, options.crawlDelay).collect { metadata -> + artifactsSeen++ + val targetMetadata = + target.queryArtifactMetadata(metadata.group, metadata.artifact) + val missingVersions = + metadata.artifactVersions.toSet() - targetMetadata.artifactVersions.toSet() + logger.info { + "[$artifactsSeen] ${metadata.group.value}:${metadata.artifact.value} " + + "versions=${metadata.artifactVersions.size} " + + "missing=${missingVersions.size} elapsed=${start.elapsedNow()}" + } + missingVersions.forEach { version -> + val coordinates = Coordinates(metadata.group, metadata.artifact, version) + val assets = + source.listArtifactVersionAssets( + coordinates, + options.transferChecksums, + options.transferSignatures, + ) + report.add(coordinates, assets) + versionsListed++ + if (versionsListed % 25 == 0) { + logger.info { + "…listed $versionsListed versions, elapsed=${start.elapsedNow()}" + } } } + } - target.copyCalls.shouldBeEmpty() - target.uploadCalls.shouldBeEmpty() - target.releaseCalls.shouldBeEmpty() + target.copyCalls.shouldBeEmpty() + target.uploadCalls.shouldBeEmpty() + target.releaseCalls.shouldBeEmpty() - val reportPath = writeReport(report) - println( - """ + val reportPath = writeReport(report) + println( + """ |Maven-sync integration test discovery report: | source: ${config.source.url} | paths: ${options.paths} | artifacts: ${report.artifactCount} | versions: ${report.versionCount} | assets: ${report.assetCount} + | elapsed: ${start.elapsedNow()} | report: $reportPath """ - .trimMargin() - ) + .trimMargin() + ) - withClue( - "No artifacts discovered — check source URL, credentials, and paths." - ) { - report.artifacts.keys.shouldNotBeEmpty() + withClue("No artifacts discovered — check source URL, credentials, and paths.") { + report.artifacts.keys.shouldNotBeEmpty() + } + report.artifacts.forEach { (artifactKey, versions) -> + withClue("Artifact $artifactKey has no versions") { + versions.keys.shouldNotBeEmpty() } - report.artifacts.forEach { (artifactKey, versions) -> - withClue("Artifact $artifactKey has no versions") { - versions.keys.shouldNotBeEmpty() - } - versions.forEach { (version, assets) -> - withClue("Artifact $artifactKey version $version has no assets") { - assets.shouldNotBeEmpty() - } + versions.forEach { (version, assets) -> + withClue("Artifact $artifactKey version $version has no assets") { + assets.shouldNotBeEmpty() } } + } - val expectedPath = System.getenv("MAVEN_SYNC_IT_EXPECTED") - if (!expectedPath.isNullOrBlank()) { - val expected = File(expectedPath).readText().trim() - withClue( - "Discovery report differs from snapshot at $expectedPath " + - "(live report: $reportPath)" - ) { - report.toJson().trim() shouldBe expected - } + val expectedPath = System.getenv("MAVEN_SYNC_IT_EXPECTED") + if (!expectedPath.isNullOrBlank()) { + val expected = File(expectedPath).readText().trim() + withClue( + "Discovery report differs from snapshot at $expectedPath " + + "(live report: $reportPath)" + ) { + report.toJson().trim() shouldBe expected } } } + } }) private fun loadIntegrationConfig(file: File): SyncConfig = @@ -179,8 +200,7 @@ private class DiscoveryReport { '\n' -> sb.append("\\n") '\r' -> sb.append("\\r") '\t' -> sb.append("\\t") - else -> - if (c.code < 0x20) sb.append("\\u%04x".format(c.code)) else sb.append(c) + else -> if (c.code < 0x20) sb.append("\\u%04x".format(c.code)) else sb.append(c) } } sb.append("\"")