t71-mutations.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. /**
  2. * 票 71 变异闸:证明契约测试对「未转工单的报警 > 工单处置 > 预警」这条序**有牙**。
  3. *
  4. * 每条变异:按字节把源码改成规则失效的形态 → 跑 CockpitMonitoringPointStatusOrderTest →
  5. * 必须转红 → 再按字节还原。全部转红、还原后复绿,闸门才算通过。
  6. *
  7. * 沿用票 52 变异闸的两条教训:
  8. * 1. 必须 '-pl zksy-admin -am' 走 reactor 重新编译,否则 zksy-system 从本地仓库 jar 解析,源码变异进不了测试;
  9. * 2. Windows 上 mvn 是 mvn.cmd,显式给路径;「构建没跑到测试」不能算打红。
  10. *
  11. * 用法:node .scratch/cockpit-real-data-wayfinding/verify/t71-mutations.mjs
  12. */
  13. import { execSync } from 'node:child_process'
  14. import { readFileSync, writeFileSync } from 'node:fs'
  15. const BACKEND = 'E:/pipe-ner-server'
  16. const MODULE = 'pipe-network-service'
  17. const TEST = 'CockpitMonitoringPointStatusOrderTest'
  18. const MVN = 'D:/Maven/apache-maven-3.9.15/bin/mvn.cmd'
  19. const RESOLVER = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatusResolver.java`
  20. const ENUM = `${BACKEND}/${MODULE}/zksy-system/src/main/java/com/zksy/cockpit/point/CockpitMonitoringPointStatus.java`
  21. const read = (p) => readFileSync(p, 'utf8')
  22. const write = (p, s) => writeFileSync(p, s)
  23. const toLf = (s) => s.replace(/\r\n/g, '\n')
  24. const ORDER_BLOCK = ` // 「未转工单的报警」已在上面拦截;剩下的「有未结束工单」压过「有未解决预警」。
  25. if (device.isWorkOrderInProgress()) {
  26. return CockpitMonitoringPointStatus.WORK_ORDER;
  27. }
  28. if (device.isWarning()) {
  29. return CockpitMonitoringPointStatus.WARNING;
  30. }`
  31. const mutations = [
  32. {
  33. id: 'M1',
  34. name: '退回原序:预警重新压过工单(两个分支换回)',
  35. file: RESOLVER,
  36. from: ORDER_BLOCK,
  37. to: ` if (device.isWarning()) {
  38. return CockpitMonitoringPointStatus.WARNING;
  39. }
  40. if (device.isWorkOrderInProgress()) {
  41. return CockpitMonitoringPointStatus.WORK_ORDER;
  42. }`,
  43. },
  44. {
  45. id: 'M2',
  46. name: '报警分支忽略承接事实(全部报警恒判 ALARM)',
  47. file: RESOLVER,
  48. from: ` return device.isAlarmsCoveredByOpenWorkOrders()
  49. ? CockpitMonitoringPointStatus.WORK_ORDER
  50. : CockpitMonitoringPointStatus.ALARM;`,
  51. to: ` return CockpitMonitoringPointStatus.ALARM;`,
  52. },
  53. {
  54. id: 'M3',
  55. name: '删掉非报警分支的工单判断(工单不再压预警)',
  56. file: RESOLVER,
  57. from: ` if (device.isWorkOrderInProgress()) {
  58. return CockpitMonitoringPointStatus.WORK_ORDER;
  59. }
  60. if (device.isWarning()) {`,
  61. to: ` if (device.isWarning()) {`,
  62. },
  63. {
  64. id: 'M4',
  65. name: '偷偷改枚举优先级(Q2 被破坏:WORK_ORDER 提到 WARNING 之前)',
  66. file: ENUM,
  67. from: ` WARNING(2, "预警"),
  68. WORK_ORDER(3, "工单处置"),`,
  69. to: ` WARNING(3, "预警"),
  70. WORK_ORDER(2, "工单处置"),`,
  71. },
  72. ]
  73. /** 跑一次测试;返回 { ran, green } —— ran=false 表示构建没跑到测试(不能算证据)。 */
  74. const runTest = () => {
  75. let out = ''
  76. try {
  77. out = execSync(
  78. `"${MVN}" -o -pl zksy-admin -am test -Dtest=${TEST} -DfailIfNoTests=false`,
  79. { cwd: `${BACKEND}/${MODULE}`, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
  80. } catch (error) {
  81. out = `${error.stdout ?? ''}${error.stderr ?? ''}`
  82. }
  83. const summary = out.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/g)
  84. if (!summary || summary.length === 0) {
  85. return { ran: false, green: false, out }
  86. }
  87. const last = summary[summary.length - 1]
  88. const [, total, failures, errors] = last.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+)/)
  89. return { ran: true, green: Number(failures) === 0 && Number(errors) === 0 && Number(total) > 0, out }
  90. }
  91. const baseline = runTest()
  92. if (!baseline.ran || !baseline.green) {
  93. console.log('基线不绿,闸门无法判定:先修好源码/测试再跑变异。')
  94. process.exit(1)
  95. }
  96. console.log('基线绿(未变异时测试全过),开始变异…\n')
  97. let red = 0
  98. let broken = 0
  99. const originals = new Map()
  100. for (const m of mutations) {
  101. if (!originals.has(m.file)) originals.set(m.file, read(m.file))
  102. const original = originals.get(m.file)
  103. const crlf = original.includes('\r\n')
  104. const norm = toLf(original)
  105. if (!norm.includes(m.from)) {
  106. console.log(`SKIP ${m.id} — anchor 未命中(源码已变,需同步变异脚本):${m.name}`)
  107. broken += 1
  108. continue
  109. }
  110. write(m.file, crlf ? norm.replace(m.from, m.to).replace(/\n/g, '\r\n') : norm.replace(m.from, m.to))
  111. const result = runTest()
  112. if (!result.ran) {
  113. console.log(`FAIL ${m.id} — 构建没跑到测试,不能算打红:${m.name}`)
  114. broken += 1
  115. } else if (!result.green) {
  116. red += 1
  117. console.log(`PASS ${m.id} — 变异被打红:${m.name}`)
  118. } else {
  119. console.log(`FAIL ${m.id} — 变异未被抓住(测试无牙):${m.name}`)
  120. }
  121. write(m.file, original)
  122. if (read(m.file) !== original) {
  123. console.log(`FAIL ${m.id} — 还原后字节不一致`)
  124. broken += 1
  125. }
  126. }
  127. const final = runTest()
  128. console.log(`\n变异打红 ${red}/${mutations.length};还原后复绿:${final.green ? '是' : '否'};异常 ${broken}`)
  129. process.exit(red === mutations.length && final.green && broken === 0 ? 0 : 1)