refactor: 빌드 로직 개선, gradle 파일 경량화

This commit is contained in:
donghyeon-ka
2026-09-18 15:22:30 +09:00
parent ace8aaaef6
commit 60b6a319e7
397 changed files with 19320 additions and 8406 deletions
@@ -2,6 +2,8 @@ plugins {
id 'ca.spring-library'
id 'ca.spring-config'
id 'java-test-fixtures'
id 'ca.auxiliary-source-set'
id 'ca.mongo-verification'
}
// Shared test code as a Gradle test-fixtures variant — ADR-BUILD-001. Applied here rather than
@@ -61,7 +63,7 @@ dependencies {
// variant whether or not anybody asks for them. ADR-BUILD-001 records that as an accepted loss —
// unconsumed fixtures are unconsumed, and a leaf that genuinely must not offer them needs a module,
// not a third convention.
strictTestLanes {
auxiliarySourceSets {
sourceSet('mongoPerformanceTest') { compilesAgainst 'main', 'testFixtures' }
}
@@ -86,18 +88,6 @@ dependencies {
testFixturesImplementation libs.toxiproxy.java
}
// Pinned server images. The design forbids `latest` for a certification lane (Task 44): a mutable
// tag makes a red run unattributable. `-PmongoPrimaryImage=` / `-PmongoCompatibilityImage=`
// override them for a one-off run.
Closure<Void> applyMongoImageSelection = { task ->
task.systemProperty 'mongodb.primary.image',
(project.findProperty('mongoPrimaryImage') ?: 'mongo:8.0.16').toString()
task.systemProperty 'mongodb.compatibility.image',
(project.findProperty('mongoCompatibilityImage') ?: 'mongo:7.0.28').toString()
task.systemProperty 'mongodb.toxiproxy.image',
(project.findProperty('mongoToxiproxyImage') ?: 'ghcr.io/shopify/toxiproxy:2.12.0').toString()
}
// Docker-backed lanes are excluded from the default unit run: they fail closed without Docker, and
// a `check` that fails on a laptop without Docker teaches people to skip `check`.
//
@@ -122,33 +112,63 @@ tasks.named('test', Test) {
// copied once per lane here and again in four other leaves.
strictTestLanes {
lane('mongoReplicaSetTest') {
integration()
tag = 'mongodb-replicaset'
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
'change stream (design §29).'
customize = { test -> applyMongoImageSelection(test) }
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
}
lane('mongoFailoverTest') {
integration()
tag = 'mongodb-failover'
description = 'Three-node replica set failover lane: primary kill, partition, unknown ' +
'commit, resume (design §29).'
customize = { test -> applyMongoImageSelection(test) }
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
}
lane('mongoMigrationTest') {
integration()
tag = 'mongodb-migration'
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
'restart (design §12).'
customize = { test -> applyMongoImageSelection(test) }
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
}
lane('mongoCompatibilityTest') {
integration()
tag = 'mongodb-compatibility'
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
customize = { test -> applyMongoImageSelection(test) }
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
}
lane('mongoSecurityIntegrationTest') {
integration()
tag = 'mongodb-security-integration'
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
'(design §26).'
customize = { test -> applyMongoImageSelection(test) }
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
}
lane('mongoStableContractTest') {
@@ -160,17 +180,19 @@ strictTestLanes {
// Driven by its own source set rather than a tag: for this shape the source set is the
// selection, so the convention asks for no tag.
lane('mongoPerformanceTest') {
performance()
sourceSet = 'mongoPerformanceTest'
description = 'Certifies contention, aggregation spill, pagination and pool resource ' +
'bounds (design §29).'
customize = { test ->
applyMongoImageSelection(test)
// Assertions on by default. They defaulted to false, so the lane measured numbers and
// compared them to nothing — a performance gate whose bounds are never evaluated is a
// report, and the release evidence called it a certification.
test.systemProperty 'performance.assertions.enabled',
(project.findProperty('performance.assertions.enabled') ?: 'true').toString()
}
systemProperty 'mongodb.primary.image',
providers.gradleProperty('mongoPrimaryImage').orElse('mongo:8.0.16').get()
systemProperty 'mongodb.compatibility.image',
providers.gradleProperty('mongoCompatibilityImage').orElse('mongo:7.0.28').get()
systemProperty 'mongodb.toxiproxy.image',
providers.gradleProperty('mongoToxiproxyImage').orElse('ghcr.io/shopify/toxiproxy:2.12.0').get()
// Assertions on by default: a performance gate without bounds is only a report.
systemProperty 'performance.assertions.enabled',
providers.gradleProperty('performance.assertions.enabled').orElse('true').get()
}
}
@@ -181,91 +203,9 @@ tasks.named('check') {
'verifyMongoReleaseContractLanes'
}
// The tag exclusion above is a claim about two task configurations. This checks the claim against
// what the two tasks actually ran, because the failure it prevents — every hermetic contract test
// executing twice per `check` — is invisible in a green build and only shows up as time.
tasks.register('verifyMongoTestLaneDisjointness') {
group = 'verification'
description = 'Fails when the unit lane and the stable contract lane execute the same test.'
dependsOn 'test', 'mongoStableContractTest'
def unitResults = layout.buildDirectory.dir('test-results/test')
def contractResults = layout.buildDirectory.dir('test-results/mongoStableContractTest')
inputs.dir(unitResults)
inputs.dir(contractResults)
outputs.file(layout.buildDirectory.file('reports/mongo-test-lane-disjointness.txt'))
doLast {
def executed = { java.io.File directory ->
def names = [] as Set
directory.listFiles({ File file -> file.name.endsWith('.xml') } as FileFilter)
?.each { file ->
new groovy.xml.XmlParser().parse(file).testcase.each { testcase ->
names << "${testcase.@classname}#${testcase.@name}".toString()
}
}
names
}
def unit = executed(unitResults.get().asFile)
def contract = executed(contractResults.get().asFile)
def overlap = unit.intersect(contract)
if (!overlap.isEmpty()) {
throw new GradleException(
"${overlap.size()} tests run in both the unit lane and the stable contract lane, " +
"so `check` executes them twice: ${overlap.take(5)}")
}
def report = outputs.files.singleFile
report.parentFile.mkdirs()
report.text = "unit=${unit.size()} contract=${contract.size()} overlap=0\n"
}
}
// Splitting the two lanes moved every tagged contract out of `test`, and the release manifest kept
// naming the lane it had left. `MongoReleaseEvidenceVerifier` resolves
// `test-results/<task>/TEST-<className>.xml`, so a contract whose class now runs somewhere else
// resolves to a file that will never exist: the Stable gate reports the transaction retry
// invariants as evidence the run failed to produce, for a suite that ran them.
//
// Checked against the XML the lanes wrote rather than against a tag table, because a tag table here
// would be a second copy of the selection above, and the copy is what drifted the first time.
tasks.register('verifyMongoReleaseContractLanes') {
group = 'verification'
description = 'Fails when a blocking release contract names a lane that did not run its class.'
dependsOn 'test', 'mongoStableContractTest'
def manifest = rootProject.file('../src/config/mongodb/release-contracts.json')
def hermeticLanes = ['test', 'mongoStableContractTest']
def resultsRoot = layout.buildDirectory.dir('test-results')
inputs.file(manifest)
inputs.dir(resultsRoot)
outputs.file(layout.buildDirectory.file('reports/mongo-release-contract-lanes.txt'))
doLast {
def contracts = new groovy.json.JsonSlurper().parse(manifest).contracts
def checked = []
def wrongLane = []
contracts.findAll { hermeticLanes.contains(it.task) }.each { contract ->
def results = resultsRoot.get().dir(contract.task).file(
"TEST-${contract.className}.xml").asFile
if (!results.isFile()) {
wrongLane << "${contract.id} names lane '${contract.task}', which did not run " +
"${contract.className}"
return
}
def suite = new groovy.xml.XmlParser().parse(results)
int executed = (suite.@tests as int) - (suite.@skipped as int)
if (executed < contract.minimumExecuted) {
wrongLane << "${contract.id} requires ${contract.minimumExecuted} executed test(s) " +
"in '${contract.task}' and the lane ran ${executed}"
}
checked << contract.id
}
if (!wrongLane.isEmpty()) {
throw new GradleException(
'the Mongo release manifest points at lanes that cannot produce its evidence: ' +
wrongLane.join('; '))
}
def report = outputs.files.singleFile
report.parentFile.mkdirs()
report.text = "hermetic release contracts verified: ${checked.join(', ')}\n"
}
}
// The two verification tasks are registered by `ca.mongo-verification`. The leaf owns only
// the semantic lane selection above and the lifecycle dependency below; JSON/JUnit parsing and
// evidence decisions live in typed Java build tooling.
// verifyMongoApiSurface — every public type this leaf exposes is a committed decision.