优化
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
export const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
|
||||
const normalize = (value) => value.replaceAll('\\', '/')
|
||||
const attribute = (source, name) => source.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1]
|
||||
|
||||
async function exists(file) {
|
||||
try {
|
||||
await stat(file)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') return false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function javaSourceCount(moduleDirectory) {
|
||||
const root = path.join(moduleDirectory, 'src', 'main', 'java')
|
||||
if (!await exists(root)) return 0
|
||||
let count = 0
|
||||
async function walk(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const target = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) await walk(target)
|
||||
else if (entry.isFile() && entry.name.endsWith('.java')) count += 1
|
||||
}
|
||||
}
|
||||
await walk(root)
|
||||
return count
|
||||
}
|
||||
|
||||
async function openApiFiles(moduleDirectory, module) {
|
||||
const root = path.join(moduleDirectory, 'src', 'main', 'resources', 'openapi')
|
||||
if (!await exists(root)) return []
|
||||
const files = []
|
||||
async function walk(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const target = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) await walk(target)
|
||||
else if (entry.isFile() && /\.(?:json|ya?ml)$/i.test(entry.name)) {
|
||||
files.push(normalize(path.join(module, path.relative(moduleDirectory, target))))
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(root)
|
||||
return files.sort()
|
||||
}
|
||||
|
||||
export function parseModules(pom) {
|
||||
const block = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1] ?? ''
|
||||
return [...block.matchAll(/<module>([^<]+)<\/module>/g)].map((match) => match[1].trim())
|
||||
}
|
||||
|
||||
export function parseServices(serviceMap) {
|
||||
return [...serviceMap.matchAll(/^ ([a-z][a-z0-9-]+):$/gm)].map((match) => match[1]).sort()
|
||||
}
|
||||
|
||||
export function parseJacocoReport(xml) {
|
||||
if (!/<report\b/.test(xml) || !/<\/report>/.test(xml)) throw new Error('not a JaCoCo report')
|
||||
const counters = [...xml.matchAll(/<counter\b([^>]*)\/?\s*>/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((source) => attribute(source, 'type') === 'LINE')
|
||||
if (counters.length === 0) throw new Error('JaCoCo LINE counter is missing')
|
||||
const summary = counters.at(-1)
|
||||
const missed = Number(attribute(summary, 'missed'))
|
||||
const covered = Number(attribute(summary, 'covered'))
|
||||
if (!Number.isInteger(missed) || !Number.isInteger(covered)) throw new Error('invalid JaCoCo LINE counter')
|
||||
const total = missed + covered
|
||||
return { missed, covered, percentage: total === 0 ? null : Number((covered * 100 / total).toFixed(2)) }
|
||||
}
|
||||
|
||||
export function parseSpotbugsReport(xml) {
|
||||
if (!/<BugCollection\b/.test(xml) || !/<\/BugCollection>/.test(xml)) throw new Error('not a SpotBugs report')
|
||||
const findings = [...xml.matchAll(/<BugInstance\b([^>]*)>/g)].map((match) => match[1])
|
||||
const byPriority = {}
|
||||
const byCategory = {}
|
||||
for (const finding of findings) {
|
||||
const priority = attribute(finding, 'priority') ?? 'unknown'
|
||||
const category = attribute(finding, 'category') ?? 'unknown'
|
||||
byPriority[priority] = (byPriority[priority] ?? 0) + 1
|
||||
byCategory[category] = (byCategory[category] ?? 0) + 1
|
||||
}
|
||||
return { findings: findings.length, byPriority, byCategory }
|
||||
}
|
||||
|
||||
async function reportState(file, parser, root) {
|
||||
const relative = normalize(path.relative(root, file))
|
||||
if (!await exists(file)) return { status: 'missing', path: relative }
|
||||
try {
|
||||
return { status: 'valid', path: relative, ...parser(await readFile(file, 'utf8')) }
|
||||
} catch (error) {
|
||||
return { status: 'invalid', path: relative, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildQualitySummary(root = repositoryRoot) {
|
||||
const modules = parseModules(await readFile(path.join(root, 'pom.xml'), 'utf8'))
|
||||
const services = parseServices((await readFile(path.join(root, 'deploy', 'service-map.yml'), 'utf8')).replaceAll('\r\n', '\n'))
|
||||
const moduleRows = []
|
||||
const documents = []
|
||||
for (const module of modules) {
|
||||
const moduleDirectory = path.join(root, module)
|
||||
const sources = await javaSourceCount(moduleDirectory)
|
||||
documents.push(...await openApiFiles(moduleDirectory, module))
|
||||
if (sources === 0) continue
|
||||
const jacoco = await reportState(path.join(moduleDirectory, 'target', 'site', 'jacoco', 'jacoco.xml'), parseJacocoReport, root)
|
||||
const spotbugs = await reportState(path.join(moduleDirectory, 'target', 'spotbugsXml.xml'), parseSpotbugsReport, root)
|
||||
spotbugs.html = await exists(path.join(moduleDirectory, 'target', 'site', 'spotbugs.html'))
|
||||
moduleRows.push({ module, javaSources: sources, jacoco, spotbugs })
|
||||
}
|
||||
const coveredServices = [...new Set(documents.map((file) => file.split('/')[0]))]
|
||||
.filter((module) => services.includes(module)).sort()
|
||||
const codeFirstServices = services.filter((service) => !coveredServices.includes(service))
|
||||
const count = (kind, status) => moduleRows.filter((row) => row[kind].status === status).length
|
||||
return {
|
||||
topology: { modules: modules.length, codeModules: moduleRows.length, services: services.length },
|
||||
openapi: { documents: documents.length, coveredServices, codeFirstServices, files: documents.sort() },
|
||||
jacoco: {
|
||||
present: count('jacoco', 'valid'), invalid: count('jacoco', 'invalid'), missing: count('jacoco', 'missing')
|
||||
},
|
||||
spotbugs: {
|
||||
present: count('spotbugs', 'valid'), invalid: count('spotbugs', 'invalid'),
|
||||
missing: count('spotbugs', 'missing'), findings: moduleRows.reduce((sum, row) => sum + (row.spotbugs.findings ?? 0), 0)
|
||||
},
|
||||
modules: moduleRows
|
||||
}
|
||||
}
|
||||
|
||||
export function requirementFailures(summary, options = {}) {
|
||||
const failures = []
|
||||
for (const row of summary.modules) {
|
||||
if (row.jacoco.status === 'invalid') failures.push(`${row.module}: invalid JaCoCo report (${row.jacoco.error})`)
|
||||
if (row.spotbugs.status === 'invalid') failures.push(`${row.module}: invalid SpotBugs report (${row.spotbugs.error})`)
|
||||
if (row.spotbugs.status === 'valid' && !row.spotbugs.html) failures.push(`${row.module}: SpotBugs HTML report is missing`)
|
||||
if (options.requireJacoco && row.jacoco.status === 'missing') failures.push(`${row.module}: JaCoCo report is missing`)
|
||||
if (options.requireSpotbugs && row.spotbugs.status === 'missing') failures.push(`${row.module}: SpotBugs report is missing`)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
function printSummary(summary) {
|
||||
process.stdout.write(`Java quality summary: ${summary.topology.modules} modules, ${summary.topology.codeModules} code modules, ${summary.topology.services} services.\n`)
|
||||
process.stdout.write(`OpenAPI: ${summary.openapi.documents} documents, ${summary.openapi.coveredServices.length}/${summary.topology.services} services; gaps: ${summary.openapi.codeFirstServices.join(', ')}.\n`)
|
||||
process.stdout.write(`JaCoCo: ${summary.jacoco.present} valid, ${summary.jacoco.missing} missing, ${summary.jacoco.invalid} invalid.\n`)
|
||||
process.stdout.write(`SpotBugs: ${summary.spotbugs.present} valid, ${summary.spotbugs.missing} missing, ${summary.spotbugs.invalid} invalid, ${summary.spotbugs.findings} findings.\n`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
const rootIndex = args.indexOf('--root')
|
||||
const outputIndex = args.indexOf('--output')
|
||||
const root = rootIndex >= 0 ? path.resolve(args[rootIndex + 1]) : repositoryRoot
|
||||
const summary = await buildQualitySummary(root)
|
||||
printSummary(summary)
|
||||
if (outputIndex >= 0) {
|
||||
const output = path.resolve(args[outputIndex + 1])
|
||||
await mkdir(path.dirname(output), { recursive: true })
|
||||
await writeFile(output, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
||||
}
|
||||
const failures = requirementFailures(summary, {
|
||||
requireJacoco: args.includes('--require-jacoco'),
|
||||
requireSpotbugs: args.includes('--require-spotbugs')
|
||||
})
|
||||
if (failures.length > 0) throw new Error(`Quality report contract failed:\n${failures.map((failure) => `- ${failure}`).join('\n')}`)
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user