feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
// 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
|
||||
// (src/config/architecture/modules.json) outranks that layout, so the module boundaries are
|
||||
// packages under dev.caskeleton.adapter.outbound.mongo and MongoModuleBoundaryTest enforces the
|
||||
// design's module dependency table.
|
||||
// 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
|
||||
// equality, and rejects any observed edge that is not declared.
|
||||
//
|
||||
// Driver and Spring Data MongoDB versions come from the Spring Boot BOM applied to every module in
|
||||
// src/build.gradle (design §4: "개별 Driver 버전 override 금지"), so nothing here pins them.
|
||||
@@ -94,9 +95,16 @@ Closure<Void> applyMongoImageSelection = { task ->
|
||||
|
||||
// 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`.
|
||||
//
|
||||
// `mongodb-contract` is excluded here too. It was not, and `check` depends on both `test` and
|
||||
// `mongoStableContractTest`, so every one of the 382 hermetic contract tests ran twice on a fresh
|
||||
// build — once in each task. The two lanes are now disjoint by construction, and
|
||||
// `MongoTestLaneDisjointnessTest` asserts it against the JUnit XML rather than trusting this
|
||||
// comment.
|
||||
tasks.named('test', Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags 'quarantine',
|
||||
'mongodb-contract',
|
||||
'mongodb-replicaset',
|
||||
'mongodb-failover',
|
||||
'mongodb-migration',
|
||||
@@ -172,15 +180,55 @@ tasks.register('mongoPerformanceTest', Test) {
|
||||
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') ?: 'false').toString()
|
||||
(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'
|
||||
dependsOn 'mongoStableContractTest', 'verifyMongoTestLaneDisjointness'
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('mongoStableContractTest', Test) {
|
||||
@@ -193,3 +241,106 @@ tasks.register('mongoStableContractTest', Test) {
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
// verifyMongoApiSurface — every public type this leaf exposes is a committed decision.
|
||||
//
|
||||
// 311 of this leaf's 313 production files declare a public top-level type. One jar means `public`
|
||||
// is public to every adopter, so the intended split between contract and implementation — `api` is
|
||||
// the surface, the rest is how it is built — is a convention the compiler does not know about.
|
||||
//
|
||||
// The full move of implementation packages under an `internal` root is a separate, mechanical
|
||||
// change; this is what keeps the surface from growing while that is pending. A snapshot does not
|
||||
// shrink anything. It makes each addition a decision somebody made in review rather than something
|
||||
// discovered later by an adopter who imported it.
|
||||
//
|
||||
// A snapshot does not shrink the surface. It makes each addition visible in review, which is the
|
||||
// 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')
|
||||
}
|
||||
|
||||
tasks.register('verifyMongoApiSurface') {
|
||||
group = 'verification'
|
||||
description = 'Fails without mutation when the committed GraphQL public API surface drifts.'
|
||||
|
||||
doLast {
|
||||
if (project.hasProperty('approveMongoApiSurfaceChange')) {
|
||||
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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user