fix(ci)+test(packaging): 关掉「门禁关死而自检全绿」三条路:outputs 绑定、判据步真跑、自检步指向
r2 复审:r1 那 11 条 + M3 已闭合(复审自己重放 12/12 红),但它新造的 40 条变异仍有 11 条漏网,
其中三条是「1 行改绿」:把 `probe.outputs.available` 改指向 `reason`/字面量(门永久关、每次绿);
判据步的退出码在标记表之外被吞(`|| echo 先忽略` / `; :` / `| cat` / `if !` / `true # 原行`,
复审真 bash 实测 exit 0);自检步被指向另一个脚本(自检根本不跑)。
三条 P1:
- 新判据 9:`probe.outputs.{available,log,png}` 必须逐个等于 `${{ steps.detect.outputs.<同名> }}`;
- 新判据 5:把 `id: judge` 的 `run` 原文真跑两次 —— 正例必须 exit 0、CPU 档日志必须 exit≠0,
且 `bash -e` 与**裸 bash** 各跑一遍(`; :` 只在后者现形,实测 `bash -e`=1 / 裸 bash=0)。
这一条一次封掉整类「吞码」,不依赖标记表;
- 判据 2:`id: wiring` 的 `run` 必须含 `gpu_shot_ci_selftest.py`,且 `vectors`/`wiring` 必须是**单条命令**
(不许换行/管道/`;`/`&`/反引号/`#`/`$(`)⇒ `|| echo 先忽略` 无从下手。
两条 P2:
- 新判据 10:`on:` 必须同时有 `push`(含 `main`)/`pull_request`/`workflow_call`;
- 「载体依赖」这条边界补进四处(自检 docstring / gpu-evidence.md §8 / README / workflow 头注释):
整个 workflow 被删掉就没有东西会跑自检,`guards.yml` 399/400 行塞不进去。
止步线(写进 docstring 与 §8):封闭清单 10 类 / 33 条变异向量;明确不覆盖「改判据脚本」「删掉整个
workflow」「求值器与 GitHub 语义差」「判据 4 的合法调用替换」;本文档不宣称「不可能被改成总是绿」;
**自 2026-11-11 起停止加固,扩项须由人类新开 mission**。
新增 `packaging/tests/gpu_shot_ci_expr.py`(`if:` 小求值器,判据脚本仍守 ≤400 行:377/215/64)。
This commit is contained in:
1 parent
2e9702790d
commit
8722c5a0ab
7 files changed
+358
-139
No files matched your search
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""`gpu-evidence.yml` 的**功能断言**部分(M253 / r1 复审返工):把 workflow 里的 `run:` 原文抽出来真跑。
|
||||
"""`gpu-evidence.yml` 的**功能断言**部分(M253 / r1+r2 复审返工):把 workflow 里的 `run:` 原文抽出来真跑。
|
||||
|
||||
为什么单独一份:判据脚本自己守 ≤400 行硬线,而这里全是「把 `run:` 落盘、用桩 env 起 bash、读
|
||||
`$GITHUB_OUTPUT` / `$GITHUB_STEP_SUMMARY`」的**执行脚手架**,与「哪条判据该红」的判断无关
|
||||
`$GITHUB_OUTPUT` / `$GITHUB_STEP_SUMMARY` / 退出码」的**执行脚手架**,与「哪条判据该红」的判断无关
|
||||
(同 `gpu_shot_selftest.py` 与 `check_gpu_shot.py` 的分工)。
|
||||
|
||||
被谁用:`packaging/tests/gpu_shot_ci_selftest.py` 的判据 4。它一次跑 7 种输入,因为字符串存在性
|
||||
检查被 r1 复审证伪过:旧版只要求「同一步的 run 里同时出现 4 个字符串」,于是把 `available` 写成
|
||||
恒 `false`(门禁永久关闭、每次运行都绿)它照样绿 —— **文案都在,执行路径却根本没走到**。
|
||||
两组功能断言,各自对应一类「绿而没判」:
|
||||
|
||||
7 种输入(前 4 条断言「跳过要留痕」,后 3 条断言「红线不许退化成跳过」):
|
||||
**A. `detect` 步(`probe_problems`)—— 跳过要留痕、红线不许退化成跳过。**
|
||||
字符串存在性检查被 r1 复审证伪过:旧版只要求「同一步的 run 里同时出现 4 个字符串」,于是把
|
||||
`available` 写成恒 `false`(门禁永久关闭、每次运行都绿)它照样绿 —— **文案都在,执行路径却没走到**。
|
||||
|
||||
| # | 输入 | 必须发生什么 |
|
||||
|---|------|-------------|
|
||||
@@ -21,18 +21,36 @@
|
||||
| S6 | dispatch 正例缺 PNG | `exit≠0`,同上 |
|
||||
| S7 | dispatch 负例只给日志 | `available=true`(`--expect-refuse` 允许省略 PNG) |
|
||||
|
||||
**B. `judge` 步(`judge_problems`)—— 判据步必须真的把红判成红。**
|
||||
r2 复审实测:判据命令后面加 `|| echo "先忽略"`、`; :`、去掉 pipefail 再 `| cat`、`if ! python3 …`、
|
||||
`true # python3 …`(保留子串锚点),五种写法的判据步**全 exit 0**(绿而没判),而标记表判据一条都没抓住。
|
||||
所以这里跑两次:正例成对必须 `exit 0`,CPU 档日志(`renderer=tiny-skia`)必须 `exit≠0`。第 2 条一红就
|
||||
同时封掉「吞码」整类,不依赖任何标记表。
|
||||
|
||||
**跑的是原文,只改一个令牌**:`run` 原文里 `python3` 换成本进程的解释器(Windows 上没有 `python3`;
|
||||
控制流、运算符、参数一字不改)。夹具用 `gpu_shot_selftest.synth_log/synth_png`,不另造第二套。
|
||||
判据步的 `cwd` 是仓库根(它要 `packaging/check_gpu_shot.py` 的相对路径)。
|
||||
|
||||
不降级:找不到能跑的 `bash` 就**判红**(「认不出/跑不了」≠「没问题」)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGING = Path(__file__).resolve().parent.parent
|
||||
REPO = PACKAGING.parent
|
||||
sys.path[:0] = [str(PACKAGING), str(Path(__file__).resolve().parent)]
|
||||
from gpu_shot_selftest import SELFTEST_SIZE, synth_log, synth_png # noqa: E402
|
||||
|
||||
_CACHE: dict[str, list[str]] = {}
|
||||
PYTHON_TOKEN = re.compile(r"\bpython3\b")
|
||||
|
||||
|
||||
def _find_bash() -> str | None:
|
||||
@@ -53,13 +71,17 @@ def _find_bash() -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _run(bash: str, script: Path, workdir: Path, env_extra: dict):
|
||||
"""跑一份 `run:` 原文,返回 `(CompletedProcess, GITHUB_OUTPUT 字典, GITHUB_STEP_SUMMARY 文本)`。"""
|
||||
def _run(bash: str, script: Path, workdir: Path, env_extra: dict, cwd: str | None = None, flags: tuple[str, ...] = ("-e",)):
|
||||
"""跑一份 `run:` 原文,返回 `(CompletedProcess, GITHUB_OUTPUT 字典, GITHUB_STEP_SUMMARY 文本)`。
|
||||
|
||||
`flags` 默认 `-e`(GitHub 的 `bash -e {0}`);判据步还会用**裸 bash**(不带 `-e`)再跑一遍 ——
|
||||
`... ; :` 这种写法只有在不靠 `-e` 兜底时才会把红吞掉(实测:`bash -e` 仍 exit 1,裸 bash exit 0)。
|
||||
"""
|
||||
out_path, sum_path = workdir / "gh_output", workdir / "gh_summary"
|
||||
env = os.environ.copy()
|
||||
env.update({"GITHUB_OUTPUT": out_path.as_posix(), "GITHUB_STEP_SUMMARY": sum_path.as_posix()})
|
||||
env.update({key: ("" if value is None else str(value)) for key, value in env_extra.items()})
|
||||
done = subprocess.run([bash, "-e", str(script)], cwd=str(workdir), env=env, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
done = subprocess.run([bash, *flags, str(script)], cwd=cwd or str(workdir), env=env, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
outputs: dict[str, str] = {}
|
||||
if out_path.exists():
|
||||
for line in out_path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
@@ -132,7 +154,7 @@ def _probe_scenarios(bash: str, script_text: str) -> list[str]:
|
||||
|
||||
|
||||
def probe_problems(script_text: str) -> list[str]:
|
||||
"""判据 4:`id: detect` 那一步的 `run` 原文实跑 7 种输入(带缓存,同文本只跑一次)。"""
|
||||
"""判据 A:`id: detect` 那一步的 `run` 原文实跑 7 种输入(带缓存,同文本只跑一次)。"""
|
||||
if not script_text.strip():
|
||||
return ["`id: detect` 那一步没有 `run:` —— 跳过路径无从执行"]
|
||||
if script_text not in _CACHE:
|
||||
@@ -143,3 +165,51 @@ def probe_problems(script_text: str) -> list[str]:
|
||||
else _probe_scenarios(bash, script_text)
|
||||
)
|
||||
return _CACHE[script_text]
|
||||
|
||||
|
||||
def _judge_scenarios(bash: str, script_text: str) -> list[str]:
|
||||
problems: list[str] = []
|
||||
with tempfile.TemporaryDirectory(prefix="ull-gpu-judge-", ignore_cleanup_errors=True) as tmp:
|
||||
root = Path(tmp)
|
||||
passing = root / "pass.stderr.log"
|
||||
passing.write_text(synth_log(size=SELFTEST_SIZE), encoding="utf-8")
|
||||
cpu = root / "cpu.stderr.log"
|
||||
cpu.write_text(synth_log(renderer="tiny-skia", size=SELFTEST_SIZE), encoding="utf-8")
|
||||
png = root / "pass.png"
|
||||
png.write_bytes(synth_png("content", SELFTEST_SIZE))
|
||||
script = root / "judge.sh"
|
||||
script.write_text(PYTHON_TOKEN.sub(lambda _match: f'"{Path(sys.executable).as_posix()}"', script_text), encoding="utf-8")
|
||||
common = {"PNG": png.as_posix(), "EXPECT_REFUSE": "false"}
|
||||
# 两种模式都跑:`-e` = GitHub 的默认 shell;裸 bash = 不靠 `-e` 兜底(`; :` 只在后者现形)。
|
||||
for mode, flags in (("bash -e", ("-e",)), ("裸 bash", ())):
|
||||
for label, log_path, want_zero in (
|
||||
("正例(六条判据全过)", passing, True),
|
||||
("CPU 档日志(`renderer=tiny-skia`,必须判红)", cpu, False),
|
||||
):
|
||||
done, _outputs, _summary = _run(bash, script, root, dict(common, LOG=log_path.as_posix()), cwd=str(REPO), flags=flags)
|
||||
if want_zero and done.returncode != 0:
|
||||
problems.append(
|
||||
f"[{mode}] 判据步对**正例**退出码 {done.returncode},应为 0 —— 「判据全过」这条路走不通了"
|
||||
f"(stderr: {(done.stderr or '').strip()[:200]})"
|
||||
)
|
||||
if not want_zero and done.returncode == 0:
|
||||
problems.append(
|
||||
f"[{mode}] 判据步对**必须判红**的输入退出码 0 —— 「绿而没判」:`|| echo` / `; :` / 管道 / `if !` / "
|
||||
"`true # 原行` / 裸 `exit 0` 都属于这一类"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def judge_problems(script_text: str) -> list[str]:
|
||||
"""判据 B:`id: judge` 那一步的 `run` 原文实跑两次(正例必须 0、CPU 档必须非 0)。"""
|
||||
if not script_text.strip():
|
||||
return ["`id: judge` 那一步没有 `run:` —— 判据步无从执行"]
|
||||
key = "judge:" + script_text
|
||||
if key not in _CACHE:
|
||||
bash = _find_bash()
|
||||
_CACHE[key] = (
|
||||
["找不到能跑的 `bash`:判据步的功能断言无法执行,本判据不降级(认不出就红)"]
|
||||
if not bash
|
||||
else _judge_scenarios(bash, script_text)
|
||||
)
|
||||
return _CACHE[key]
|
||||
Reference in new issue
Block a user