refactor: 각 어댑터터별 리펙토링 진행
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
// modules map here).
|
||||
//
|
||||
// The design models the platform as 19 Stable and 12 Advanced Gradle modules under
|
||||
// `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed 19-leaf registry
|
||||
// `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed module registry
|
||||
// (src/config/architecture/modules.json) outranks that layout, so the module boundaries are
|
||||
// packages under dev.caskeleton.adapter.outbound.mongo. MongoModuleBoundaryTest holds a closed
|
||||
// edge matrix — every package and what it may import — compares it against the tree for exact
|
||||
@@ -43,33 +43,20 @@ dependencies {
|
||||
// The testkit is its own source set rather than part of `test` because several lanes consume it and
|
||||
// because the design forbids a production module from depending on the testkit. Declaring its
|
||||
// dependencies only on the test configurations gives that guarantee without a new Gradle project.
|
||||
sourceSets {
|
||||
testkit {
|
||||
java.srcDir 'src/testkit/java'
|
||||
resources.srcDir 'src/testkit/resources'
|
||||
compileClasspath += sourceSets.main.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
mongoPerformanceTest {
|
||||
java.srcDir 'src/mongoPerformanceTest/java'
|
||||
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
// The testkit compiles against exactly what a test does: testImplementation already extends
|
||||
// implementation, so this is the module's own dependencies plus the test libraries.
|
||||
testkitImplementation.extendsFrom testImplementation
|
||||
testkitRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
mongoPerformanceTestImplementation.extendsFrom testImplementation
|
||||
mongoPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
strictTestLanes {
|
||||
// The testkit compiles against exactly what a test does: `implementation` inheritance runs
|
||||
// through testImplementation, so this is the module's own dependencies plus the test libraries.
|
||||
sourceSet('testkit') { compilesAgainst 'main' }
|
||||
sourceSet('mongoPerformanceTest') { compilesAgainst 'main', 'testkit' }
|
||||
}
|
||||
|
||||
// Every test lane compiles and runs against the testkit.
|
||||
sourceSets.test {
|
||||
compileClasspath += sourceSets.testkit.output
|
||||
runtimeClasspath += sourceSets.testkit.output
|
||||
//
|
||||
// No publishAs: this leaf's testkit is consumed inside the leaf and is not offered to the
|
||||
// composition root, unlike the JPA one whose ArchUnit rule pack the root applies to the production
|
||||
// graph. Publishing is opt-in in the convention precisely so that difference stays a decision.
|
||||
testkitPublisher {
|
||||
consumedBy 'test'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -113,85 +100,62 @@ tasks.named('test', Test) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('mongoReplicaSetTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
|
||||
'change stream (design §29).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-replicaset' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
// Six lanes, declared rather than assembled. `ca.strict-test-lane` owns testClassesDirs, classpath,
|
||||
// tag selection, failOnNoDiscoveredTests and the up-to-date refusal — the five lines that used to be
|
||||
// copied once per lane here and again in four other leaves.
|
||||
strictTestLanes {
|
||||
lane('mongoReplicaSetTest') {
|
||||
tag = 'mongodb-replicaset'
|
||||
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
|
||||
'change stream (design §29).'
|
||||
customize = { test -> applyMongoImageSelection(test) }
|
||||
}
|
||||
lane('mongoFailoverTest') {
|
||||
tag = 'mongodb-failover'
|
||||
description = 'Three-node replica set failover lane: primary kill, partition, unknown ' +
|
||||
'commit, resume (design §29).'
|
||||
customize = { test -> applyMongoImageSelection(test) }
|
||||
}
|
||||
lane('mongoMigrationTest') {
|
||||
tag = 'mongodb-migration'
|
||||
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
|
||||
'restart (design §12).'
|
||||
customize = { test -> applyMongoImageSelection(test) }
|
||||
}
|
||||
lane('mongoCompatibilityTest') {
|
||||
tag = 'mongodb-compatibility'
|
||||
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
|
||||
customize = { test -> applyMongoImageSelection(test) }
|
||||
}
|
||||
lane('mongoSecurityIntegrationTest') {
|
||||
tag = 'mongodb-security-integration'
|
||||
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
|
||||
'(design §26).'
|
||||
customize = { test -> applyMongoImageSelection(test) }
|
||||
}
|
||||
|
||||
// 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') {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('mongoFailoverTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Three-node replica set failover lane: primary kill, partition, unknown commit, ' +
|
||||
'resume (design §29).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-failover' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoMigrationTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
|
||||
'restart (design §12).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-migration' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoCompatibilityTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-compatibility' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoSecurityIntegrationTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
|
||||
'(design §26).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-security-integration' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoPerformanceTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Certifies contention, aggregation spill, pagination and pool resource bounds ' +
|
||||
'(design §29).'
|
||||
testClassesDirs = sourceSets.mongoPerformanceTest.output.classesDirs
|
||||
classpath = sourceSets.mongoPerformanceTest.runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
applyMongoImageSelection(it)
|
||||
// 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.
|
||||
systemProperty 'performance.assertions.enabled',
|
||||
(project.findProperty('performance.assertions.enabled') ?: 'true').toString()
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
// `check` gains only the hermetic lanes. The Docker-backed ones stay opt-in for the reason above.
|
||||
tasks.named('check') {
|
||||
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness'
|
||||
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness',
|
||||
'verifyMongoReleaseContractLanes'
|
||||
}
|
||||
|
||||
// The tag exclusion above is a claim about two task configurations. This checks the claim against
|
||||
@@ -231,6 +195,55 @@ tasks.register('verifyMongoTestLaneDisjointness') {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('mongoStableContractTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' +
|
||||
@@ -257,95 +270,14 @@ tasks.register('mongoStableContractTest', Test) {
|
||||
// prerequisite for shrinking it: the `api` and `spi` packages are the surface an adopter is meant
|
||||
// to use, and everything else in this file is a candidate for becoming internal when the leaf is
|
||||
// split into capability artifacts. Until then the number cannot grow by accident.
|
||||
def mongoApiSurfaceFile = rootProject.file('../docs/architecture/mongo-api-surface.txt')
|
||||
|
||||
Closure<String> renderMongoApiSurface = {
|
||||
def sourceRoot = file('src/main/java')
|
||||
def typePattern = ~/(?m)^public\s+(?:final\s+|abstract\s+|sealed\s+|non-sealed\s+)*(class|interface|enum|record|@interface)\s+(\w+)/
|
||||
def packagePattern = ~/(?m)^package\s+([\w.]+)\s*;/
|
||||
List<String> types = []
|
||||
sourceRoot.eachFileRecurse { candidate ->
|
||||
if (!candidate.isFile() || !candidate.name.endsWith('.java')) {
|
||||
return
|
||||
}
|
||||
String text = candidate.getText('UTF-8')
|
||||
def packageMatcher = packagePattern.matcher(text)
|
||||
if (!packageMatcher.find()) {
|
||||
return
|
||||
}
|
||||
String packageName = packageMatcher.group(1)
|
||||
def typeMatcher = typePattern.matcher(text)
|
||||
while (typeMatcher.find()) {
|
||||
types << "${packageName}.${typeMatcher.group(2)}".toString()
|
||||
}
|
||||
}
|
||||
types = types.unique().toSorted()
|
||||
String header =
|
||||
"# MongoDB leaf public API surface — every public top-level type in src/main/java.\n" +
|
||||
"# A public type in a single-jar leaf is reachable from every adopter's code, so\n" +
|
||||
"# additions are reviewed rather than discovered. `api` is the intended external\n" +
|
||||
"# surface; the rest is implementation that has not been moved under an internal\n" +
|
||||
"# root yet.\n" +
|
||||
"# Update only after review with:\n" +
|
||||
"# ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface -PapproveMongoApiSurfaceChange\n" +
|
||||
"# types: ${types.size()}\n"
|
||||
header + (types.isEmpty() ? '' : types.join('\n') + '\n')
|
||||
}
|
||||
|
||||
// The approval flag is read at configuration time and carried in, not fetched from `project`
|
||||
// inside doLast. Task.project at execution time is deprecated and fails under Gradle 10, and it is
|
||||
// incompatible with the configuration cache — which this build will need before it can adopt one.
|
||||
boolean mongoApiSurfaceUpdateApproved = project.hasProperty('approveMongoApiSurfaceChange')
|
||||
|
||||
tasks.register('verifyMongoApiSurface') {
|
||||
group = 'verification'
|
||||
description = 'Fails without mutation when the committed GraphQL public API surface drifts.'
|
||||
|
||||
doLast {
|
||||
if (mongoApiSurfaceUpdateApproved) {
|
||||
throw new GradleException(
|
||||
'verifyMongoApiSurface is read-only; use updateMongoApiSurface to record an ' +
|
||||
'approved change.')
|
||||
}
|
||||
String rendered = renderMongoApiSurface()
|
||||
if (!mongoApiSurfaceFile.isFile()) {
|
||||
throw new GradleException(
|
||||
"verifyMongoApiSurface: missing committed baseline ${mongoApiSurfaceFile}")
|
||||
}
|
||||
String committed = mongoApiSurfaceFile.getText('UTF-8')
|
||||
if (committed != rendered) {
|
||||
List<String> committedTypes = committed.readLines().findAll { !it.startsWith('#') }
|
||||
List<String> renderedTypes = rendered.readLines().findAll { !it.startsWith('#') }
|
||||
List<String> added = (renderedTypes - committedTypes).toSorted()
|
||||
List<String> removed = (committedTypes - renderedTypes).toSorted()
|
||||
throw new GradleException(
|
||||
"verifyMongoApiSurface: the public API surface changed.\n" +
|
||||
(added.isEmpty() ? '' : " added:\n " + added.join('\n ') + '\n') +
|
||||
(removed.isEmpty() ? '' : " removed:\n " + removed.join('\n ') + '\n') +
|
||||
"Review the change, then record it with:\n" +
|
||||
" ./gradlew :adapter:outbound:persistence-mongo:updateMongoApiSurface " +
|
||||
"-PapproveMongoApiSurfaceChange")
|
||||
}
|
||||
logger.lifecycle('verifyMongoApiSurface: OK — the committed public API surface is unchanged.')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('updateMongoApiSurface') {
|
||||
group = 'verification'
|
||||
description = 'Rewrites the committed GraphQL public API surface baseline after review.'
|
||||
|
||||
doLast {
|
||||
if (!project.hasProperty('approveMongoApiSurfaceChange')) {
|
||||
throw new GradleException(
|
||||
'updateMongoApiSurface requires -PapproveMongoApiSurfaceChange: growing the ' +
|
||||
'public surface is a review decision, not a build step.')
|
||||
}
|
||||
mongoApiSurfaceFile.parentFile.mkdirs()
|
||||
mongoApiSurfaceFile.setText(renderMongoApiSurface(), 'UTF-8')
|
||||
logger.lifecycle("updateMongoApiSurface: wrote ${mongoApiSurfaceFile}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('check') {
|
||||
dependsOn tasks.named('verifyMongoApiSurface')
|
||||
apiSurface {
|
||||
label = 'Mongo'
|
||||
baseline = rootProject.file('../docs/architecture/mongo-api-surface.txt')
|
||||
description = 'MongoDB leaf public API surface — every public top-level type in src/main/java.'
|
||||
rationale = [
|
||||
'A public type in a single-jar leaf is reachable from every adopter\'s code, so',
|
||||
'additions are reviewed rather than discovered. `api` is the intended external',
|
||||
'surface; the rest is implementation that has not been moved under an internal',
|
||||
'root yet.',
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user