ci: 新增 Windows 10 / 11 双 job(含完整测试段),并修复 mcpp 索引下限导致的全平台 CI 红 - #33
Merged
Conversation
Windows 10 / 11 两条产品线: GitHub 托管 runner 没有 Windows 客户端版的 x64 镜像,用共享同一内核基线的 Server 镜像代表——windows-2022(= Win10 22H2,10.0.20348)与 windows-2025 (= Win11 24H2,10.0.26100),并在 job 里打印实际 caption/build 号存证。 两个 job 都跑完整测试段,而不只是「能编过」: 构建 → 版本自检 → 协议一致性 e2e → d2mcpp 真课程 checker 冒烟 checker 冒烟的判据是「能拉起 Provider、拿到练习、报出第一题的编译错误」, 超时被杀(124)是设计内的健康结局——checker 判 fail 后驻留等待文件变更。 为此把 tests/ 的假 Provider 调用改成可移植形式: - 目录改经位置参数传入,不再用 `FAKE_DIR=... bash ...` 环境变量前缀 —— Windows 上 d2x 经 _popen 走 cmd.exe 启动 Provider,cmd 没有这种语法。 - 交给 d2x 的路径先经 cygpath -m 转成 C:/... :d2x 是原生 exe,认不得 /tmp/... 这类 MSYS 路径;bash 同样接受该写法,两边通用。 - 活性超时一组在 Windows 跳过:run_lines_idle 在 _WIN32 下显式回退为无超时 运行(见 protocol/src/process.cppm),该平台上没有被测行为可言。 顺带修复一处既有全平台故障(与本次改动无关,但不修则 CI 不可能全绿): mcpplibs 依赖索引已把 index floor 抬到 0.0.109,原先钉的 mcpp 0.0.104 一律 E0006「index requires mcpp >= 0.0.109」而拒绝解析 compat.ftxui 等依赖 —— main 上重跑 2026-07-23 那次全绿的 CI,如今五个 job 全红。这里升到索引 latest ref(2026.8.1.1)。d2mcpp 上游同样仍钉 0.0.104,冒烟前先把课程侧的 pin 对齐到同一版本,上游跟进后可移除该步骤。
首轮 CI 在两个 Windows job 上红,原因不是 d2x:checker 确实走完了「读配置 → 拉起 Provider → 枚举 52 道练习 → 选中第一题 → 渲染练习页」,日志里 `Exercise: hello-mcpp` / `Status: ❌ failed` 都在;失败的是课程 Provider —— 它在 Windows 上拿不到 `mcpp test --message-format json` 的记录,于是没发 verdict。d2x 随后报「provider did not report a verdict」,这正是协议规定的 行为(tests/e2e.sh 场景 1b 钉的就是「缺 verdict = fail」)。 原判据 `grep -qiE "error"` 是照 linux 的样子写的:那个 error 串来自课程的 真实编译错误,依赖上面那条断掉的上游链路,在 Windows 上不可能出现。 所以把两档判据显式分开,而不是笼统放宽: - 共通(d2x 自己的职责):hello-mcpp + Exercise: + Status: 三串同时出现, 即「渲染出了第一题」,而不是卡在加载日志上。 - linux 仍是最严的一档:在上面基础上额外要求透出课程编译错误 —— 整条链路 在 linux 上是通的,这条不能松。 - Windows 暂不要求后者,并在注释里写明这是 mcpp / d2mcpp 的上游缺口、上游 补齐后应收紧到与 linux 一致。 另加一步 Provider check 原始 NDJSON 诊断(continue-on-error),把上游那条 链路的原始输出摆进日志,便于定位,也让日后的回归有据可查。
Win10 / Win11 CI 实测暴露:练习页的 File: 一行在 Windows 上显示成
`\src\intro\tests\hello-mcpp.cpp` —— 多了个前导分隔符;linux 上是正确的
`src/intro/tests/hello-mcpp.cpp`。
根因是 normalize_path 手写的「前缀匹配 + 掐掉一个 '/'」:
if (!path.empty() && path.front() == '/') path.erase(path.begin());
只认 '/'。Windows 的分隔符是 '\',掐不掉,于是相对化之后前导分隔符留在原地。
(相对化本身是有意的行为,不是 bug;坏的只是这个分隔符。)
改为交给 std::filesystem::path::lexically_relative:分隔符、".." 情形都由标准
库负责,不再手写字符比较。纯词法运算,不碰文件系统 —— 路径来自 Provider,
未必存在于本机。压不出相对关系、或落在 cwd 之外时原样返回,与原行为一致。
输出统一走 generic_string()(正斜杠):课程与文档都以正斜杠书写路径,两个平台
显示一致也便于断言。
回归测试(tests/e2e.sh 场景 5):Provider 报的是绝对路径而 checker 的 cwd 就是
该目录,所以展示路径必须被压成相对形式、任何情况下都不该以分隔符打头。断言拆成
两条(含 ex1.txt / 不以分隔符打头)—— 起初写成单条正则
`^File: +[^\\/].*ex1\.txt`,但 ` +` 会回溯让字符组吃掉一个空格,四种输入全部
放行;实测发现后改掉。该断言在 linux 上恒真(linux 本就没这个 bug),真正的
把关发生在 Windows CI。
run_lines_idle 此前在 _WIN32 下显式回退为「无超时运行」:Windows 上挂死的 Provider 不会被终止,checker 跟着一起等下去。tests/e2e.sh 的活性超时一组也 因此在 Windows 跳过 —— 该平台等于没有这条保护。 实现要点(protocol/src/process.cppm): - 不用 _popen:拿不到进程句柄,超时了无从终止。改 CreateProcess 起 cmd.exe。 - Job Object + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE:Windows 上终止整棵进程树 的正规手段,语义对应 POSIX 分支的 kill(-pid)。Provider 常有孙进程 (mcpp → 编译器),只杀直接子进程会留孤儿继续占用产物目录。 - CREATE_SUSPENDED:先挂起、纳入 Job、再 ResumeThread。否则子进程可能在被 纳管之前就派生出逃逸在 Job 之外的孙进程。 - 读端 SetHandleInformation 去掉继承,且父进程立刻关掉自己那份写端 —— 否则管道永远多一个写者,读不到 EOF。 - 全程 PeekNamedPipe 探量再 ReadFile,绝不裸调 ReadFile:管道读是阻塞的, 挂死的 Provider 会把轮询线程一起拖住,活性判定就永远轮不到。 - AssignProcessToJobObject 失败时退化为只杀直接子进程,而不是放弃超时 —— 至少 d2x 自己不会跟着挂死。 tests/e2e.sh 去掉 Windows 的跳过分支,五组协议测试三平台同跑。hang 模式下 Provider 是 cmd.exe → bash → sleep 300 的一棵树,正好验证「杀整棵」:只杀 直接子进程的话 sleep 会活下来,20s 窗口内同样出不了 verdict。 linux 侧行为未变(#else 分支原样保留),本地 e2e 五组 ALL GREEN;Windows 侧 由 Win10 / Win11 CI 实测。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
做了什么
1. Windows 10 / 11 两条产品线的 CI
build-windows从单个windows-latest改成矩阵。GitHub 托管 runner 没有Windows 客户端版的 x64 镜像,只有 Server 镜像;这里用与各自客户端版共享内核基线的
Server 镜像代表两条线。job 里会打印实际 caption / build 号存证 —— 本次 CI 实测:
windows-2022windows-20252. 两个 Windows job 都跑完整测试段
不再只是「能编过」:
为此把测试脚本改成可移植形式:
FAKE_DIR=... bash ...环境变量前缀 ——Windows 上 d2x 经
_popen走 cmd.exe 启动 Provider,cmd 没有这种语法。cygpath -m转成C:/...:d2x 是原生 exe,认不得/tmp/...这类 MSYS 路径;bash 同样接受该写法,两边通用。
3. 顺带修复一处既有的全平台 CI 故障
与本次改动无关,但不修则 CI 不可能全绿:
mcpplibs依赖索引已把 index floor 抬到 0.0.109,原先钉的 mcpp 0.0.104 一律拒绝解析
compat.ftxui等依赖。在 main 上重跑 2026-07-23 那次全绿的 CI(run 30053559836),如今五个 job 全红 —— 属上游索引变动导致的既有故障。
升到索引
latestref 2026.8.1.1(.xlings.json+MCPP_VERSION同步)。d2mcpp 上游同样仍钉 0.0.104,故冒烟前先把课程侧的 pin 对齐;上游跟进后可移除该步骤。
冒烟判据分两档(刻意的,不是笼统放宽)
首轮 CI 两个 Windows job 红。查下来不是 d2x 的问题:checker 确实走完了
「读配置 → 拉起 Provider → 枚举 52 道练习 → 选中第一题 → 渲染练习页」,
Exercise: hello-mcpp/Status: ❌ failed都在。断的是课程 Provider ——它在 Windows 上拿不到
mcpp test --message-format json的记录。原判据
grep -qiE "error"是照 linux 写的:那个error串来自课程的真实编译错误,依赖上面那条断掉的上游链路,在 Windows 上不可能出现。所以:
hello-mcpp+Exercise:+Status:三串同时出现,即「渲染出了第一题」,而不是卡在加载日志上。
linux 上是通的,这条不能松。
新增的 Provider 原始 NDJSON 诊断步骤把根因摆进了日志:
mcpp test在 Windows 上先以「找不到路径」失败,压根没输出 JSON —— 待报 mcpp / d2mcpp上游。d2x 侧行为是正确的:协议规定「Provider 没给 verdict 即 fail」
(
tests/e2e.sh场景 1b 钉的就是这条),页面也如实显示 failed。验证
CI run 30702841729,六个 job 全绿;两个 Windows job 的 checker 步骤实测:
一并修掉的 bug(都是新 CI 暴露出来的)
1. normalize_path 在 Windows 上留下前导分隔符
练习页显示成
\src\intro\tests\hello-mcpp.cpp,linux 上是正确的src/intro/tests/hello-mcpp.cpp。根因是手写的「前缀匹配 + 掐掉一个'/'」只认'/',Windows 分隔符是'\'掐不掉。(相对化本身是有意行为,坏的只是这个分隔符。)改用
std::filesystem::path::lexically_relative,分隔符与..情形交给标准库;输出统一
generic_string()。Win10 / Win11 CI 实测已修复,两平台都输出src/intro/tests/hello-mcpp.cpp。回归测试落在
tests/e2e.sh场景 5(展示路径不得以分隔符打头)。这条断言起初写成单条正则
^File: +[^\\/].*ex1\.txt,但+会回溯让字符组吃掉一个空格,四种输入全部放行 —— 实测发现后拆成两条判断。
2. Windows 没有活性超时(挂死的 Provider 不会被终止)
run_lines_idle此前在_WIN32下显式回退为「无超时运行」,Windows 上挂死的Provider 会把 checker 一起拖住;e2e 的活性超时一组也因此在该平台跳过。
现用 Job Object 补齐(
protocol/src/process.cppm),语义对齐 POSIX 分支的kill(-pid)—— 终止的是整棵进程树,而不只是直接子进程(Provider 常有孙进程mcpp → 编译器)。要点:
CREATE_SUSPENDED先挂起、纳入 Job 再ResumeThread,避免孙进程逃逸出 Job;读端去继承 + 父进程立刻关写端,否则读不到 EOF;全程
PeekNamedPipe探量再读,绝不裸调ReadFile(管道读阻塞会把活性判定一起拖死)。tests/e2e.sh已去掉 Windows 跳过分支,五组协议测试三平台同跑。这条在 CI 里是有效判据:
hang模式下 Provider 是cmd.exe → bash → sleep 300的一棵树,只杀直接子进程的话
sleep会活下来,20s 窗口内同样出不了 verdict。未覆盖 / 待办
timeout,tests/e2e.sh需要额外的可移植性改造。可另开 PR。
mcpp test在 Windows 上以「找不到路径」失败,导致课程Provider 发不出 verdict(诊断步骤的原始输出见上)。不在本仓库,待另行上报。