| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- /**
- * Task 3 新逻辑的**两向变异**驱动器。
- *
- * 形状抄自同目录 `fe-update-mutate.mjs`(那里有「先看 git status 干净」那段注意事项)。
- *
- * 为什么需要它:`fe-update-harness.mjs` 绿 90 条**本身不证明**那些断言有牙 ——
- * 一条恒真的断言与一条真钉子在这份读数上长得一模一样。这里按两向走:
- * · 该红的必须红(三个真变异各打一处本轮新写的判定)
- * · **无害改动必须实测仍绿**(换两行没有依赖关系的声明顺序)—— 否则红可能只是
- * 「台子对任何改动都敏感」,而不是「这条断言钉住了这件事」
- *
- * ★ 每一轮都:改前存字节 → 断言替换**确实生效**(命中次数)→ 跑台子 → **按字节还原**
- * → 复核 sha256 回到改前。还原动作本身也要被验证(见 memory「变异验证要两向」第三种)。
- *
- * 用法:node fe-task3-mutate.mjs
- * 退出码:只有「还原后 sha 不符」或「无害变异竟然红了」才非 0。
- */
- import fs from "node:fs";
- import crypto from "node:crypto";
- import { spawnSync } from "node:child_process";
- import { fileURLToPath } from "node:url";
- const HERE = fileURLToPath(new URL(".", import.meta.url));
- const HARNESS = HERE + "fe-update-harness.mjs";
- const PAGE = "D:/project2/payment-platform/frontend/src/views/module_payment/collection/onboarding/index.vue";
- const sha = (buf) => crypto.createHash("sha256").update(buf).digest("hex");
- const MUTATIONS = [
- {
- id: "M-APPROVED-ALSO-SUBMIT",
- why: "把「可以收款了」那一档也挂上提交审核入口",
- expectRed: ["A7", "A8"],
- find: ` case "APPROVED":\n return [{ ...edit, primary: true }];`,
- repl: ` case "APPROVED":\n return [{ ...edit, primary: true }, submit];`,
- },
- {
- id: "M-409-CODE-CHANGED",
- why: "把 409 那个码值换成表外的值 ⇒ 409 那条分支永不进入",
- // ★ 起初这里写的是 ["S2","S3","S4"],实测 **S3 没红** ⇒ 追下去发现 S3 是条恒真的断言
- // (理由写在台子里 S3 被删处)。这条 `expectRed` 按**实测**改,不是按我当初的预期改。
- expectRed: ["S2", "S4"],
- find: `if (e?.data?.code === 409) {`,
- repl: `if (e?.data?.code === 999999) {`,
- },
- {
- id: "M-LIST-ONE-LAYER",
- why: "列表只解一层 `.data`(把 payload 抬高一层的读法)",
- expectRed: ["L1", "L2"],
- // ★ 锚点必须**唯一**:`res.data.data?.list` 这个串在同一文件的 javadoc 里也出现过一次
- // ⇒ 裸用它会让替换打到注释上(驱动器的命中次数护栏第一次就是这么把它拦下来的)。
- find: `list.value = res.data.data?.list || [];`,
- repl: `list.value = res.data?.list || [];`,
- },
- {
- id: "H-REORDER-DECLS",
- why: "无害:把两个没有依赖关系的声明换个顺序(行数不变)",
- expectRed: [],
- find: ` const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };\n const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };`,
- repl: ` const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };\n const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };`,
- },
- ];
- const original = fs.readFileSync(PAGE);
- const baseSha = sha(original);
- console.log(`基线 sha256 = ${baseSha}`);
- console.log(`基线字节 = ${original.length}`);
- function runHarness() {
- // ★ 用 spawnSync 而不是 execFileSync:变异体**有可能把台子打崩**,而崩掉的读数依然有用
- // (「预期该红的」可以从崩溃前的 FAIL 行里认出来)。execFileSync 遇到非 0 退出码会**抛异常**
- // ⇒ 驱动器自己死掉,后面几个变异一个都不跑。实测踩过:M-LIST-ONE-LAYER 让 L2 抛了
- // `TypeError: Cannot read properties of undefined`,驱动器当场终止。
- const r = spawnSync(process.execPath, [HARNESS, PAGE], {
- encoding: "utf8",
- stdio: ["ignore", "pipe", "ignore"], // stderr 丢掉(桩的告警都在那边)
- });
- const out = r.stdout || "";
- const m = out.match(/^SUMMARY .*$/m);
- const fail = (out.match(/^ FAIL {2}(.+?) {2}\(actual=/gm) || []).map((s) =>
- s.replace(/^ FAIL {2}/, "").replace(/ {2}\(actual=$/, "")
- );
- return {
- summary: m ? m[0] : `**台子没跑完(status=${r.status})⇒ 没打 SUMMARY**`,
- fail,
- complete: !!m,
- };
- }
- let bad = 0;
- for (const mut of MUTATIONS) {
- const src = original.toString("utf8");
- const hits = src.split(mut.find).length - 1;
- if (hits !== 1) {
- console.log(`\n[${mut.id}] **变异体没生效**(锚点命中 ${hits} 次,应为 1)—— 读数作废`);
- bad++;
- continue;
- }
- fs.writeFileSync(PAGE, src.replace(mut.find, mut.repl), "utf8");
- const mutatedSha = sha(fs.readFileSync(PAGE));
- let res;
- try {
- res = runHarness();
- } finally {
- fs.writeFileSync(PAGE, original); // 按字节还原
- }
- const backSha = sha(fs.readFileSync(PAGE));
- const restored = backSha === baseSha;
- const missing = mut.expectRed.filter((tag) => !res.fail.some((f) => f.startsWith(tag)));
- // ★ 「没跑完」必须自成一档:那时 `fail` 是从崩溃前的残缺 stdout 里抠出来的,
- // 拿它去判「无害改动仍绿」会把「台子崩了」读成「没红 ⇒ 通过」。
- let verdict;
- if (!restored) verdict = "**还原失败**";
- else if (!res.complete) verdict = "**台子没跑完 ⇒ 读数不完整(先看它崩在哪)**";
- else if (mut.expectRed.length === 0)
- verdict = res.fail.length === 0 ? "OK(无害改动仍绿)" : "**无害改动竟红了 ⇒ 台子对无关改动也敏感**";
- else verdict = missing.length === 0 ? "OK(该红的都红了)" : `**预期该红却没红:${missing.join(", ")}**`;
- if (verdict.includes("**")) bad++;
- console.log(`\n[${mut.id}] ${mut.why}`);
- console.log(` 变异后 sha = ${mutatedSha}`);
- console.log(` ${res.summary}`);
- console.log(` 红的用例 = ${res.fail.length ? res.fail.join(" | ") : "(无)"}`);
- console.log(` 还原后 sha = ${backSha} ${restored ? "== 基线 OK" : "**!= 基线**"}`);
- console.log(` ⇒ ${verdict}`);
- }
- console.log(`\n基线复核 sha = ${sha(fs.readFileSync(PAGE))} ${sha(fs.readFileSync(PAGE)) === baseSha ? "(未被留脏)" : "**留脏**"}`);
- console.log(bad === 0 ? "\nMUTATE-SUMMARY: 全绿" : `\nMUTATE-SUMMARY: ${bad} 处不合格`);
- process.exitCode = bad === 0 ? 0 : 1;
|