refactor: 빌드 로직 개선, gradle 파일 경량화
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user