| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- /**
- * 票 52 变异闸:证明契约测试对「单设备工单处置优先」这条规则**有牙**。
- *
- * 每条变异:按字节把源码改成**规则失效**的形态 → 跑
- * BusinessCockpitDeviceFactResolverTest → 必须转红 → 再按字节还原。
- * 全部转红、且还原后复绿,闸门才算通过。
- *
- * 两个必须踩住的坑(首版都踩了,留档):
- * 1. 用 `mvn -pl zksy-admin test` 会把 zksy-system 从**本地仓库 jar** 解析,
- * 源码变异根本进不了测试 —— 必须 `-pl zksy-admin -am` 走 reactor 重新编译。
- * 2. Windows 上 `mvn` 是 `mvn.cmd`,`execFileSync('mvn', ...)` 直接 ENOENT;
- * 那样每次"失败"都会被误判成"打红"。这里显式用 shell + mvn.cmd 路径,
- * 并把「构建失败」与「测试计数含非零失败」分开判定。
- *
- * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t52-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'
- /**
- * 变异闸覆盖**三个测试类**:判定单源(resolver/facts)、事实透传接缝(分布适配器)、
- * 以及服务层重建设备时的透传(分布控制器)。少了最后一层,
- * 「effectiveDevice 丢事实」这类变异抓不住 —— 首版就是这么漏掉 M7 的。
- */
- const TEST = 'BusinessCockpitDeviceFactResolverTest,BusinessCockpitMonitoringDistributionAdapterTest,CockpitMonitoringDistributionControllerTest'
- 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 FACTS = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/BusinessCockpitDeviceFactResolver.java`
- const DIST_ADAPTER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringDistributionAdapter.java`
- const DIST_SERVICE = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionService.java`
- const read = (p) => readFileSync(p, 'utf8')
- const write = (p, s) => writeFileSync(p, s)
- const toLf = (s) => s.replace(/\r\n/g, '\n')
- const mutations = [
- {
- id: 'M1',
- name: 'deviceStatus 退回「报警恒压工单」(删掉承接分支)',
- file: RESOLVER,
- from: ` return device.isAlarmsCoveredByOpenWorkOrders()
- ? CockpitMonitoringPointStatus.WORK_ORDER
- : CockpitMonitoringPointStatus.ALARM;`,
- to: ` return CockpitMonitoringPointStatus.ALARM;`,
- },
- {
- id: 'M2',
- name: '承接判据放宽为「至少一条」(逐条比对改为只看非空)',
- file: FACTS,
- from: ` if (orderIds.size() != unhandled.size()) {
- return false;
- }`,
- to: ` if (orderIds.isEmpty()) {
- return false;
- }`,
- },
- {
- id: 'M3',
- name: '把已结案/已取消也当成承接(isOpenOrderStatus 恒真)',
- file: FACTS,
- from: ` return (orderStatus >= 0 && orderStatus <= 4) || orderStatus == 6;`,
- to: ` return true;`,
- },
- {
- id: 'M4',
- name: '空集视为「全部都有工单」(unhandled.isEmpty 返回 true)',
- file: FACTS,
- from: ` if (unhandled.isEmpty()) {
- return false;
- }`,
- to: ` if (unhandled.isEmpty()) {
- return true;
- }`,
- },
- {
- id: 'M5',
- name: '报警范围放宽为全部历史流水(去掉 alarm_status 过滤)',
- file: FACTS,
- from: ` .eq(AlarmData::getDeviceCode, deviceCode.trim())
- .eq(AlarmData::getAlarmStatus, UNHANDLED_ALARM_STATUS)));`,
- to: ` .eq(AlarmData::getDeviceCode, deviceCode.trim())));`,
- },
- {
- id: 'M6',
- name: '分布适配器不再透传承接事实(快照丢事实 → 出口永远判 ALARM)',
- file: DIST_ADAPTER,
- from: ` CockpitCreatedAt.format(device.getCreateTime()),
- fact.isAlarmsCoveredByOpenWorkOrders()));`,
- to: ` CockpitCreatedAt.format(device.getCreateTime()),
- false));`,
- },
- {
- id: 'M7',
- name: '分布服务 effectiveDevice 重建时丢承接事实',
- file: DIST_SERVICE,
- from: ` : "ONLINE".equals(runtime.monitoringState) || "OFFLINE".equals(runtime.monitoringState),
- source.isAlarmsCoveredByOpenWorkOrders());`,
- to: ` : "ONLINE".equals(runtime.monitoringState) || "OFFLINE".equals(runtime.monitoringState),
- false);`,
- },
- ]
- /** 跑一次测试;返回 { 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)
|