refactor: 각 어댑터터별 리펙토링 진행

This commit is contained in:
DongHyeonka
2026-08-24 18:26:40 +09:00
parent e98b56eb03
commit 0137263441
439 changed files with 31935 additions and 4719 deletions
+14 -27
View File
@@ -1,6 +1,6 @@
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.xml.XmlSlurper
import dev.caskeleton.buildlogic.JUnitEvidence
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.Instant
@@ -72,33 +72,20 @@ Closure<String> runJpaEvidenceCommand = { List<String> command ->
Closure<Map<String, Object>> readJpaJUnitResult = { Test testTask ->
File resultDirectory = testTask.reports.junitXml.outputLocation.get().asFile
List<File> resultFiles = resultDirectory.isDirectory()
? (resultDirectory.listFiles() ?: [] as File[])
.findAll { File result -> result.name.startsWith('TEST-') && result.name.endsWith('.xml') }
.toSorted { File left, File right -> left.name <=> right.name }
: []
if (resultFiles.isEmpty()) {
throw new GradleException(
"${testTask.path}: JUnit XML evidence is missing from ${resultDirectory}")
}
int executed = 0
int skipped = 0
int failures = 0
int errors = 0
Set<String> selectors = new TreeSet<>()
resultFiles.each { File resultFile ->
def suite = new XmlSlurper(false, false).parse(resultFile)
executed += (suite.@tests.text() ?: '0') as int
skipped += (suite.@skipped.text() ?: '0') as int
failures += (suite.@failures.text() ?: '0') as int
errors += (suite.@errors.text() ?: '0') as int
suite.testcase.each { Object rawCase ->
String className = rawCase.@classname.text()
String methodName = rawCase.@name.text().replaceFirst(/\([^)]*\)$/, '')
selectors << "${className}#${methodName}".toString()
}
// Read through the shared reader rather than a second XmlSlurper. This one did not disable
// DOCTYPE processing and counted a skipped case as an executed class; junit-evidence.gradle did
// neither. Same format, two readers, two answers — the shared one keeps the stricter answer.
def results
try {
results = JUnitEvidence.read(testTask.path, resultDirectory)
} catch (IllegalStateException unreadable) {
throw new GradleException(unreadable.message, unreadable)
}
int executed = results.tests
int skipped = results.skipped
int failures = results.failures
int errors = results.errors
Set<String> selectors = new TreeSet<>(results.executedSelectors)
[
tasks: [testTask.path],
-101
View File
@@ -1,101 +0,0 @@
import groovy.xml.XmlSlurper
Closure<Map<String, Object>> readJUnitEvidence = { String evidenceName, File resultDirectory ->
List<File> resultFiles = resultDirectory.isDirectory()
? rootProject.fileTree(resultDirectory) {
include 'TEST-*.xml'
}.files.toList().sort { left, right -> left.path <=> right.path }
: []
if (resultFiles.isEmpty()) {
throw new GradleException(
"${evidenceName}: no JUnit XML result files in ${resultDirectory}")
}
int totalTests = 0
int totalSkipped = 0
int totalFailures = 0
int totalErrors = 0
Set<String> executedClasses = new LinkedHashSet<>()
resultFiles.each { File resultFile ->
XmlSlurper parser = new XmlSlurper(false, false)
parser.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true)
def suite
try {
suite = parser.parse(resultFile)
} catch (Exception exception) {
throw new GradleException(
"${evidenceName}: unreadable JUnit XML ${resultFile.name}", exception)
}
if (suite.name() != 'testsuite') {
throw new GradleException(
"${evidenceName}: ${resultFile.name} root must be testsuite")
}
Map<String, Integer> counts = [:]
['tests', 'skipped', 'failures', 'errors'].each { String attribute ->
String rawValue = suite.attributes()[attribute]?.toString()
if (!(rawValue ==~ /\d+/)) {
throw new GradleException(
"${evidenceName}: ${resultFile.name} has invalid ${attribute}='${rawValue}'")
}
counts[attribute] = rawValue.toInteger()
}
totalTests += counts.tests
totalSkipped += counts.skipped
totalFailures += counts.failures
totalErrors += counts.errors
suite.testcase.each { testCase ->
String className = testCase.attributes().classname?.toString()
if (className != null && !className.isBlank() && testCase.skipped.isEmpty()) {
executedClasses.add(className)
}
}
}
[
tests : totalTests,
skipped : totalSkipped,
failures : totalFailures,
errors : totalErrors,
executedClasses: executedClasses
]
}
Closure<Map<String, Object>> verifyNoSkipJUnitXml = {
String evidenceName, File resultDirectory ->
Map<String, Object> evidence = readJUnitEvidence(evidenceName, resultDirectory)
if (evidence.tests <= 0) {
throw new GradleException(
"${evidenceName}: requires a positive executed test count")
}
if (evidence.skipped > 0) {
throw new GradleException(
"${evidenceName}: forbids skipped tests: ${evidence.skipped}")
}
if (evidence.failures > 0 || evidence.errors > 0) {
throw new GradleException(
"${evidenceName}: failures=${evidence.failures}, errors=${evidence.errors}")
}
logger.lifecycle("${evidenceName}: ${evidence.tests} tests, ${evidence.skipped} skipped")
evidence
}
Closure<Map<String, Object>> verifyRequiredJUnitClasses = {
String evidenceName, File resultDirectory, List<String> requiredClasses ->
Map<String, Object> evidence = verifyNoSkipJUnitXml(evidenceName, resultDirectory)
Set<String> executedClasses = evidence.executedClasses as Set<String>
List<String> missingClasses = requiredClasses.findAll { String requiredClass ->
!executedClasses.any { String executedClass ->
executedClass == requiredClass || executedClass.startsWith(requiredClass + '$')
}
}
if (!missingClasses.isEmpty()) {
throw new GradleException(
"${evidenceName}: no executed test cases for required classes: ${missingClasses}")
}
evidence
}
rootProject.ext.readJUnitEvidence = readJUnitEvidence
rootProject.ext.verifyNoSkipJUnitXml = verifyNoSkipJUnitXml
rootProject.ext.verifyRequiredJUnitClasses = verifyRequiredJUnitClasses
@@ -46,6 +46,11 @@ List<File> notificationApiSourceRoots = [
File notificationApiSnapshotFile =
rootProject.file('../docs/notification/api-surface-snapshot.txt')
boolean notificationApiUpdateApproved = project.hasProperty('approveNotificationApiChange')
// Growing the surface is a second decision. Approving additions one at a time is how a leaf grows
// past the size that would have justified splitting it, with every step reviewed and the total
// never discussed. A change that removes more than it adds needs only the approval.
boolean notificationApiCeilingRaiseApproved =
project.hasProperty('raiseNotificationApiCeiling')
tasks.register('verifyNotificationApiSurface') {
group = 'verification'
@@ -105,6 +110,21 @@ tasks.register('updateNotificationApiSurface') {
'updateNotificationApiSurface requires -PapproveNotificationApiChange')
}
String canonical = renderNotificationApiSurface(notificationApiSourceRoots)
if (notificationApiSnapshotFile.isFile() && !notificationApiCeilingRaiseApproved) {
int committedCount = notificationApiSnapshotFile.readLines('UTF-8')
.count { !it.startsWith('#') && !it.trim().isEmpty() }
int renderedCount = canonical.readLines()
.count { !it.startsWith('#') && !it.trim().isEmpty() }
if (renderedCount > committedCount) {
throw new GradleException(
"updateNotificationApiSurface: the public surface would grow from " +
"${committedCount} to ${renderedCount} types.\n" +
'Either land the addition together with a removal that pays for it, ' +
'or raise the ceiling deliberately:\n' +
' ./gradlew updateNotificationApiSurface ' +
'-PapproveNotificationApiChange -PraiseNotificationApiCeiling')
}
}
if (!notificationApiSnapshotFile.parentFile.isDirectory()
&& !notificationApiSnapshotFile.parentFile.mkdirs()) {
throw new GradleException(
@@ -30,6 +30,18 @@ tasks.register('verifyNotificationConfiguration') {
// is delimited by its own comment marker rather than by indentation counting, so a reformat
// does not silently empty this set.
String yaml = applicationYaml.getText('UTF-8')
// The tree's own comment tells a template user where the reference is. It named a file that
// does not exist, which is the same failure as an out-of-date reference and harder to
// notice: the reader concludes the documentation is missing rather than that the pointer
// is. Checked here because this task already owns the agreement between the two.
if (!yaml.contains('docs/notification/' + referenceDocument.name)) {
throw new GradleException(
'verifyNotificationConfiguration: application.yml does not point at ' +
"docs/notification/${referenceDocument.name}, so the tree tells a " +
'reader to consult a document this task does not verify.')
}
int treeStart = yaml.indexOf(' notification:\n platform:')
if (treeStart < 0) {
throw new GradleException(
-157
View File
@@ -1,157 +0,0 @@
import groovy.json.JsonSlurper
def verifyRuntimeModuleMembership = tasks.register('verifyRuntimeModuleMembership') {
group = 'verification'
description = 'Verifies registry runtime membership against both shipped composition roots.'
File registryFile = rootProject.file('config/architecture/modules.json')
inputs.file(registryFile)
doLast {
if (!registryFile.isFile()) {
throw new GradleException("Missing module registry: ${registryFile}")
}
def registry = new JsonSlurper().parse(registryFile)
if (!(registry instanceof Map) || !(registry.modules instanceof List)) {
throw new GradleException('Module registry needs a modules list.')
}
if (!(registry.runtime_compositions instanceof List) || registry.runtime_compositions.isEmpty()) {
throw new GradleException('Module registry needs a non-empty runtime_compositions list.')
}
List<String> compositionIds = registry.runtime_compositions.withIndex().collect {
Object value, int index ->
if (!(value instanceof String) || (value as String).isBlank()) {
throw new GradleException(
"runtime_compositions entry ${index} must be a nonblank string.")
}
value as String
}
if (compositionIds.toSet().size() != compositionIds.size()) {
throw new GradleException('runtime_compositions must not contain duplicates.')
}
Map<String, Object> modulesById = [:]
Map<String, String> moduleIdByGradlePath = [:]
registry.modules.eachWithIndex { Object rawModule, int index ->
if (!(rawModule instanceof Map)) {
throw new GradleException("Module registry entry ${index} must be an object.")
}
Map<String, Object> module = rawModule as Map<String, Object>
String moduleId = module.id as String
if (moduleId == null || moduleId.isBlank()) {
throw new GradleException("Module registry entry ${index} needs a nonblank id.")
}
if (!(module.runtime_memberships instanceof List)) {
throw new GradleException(
"Module registry entry '${moduleId}' needs a runtime_memberships list.")
}
List<String> memberships = module.runtime_memberships.withIndex().collect {
Object membership, int membershipIndex ->
if (!(membership instanceof String) || (membership as String).isBlank()) {
throw new GradleException(
"Module registry entry '${moduleId}' has a blank/non-string " +
"runtime membership at index ${membershipIndex}.")
}
membership as String
}
if (memberships.toSet().size() != memberships.size()) {
throw new GradleException(
"Module registry entry '${moduleId}' has duplicate runtime memberships.")
}
Set<String> unknownMemberships = memberships.toSet() - compositionIds.toSet()
if (!unknownMemberships.isEmpty()) {
throw new GradleException(
"Module registry entry '${moduleId}' has unknown runtime membership(s) " +
"${unknownMemberships.toSorted()}.")
}
if (modulesById.put(moduleId, module) != null) {
throw new GradleException("Module registry contains duplicate id '${moduleId}'.")
}
String gradlePath = module.gradle_path as String
if (gradlePath == null || gradlePath.isBlank()) {
throw new GradleException(
"Module registry entry '${moduleId}' needs a nonblank gradle_path.")
}
if (moduleIdByGradlePath.put(gradlePath, moduleId) != null) {
throw new GradleException(
"Module registry contains duplicate Gradle path '${gradlePath}'.")
}
}
compositionIds.each { String compositionId ->
Map<String, Object> composition = modulesById[compositionId] as Map<String, Object>
if (composition == null) {
throw new GradleException(
"Runtime composition '${compositionId}' is not a registered module id.")
}
List<String> ownMemberships = composition.runtime_memberships as List<String>
if (!ownMemberships.contains(compositionId)) {
throw new GradleException(
"Runtime composition '${compositionId}' must include itself in runtime_memberships.")
}
String compositionGradlePath = composition.gradle_path as String
Project compositionProject = rootProject.findProject(compositionGradlePath)
if (compositionProject == null) {
throw new GradleException(
"Runtime composition '${compositionId}' references missing Gradle project " +
"'${compositionGradlePath}'.")
}
Set<String> expected = registry.modules.findAll { Object rawModule ->
Map<String, Object> module = rawModule as Map<String, Object>
(module.runtime_memberships as List).contains(compositionId) &&
module.id != compositionId
}.collect { Object rawModule ->
(rawModule as Map<String, Object>).id as String
}.toSet()
// The resolved runtime closure, not the declared dependency list.
//
// A declared-dependency comparison cannot see a leaf that arrives transitively — a
// starter pulling in six internal modules puts all six in the bootJar while the registry
// records them as belonging to no runtime at all. The membership list then stays clean
// precisely because it is not looking at what ships. Resolving runtimeClasspath asks the
// question the jar answers.
def runtimeClasspath = compositionProject.configurations.findByName('runtimeClasspath')
if (runtimeClasspath == null) {
throw new GradleException(
"Runtime composition '${compositionId}' has no runtimeClasspath configuration.")
}
Set<String> actual = runtimeClasspath.incoming.resolutionResult.allComponents
.findAll { it.id instanceof org.gradle.api.artifacts.component.ProjectComponentIdentifier }
.collect { (it.id as org.gradle.api.artifacts.component.ProjectComponentIdentifier).projectPath }
.findAll { String projectPath -> projectPath != compositionProject.path }
.collect { String projectPath ->
String dependencyId = moduleIdByGradlePath[projectPath]
if (dependencyId == null) {
throw new GradleException(
"Runtime composition '${compositionId}' resolves unregistered " +
"Gradle project '${projectPath}' onto its runtime classpath.")
}
dependencyId
}
.toSet()
Set<String> unregistered = actual - expected
Set<String> missing = expected - actual
if (!unregistered.isEmpty() || !missing.isEmpty()) {
List<String> violations = []
if (!unregistered.isEmpty()) {
violations << "unregistered runtime dependencies ${unregistered.toSorted()}"
}
if (!missing.isEmpty()) {
violations << "missing registered runtime dependencies ${missing.toSorted()}"
}
throw new GradleException(
"Runtime composition '${compositionId}' membership drift: " +
violations.join('; ') + '.')
}
}
logger.lifecycle(
"verifyRuntimeModuleMembership: ${compositionIds.size()} runtime composition(s) " +
'match the registry')
}
}
rootProject.ext.verifyRuntimeModuleMembership = verifyRuntimeModuleMembership
-113
View File
@@ -1,113 +0,0 @@
// Owner-local convention for release/qualification lanes that must never pass without executing
// every explicitly required JUnit class. Apply junit-evidence.gradle before this script.
ext.registerStrictQualificationTest = { Map<String, ?> specification ->
String taskName = specification.name as String
def qualificationSourceSet = specification.sourceSet
List<String> requiredClasses = (specification.requiredClasses ?: []) as List<String>
if (taskName == null || taskName.isBlank()) {
throw new GradleException('A strict qualification task name is required.')
}
if (qualificationSourceSet == null) {
throw new GradleException("${taskName} requires an owner source set.")
}
if (!sourceSets.findByName(qualificationSourceSet.name).is(qualificationSourceSet)) {
throw new GradleException(
"${taskName} source set '${qualificationSourceSet.name}' does not belong to owner project ${project.path}.")
}
if (requiredClasses.isEmpty() || requiredClasses.any { it == null || it.isBlank() }) {
throw new GradleException("${taskName} must name at least one required test FQCN.")
}
if (requiredClasses.toSet().size() != requiredClasses.size()) {
throw new GradleException("${taskName} contains duplicate required test FQCNs.")
}
def junitXmlOutput = specification.junitXmlOutput ?:
layout.buildDirectory.dir("test-results/${taskName}")
def binaryResultsOutput = specification.binaryResultsOutput ?:
layout.buildDirectory.dir("test-results/${taskName}/binary")
def requiredClassesCheck = tasks.register("${taskName}RequiredClasses") {
group = 'verification'
description = "Fails when ${taskName} did not compile every required test class."
dependsOn qualificationSourceSet.classesTaskName
inputs.files(qualificationSourceSet.output.classesDirs)
outputs.upToDateWhen { false }
doLast {
Set<File> classDirectories = qualificationSourceSet.output.classesDirs.files
boolean hasAnyClass = classDirectories.any { File directory ->
directory.isDirectory() &&
!fileTree(directory).matching { include '**/*.class' }.isEmpty()
}
if (!hasAnyClass) {
throw new GradleException(
"${taskName} source set produced no test class files.")
}
List<String> missingClasses = requiredClasses.findAll { String requiredClass ->
String relativeClassFile = requiredClass.replace('.', '/') + '.class'
!classDirectories.any { File directory ->
new File(directory, relativeClassFile).isFile()
}
}
if (!missingClasses.isEmpty()) {
throw new GradleException(
"${taskName} is missing required test class files: ${missingClasses}")
}
File staleEvidence = junitXmlOutput.get().asFile
if (staleEvidence.exists() && !project.delete(staleEvidence)) {
throw new GradleException(
"${taskName} could not delete stale JUnit XML: ${staleEvidence}")
}
}
}
def qualificationTest = tasks.register(taskName, Test) {
group = 'verification'
description = specification.description ?:
"Runs exact no-skip qualification evidence for ${project.path}."
dependsOn requiredClassesCheck
testClassesDirs = qualificationSourceSet.output.classesDirs
classpath = qualificationSourceSet.runtimeClasspath
useJUnitPlatform()
filter {
requiredClasses.each { String requiredClass ->
includeTestsMatching(requiredClass)
}
failOnNoMatchingTests = true
}
failOnNoDiscoveredTests = true
reports.junitXml.required = true
reports.junitXml.outputLocation = junitXmlOutput
reports.html.required = false
binaryResultsDirectory = binaryResultsOutput
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
afterSuite { descriptor, result ->
if (descriptor.parent == null && result.skippedTestCount > 0) {
throw new GradleException(
"${taskName} forbids skipped tests: ${result.skippedTestCount}")
}
}
}
def evidenceCheck = tasks.register("${taskName}Evidence") {
group = 'verification'
description = "Fails unless ${taskName} executed every required test class without skips."
mustRunAfter qualificationTest
outputs.upToDateWhen { false }
doLast {
if (!rootProject.ext.has('verifyRequiredJUnitClasses')) {
throw new GradleException(
"${taskName} requires gradle/junit-evidence.gradle.")
}
rootProject.ext.verifyRequiredJUnitClasses(
taskName, junitXmlOutput.get().asFile, requiredClasses)
}
}
qualificationTest.configure {
finalizedBy evidenceCheck
}
qualificationTest
}