From bcd4f97e37a2505e08d5e799fa6abb2245104126 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 11:23:41 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20=E5=8F=98=E5=BC=82?= =?UTF-8?q?=E4=BD=93=E8=AE=A1=E6=95=B0=E6=94=B6=E8=BF=9B=E4=BB=93=E5=BA=93?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E5=89=8D=E9=9D=A2=E6=8A=A5=E8=BF=87=20?= =?UTF-8?q?40/48/58=20=E5=9B=9B=E4=B8=AA=E6=95=B0=EF=BC=8C=E6=A0=B9?= =?UTF-8?q?=E5=9B=A0=E6=98=AF"job=20=E9=9B=86=E5=90=88"=E4=BB=8E=E6=B2=A1?= =?UTF-8?q?=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 用 `/tmp/mut/` 复算后指出:48 也不对。他是对的,而且**不是记性问题、是口径问题**—— 我把 9 个 `jobs*.json` 的条目**直接相加**,没做归一:同一个变异体在跨批重锚时被键了多次 (13 组重复、15 条冗余),最典型的是「计划不搬运模糊值」同时挂在 `jobs-b3`/`jobs-blur`/`jobs-blur2` 三个文件、三个不同 `test` 键上 —— 于是"按判据文件分组求和"必然把它算三次, 而"跑在新增判据上的是多少"在交叉归类下**没有唯一答案**(41 或 35)。 更根本的是 pi 指出的第二层:**那个统计脚本根本不在 `/tmp/mut/` 里**(他为了复算是现写的), 而且 `/tmp` 会被清、不在仓库里 ⇒ 变异体数字**只活在信里**。 这一路已经立过同形状的规则(余额打在 `RESULT` 行、权威源在文件里),这条当时漏了。 ## 做了什么 - `client/electron/test/mutants/`:把 `mut.py`、`run.sh`、`jobs*.json`、`baseline.sha` 从 `/tmp` 挪进仓库(`/tmp` 会清、复核方够不着)。 - `mutants/summary.py`:**口径的唯一权威**,定义写死在代码里: · 不同变异体 = 按 (file, pat, repl) 去重(`retired` 不计); · 跑起来 = 锚点在该文件里**恰好命中 1 次**(与 mut.py 同一条件); · `on_new_criteria` = 该变异体的**每一个** test 键都指向本批新增的两个判据文件 (口径 A —— 不因交叉归类虚高;另报口径 B 作参考,它只增不减,不拿来报数)。 - `run-all.mjs` 的 `RESULT` 行播报它,并**顺带自证基线**:跑不起 `summary.py` 时 **不静默**(打印 status 与 stderr 末行)——我第一版路径写错,只看到"计数未知", 真因(`can't open file …/test/test/mutants/summary.py`)被吞掉了。 - `mutants/test-keys.json`:`test` 键 → 判据文件的**唯一来源**(`mut.py` 与 `summary.py` 共用)。此前两处各写一份,分叉过一次:键名从旧名换成 API 名后 `summary.py` 那份没跟上, 于是所有锚点被判 `hits=-1`、报出"51 个变异体全部 skipped"。 - 无歧义口径下的**当前真值**:`mutants=48 ran=48 skipped=0 on_new_criteria=36 baseline=7/7✓` (口径 B = 41;原始条目 66,其中 `retired` 5)。 - 清掉 pi 指出的三类脏数据: · **过期条目**(锚点是修复前的旧写法,`hits=0`)标 `retired` 5 条 —— 它们**不是"没跑成的变异体"**,重锚后都跑过、都红了;留着只会把 skipped 一直抬高; · **重复计数**(multipart 那条在两个文件里各一次)去重; · **真 skip** 的 multipart 锚点切片成 `name: 'file',\n contentType: mimeType` ⇒ 真的跑起来了 (此前命中 2 次,因为 `ApiClient.ets` 有两个 multipart 构造器)。 - 修两处并发/竞争:`mut.py` 的 `tempfile.mktemp()`(Py3 起 deprecated,**TOCTOU**)→ `mkstemp`; `run.sh` 的固定 `/tmp/mut/bak` → 按 `$$-$RANDOM` 唯一(并行跑会互相覆盖备份)。 ## 未做(如实说) - **`AdminUsersPage.ets` 有一处不是我做的改动留在工作树里**(11:20:48,我 11:21 的提交之后): `Chip(text, bg, fg: string)` → `ResourceColor`。核实过是**正确的 ArkTS 修法** (`Theme.surfaceMuted`/`textSubtle` 是 `Resource`、`chipNeutralBg` 是 `string`, 第 368 行的三目因此是 `Resource | string` ⇒ 旧签名**编译不过**)。 我**没有提交也没有回退**它 —— 工作树是共享的,不该替别人提交别人的活。 基线因此重算了(`baseline.sha` 顶部记了原因与哈希来源,重算本身是**有意动作**: 随手重算会把"某次变异没还原"永久掩盖掉)。 - Go 侧 `debt_registry_test.go` 仍未跑(沙箱无 Go 模块缓存),只做了 `gofmt`。 --- client/electron/test/mutants/baseline.sha | 20 ++ .../electron/test/mutants/jobs/jobs-all.json | 233 ++++++++++++++++++ .../electron/test/mutants/jobs/jobs-b3.json | 1 + .../electron/test/mutants/jobs/jobs-blur.json | 45 ++++ .../test/mutants/jobs/jobs-blur2.json | 59 +++++ .../electron/test/mutants/jobs/jobs-neg0.json | 1 + .../electron/test/mutants/jobs/jobs-one.json | 6 + .../electron/test/mutants/jobs/jobs-pi.json | 18 ++ client/electron/test/mutants/jobs/jobs1.json | 54 ++++ client/electron/test/mutants/jobs/jobs2.json | 22 ++ client/electron/test/mutants/mut.py | 36 +++ client/electron/test/mutants/run.sh | 29 +++ client/electron/test/mutants/summary.py | 118 +++++++++ client/electron/test/mutants/test-keys.json | 14 ++ client/electron/test/run-all.mjs | 28 ++- 15 files changed, 683 insertions(+), 1 deletion(-) create mode 100644 client/electron/test/mutants/baseline.sha create mode 100644 client/electron/test/mutants/jobs/jobs-all.json create mode 100644 client/electron/test/mutants/jobs/jobs-b3.json create mode 100644 client/electron/test/mutants/jobs/jobs-blur.json create mode 100644 client/electron/test/mutants/jobs/jobs-blur2.json create mode 100644 client/electron/test/mutants/jobs/jobs-neg0.json create mode 100644 client/electron/test/mutants/jobs/jobs-one.json create mode 100644 client/electron/test/mutants/jobs/jobs-pi.json create mode 100644 client/electron/test/mutants/jobs/jobs1.json create mode 100644 client/electron/test/mutants/jobs/jobs2.json create mode 100644 client/electron/test/mutants/mut.py create mode 100644 client/electron/test/mutants/run.sh create mode 100644 client/electron/test/mutants/summary.py create mode 100644 client/electron/test/mutants/test-keys.json diff --git a/client/electron/test/mutants/baseline.sha b/client/electron/test/mutants/baseline.sha new file mode 100644 index 0000000..12a998f --- /dev/null +++ b/client/electron/test/mutants/baseline.sha @@ -0,0 +1,20 @@ +# 变异体基线的**自证底本**:每个被变异过的文件在此记下"未变异"时的 sha256。 +# 跑完变异后 `sha256sum -c baseline.sha` 必须全 OK(summary.py 把结果打进 RESULT 行)。 +# +# 取基线是**有意的动作**,不是随手重算 —— 重算会把"某次变异没还原"永久掩盖掉。 +# 每次重算都要在此记一行"为什么": +# 2026-09-15 11:22 dsh:`AdminUsersPage.ets` 的哈希变了,**不是变异残留**。 +# 该文件被**另一个会话/进程**改过(11:20:48,我 11:21 的提交之后): +# `Chip(text, bg: string, fg: string)` → `Chip(text, bg: ResourceColor, fg: ResourceColor)`。 +# 核实过这是**正确的 ArkTS 修法**:`Theme.surfaceMuted`/`textSubtle` 是 +# `Resource`($r(sys.color.*)),`Theme.chipNeutralBg` 是 `string`, +# 第 368 行那个三目因此是 `Resource | string` ⇒ 旧签名**编译不过**。 +# 我没有提交它(不是我的改动,工作树是共享的,不该替别人提交或回退)。 +# 复核者若要判断"是不是变异残留",看这条记录 + `git diff HEAD -- <该文件>` 即可。 +4f3e0802346ba93740d7a6989fa6a9ef7dce16d1db59ea7402ff554127b07e3e client/harmony/entry/src/main/ets/model/AdminUsers.ts +c465b178ec1853ba66ac619e0d5614f48aef66db2ed2fecba25a4ae10e3dd13b client/harmony/entry/src/main/ets/model/ImagePrep.ts +bcf0f2e0cf159f3068181c015569a5d87765ce2cc39b546c401143434e76503a client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets +64ff7f0928e2c67c30ba75fe8f50d70ba6b48f0377c9e9d781b83b2a2e84a328 client/harmony/entry/src/main/ets/common/BackgroundPicker.ets +4c6b91b9211b457e0e121735c9ff94fd33a76bff99945642aec35566416d1e3b client/harmony/entry/src/main/ets/pages/SettingsPage.ets +0e2610589616aad712a33747c904201490c7019a44ed81b0a611e31c7f544015 client/harmony/entry/src/main/ets/api/ApiClient.ets +da65447b48d137e500effed9a014a011c2a30ac8bcafa80814c805b7b854e636 client/harmony/entry/src/main/ets/api/AppearanceApi.ets diff --git a/client/electron/test/mutants/jobs/jobs-all.json b/client/electron/test/mutants/jobs/jobs-all.json new file mode 100644 index 0000000..fd07549 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-all.json @@ -0,0 +1,233 @@ +[ + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": "return role === 'admin';", + "repl": "return role !== undefined && role.trim().toLowerCase() === 'admin';", + "test": "admin", + "why": "角色判定放宽" + }, + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": " if \\(!found\\) \\{\n out\\.push\\(item\\);\n \\}\n return out;", + "repl": " if (!found) {\n list.push(item);\n }\n return list;", + "test": "admin", + "why": "toggled 就地改入参" + }, + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": "if \\(v === undefined \\|\\| v\\.length === 0\\) \\{", + "repl": "if (v === undefined) {", + "test": "admin", + "why": "空串不当从未登录" + }, + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": " if \\(user\\.role === 'admin'\\) \\{\n return false;\n \\}\n", + "repl": "", + "test": "admin", + "why": "管理员也打受限徽标" + }, + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": "return '操作失败(服务端没有给原因)';", + "repl": "return '操作失败';", + "test": "admin", + "why": "兜底句不说原因缺席" + }, + { + "file": "client/harmony/entry/src/main/ets/model/AdminUsers.ts", + "pat": " return message;\n \\}\n if \\(!isApiError", + "repl": " return '操作失败';\n }\n if (!isApiError", + "test": "admin", + "why": "吞掉服务端文案" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "const longest: number = Math\\.max\\(width, height\\);", + "repl": "const longest: number = width;", + "test": "prep", + "why": "缩放按宽算" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "const scale: number = Math\\.min\\(1, maxEdge / longest\\);", + "repl": "const scale: number = maxEdge / longest;", + "test": "prep", + "why": "小图被放大" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "second\\.maxEdge = RETRY_EDGE;", + "repl": "second.maxEdge = MAX_EDGE;", + "test": "prep", + "why": "退档不降分辨率" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "second\\.quality = RETRY_QUALITY;", + "repl": "second.quality = FIRST_QUALITY;", + "test": "prep", + "why": "退档不降质量" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "return actualBytes > MAX_UPLOAD_BYTES;", + "repl": "return actualBytes >= MAX_UPLOAD_BYTES;", + "test": "prep", + "why": "等于上限也退档" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "if \\(sizeBytes > MAX_SOURCE_BYTES\\) \\{", + "repl": "if (sizeBytes >= MAX_SOURCE_BYTES) {", + "test": "prep", + "why": "边界 >= 而不是 >" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "if \\(!mimeType\\.startsWith\\('image/'\\)\\) \\{", + "repl": "if (false) {", + "test": "prep", + "why": "不判类型" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "return '上传失败:' \\+ serverMessage;", + "repl": "return '上传失败';", + "test": "prep", + "why": "吞掉上传失败文案" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "export const MAX_UPLOAD_BYTES: number = 3_670_016;", + "repl": "export const MAX_UPLOAD_BYTES: number = 6_291_456;", + "test": "prep", + "why": "上限超过服务端 4MiB" + }, + { + "file": "client/harmony/entry/src/main/ets/model/ImagePrep.ts", + "pat": "export const MAX_EDGE: number = 2560;", + "repl": "export const MAX_EDGE: number = 4096;", + "test": "prep", + "why": "首档最长边改 4096" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets", + "pat": " if \\(isRestricted\\(user\\)\\) \\{", + "repl": " if (false) {", + "test": "admin", + "why": "去掉受限徽标" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets", + "pat": " onlyStatus\\.status = 'active';", + "repl": " onlyStatus.status = 'active';\n onlyStatus.display_name = this.editDisplayName;", + "test": "admin", + "why": "启停顺带发 display_name" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets", + "pat": " \\.backgroundColor\\(Theme\\.surface\\)\n \\.borderRadius\\(Theme\\.radiusCard\\)\n \\.onClick\\(\\(\\) => \\{ this\\.toggleEdit\\(user\\); \\}\\)", + "repl": " .backgroundColor('#123456')\n .onClick(() => { this.toggleEdit(user); })", + "test": "admin", + "why": "写死色值 + 去卡片圆角" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat": " this\\.isAdmin = false;\n \\}", + "repl": " this.isAdmin = true;\n }", + "test": "admin", + "why": "身份读不到时乐观显示入口" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat": " if \\(this\\.isAdmin\\) \\{", + "repl": " if (true) {", + "test": "admin", + "why": "管理入口不做门禁" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat": " @State isAdmin: boolean = false;", + "repl": " @State isAdmin: boolean = true;", + "test": "admin", + "why": "isAdmin 初值 true" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "if \\(!overLimit\\) \\{\n\\s*break;", + "repl": "if (true) {\n break;", + "test": "prep", + "why": "永远只压第一档" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "if \\(packed === null \\|\\| overLimit\\)", + "repl": "if (packed === null)", + "test": "prep", + "why": "两档都超限不拒绝" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "overLimit = shouldRetryWithActual\\(usedBytes\\);", + "repl": "overLimit = false;", + "test": "prep", + "why": "结论恒 false" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "await packer\\.release\\(\\);", + "repl": "packer.release();", + "test": "prep", + "why": "packer.release 不 await" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "await head\\.release\\(\\);", + "repl": "head.release();", + "test": "prep", + "why": "head.release 不 await" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "desiredSize: \\{ width: size\\.width, height: size\\.height \\}", + "repl": "desiredSize: { width: info.size.width, height: info.size.height }", + "test": "prep", + "why": "按原图尺寸解码" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": " quality: Math\\.round\\(quality \\* 100\\)", + "repl": " quality: quality", + "test": "prep", + "why": "quality 不换算" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": " this\\.onUploaded\\(UPLOAD_OK_HINT\\);", + "repl": " this.bgKind = 'image';", + "test": "prep", + "why": "组件自己宣布成功" + }, + { + "file": "client/harmony/entry/src/main/ets/api/ApiClient.ets", + "pat": "name: 'file',\n contentType: mimeType", + "repl": "name: 'image',", + "test": "prep", + "why": "multipart 字段名不是 file(uploadBytes 那一处;此前锚点命中 2 次跑不起来,已切片)" + }, + { + "file": "client/harmony/entry/src/main/ets/api/AppearanceApi.ets", + "pat": "fileName, 'image/jpeg'\\);", + "repl": "fileName, 'application/octet-stream');", + "test": "prep", + "why": "上传 mime 发错" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/SettingsPage.ets", + "pat": " await store\\.syncFromServer\\(ctx, client\\);\n this\\.adoptSnapshot\\(store\\.current\\(\\), store\\.statusText\\(\\)\\);\n \\}\n\n /\\*\\* 把 store 里的快照复制进 @State", + "repl": " this.bgKind = 'image';\n }\n\n /** 把 store 里的快照复制进 @State", + "test": "prep", + "why": "上传成功后不重新同步" + } +] diff --git a/client/electron/test/mutants/jobs/jobs-b3.json b/client/electron/test/mutants/jobs/jobs-b3.json new file mode 100644 index 0000000..5181a2d --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-b3.json @@ -0,0 +1 @@ +[{"file":"client/harmony/entry/src/main/ets/model/Wallpaper.ts","pat":"plan\\.blurPx = normalizeBlur\\(blurPx\\);","repl":"plan.blurPx = 0;","test":"wallpaper","why":"计划不搬运模糊值"}] diff --git a/client/electron/test/mutants/jobs/jobs-blur.json b/client/electron/test/mutants/jobs/jobs-blur.json new file mode 100644 index 0000000..d818e19 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-blur.json @@ -0,0 +1,45 @@ +[ + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.blur\\(this\\.bgPlan\\.blurPx\\)", + "repl": ".blur(0)", + "test": "prep", + "why": "壁纸层忘了糊(滑杆能拖、壁纸不动)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.backgroundBlurStyle\\(BLUR_STYLE_OF\\[blurStyleFor\\(this\\.bgPlan\\.blurPx\\)\\] \\?\\? BlurStyle\\.NONE\\)", + "repl": ".backgroundBlurStyle(Theme.navMaterial)", + "test": "prep", + "why": "导航条材质又写死成固定档(不再跟用户偏好)", + "retired": "那句已按 pi 的复核改掉(导航条回到 Theme.navMaterial 固定档);当前形态由 jobs-pi.json 的锚点覆盖" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "'COMPONENT_THIN': BlurStyle\\.COMPONENT_THIN,", + "repl": "'COMPONENT_THINN': BlurStyle.COMPONENT_THIN,", + "test": "admin", + "why": "(锚点:那张表的键写错,判据在 appearance)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": " 'COMPONENT_REGULAR': BlurStyle\\.COMPONENT_REGULAR,\n", + "repl": "", + "test": "admin", + "why": "(锚点:表里漏一档)" + }, + { + "file": "client/harmony/entry/src/main/ets/model/Wallpaper.ts", + "pat": "plan\\.blurPx = normalizeBlur\\(blurPx\\);", + "repl": "plan.blurPx = 0;", + "test": "admin", + "why": "(锚点:计划不搬运模糊值)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "dark, snap\\.bgBlur\\)", + "repl": "dark, 0)", + "test": "admin", + "why": "(锚点:页面没把模糊档传给计划)" + } +] diff --git a/client/electron/test/mutants/jobs/jobs-blur2.json b/client/electron/test/mutants/jobs/jobs-blur2.json new file mode 100644 index 0000000..03acc84 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-blur2.json @@ -0,0 +1,59 @@ +[ + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.blur\\(this\\.bgPlan\\.blurPx\\)", + "repl": ".blur(0)", + "test": "appearance", + "why": "壁纸层忘了糊(滑杆能拖、壁纸不动)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.backgroundBlurStyle\\(BLUR_STYLE_OF\\[blurStyleFor\\(this\\.bgPlan\\.blurPx\\)\\] \\?\\? BlurStyle\\.NONE\\)", + "repl": ".backgroundBlurStyle(Theme.navMaterial)", + "test": "appearance", + "why": "导航条材质写死成固定档(不跟用户偏好)", + "retired": "同 jobs-blur.json 那条:旧写法已按 pi 的复核改掉;由 jobs-pi.json 的当前形态锚点覆盖" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "'COMPONENT_THIN': BlurStyle\\.COMPONENT_THIN,", + "repl": "'COMPONENT_THINN': BlurStyle.COMPONENT_THIN,", + "test": "appearance", + "why": "档位名 → 枚举表的键写错(真机上表现为拖滑杆没反应)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": " 'COMPONENT_REGULAR': BlurStyle\\.COMPONENT_REGULAR,\n", + "repl": "", + "test": "appearance", + "why": "表里漏一档(该档静默回落成不模糊)" + }, + { + "file": "client/harmony/entry/src/main/ets/model/Wallpaper.ts", + "pat": "plan\\.blurPx = normalizeBlur\\(blurPx\\);", + "repl": "plan.blurPx = 0;", + "test": "appearance", + "why": "计划不搬运模糊值" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "dark, snap\\.bgBlur\\)", + "repl": "dark, 0)", + "test": "appearance", + "why": "页面没把模糊档传给计划" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.blur\\(this\\.bgPlan\\.blurPx\\)", + "repl": ".blur(this.bgPlan.blurPx).blur(this.bgPlan.blurPx)", + "test": "nav", + "why": "壁纸层糊两遍(同一张底糊两次)" + }, + { + "file": "client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat": "\\.blur\\(this\\.bgPlan\\.blurPx\\)", + "repl": ".backgroundBlurStyle(BlurStyle.COMPONENT_THICK)", + "test": "nav", + "why": "壁纸层改用面板材质(同一张底糊两遍的形状)" + } +] diff --git a/client/electron/test/mutants/jobs/jobs-neg0.json b/client/electron/test/mutants/jobs/jobs-neg0.json new file mode 100644 index 0000000..a022824 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-neg0.json @@ -0,0 +1 @@ +[{"file":"client/harmony/entry/src/main/ets/model/Wallpaper.ts","pat":" if \\(!\\(r > 0\\)\\) \\{\n return 0;\n \\}","repl":" if (r < 0) {\n return 0;\n }","test":"wallpaper","why":"把 -0 那个修复改回去(判据该红)"}] diff --git a/client/electron/test/mutants/jobs/jobs-one.json b/client/electron/test/mutants/jobs/jobs-one.json new file mode 100644 index 0000000..2c196b1 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-one.json @@ -0,0 +1,6 @@ +[ + {"file":"client/harmony/entry/src/main/ets/api/ApiClient.ets", + "pat":"name: 'file',\n contentType: mimeType", + "repl":"name: 'image',\n contentType: mimeType", + "test":"prep","why":"multipart 字段名不是 file(uploadBytes 那一处)"} +] diff --git a/client/electron/test/mutants/jobs/jobs-pi.json b/client/electron/test/mutants/jobs/jobs-pi.json new file mode 100644 index 0000000..76d0568 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs-pi.json @@ -0,0 +1,18 @@ +[ + {"file":"client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat":"\\.backgroundBlurStyle\\(NAV_MATERIAL_OF\\[navMaterialFor\\(this\\.bgPlan\\.blurPx\\)\\] \\?\\? Theme\\.navMaterial\\)", + "repl":".backgroundBlurStyle(BlurStyle.COMPONENT_THICK)", + "test":"cross","why":"导航条绕开 Theme.navMaterial ⇒ navMaterial 变孤儿(pi 抓到的那个形状)"}, + {"file":"client/harmony/entry/src/main/ets/model/Appearance.ts", + "pat":" if \\(tier === 'NONE'\\) \\{\n // 壁纸可以清晰,导航条仍然要有玻璃\n return 'COMPONENT_THIN';\n \\}\n return tier;", + "repl":" return tier;", + "test":"nav","why":"去掉下限 ⇒ bg_blur=0 时导航条变透明(pi 抓到的那个真缺陷)"}, + {"file":"client/harmony/entry/src/main/ets/model/Appearance.ts", + "pat":"export function navMaterialFor\\(bgBlur: number\\): string \\{\n const tier: string = blurStyleFor\\(bgBlur\\);", + "repl":"export function navMaterialFor(bgBlur: number): string {\n const tier: string = 'COMPONENT_THIN';", + "test":"nav","why":"恒定最薄档 ⇒ 滑杆在导航条上是死控件(判据该红)"}, + {"file":"client/harmony/entry/src/main/ets/pages/MainPage.ets", + "pat":" 'COMPONENT_THICK': BlurStyle\\.COMPONENT_THICK\n\\};", + "repl":"};", + "test":"appearance","why":"导航空表里漏最厚档(该档静默回落成 fallback)"} +] diff --git a/client/electron/test/mutants/jobs/jobs1.json b/client/electron/test/mutants/jobs/jobs1.json new file mode 100644 index 0000000..f37e7b6 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs1.json @@ -0,0 +1,54 @@ +[ + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "if \\(!shouldRetryWithActual\\(usedBytes\\)\\) \\{\n\\s*break;", + "repl": "if (true) {\n break;", + "test": "prep", + "why": "永远只压第一档(不看真实字节数)", + "retired": "重锚后见 jobs2.json / jobs-all.json 的 `!overLimit` 那条(已跑已红)" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "if \\(packed === null \\|\\| shouldRetryWithActual\\(usedBytes\\)\\)", + "repl": "if (packed === null)", + "test": "prep", + "why": "两档都超限时不拒绝(把超限图直传成 413)", + "retired": "重锚后见 jobs2.json 的 `packed === null || overLimit`(已跑已红)" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "await packer\\.release\\(\\);", + "repl": "packer.release();", + "test": "prep", + "why": "ImagePacker.release 不 await" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "await head\\.release\\(\\);", + "repl": "head.release();", + "test": "prep", + "why": "head.release 不 await" + }, + { + "file": "client/harmony/entry/src/main/ets/api/ApiClient.ets", + "pat": "name: 'file',", + "repl": "name: 'image',", + "test": "prep", + "why": "multipart 字段名不是 file", + "retired": "同一条在 jobs-one.json 里有切片后的精确锚点(已跑已红)" + }, + { + "file": "client/harmony/entry/src/main/ets/api/ApiClient.ets", + "pat": "data: data", + "repl": "data: ''", + "test": "prep", + "why": "内存字节没放进 data(改走 filePath 那条不存在的路)" + }, + { + "file": "client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat": "desiredSize: \\{ width: size\\.width, height: size\\.height \\}", + "repl": "desiredSize: { width: info.size.width, height: info.size.height }", + "test": "prep", + "why": "不按目标尺寸解码(4K 图整图解码,低端机爆内存)" + } +] diff --git a/client/electron/test/mutants/jobs/jobs2.json b/client/electron/test/mutants/jobs/jobs2.json new file mode 100644 index 0000000..60ebfc8 --- /dev/null +++ b/client/electron/test/mutants/jobs/jobs2.json @@ -0,0 +1,22 @@ +[ + {"file":"client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat":"if \\(!overLimit\\) \\{\n\\s*break;", + "repl":"if (true) {\n break;", + "test":"prep","why":"永远只压第一档(重测)"}, + {"file":"client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat":"if \\(packed === null \\|\\| overLimit\\)", + "repl":"if (packed === null)", + "test":"prep","why":"两档都超限时不拒绝(重测)"}, + {"file":"client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat":"overLimit = shouldRetryWithActual\\(usedBytes\\);", + "repl":"overLimit = false;", + "test":"prep","why":"结论永远为 false(超限图被放过去)"}, + {"file":"client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat":"if \\(packed === null \\|\\| overLimit\\) \\{\n\\s*this\\.fail\\(TOO_LARGE_REASON\\);", + "repl":"if (packed === null) {\n this.fail(TOO_LARGE_REASON);", + "test":"prep","why":"拒绝分支里不带原因"}, + {"file":"client/harmony/entry/src/main/ets/common/BackgroundPicker.ets", + "pat":"desiredSize: \\{ width: size\\.width, height: size\\.height \\}", + "repl":"desiredSize: { width: info.size.width, height: info.size.height }", + "test":"prep","why":"不按目标尺寸解码(4K 图整图解码)"} +] diff --git a/client/electron/test/mutants/mut.py b/client/electron/test/mutants/mut.py new file mode 100644 index 0000000..0f7371f --- /dev/null +++ b/client/electron/test/mutants/mut.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""按"行范围替换"做变异(避开多行锚点的空白问题),跑判据,然后**逐字节还原**。""" +import subprocess, sys, hashlib, shutil, tempfile, os, re + +# test 键 → 判据文件:**唯一来源**是同目录的 test-keys.json +# (此前这里与 summary.py 各有一份,分叉过一次 —— 见那个文件的 `_` 说明) +import json as _json, os as _os +_KEYS = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'test-keys.json'), encoding='utf-8')) +TESTS = {k: v for k, v in _KEYS.items() if k != '_'} + +def run(path, pat, repl, testname, why): + # ★ 不用 tempfile.mktemp():它自 Py3 起 deprecated(**TOCTOU 竞争**:名字先给你、 + # 文件后创建,中间别人能占位)。而且它直接接受一个"还没创建的文件名", + # 正好踩在那个竞争上。改成**先原子创建**再往里拷。 + # (pi 2026-09-15 指出;同一族的另一个坑在 run.sh:那份用的是**固定**的 + # /tmp/mut/bak,两个变异并行跑会互相覆盖 —— 已改成按 pid/随机后缀。) + _fd, bak = tempfile.mkstemp(prefix='mut-bak-', suffix='.orig') + os.close(_fd) + shutil.copy2(path, bak) + before = hashlib.sha256(open(path,'rb').read()).hexdigest() + src = open(path, encoding='utf-8').read() + hits = list(re.finditer(pat, src)) + if len(hits) != 1: + print(f" [SKIP] 匹配 {len(hits)} 次(要 1 次): {why}") + os.unlink(bak); return + open(path,'w',encoding='utf-8').write(src[:hits[0].start()] + repl + src[hits[0].end():]) + r = subprocess.run(['node','--test',TESTS[testname]], capture_output=True, text=True) + shutil.copy2(bak, path); os.unlink(bak) + after = hashlib.sha256(open(path,'rb').read()).hexdigest() + assert before == after, f"!! 还原失败 {path} —— 必须立刻处理" + print(f" [{'红不了·缺陷' if r.returncode==0 else '红✓'}] {why}") + +if __name__ == '__main__': + import json + for job in json.load(open(sys.argv[1], encoding='utf-8')): + run(job['file'], job['pat'], job['repl'], job['test'], job['why']) diff --git a/client/electron/test/mutants/run.sh b/client/electron/test/mutants/run.sh new file mode 100644 index 0000000..704bbdb --- /dev/null +++ b/client/electron/test/mutants/run.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# 变异测试跑一次:$1=文件 $2=old $3=new $4=判据文件 $5=说明 +set -u +f="$1"; old="$2"; new="$3"; t="$4"; why="$5" +BAK="${TMPDIR:-/tmp}/mut-bak-$$-$RANDOM.orig" # ★ 不用固定名:并行跑会互相覆盖 +cp -a "$f" "$BAK" +before=$(sha256sum < "$f") +python3 - "$f" "$old" "$new" <<'PY' +import sys +p,old,new=sys.argv[1],sys.argv[2],sys.argv[3] +s=open(p,encoding='utf-8').read() +n=s.count(old) +if n!=1: + print(f" [SKIP] 锚点出现 {n} 次(要恰好 1 次):{old[:60]!r}"); sys.exit(3) +open(p,'w',encoding='utf-8').write(s.replace(old,new,1)) +PY +rc=$? +if [ $rc -ne 0 ]; then cp -a "$BAK" "$f"; rm -f "$BAK"; echo " [SKIP] $why"; exit 0; fi +node --test "$t" >/tmp/mut/out.txt 2>&1 +trc=$? +cp -a "$BAK" "$f" +rm -f "$BAK" +after=$(sha256sum < "$f") +if [ "$before" != "$after" ]; then echo " [!! 还原失败] $f 必须立刻处理"; exit 2; fi +if [ $trc -eq 0 ]; then + echo " [红不了·缺陷] $why" +else + echo " [红✓] $why" +fi diff --git a/client/electron/test/mutants/summary.py b/client/electron/test/mutants/summary.py new file mode 100644 index 0000000..684f01a --- /dev/null +++ b/client/electron/test/mutants/summary.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +变异体统计 —— **口径的唯一权威**(此前数字只活在信里,换过 40/48/58/41 四种说法)。 + +为什么需要它:同一个变异体可能出现在多个 `jobs*.json` 里、挂着不同的 `test` 键 +(跨批重锚留下的),于是"按判据文件分组求和"会把它算多次 —— 而"job 集合是什么" +以前没有定义,谁算都能得一个数。 + +口径(写死在这里,别在信里另说一套): + · 不同变异体 = 按 (file, pat, repl) 三元组去重(`retired: true` 的条目不计); + · 跑起来 = 该三元组的锚点在该文件里**恰好命中 1 次**(与 mut.py 同一条件); + · 跳过 = 锚点命中 ≠ 1; + · on_new_criteria = 该变异体的**每一个** test 键都指向本次新增的两个判据文件 + (口径 A:回答"新判据抓住了多少",不因交叉归类虚高); + · 另报口径 B(任一 test 键挂新增文件)作参考 —— 它**只增不减**,别拿来报数。 + +用法:python3 client/electron/test/mutants/summary.py +""" +import glob +import json +import os +import re +import subprocess +import sys +from collections import defaultdict + +HERE = os.path.dirname(os.path.abspath(__file__)) +JOBS_DIR = os.path.join(HERE, 'jobs') +# mutants/ → test/ → electron/ → client/ → 仓库根 +REPO = os.path.abspath(os.path.join(HERE, '..', '..', '..', '..')) + +# test 键 → 判据文件:**唯一来源**是同目录的 test-keys.json(与 mut.py 共用一份) +_KEYS = json.load(open(os.path.join(HERE, 'test-keys.json'), encoding='utf-8')) +TESTFILE = {k: v for k, v in _KEYS.items() if k != '_'} +NEW_CRITERIA = { + 'test/harmony-admin.test.mjs', + 'test/harmony-imageprep.test.mjs', +} + + +def main(): + entries = [] + for f in sorted(glob.glob(os.path.join(JOBS_DIR, 'jobs*.json'))): + for j in json.load(open(f, encoding='utf-8')): + j['_from'] = os.path.basename(f) + entries.append(j) + + active = [e for e in entries if not e.get('retired')] + retired = len(entries) - len(active) + + def anchor_hits(e): + path = e['file'] if os.path.isabs(e['file']) else os.path.join(REPO, e['file']) + try: + src = open(path, encoding='utf-8').read() + except OSError: + return -1 + return len(list(re.finditer(e['pat'], src))) + + by = defaultdict(list) + for e in active: + by[(e['file'], e['pat'], e['repl'])].append(e) + + ran = skipped = only_new = any_new = 0 + skipped_detail = [] + for key, group in by.items(): + h = anchor_hits(group[0]) + if h != 1: + skipped += 1 + skipped_detail.append((key, group, h)) + continue + ran += 1 + files = {TESTFILE.get(e['test'], e['test']) for e in group} + if files <= NEW_CRITERIA: + only_new += 1 + if files & NEW_CRITERIA: + any_new += 1 + + # 基线自证:变异跑完必须**逐字节还原**。这一条以前只在信里说("sha256sum -c 7/7 OK")—— + # 与"数字只在信里"同一个毛病。挪进 RESULT 行:万一某次变异把文件写坏了, + # 套件这一行会直接显形,而不是等下一个人去信里找。 + baseline_ok = None + bl = os.path.join(HERE, 'baseline.sha') + if os.path.exists(bl): + try: + r = subprocess.run(['sha256sum', '-c', bl], cwd=REPO, + capture_output=True, text=True, timeout=60) + # 底本里允许 `#` 注释(记"为什么重算基线"用)—— 计数与校验都要跳过它们, + # 否则注释会被当成"一项没还原"(我第一次加注释就踩了这个) + total = sum(1 for ln in open(bl, encoding='utf-8') + if ln.strip() and not ln.lstrip().startswith('#')) + ok = sum(1 for ln in (r.stdout or '').splitlines() if ln.endswith(': OK')) + baseline_ok = (ok == total, ok, total) + except Exception: + baseline_ok = (False, -1, -1) + + bl_note = '' + if baseline_ok is None: + bl_note = ' baseline=(没有 baseline.sha)' + else: + good, ok, total = baseline_ok + bl_note = f' baseline={ok}/{total}' + ('✓' if good else '✗**有文件没还原**') + + print(f'RESULT mutants={len(by)} ran={ran} skipped={skipped} ' + f'on_new_criteria={only_new}' + f'(口径A=只挂新判据 {only_new} / 口径B=任一挂新判据 {any_new} / ' + f'原始条目 {len(entries)},其中 retired {retired})' + bl_note) + if skipped_detail: + print('跳过(锚点命中≠1 ⇒ 跑不起来):') + for key, group, h in skipped_detail: + srcs = ', '.join(sorted({e['_from'] for e in group})) + print(f' hits={h} {key[0]} 「{group[0].get("why", "")}」 ({len(group)} 条条目:{srcs})') + print(' ⚠️ hits=0 通常是**过期条目**(锚点是旧写法)—— 请标 retired 或删除,') + print(' 否则它会把 skipped 一直抬高(方向与"让欠账显形"相反)。') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/client/electron/test/mutants/test-keys.json b/client/electron/test/mutants/test-keys.json new file mode 100644 index 0000000..78b8596 --- /dev/null +++ b/client/electron/test/mutants/test-keys.json @@ -0,0 +1,14 @@ +{ + "_": [ + "test 键 → 判据文件。**唯一来源**:mut.py 与 summary.py 都读这一份。", + "为什么要抽出来:两处各写一份时它们分叉过 —— summary.py 那份没跟上键名变化,", + "于是所有锚点被判 hits=-1('文件读不到'),报出 51 个变异体全部 skipped。", + "这个文件同时充当 mut.py 的 --list 与未知键的报错依据。" + ], + "admin": "test/harmony-admin.test.mjs", + "prep": "test/harmony-imageprep.test.mjs", + "appearance": "test/harmony-appearance.test.mjs", + "nav": "test/harmony-nav.test.mjs", + "cross": "test/cross-client-theme.test.mjs", + "wallpaper": "test/harmony-presets.test.mjs" +} diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 3b1c18e..3db5361 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -564,9 +564,35 @@ try { } catch (e) { debtsLine = ` debts=登记读不到(${e.code || e.message})`; } +/* + * 变异体计数**也进这一行**(pi 2026-09-15):此前它只活在信里,于是同一个量 + * 换过 40/48/58/41 四种说法,而"job 集合是什么"没有定义 —— 复核方拿到的是一个 + * "我复算不出你那个数、而且你的报数方法也不在场"的目录。 + * ⇒ 口径写死在 `test/mutants/summary.py`(唯一权威),这里只**播报**它, + * 并顺着同一条"要提醒人的东西必须走默认路径"的规则。 + * 跑不起来(summary.py 不在、python3 不在)**不静默** —— 明说"没跑", + * 免得下一个人把"这行没提"当成"没问题"。 + */ +let mutantsLine = ''; +try { + // ★ 路径要 join(HERE, …):`HERE` 是 `test/`(不是 `client/electron/`)—— + // 我第一版写成相对 `client/electron/` 的 'test/mutants/summary.py', + // 于是 python 报 "can't open file …/test/test/mutants/summary.py"(status=2), + // 而这里只看到"没打出 RESULT"。**错误信息被吞掉**正是这行注释要提醒的事: + // 播报失败时要把 stderr 一起说出来,别只说"未知"。 + const sp = spawnSync('python3', [join(HERE, 'mutants', 'summary.py')], + { cwd: HERE, encoding: 'utf8', timeout: 30000 }); + const m = /(RESULT mutants=\d+ ran=\d+ skipped=\d+ on_new_criteria=\d+)/.exec(sp.stdout || ''); + mutantsLine = m + ? ` ${m[1]}` + : ` mutants=(summary.py 没打出 RESULT:status=${sp.status} ${(sp.stderr || '').trim().split('\n').slice(-1)[0] || ''})`; +} catch (e) { + mutantsLine = ` mutants=(跑不了 summary.py:${e.code || e.message})`; +} + console.log(`RESULT phase=${PHASE} static=${STATIC_ONLY.length}${debtsLine} probe=${probeSummary}` + (unknownProbes.length ? `(探针跑不了:${unknownProbes.join('、')} —— 已按到期处理)` - : '(只能验形态的判据:到期前提成立就自动变红)')); + : '(只能验形态的判据:到期前提成立就自动变红)') + mutantsLine); console.log(`\n========== 判据汇总 ==========`); if (reds.length === 0 && brokens.length === 0) {