Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 79 additions & 59 deletions src/test/kotlin/io/cloudshiftdev/mavensync/MavenSyncIntegrationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 =
Expand Down Expand Up @@ -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("\"")
Expand Down
Loading