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(/([\s\S]*?)<\/modules>/)?.[1] ?? '' return [...body.matchAll(/([^<]+)<\/module>/g)].map((match) => match[1].trim()) } const parsePom = (xml) => { const project = xml.replace(/[\s\S]*?<\/parent>/, '') return { artifactId: project.match(/([^<]+)<\/artifactId>/)?.[1]?.trim(), springBoot: /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 22 modules and service-map freezes 15 executable services', async () => { const modules = parseRootModules(await read('pom.xml')) const services = parseServiceMap(await read('deploy/service-map.yml')) assert.equal(modules.length, 22) assert.equal(new Set(modules).size, 22) assert.equal(services.size, 15) 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('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(/\s*org\.jacoco<\/groupId>[\s\S]*?<\/plugin>/)?.[0] ?? '' assert.match(jacoco, /jacoco-maven-plugin<\/artifactId>/) assert.match(jacoco, /prepare-agent<\/goal>/) assert.match(jacoco, /verify<\/phase>/) assert.match(jacoco, /report<\/goal>/) assert.match(jacoco, /HTML<\/format>/) assert.match(jacoco, /XML<\/format>/) assert.doesNotMatch(jacoco, /check<\/goal>|/) assert.match(gitlab, /^ - "\*\*\/target\/site\/jacoco\/"$/m) assert.match(gitlab, /^ - "\*\*\/target\/jacoco\.exec"$/m) }) test('regional Compose and Nacos manifests cover the service-map topology', async () => { const services = parseServiceMap(await read('deploy/service-map.yml')) const composeByRegion = new Map([ ['CN', parseCompose(await read('deploy/docker-compose.yml'))], ['OVERSEAS', parseCompose(await read('deploy/docker-compose.overseas.yml'))] ]) for (const [module, service] of services) { const regions = service.regions ?? ['CN'] for (const [region, compose] of composeByRegion) { assert.equal(compose.has(module), regions.includes(region), `${module} Compose region drifted for ${region}`) if (!regions.includes(region)) continue assert.equal(compose.get(module), service.port, `${module} internal port drifted for ${region}`) await access(new URL(`../config/nacos/${region.toLowerCase()}/${service.artifactId}.yml`, import.meta.url)) } } assert.deepEqual( sorted(new Set([...composeByRegion.values()].flatMap((compose) => [...compose.keys()]))), sorted(services.keys()) ) })