t52-mutations.mjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * 票 52 变异闸:证明契约测试对「单设备工单处置优先」这条规则**有牙**。
  3. *
  4. * 每条变异:按字节把源码改成**规则失效**的形态 → 跑
  5. * BusinessCockpitDeviceFactResolverTest → 必须转红 → 再按字节还原。
  6. * 全部转红、且还原后复绿,闸门才算通过。
  7. *
  8. * 两个必须踩住的坑(首版都踩了,留档):
  9. * 1. 用 `mvn -pl zksy-admin test` 会把 zksy-system 从**本地仓库 jar** 解析,
  10. * 源码变异根本进不了测试 —— 必须 `-pl zksy-admin -am` 走 reactor 重新编译。
  11. * 2. Windows 上 `mvn` 是 `mvn.cmd`,`execFileSync('mvn', ...)` 直接 ENOENT;
  12. * 那样每次"失败"都会被误判成"打红"。这里显式用 shell + mvn.cmd 路径,
  13. * 并把「构建失败」与「测试计数含非零失败」分开判定。
  14. *
  15. * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t52-mutations.mjs
  16. */
  17. import { execSync } from 'node:child_process'
  18. import { readFileSync, writeFileSync } from 'node:fs'
  19. const BACKEND = 'E:/pipe-ner-server'
  20. const MODULE = 'pipe-network-service'
  21. /**
  22. * 变异闸覆盖**三个测试类**:判定单源(resolver/facts)、事实透传接缝(分布适配器)、
  23. * 以及服务层重建设备时的透传(分布控制器)。少了最后一层,
  24. * 「effectiveDevice 丢事实」这类变异抓不住 —— 首版就是这么漏掉 M7 的。
  25. */
  26. const TEST = 'BusinessCockpitDeviceFactResolverTest,BusinessCockpitMonitoringDistributionAdapterTest,CockpitMonitoringDistributionControllerTest'
  27. const MVN = 'D:\\Maven\\apache-maven-3.9.15\\bin\\mvn.cmd'
  28. const RESOLVER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatusResolver.java`
  29. const FACTS = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/BusinessCockpitDeviceFactResolver.java`
  30. const DIST_ADAPTER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/monitoring/BusinessCockpitMonitoringDistributionAdapter.java`
  31. const DIST_SERVICE = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/monitoring/CockpitMonitoringDistributionService.java`
  32. const read = (p) => readFileSync(p, 'utf8')
  33. const write = (p, s) => writeFileSync(p, s)
  34. const toLf = (s) => s.replace(/\r\n/g, '\n')
  35. const mutations = [
  36. {
  37. id: 'M1',
  38. name: 'deviceStatus 退回「报警恒压工单」(删掉承接分支)',
  39. file: RESOLVER,
  40. from: ` return device.isAlarmsCoveredByOpenWorkOrders()
  41. ? CockpitMonitoringPointStatus.WORK_ORDER
  42. : CockpitMonitoringPointStatus.ALARM;`,
  43. to: ` return CockpitMonitoringPointStatus.ALARM;`,
  44. },
  45. {
  46. id: 'M2',
  47. name: '承接判据放宽为「至少一条」(逐条比对改为只看非空)',
  48. file: FACTS,
  49. from: ` if (orderIds.size() != unhandled.size()) {
  50. return false;
  51. }`,
  52. to: ` if (orderIds.isEmpty()) {
  53. return false;
  54. }`,
  55. },
  56. {
  57. id: 'M3',
  58. name: '把已结案/已取消也当成承接(isOpenOrderStatus 恒真)',
  59. file: FACTS,
  60. from: ` return (orderStatus >= 0 && orderStatus <= 4) || orderStatus == 6;`,
  61. to: ` return true;`,
  62. },
  63. {
  64. id: 'M4',
  65. name: '空集视为「全部都有工单」(unhandled.isEmpty 返回 true)',
  66. file: FACTS,
  67. from: ` if (unhandled.isEmpty()) {
  68. return false;
  69. }`,
  70. to: ` if (unhandled.isEmpty()) {
  71. return true;
  72. }`,
  73. },
  74. {
  75. id: 'M5',
  76. name: '报警范围放宽为全部历史流水(去掉 alarm_status 过滤)',
  77. file: FACTS,
  78. from: ` .eq(AlarmData::getDeviceCode, deviceCode.trim())
  79. .eq(AlarmData::getAlarmStatus, UNHANDLED_ALARM_STATUS)));`,
  80. to: ` .eq(AlarmData::getDeviceCode, deviceCode.trim())));`,
  81. },
  82. {
  83. id: 'M6',
  84. name: '分布适配器不再透传承接事实(快照丢事实 → 出口永远判 ALARM)',
  85. file: DIST_ADAPTER,
  86. from: ` CockpitCreatedAt.format(device.getCreateTime()),
  87. fact.isAlarmsCoveredByOpenWorkOrders()));`,
  88. to: ` CockpitCreatedAt.format(device.getCreateTime()),
  89. false));`,
  90. },
  91. {
  92. id: 'M7',
  93. name: '分布服务 effectiveDevice 重建时丢承接事实',
  94. file: DIST_SERVICE,
  95. from: ` : "ONLINE".equals(runtime.monitoringState) || "OFFLINE".equals(runtime.monitoringState),
  96. source.isAlarmsCoveredByOpenWorkOrders());`,
  97. to: ` : "ONLINE".equals(runtime.monitoringState) || "OFFLINE".equals(runtime.monitoringState),
  98. false);`,
  99. },
  100. ]
  101. /** 跑一次测试;返回 { ran, green } —— ran=false 表示构建没跑到测试(不能算证据)。 */
  102. const runTest = () => {
  103. let out = ''
  104. try {
  105. out = execSync(
  106. `"${MVN}" -o -pl zksy-admin -am test -Dtest=${TEST} -DfailIfNoTests=false`,
  107. { cwd: `${BACKEND}/${MODULE}`, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
  108. } catch (error) {
  109. out = `${error.stdout ?? ''}${error.stderr ?? ''}`
  110. }
  111. const summary = out.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/g)
  112. if (!summary || summary.length === 0) {
  113. return { ran: false, green: false, out }
  114. }
  115. const last = summary[summary.length - 1]
  116. const [, total, failures, errors] = last.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/)
  117. return { ran: true, green: Number(failures) === 0 && Number(errors) === 0 && Number(total) > 0, out }
  118. }
  119. const baseline = runTest()
  120. if (!baseline.ran || !baseline.green) {
  121. console.log('基线不绿,闸门无法判定:先修好源码/测试再跑变异。')
  122. process.exit(1)
  123. }
  124. console.log('基线绿(未变异时测试全过),开始变异…\n')
  125. let red = 0
  126. let broken = 0
  127. const originals = new Map()
  128. for (const m of mutations) {
  129. if (!originals.has(m.file)) originals.set(m.file, read(m.file))
  130. const original = originals.get(m.file)
  131. const crlf = original.includes('\r\n')
  132. const norm = toLf(original)
  133. if (!norm.includes(m.from)) {
  134. console.log(`SKIP ${m.id} — anchor 未命中(源码已变,需同步变异脚本):${m.name}`)
  135. broken += 1
  136. continue
  137. }
  138. write(m.file, crlf ? norm.replace(m.from, m.to).replace(/\n/g, '\r\n') : norm.replace(m.from, m.to))
  139. const result = runTest()
  140. if (!result.ran) {
  141. console.log(`FAIL ${m.id} — 构建没跑到测试,不能算打红:${m.name}`)
  142. broken += 1
  143. } else if (!result.green) {
  144. red += 1
  145. console.log(`PASS ${m.id} — 变异被打红:${m.name}`)
  146. } else {
  147. console.log(`FAIL ${m.id} — 变异未被抓住(测试无牙):${m.name}`)
  148. }
  149. write(m.file, original)
  150. if (read(m.file) !== original) {
  151. console.log(`FAIL ${m.id} — 还原后字节不一致`)
  152. broken += 1
  153. }
  154. }
  155. const final = runTest()
  156. console.log(`\n变异打红 ${red}/${mutations.length};还原后复绿:${final.green ? '是' : '否'};异常 ${broken}`)
  157. process.exit(red === mutations.length && final.green && broken === 0 ? 0 : 1)