Files
baiyue-firstleg/scripts/java-topology-contract.test.mjs
2026-09-29 18:47:37 +08:00

194 lines
8.9 KiB
JavaScript

import assert from 'node:assert/strict'
import { access, readFile } from 'node:fs/promises'
import test from 'node:test'
const read = async (path) => (await readFile(new URL(`../${path}`, import.meta.url), 'utf8'))
.replaceAll('\r\n', '\n')
const sorted = (values) => [...values].sort()
const parseRootModules = (xml) => {
const body = xml.match(/<modules>([\s\S]*?)<\/modules>/)?.[1] ?? ''
return [...body.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
}
const parsePom = (xml) => {
const project = xml.replace(/<parent>[\s\S]*?<\/parent>/, '')
return {
artifactId: project.match(/<artifactId>([^<]+)<\/artifactId>/)?.[1]?.trim(),
springBoot: /<artifactId>spring-boot-maven-plugin<\/artifactId>/.test(project)
}
}
const parseServiceMap = (yaml) => {
const services = new Map()
let current
for (const line of yaml.split('\n')) {
const service = line.match(/^ ([a-z][a-z0-9-]+):$/)
if (service) {
current = {}
services.set(service[1], current)
continue
}
const property = line.match(/^ ([a-zA-Z]+): (.+)$/)
if (!current || !property) continue
const [, key, raw] = property
current[key] = key === 'port'
? Number(raw)
: key === 'regions'
? raw.slice(1, -1).split(',').map((value) => value.trim())
: raw
}
return services
}
const parseGroovyList = (source, variable) => {
const body = source.match(new RegExp(`def ${variable} = \\[([\\s\\S]*?)\\n\\]`))?.[1] ?? ''
return [...body.matchAll(/'([^']+)'/g)].map((match) => match[1])
}
const parseGroovyMap = (source, variable) => {
const body = source.match(new RegExp(`def ${variable} = \\[([\\s\\S]*?)\\n\\]`))?.[1] ?? ''
return new Map([...body.matchAll(/'([^']+)':\s*'([^']+)'/g)]
.map((match) => [match[1], match[2]]))
}
const parseCompose = (yaml) => {
const body = yaml.match(/^services:\n([\s\S]*?)(?=^networks:)/m)?.[1] ?? ''
const matches = [...body.matchAll(/^ ([a-z][a-z0-9-]+):\s*$/gm)]
return new Map(matches.map((match, index) => {
const block = body.slice(match.index, matches[index + 1]?.index ?? body.length)
return [match[1], Number(block.match(/^ SERVER_PORT: (\d+)$/m)?.[1])]
}))
}
test('root POM freezes 20 modules and service-map freezes 13 executable services', async () => {
const modules = parseRootModules(await read('pom.xml'))
const services = parseServiceMap(await read('deploy/service-map.yml'))
assert.equal(modules.length, 20)
assert.equal(new Set(modules).size, 20)
assert.equal(services.size, 13)
const bootModules = []
for (const module of modules) {
const pom = parsePom(await read(`${module}/pom.xml`))
if (pom.springBoot) bootModules.push(module)
if (!services.has(module)) continue
const service = services.get(module)
assert.equal(service.artifactId, pom.artifactId, `${module} artifactId drifted`)
assert.equal(service.jar, `${module}/target/${pom.artifactId}-1.0.0-SNAPSHOT.jar`)
}
assert.deepEqual(sorted(bootModules), sorted(services.keys()))
assert.equal(new Set([...services.values()].map(({ port }) => port)).size, services.size)
})
test('Jenkins and GitLab package exactly the service-map modules', async () => {
const services = parseServiceMap(await read('deploy/service-map.yml'))
const expected = sorted(services.keys())
const coordinator = await read('Jenkinsfile')
const servicePipeline = await read('Jenkinsfile.service')
const serviceJars = parseGroovyMap(servicePipeline, 'serviceJars')
const gitlab = await read('.gitlab-ci.yml')
const gitlabArtifacts = [...gitlab.matchAll(/^ - ([a-z][a-z0-9-]+)\/target\/\*\.jar$/gm)]
.map((match) => match[1])
assert.deepEqual(sorted(parseGroovyList(coordinator, 'serviceModules')), expected)
assert.deepEqual(sorted(parseGroovyList(servicePipeline, 'serviceModules')), expected)
assert.deepEqual(sorted(serviceJars.keys()), expected)
assert.deepEqual(sorted(gitlabArtifacts), expected)
for (const [module, service] of services) assert.equal(serviceJars.get(module), service.jar)
})
test('GitLab runs the topology contract in a dedicated Node job', async () => {
const gitlab = await read('.gitlab-ci.yml')
const topologyJob = gitlab.match(/^Java拓扑合同:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? ''
assert.match(topologyJob, /^ stage: verify$/m)
assert.match(topologyJob, /^ image: node:[^\s]+$/m)
assert.match(topologyJob, /^ - node --test scripts\/java-topology-contract\.test\.mjs$/m)
assert.match(packageJob, /^ - Java拓扑合同$/m)
})
test('GitLab validates every static OpenAPI document before packaging', async () => {
const gitlab = await read('.gitlab-ci.yml')
const openapiJob = gitlab.match(/^OpenAPI有效性:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? ''
assert.match(openapiJob, /^ stage: verify$/m)
assert.match(openapiJob, /^ image: node:[^\s]+$/m)
assert.match(openapiJob, /^ - npm ci --ignore-scripts --prefix scripts\/openapi-validation$/m)
assert.match(openapiJob, /^ - npm test --prefix scripts\/openapi-validation$/m)
assert.match(openapiJob, /^ - npm run validate --prefix scripts\/openapi-validation$/m)
assert.doesNotMatch(openapiJob, /\bnpx\b|@latest/)
assert.match(packageJob, /^ - Java拓扑合同$/m)
assert.match(packageJob, /^ - OpenAPI有效性$/m)
assert.match(packageJob, /^ - 校验与测试$/m)
})
test('Jenkins cannot invoke the Node contract without declaring a Node runtime', async () => {
for (const path of ['Jenkinsfile', 'Jenkinsfile.service']) {
const pipeline = await read(path)
if (!pipeline.includes('java-topology-contract.test.mjs')) continue
assert.match(
pipeline,
/(?:nodejs\s*\(|NodeJSInstallation|docker(?:\.image\([^)]*node:|\s+run[^\n]*\bnode:))/,
`${path} must declare how Node is provided before invoking the topology contract`
)
}
})
test('Maven verify publishes JaCoCo reports without enforcing a threshold', async () => {
const pom = await read('pom.xml')
const gitlab = await read('.gitlab-ci.yml')
const jacoco = pom.match(/<plugin>\s*<groupId>org\.jacoco<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? ''
assert.match(jacoco, /<artifactId>jacoco-maven-plugin<\/artifactId>/)
assert.match(jacoco, /<goal>prepare-agent<\/goal>/)
assert.match(jacoco, /<phase>verify<\/phase>/)
assert.match(jacoco, /<goal>report<\/goal>/)
assert.match(jacoco, /<format>HTML<\/format>/)
assert.match(jacoco, /<format>XML<\/format>/)
assert.doesNotMatch(jacoco, /<goal>check<\/goal>|<rules>/)
assert.match(gitlab, /^ - "\*\*\/target\/site\/jacoco\/"$/m)
assert.match(gitlab, /^ - "\*\*\/target\/jacoco\.exec"$/m)
})
test('SpotBugs remains an opt-in non-blocking report without quality thresholds', async () => {
const pom = await read('pom.xml')
const gitlab = await read('.gitlab-ci.yml')
const profile = pom.match(/<profile>\s*<id>static-analysis-report<\/id>[\s\S]*?<\/profile>/)?.[0] ?? ''
const spotbugs = profile.match(/<plugin>\s*<groupId>com\.github\.spotbugs<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? ''
const reportJob = gitlab.match(/^静态分析报告:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const verifyJob = gitlab.match(/^校验与测试:\n([\s\S]*?)(?=^[^\s#].*:\n)/m)?.[1] ?? ''
const packageJob = gitlab.match(/^打包服务:\n([\s\S]*)/m)?.[1] ?? ''
assert.match(pom, /<spotbugs-maven-plugin\.version>4\.9\.8\.5<\/spotbugs-maven-plugin\.version>/)
assert.match(spotbugs, /<artifactId>spotbugs-maven-plugin<\/artifactId>/)
assert.match(spotbugs, /<phase>verify<\/phase>/)
assert.match(spotbugs, /<goal>spotbugs<\/goal>/)
assert.match(spotbugs, /<xmlOutput>true<\/xmlOutput>/)
assert.match(spotbugs, /<htmlOutput>true<\/htmlOutput>/)
assert.match(spotbugs, /<failOnError>false<\/failOnError>/)
assert.doesNotMatch(spotbugs, /<goal>check<\/goal>|<rules>|<threshold>/)
assert.doesNotMatch(pom.replace(profile, ''), /<artifactId>spotbugs-maven-plugin<\/artifactId>/)
assert.match(reportJob, /^ allow_failure: true$/m)
assert.match(reportJob, /-Pstatic-analysis-report/)
assert.match(reportJob, /^ - "\*\*\/target\/spotbugs\*\.xml"$/m)
assert.match(reportJob, /^ - "\*\*\/target\/site\/spotbugs\.html"$/m)
assert.doesNotMatch(verifyJob, /static-analysis-report|spotbugs/i)
assert.doesNotMatch(packageJob, /^ - 静态分析报告$/m)
})
test('one TEST Compose and Nacos manifest cover every retained service', async () => {
const services = parseServiceMap(await read('deploy/service-map.yml'))
const compose = parseCompose(await read('deploy/docker-compose.yml'))
assert.deepEqual(sorted(compose.keys()), sorted(services.keys()))
assert.equal(services.size, 13)
for (const [module, service] of services) {
assert.equal(compose.get(module), service.port, module)
await access(new URL(`../config/nacos/test/${service.artifactId}.yml`, import.meta.url))
}
})