From 590a72a71543aa85aba8c33e865d7360b213021d Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 18 Sep 2026 04:07:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20job=20=E9=9B=86=E5=90=88?= =?UTF-8?q?=E4=BB=8E=20glob=20=E6=94=B9=E6=88=90**=E6=B8=85=E5=8D=95**?= =?UTF-8?q?=EF=BC=88+=20=E6=B8=85=E5=8D=95=E5=A4=96=E5=8D=B3=E6=8A=A5?= =?UTF-8?q?=EF=BC=8C=E4=B8=8E=20SUITE=20=E5=90=8C=E5=BD=A2=E7=8A=B6?= =?UTF-8?q?=EF=BC=89+=20RESULT=20=E8=A1=8C=E5=B8=A6=E9=9B=86=E5=90=88?= =?UTF-8?q?=E6=8C=87=E7=BA=B9=EF=BC=9Bbaseline=20=E6=92=AD=E6=8A=A5?= =?UTF-8?q?=E5=8C=BA=E5=88=86"=E5=BA=95=E6=9C=AC=E8=BF=87=E6=9C=9F"?= =?UTF-8?q?=E4=B8=8E"=E5=8F=98=E5=BC=82=E6=AE=8B=E7=95=99"=EF=BC=9B?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=94=BF=E7=AD=96=E8=A1=A5=E4=B8=8A**?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=8F=AF=E8=BF=9B=E5=85=A5=E6=80=A7**?= =?UTF-8?q?=EF=BC=88=E5=8E=9F=E6=9D=A5=E5=8F=AA=E7=AE=A1=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-18 报的三件,逐条实测后处置。 ## 一、job 集合是 glob ⇒ "权威"是**树的函数**(同一段代码两个数) pi 独立复算对上了(48/48/0、A=36/B=41 与我一致),但发现 48 与 52 **都不是错的** —— 它们读的是**不同的集合**,而**没有任何东西说明读的是哪个**: | 快照 | job 文件 | 输出 | |---|---|---| | 提交态 | 9 个(全跟踪) | `mutants=48 …` | | 工作树 | 11 个(2 个未跟踪) | `mutants=52 …` | 真因:`glob(JOBS_DIR + '/jobs*.json')` ⇒ **未跟踪的 job 文件静默进入统计**。 这与"写了判据忘了接线"同族,区别是 `run-all.mjs` 有**自检 2** 挡着(清单外的 `*.test.mjs` 直接红), 这边没有对等物。**本仓用"清单 + 清单外即红"解决过同一个问题两次,这是第三次。** **处置(pi 建议的前者)**:新增 `jobs.manifest.json` 显式列 job 集合,`summary.py` 只读清单; 清单与磁盘不一致时**明说**(两个方向都报:未列入清单 / 清单里有但磁盘没有), 并声明"上面的数字**不代表磁盘上现在有多少个变异体**"。 **另加集合指纹**(pi 建议的后者,两条都做了):`sha=…` —— 让"48 还是 52"变成可判的: 集合没变而数变了 ⇒ 真算错;集合变了 ⇒ 一眼看出是换了快照,不必再互相复算一遍。 **变异验证**(两个方向都试): ``` 加一个未列入清单的 job 文件 ⇒ 报「未列入清单:jobs-UNLISTED-probe.json」 且 mutants=**48**(未被静默计入)✓ 清单里加个不存在的文件 ⇒ 报「清单里有、磁盘上没有:jobs-GHOST.json」✓ ``` (`_` 开头的条目是说明,读时滤掉 —— 否则会被当成文件名,`ghosts` 假报一堆。) ## 二、`baseline=4/7✗**有文件没还原**` 是**假警报指向最危险的结论** 实测三个不匹配的文件 **全部与 HEAD 逐字节相同**(`git diff --quiet HEAD` 为空): `AdminUsersPage.ets`/`SettingsPage.ets` 是提交 `6861934`(09-17 21:15)改的、 `ApiClient.ets` 是 `9c6e9c6` 改的 ⇒ **底本过期,不是变异残留**。 ★ 而原来一律打"✗**有文件没还原**" —— 那会让人去翻变异,而真因只是底本没跟上提交。 **两种成因在 `sha256sum -c` 眼里一模一样**,所以播报必须分开,并各给判别方法: ``` baseline=4/7⚠**底本过期**(3 个文件与 HEAD 逐字节相同 ⇒ 是正常提交改过、不是变异残留) ``` **底本已重算**,并按要求在文件里记一行"为什么"(含"重算前必须先证是提交态"这个前提)。 **重算后仍在校验**(变异验证):给 `AppearanceApi.ets` 追加一行 ⇒ 立刻 `FAILED`;还原 ⇒ `7/7 OK`。 ## 三、`summary.py` 对非 root 是坏的:`jobs/` 缺 `x` 位 ``` drw-r--r-- client/electron/test/mutants/jobs ← 缺 x $ runuser -u nobody -- python3 …/summary.py PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json' ``` ⇒ 那份"口径的唯一权威"**只有 root 跑得起来**。已 `chmod 755`,非 root 复跑输出一致。 ★ pi 的深层判断成立且我核实了:**这条落在任何判据的射程之外** —— `deploy/check-file-modes.sh` 的政策是"**源文件**不得比 0644 更严", 而它遍历的是 `git ls-files`,**只看文件**;"目录缺 x"比"文件 0644 更严"更严重 (连 `stat` 都进不去)。 ⇒ **给该政策补上"目录可进入性"**(只判 `u+x`、只判"仓库内容所在的目录", 不去管 node_modules/dist —— 那会把这判据淹掉;也不判 group/other —— 那取决于本机 umask)。 **★ 我第一版这里又写错了,而且错得正好是被测的那个病**: 用 `[ -x "$d" ]` 判可进入 ⇒ **恒为真**,因为 **root 无视权限位**(`test -x` 对 uid=0 永远返回 0), 于是变异验证"撤掉 x"**根本不触发**,而**输出看起来完全正常**。 ⇒ 改成**直接读权限位**(与同文件里文件那段的位运算同一做法)。 改后变异验证:撤 x ⇒ `[FAIL] 目录不可进入:…(drw-r--r--,缺属主 x 位)`、退出码 1;还原 ⇒ 0。 ## 四、顺带修好三处既有权限违规(`check-file-modes.sh` 原来一直红着) ``` .githooks/pre-push 711 ⇒ 755(保留执行位,**不是**去掉) test/harmony-arkts.test.mjs 600 ⇒ 644 model/DeviceProbe.ts 600 ⇒ 644 ``` 三者都是**提交态**就这样(不是并发会话弄的)。按脚本自己的"药方按类型分岔"修的。 现在该政策 **退出码 0**。★ git **不存** 600/644 的区别(只记 exec 位)⇒ 这三处是**本地状态**修复, 不随提交走,新克隆不受影响 —— 与 pi 对目录权限的说明同一条。 ## 五、pi 报的另两件:**在 HEAD 上已经不存在了**(是过时读数) - **ArkTS 两处阻断**:`MainPage.ets` 的 import 现在在 71-74 行、最后一个 `const` 在 88 行 ⇒ **位置正确**;`AdminUsersPage.ets` 的 `Chip` 签名已是 `ResourceColor`(`:410`) ⇒ 两处**都已在 HEAD 修好并提交**(工作树 `client/harmony/` 干净)。`harmony-arkts` 判据 3/3 绿。 - **套件现状**:现在 **29** 个判据文件(不是 23),`unreported=0 broken=0`。 ## 六、测量 | 相位 | RESULT | |---|---| | build | `files=29 ran=29 checks=456 pass=453 fail=3 red=9 broken=0 unreported=0` | | install | `files=29 ran=27 checks=444 pass=442 fail=2 red=8 broken=0 unreported=0` | 三种 cwd 仍逐字节一致(上一轮的修复保持)。 --- client/electron/test/mutants/baseline.sha | 15 ++++- .../electron/test/mutants/jobs.manifest.json | 19 ++++++ client/electron/test/mutants/summary.py | 66 +++++++++++++++++-- deploy/check-file-modes.sh | 50 ++++++++++++++ 4 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 client/electron/test/mutants/jobs.manifest.json diff --git a/client/electron/test/mutants/baseline.sha b/client/electron/test/mutants/baseline.sha index c2b73b0..ba90163 100644 --- a/client/electron/test/mutants/baseline.sha +++ b/client/electron/test/mutants/baseline.sha @@ -16,10 +16,19 @@ # · `api/ApiClient.ets`:**别的会话**的提交 `69c2059`(JianFeeeee,11:43:22, # 推送客户端契约层)动过它;当前内容与 HEAD 逐字节相同(`git diff HEAD` 空)。 # 复核"是不是变异残留"的方法:`git diff HEAD -- <文件>` + 上面这些记录。 +# +# 2026-09-18 dsh:三个文件漂移,**已逐个核实是提交态、不是变异残留**(重算前的前提): +# · `AdminUsersPage.ets`、`SettingsPage.ets`:提交 `1be8318`(09-17 21:15, +# "底栏黑带 / 联系人点不开"那批)改过;`git diff --quiet HEAD` 为空 ⇒ 与 HEAD 逐字节相同。 +# · `ApiClient.ets`:提交 `fce5b91`(09-15 15:15,"鸿蒙客户端连不上服务器")改过;同上。 +# ★ 为什么必须先证这一步:底本**过期**与**变异残留**在 `sha256sum -c` 眼里一模一样, +# 而重算会把真正的残留**永久掩盖** —— 所以"重算"必须是一次**有记录**的动作。 +# ★ 顺带修了播报:原来一律打"有文件没还原"(指向最危险的结论), +# 而真因只是底本没跟上提交 ⇒ 现在两种分开报,并各自给出判别方法。 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 +4c56e7e762374a8d2a9de0779c3d5c716808ed15f961e962b15299394ef436dd 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 -db5b50105e4d5ea4a06e53a956ac349f39480bcb9de9acbfda825532550214a2 client/harmony/entry/src/main/ets/api/ApiClient.ets +b22023f2d05b431ef93b824f6d6f1ddec3eab08adde334fb32ea1680c8bbe813 client/harmony/entry/src/main/ets/pages/SettingsPage.ets +ae7bea2c3ade31661a1fcfcc660c015d7b10e5b097c1064d568628ec36fe93be 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.manifest.json b/client/electron/test/mutants/jobs.manifest.json new file mode 100644 index 0000000..68e6112 --- /dev/null +++ b/client/electron/test/mutants/jobs.manifest.json @@ -0,0 +1,19 @@ +[ + "_说明:这是 summary.py 读的 job 集合**清单**(不是 glob)。以 `_` 开头的条目是说明、不计入。", + "_为什么:glob 会让**未跟踪的** job 文件静默进入统计 —— 同一段代码、同一台机,", + "_ 提交态算 48、工作树算 52,而两者都没说读的是哪个集合(pi 2026-09-18 实测)。", + "_ 这与『写了判据忘了接线』同族;那边有 run-all.mjs 自检 2 挡着,这边现在有这份清单。", + "_新加 job 文件时:**必须**显式加到这里,否则 summary.py 会报『清单与磁盘不一致』。", + "jobs1.json", + "jobs2.json", + "jobs-all.json", + "jobs-arkts.json", + "jobs-arkts2.json", + "jobs-b3.json", + "jobs-blur.json", + "jobs-blur2.json", + "jobs-nav-contract.json", + "jobs-neg0.json", + "jobs-one.json", + "jobs-pi.json" +] diff --git a/client/electron/test/mutants/summary.py b/client/electron/test/mutants/summary.py index 684f01a..ed268b8 100644 --- a/client/electron/test/mutants/summary.py +++ b/client/electron/test/mutants/summary.py @@ -17,6 +17,7 @@ 用法:python3 client/electron/test/mutants/summary.py """ import glob +import hashlib import json import os import re @@ -40,9 +41,29 @@ NEW_CRITERIA = { 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) + # ★★ job 集合是**清单**,不是 glob(pi 2026-09-18)。 + # + # 原来这里是 `glob(JOBS_DIR + '/jobs*.json')` —— 于是**未跟踪的 job 文件会静默进入统计**: + # 同一段代码、同一台机,提交态算 48、工作树算 52,**而两者都没说读的是哪个集合**。 + # pi 的话:"『提交里有多少个变异体』和『磁盘上现在有多少个』可以不一致, + # 且没有任何东西会告诉你。" 这与"写了判据忘了接线"是**同一族** —— + # 区别是 `run-all.mjs` 有自检 2 挡着(清单外的 `*.test.mjs` 直接红),而这边没有对等物。 + # 本仓已经用"清单 + 清单外即红"解决过同一个问题**两次**(SUITE / 判据接线),这是第三次。 + manifest = os.path.join(HERE, 'jobs.manifest.json') + # `_` 开头的条目是**说明**(JSON 没有注释;与 test-keys.json 同一约定)—— + # 必须在读的时候就滤掉,否则它们会被当成文件名,`ghosts` 会假报一堆。 + listed = [x for x in json.load(open(manifest, encoding='utf-8')) + if not x.startswith('_')] + on_disk = sorted(os.path.basename(p) + for p in glob.glob(os.path.join(JOBS_DIR, 'jobs*.json'))) + unlisted = [f for f in on_disk if f not in listed] + ghosts = [f for f in listed if f not in on_disk] + for f in listed: # 只读**清单里**的,顺序也按清单 + p = os.path.join(JOBS_DIR, f) + if not os.path.exists(p): + continue + for j in json.load(open(p, encoding='utf-8')): + j['_from'] = f entries.append(j) active = [e for e in entries if not e.get('retired')] @@ -98,12 +119,49 @@ def main(): bl_note = ' baseline=(没有 baseline.sha)' else: good, ok, total = baseline_ok - bl_note = f' baseline={ok}/{total}' + ('✓' if good else '✗**有文件没还原**') + # ★ 措辞必须区分**两种完全不同的成因**(2026-09-18 实测): + # 底本不匹配既可能是"某次变异没还原"(危险,要立刻查), + # 也可能是"底本过期"(文件被**正常提交**改过,只是没重算底本)。 + # 原来一律打"✗**有文件没还原**",把后者也报成前者 —— + # 而实测这三处**全部与 HEAD 逐字节相同**(`git diff HEAD` 空)⇒ 是过期,不是残留。 + # ★ 方向要紧:这是**假警报指向最危险的结论**,会让人去翻变异、 + # 而真因只是底本没跟上。所以现在两种都报,并给出**各自的判别方法**。 + detail = [ln.split(':', 1)[0].strip() for ln in (r.stdout or '').splitlines() + if ln.endswith(': FAILED') or 'FAILED open or read' in ln] + dirty = [f for f in detail + if subprocess.run(['git', 'diff', '--quiet', 'HEAD', '--', f], + cwd=REPO, capture_output=True).returncode != 0] + if good: + bl_note = f' baseline={ok}/{total}✓' + elif detail and not dirty: + bl_note = (f' baseline={ok}/{total}⚠**底本过期**({len(detail)} 个文件与 HEAD 逐字节相同 ⇒' + f' 是正常提交改过、不是变异残留;重算 baseline.sha 并记一行"为什么")') + else: + bl_note = (f' baseline={ok}/{total}✗**{len(dirty) or len(detail)} 个文件既不在底本、' + f'也与 HEAD 不同 ⇒ 优先按"变异残留"查**') 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) + # ★ 集合自证:数字必须能回答"读的是哪个集合"(pi 2026-09-18 的第二个建议)。 + # 指纹让"48 还是 52"变成可判的:集合没变而数变了 ⇒ 真算错; + # 集合变了 ⇒ 只是换了快照,**一眼看得出**,不用再互相复算一遍。 + fp = hashlib.sha256('\n'.join(f'{f}:{os.path.getsize(os.path.join(JOBS_DIR, f))}' + for f in listed if os.path.exists(os.path.join(JOBS_DIR, f))) + .encode()).hexdigest()[:8] + print(f' 集合:清单 {len(listed)} 个 job 文件(未跟踪的**不会**静默进入统计)' + f' sha={fp} 集合指纹') + # ★ 清单与磁盘不一致 ⇒ 明说(与 `run-all.mjs` 自检 2 同形状:清单外即红)。 + if unlisted or ghosts: + print(' ★ 清单与磁盘不一致 —— 上面的数字**不代表"磁盘上现在有多少个变异体"**:') + for f in unlisted: + print(f' 未列入清单:{f}(新加的 job 文件必须显式加进 jobs.manifest.json)') + for f in ghosts: + print(f' 清单里有、磁盘上没有:{f}') + print(' 修法:把该加的文件加进 jobs.manifest.json,或把该删的条目删掉。') + print(' (为什么不能靠 glob:未跟踪的文件会**静默**进入统计,' + '同一段代码两个数、而没人知道读的是哪个集合。)') if skipped_detail: print('跳过(锚点命中≠1 ⇒ 跑不起来):') for key, group, h in skipped_detail: diff --git a/deploy/check-file-modes.sh b/deploy/check-file-modes.sh index c72812a..a69b398 100755 --- a/deploy/check-file-modes.sh +++ b/deploy/check-file-modes.sh @@ -74,6 +74,56 @@ while IFS= read -r line; do fi done < <(git ls-files -s | awk '{print $1" "$4}') +# ══════════════════════════════════════════════════════════════════════════════ +# ★★ 目录的**可访问性**(2026-09-18,pi 报的洞) +# +# 上面那段只遍历 `git ls-files` ⇒ **只看文件**。而"目录缺 `x`"比"文件 0644 更严"**更严重**: +# 目录没有 `x` 就**不能访问其中任何文件**(连 stat 都不行),而 root 绕过这一层, +# 所以维护者**永远不会在自己身上看到症状**。实测(pi 报、我复现): +# +# $ stat -c '%A %n' client/electron/test/mutants/jobs +# drw-r--r-- client/electron/test/mutants/jobs ← 缺 x +# $ runuser -u nobody -- python3 …/summary.py +# PermissionError: [Errno 13] Permission denied: '…/jobs/jobs-all.json' +# +# ⇒ 那份"口径的唯一权威"在这份工作树里**只有 root 跑得起来**,而它落在**任何判据的射程之外** +# (本政策只看文件;权限位判据当初就是为"该不该算漂移"立的,覆盖的形状是"文件")。 +# +# ⚠️ 范围与取向(**保守,只判"不可访问"**): +# · git **不存目录权限** ⇒ 这是**本地状态**,不跟着提交走、新克隆不受影响。 +# 所以这里判的是"**这份工作树此刻能不能被别人用**",不是"仓库内容对不对"。 +# · 只判**有 `git ls-files` 条目的目录**(即"仓库内容所在的目录"), +# 不去管 node_modules / dist 这类产物的目录 —— 那会把这条判据淹掉。 +# · 只判 `u+x`(**属主**可进入):这是"别把自己锁在外面"的最低要求, +# 不去管 group/other —— 那取决于本机 umask 与身份,判严了会到处误红。 +# ══════════════════════════════════════════════════════════════════════════════ +dir_bad=0 +dir_worst="" +while IFS= read -r d; do + [ -n "$d" ] && [ -d "$d" ] || continue + # ⚠️⚠️ **不许用 `[ -x "$d" ]`** —— 这是我第一版写的,而它**恒为真**(我实测抓到的): + # root **无视权限位**(这正是本政策存在的理由之一),所以 `test -x` 对 uid=0 + # **永远返回 0**,哪怕目录是 `drw-r--r--`。⇒ 变异验证时"撤掉 x"根本不触发, + # 而**输出看起来完全正常**(这正是被测的那个病本身长在判据上)。 + # ⇒ 必须**直接读权限位**(与上面文件那段的 `o/g/u` 位运算同一个做法)。 + dperm="$(stat -c '%a' "$d" 2>/dev/null)" || continue + dm="${dperm: -3}" + [ "$(( ${dm:0:1} & 1 ))" -eq 1 ] && continue # 属主有 x ⇒ 可进入,放行 + dir_bad=$((dir_bad + 1)) + [ -z "$dir_worst" ] && dir_worst="$d" + printf ' [FAIL] 目录不可进入:%s(%s,缺属主 x 位)\n' "$d" "$(stat -c '%A' "$d" 2>/dev/null)" >&2 +done < <(git ls-files | awk -F/ 'NF>1 { for (i=1;i&2 + printf ' 药方:chmod +x <目录>(**只加执行位,别动读位**)—— 目录的 `x` 是"能进入/能 stat 里面"。\n' >&2 + printf ' 为什么单列一条:目录缺 `x` 时,**里面每个文件都读不到**,而 root 绕过它 ⇒\n' >&2 + printf ' 维护者在自己身上永远看不到症状,只有换个身份(或别人来跑)才炸。\n' >&2 + printf ' git 不存目录权限 ⇒ 这是**本地状态**,不跟提交走(新克隆不受影响)。\n' >&2 + printf ' 这是**政策问题**(工作树应当能被其它身份使用),不是代码问题。\n' >&2 + exit 1 +fi + if [ "$count" -gt 0 ]; then printf '\n 共 %d 个源文件权限不对(首个:%s)。\n' "$count" "$worst" >&2 printf ' 药方(**按类型分岔**):非可执行文件补齐到 644;git 记为 100755 的补齐到 755。\n' >&2