/** * 票 71 变异闸:证明契约测试对「未转工单的报警 > 工单处置 > 预警」这条序**有牙**。 * * 每条变异:按字节把源码改成规则失效的形态 → 跑 CockpitMonitoringPointStatusOrderTest → * 必须转红 → 再按字节还原。全部转红、还原后复绿,闸门才算通过。 * * 沿用票 52 变异闸的两条教训: * 1. 必须 '-pl zksy-admin -am' 走 reactor 重新编译,否则 zksy-system 从本地仓库 jar 解析,源码变异进不了测试; * 2. Windows 上 mvn 是 mvn.cmd,显式给路径;「构建没跑到测试」不能算打红。 * * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t71-mutations.mjs */ import { execSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' const BACKEND = 'E:/pipe-ner-server' const MODULE = 'pipe-network-service' const TEST = 'CockpitMonitoringPointStatusOrderTest' const MVN = 'D:/Maven/apache-maven-3.9.15/bin/mvn.cmd' const RESOLVER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatusResolver.java` const ENUM = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatus.java` const read = (p) => readFileSync(p, 'utf8') const write = (p, s) => writeFileSync(p, s) const toLf = (s) => s.replace(/\r\n/g, '\n') const ORDER_BLOCK = ` // 「未转工单的报警」已在上面拦截;剩下的「有未结束工单」压过「有未解决预警」。 if (device.isWorkOrderInProgress()) { return CockpitMonitoringPointStatus.WORK_ORDER; } if (device.isWarning()) { return CockpitMonitoringPointStatus.WARNING; }` const mutations = [ { id: 'M1', name: '退回原序:预警重新压过工单(两个分支换回)', file: RESOLVER, from: ORDER_BLOCK, to: ` if (device.isWarning()) { return CockpitMonitoringPointStatus.WARNING; } if (device.isWorkOrderInProgress()) { return CockpitMonitoringPointStatus.WORK_ORDER; }`, }, { id: 'M2', name: '报警分支忽略承接事实(全部报警恒判 ALARM)', file: RESOLVER, from: ` return device.isAlarmsCoveredByOpenWorkOrders() ? CockpitMonitoringPointStatus.WORK_ORDER : CockpitMonitoringPointStatus.ALARM;`, to: ` return CockpitMonitoringPointStatus.ALARM;`, }, { id: 'M3', name: '删掉非报警分支的工单判断(工单不再压预警)', file: RESOLVER, from: ` if (device.isWorkOrderInProgress()) { return CockpitMonitoringPointStatus.WORK_ORDER; } if (device.isWarning()) {`, to: ` if (device.isWarning()) {`, }, { id: 'M4', name: '偷偷改枚举优先级(Q2 被破坏:WORK_ORDER 提到 WARNING 之前)', file: ENUM, from: ` WARNING(2, "预警"), WORK_ORDER(3, "工单处置"),`, to: ` WARNING(3, "预警"), WORK_ORDER(2, "工单处置"),`, }, ] /** 跑一次测试;返回 { ran, green } —— ran=false 表示构建没跑到测试(不能算证据)。 */ const runTest = () => { let out = '' try { out = execSync( `"${MVN}" -o -pl zksy-admin -am test -Dtest=${TEST} -DfailIfNoTests=false`, { cwd: `${BACKEND}/${MODULE}`, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) } catch (error) { out = `${error.stdout ?? ''}${error.stderr ?? ''}` } const summary = out.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/g) if (!summary || summary.length === 0) { return { ran: false, green: false, out } } const last = summary[summary.length - 1] const [, total, failures, errors] = last.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/) return { ran: true, green: Number(failures) === 0 && Number(errors) === 0 && Number(total) > 0, out } } const baseline = runTest() if (!baseline.ran || !baseline.green) { console.log('基线不绿,闸门无法判定:先修好源码/测试再跑变异。') process.exit(1) } console.log('基线绿(未变异时测试全过),开始变异…\n') let red = 0 let broken = 0 const originals = new Map() for (const m of mutations) { if (!originals.has(m.file)) originals.set(m.file, read(m.file)) const original = originals.get(m.file) const crlf = original.includes('\r\n') const norm = toLf(original) if (!norm.includes(m.from)) { console.log(`SKIP ${m.id} — anchor 未命中(源码已变,需同步变异脚本):${m.name}`) broken += 1 continue } write(m.file, crlf ? norm.replace(m.from, m.to).replace(/\n/g, '\r\n') : norm.replace(m.from, m.to)) const result = runTest() if (!result.ran) { console.log(`FAIL ${m.id} — 构建没跑到测试,不能算打红:${m.name}`) broken += 1 } else if (!result.green) { red += 1 console.log(`PASS ${m.id} — 变异被打红:${m.name}`) } else { console.log(`FAIL ${m.id} — 变异未被抓住(测试无牙):${m.name}`) } write(m.file, original) if (read(m.file) !== original) { console.log(`FAIL ${m.id} — 还原后字节不一致`) broken += 1 } } const final = runTest() console.log(`\n变异打红 ${red}/${mutations.length};还原后复绿:${final.green ? '是' : '否'};异常 ${broken}`) process.exit(red === mutations.length && final.green && broken === 0 ? 0 : 1)