chore: record pre-existing uncommitted repository state
Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1a3b560678
commit
5f10b791d3
@@ -0,0 +1,78 @@
|
||||
import java.util.regex.Pattern
|
||||
import org.gradle.api.tasks.bundling.Jar
|
||||
|
||||
Closure<Boolean> isTraceableArchiveFor = { Jar archiveTask, String fileName ->
|
||||
String baseName = Pattern.quote(archiveTask.archiveBaseName.get())
|
||||
String classifier = archiveTask.archiveClassifier.orNull
|
||||
String classifierPart = classifier == null || classifier.isBlank()
|
||||
? ''
|
||||
: "-${Pattern.quote(classifier)}"
|
||||
fileName ==~ /^${baseName}-\d+\.\d+\.\d+\+[0-9a-f]{7,40}${classifierPart}\.jar$/
|
||||
}
|
||||
|
||||
Closure<List<File>> staleTraceableArchivesFor = { Jar archiveTask ->
|
||||
File outputDirectory = archiveTask.destinationDirectory.get().asFile
|
||||
if (!outputDirectory.isDirectory()) {
|
||||
return []
|
||||
}
|
||||
|
||||
String currentName = archiveTask.archiveFileName.get()
|
||||
List<File> stale = outputDirectory.listFiles({ File ignored, String fileName ->
|
||||
isTraceableArchiveFor(archiveTask, fileName) && fileName != currentName
|
||||
} as FilenameFilter)?.toList() ?: []
|
||||
stale.sort { it.name }
|
||||
}
|
||||
|
||||
tasks.register('cleanStaleTraceableJars') {
|
||||
group = 'build'
|
||||
description = 'Explicitly deletes older git-revision JARs from leaf build/libs directories.'
|
||||
notCompatibleWithConfigurationCache(
|
||||
'Inspects subproject Jar task models at execution time')
|
||||
|
||||
doLast {
|
||||
int deleted = 0
|
||||
subprojects.each { subproject ->
|
||||
subproject.tasks.withType(Jar).each { Jar archiveTask ->
|
||||
staleTraceableArchivesFor(archiveTask).each { File stale ->
|
||||
if (!stale.delete()) {
|
||||
throw new GradleException("cleanStaleTraceableJars: failed to delete ${stale}")
|
||||
}
|
||||
deleted++
|
||||
logger.lifecycle("cleanStaleTraceableJars: deleted ${stale}")
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.lifecycle("cleanStaleTraceableJars: deleted ${deleted} stale archive(s).")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('verifyNoStaleTraceableJars') {
|
||||
group = 'verification'
|
||||
description = 'Fails without mutation when leaf build/libs directories retain old traceable JARs.'
|
||||
notCompatibleWithConfigurationCache(
|
||||
'Inspects subproject Jar task models at execution time')
|
||||
|
||||
doLast {
|
||||
List<String> violations = []
|
||||
subprojects.each { subproject ->
|
||||
subproject.tasks.withType(Jar).each { Jar archiveTask ->
|
||||
List<String> staleJars =
|
||||
staleTraceableArchivesFor(archiveTask).collect { File stale -> stale.name }
|
||||
if (!staleJars.isEmpty()) {
|
||||
violations <<
|
||||
"${archiveTask.path}: stale JAR(s) ${staleJars}; " +
|
||||
"current archive is ${archiveTask.archiveFileName.get()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"verifyNoStaleTraceableJars: ${violations.size()} archive task(s) retain old " +
|
||||
"traceable JARs. Run cleanStaleTraceableJars explicitly if removal is " +
|
||||
"intended.\n ${violations.join('\n ')}")
|
||||
}
|
||||
logger.lifecycle(
|
||||
'verifyNoStaleTraceableJars: OK — no stale traceable JARs in build/libs.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
Closure<String> renderPublicPathSnapshot = { File environmentFile ->
|
||||
if (!environmentFile.isFile()) {
|
||||
throw new GradleException(
|
||||
"missing public-path environment file ${environmentFile}")
|
||||
}
|
||||
|
||||
def valuePattern = ~/^SECURITY_PUBLIC_PATHS=(.*)$/
|
||||
String raw = environmentFile.readLines('UTF-8').findResult { String line ->
|
||||
def matcher = valuePattern.matcher(line)
|
||||
matcher.matches() ? matcher.group(1) : null
|
||||
} ?: ''
|
||||
List<String> publicPaths = raw.split(',')
|
||||
.collect { String value -> value.trim() }
|
||||
.findAll { String value -> !value.isEmpty() }
|
||||
.toSorted()
|
||||
|
||||
String header =
|
||||
"# feature-security-operational-baseline D5 — deny-by-default public path snapshot.\n" +
|
||||
"# SSOT: SECURITY_PUBLIC_PATHS (src/.env) -> SecurityConfig permitAll(); " +
|
||||
"anyRequest authenticated.\n" +
|
||||
"# Update only after review with: ./gradlew updatePublicPathSnapshot " +
|
||||
"-PapprovePublicPathChange\n"
|
||||
header + (publicPaths.isEmpty() ? '' : publicPaths.join('\n') + '\n')
|
||||
}
|
||||
|
||||
File publicPathEnvironmentFile = rootProject.file('.env')
|
||||
File publicPathSnapshotFile =
|
||||
rootProject.file('../docs/security/public-paths-snapshot.txt')
|
||||
boolean publicPathUpdateApproved = project.hasProperty('approvePublicPathChange')
|
||||
def existingPublicPathEnvironment = providers.provider {
|
||||
publicPathEnvironmentFile.isFile() ? publicPathEnvironmentFile : null
|
||||
}
|
||||
def existingPublicPathSnapshot = providers.provider {
|
||||
publicPathSnapshotFile.isFile() ? publicPathSnapshotFile : null
|
||||
}
|
||||
|
||||
tasks.register('verifyPublicPathSnapshot') {
|
||||
group = 'verification'
|
||||
description = 'Fails without mutation when the committed deny-by-default public path baseline drifts.'
|
||||
inputs.file(existingPublicPathEnvironment).optional()
|
||||
inputs.file(existingPublicPathSnapshot).optional()
|
||||
inputs.property('updateApprovalRequested', publicPathUpdateApproved)
|
||||
|
||||
doLast {
|
||||
if (publicPathUpdateApproved) {
|
||||
throw new GradleException(
|
||||
'verifyPublicPathSnapshot is read-only; use updatePublicPathSnapshot ' +
|
||||
'-PapprovePublicPathChange for an intentional update.')
|
||||
}
|
||||
String canonical
|
||||
try {
|
||||
canonical = renderPublicPathSnapshot(publicPathEnvironmentFile)
|
||||
} catch (GradleException exception) {
|
||||
throw new GradleException(
|
||||
"verifyPublicPathSnapshot: ${exception.message}", exception)
|
||||
}
|
||||
if (!publicPathSnapshotFile.isFile()) {
|
||||
throw new GradleException(
|
||||
"verifyPublicPathSnapshot: missing committed baseline ${publicPathSnapshotFile}")
|
||||
}
|
||||
|
||||
String existing = publicPathSnapshotFile.getText('UTF-8')
|
||||
if (existing != canonical) {
|
||||
throw new GradleException(
|
||||
"verifyPublicPathSnapshot: the deny-by-default public path surface changed.\n" +
|
||||
" expected (snapshot):\n${existing}\n" +
|
||||
" actual (SECURITY_PUBLIC_PATHS):\n${canonical}\n" +
|
||||
'A protected endpoint may now be public. Review the change, then run:\n' +
|
||||
' ./gradlew updatePublicPathSnapshot -PapprovePublicPathChange')
|
||||
}
|
||||
logger.lifecycle(
|
||||
'verifyPublicPathSnapshot: OK — committed public paths are unchanged.')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('updatePublicPathSnapshot') {
|
||||
group = 'build setup'
|
||||
description = 'Explicitly updates the committed public path baseline after security review.'
|
||||
inputs.file(existingPublicPathEnvironment).optional()
|
||||
inputs.property('approved', publicPathUpdateApproved)
|
||||
outputs.file(publicPathSnapshotFile)
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
doLast {
|
||||
if (!publicPathUpdateApproved) {
|
||||
throw new GradleException(
|
||||
'updatePublicPathSnapshot requires -PapprovePublicPathChange')
|
||||
}
|
||||
String canonical
|
||||
try {
|
||||
canonical = renderPublicPathSnapshot(publicPathEnvironmentFile)
|
||||
} catch (GradleException exception) {
|
||||
throw new GradleException(
|
||||
"updatePublicPathSnapshot: ${exception.message}", exception)
|
||||
}
|
||||
if (!publicPathSnapshotFile.parentFile.isDirectory()
|
||||
&& !publicPathSnapshotFile.parentFile.mkdirs()) {
|
||||
throw new GradleException(
|
||||
"updatePublicPathSnapshot: failed to create ${publicPathSnapshotFile.parentFile}")
|
||||
}
|
||||
publicPathSnapshotFile.setText(canonical, 'UTF-8')
|
||||
logger.lifecycle(
|
||||
"updatePublicPathSnapshot: wrote reviewed baseline ${publicPathSnapshotFile}")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
redis.minimum.image=redis:7.2.14-alpine@sha256:dfa18828cbc07b3ae6a95ec7343f6c214fdee2d836197b4be8e9904420762cd8
|
||||
redis.below-minimum.image=redis:7.0.15-alpine@sha256:c9d92d840fd011c908f040592857c724ae6d877f2aba5c40ad963276507386b2
|
||||
redis.next-minor.image=redis:7.4.9-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||
redis.approved.image=redis:7.4.9-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||
toxiproxy.image=ghcr.io/shopify/toxiproxy:2.12.0@sha256:9378ed52a28bc50edc1350f936f518f31fa95f0d15917d6eb40b8e376d1a214e
|
||||
@@ -0,0 +1,150 @@
|
||||
import groovy.json.JsonSlurper
|
||||
import org.gradle.api.artifacts.ProjectDependency
|
||||
|
||||
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()
|
||||
|
||||
Set<String> actual = ['api', 'implementation', 'compileOnly', 'runtimeOnly']
|
||||
.collect { String configurationName ->
|
||||
compositionProject.configurations.findByName(configurationName)
|
||||
}
|
||||
.findAll { it != null }
|
||||
.collectMany { configuration ->
|
||||
configuration.dependencies.withType(ProjectDependency).collect {
|
||||
ProjectDependency dependency ->
|
||||
String dependencyId = moduleIdByGradlePath[dependency.path]
|
||||
if (dependencyId == null) {
|
||||
throw new GradleException(
|
||||
"Runtime composition '${compositionId}' depends on unregistered " +
|
||||
"Gradle project '${dependency.path}'.")
|
||||
}
|
||||
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
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Classpath
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.process.CommandLineArgumentProvider
|
||||
|
||||
abstract class MockitoAgentArgumentProvider implements CommandLineArgumentProvider {
|
||||
@Classpath
|
||||
abstract ConfigurableFileCollection getMockitoCoreClasspath()
|
||||
|
||||
@Input
|
||||
abstract Property<String> getOwner()
|
||||
|
||||
@Override
|
||||
Iterable<String> asArguments() {
|
||||
List<File> candidates = mockitoCoreClasspath.files.findAll { File file ->
|
||||
file.isFile() && file.name ==~ 'mockito-core-[^/]+\\.jar'
|
||||
}.sort { File left, File right -> left.absolutePath <=> right.absolutePath }
|
||||
|
||||
if (candidates.size() != 1) {
|
||||
throw new GradleException(
|
||||
"${owner.get()}: expected exactly one mockito-core JAR for the test JVM, " +
|
||||
"found ${candidates.size()}: " +
|
||||
candidates.collect { it.absolutePath })
|
||||
}
|
||||
|
||||
File mockitoCore = candidates[0]
|
||||
["-javaagent:${mockitoCore.absolutePath}", '-Xshare:off']
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.subprojects { Project target ->
|
||||
target.pluginManager.withPlugin('java') {
|
||||
target.pluginManager.withPlugin('io.spring.dependency-management') {
|
||||
def mockitoAgentDependencies =
|
||||
target.configurations.dependencyScope('mockitoAgentDependencies')
|
||||
def mockitoAgent = target.configurations.resolvable('mockitoAgent') {
|
||||
description = 'Mockito core JAR used only as a Test JVM startup agent.'
|
||||
extendsFrom(mockitoAgentDependencies.get())
|
||||
transitive = false
|
||||
}
|
||||
|
||||
target.dependencies.add(
|
||||
mockitoAgentDependencies.get().name, 'org.mockito:mockito-core')
|
||||
|
||||
target.tasks.withType(Test).configureEach {
|
||||
def provider = target.objects.newInstance(MockitoAgentArgumentProvider)
|
||||
provider.mockitoCoreClasspath.from(mockitoAgent)
|
||||
provider.owner.set("${target.path}:${name}")
|
||||
jvmArgumentProviders.add(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in New Issue
Block a user