-
Notifications
You must be signed in to change notification settings - Fork 112
Expand file tree
/
Copy pathrunner.cppm
More file actions
283 lines (252 loc) · 11.3 KB
/
Copy pathrunner.cppm
File metadata and controls
283 lines (252 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// 调 `mcpp test --message-format json` 验证单个练习,并合并侧信道判定。
//
// 这一层不再生成任何清单、不再自己算退出码语义 —— 编译/运行的事实由
// mcpp 的 JSON 记录提供(status/exit_code/compile_output/run_output),
// 运行期语义(哪条断言挂了、路障拆没拆)由 harness 的侧信道 v2 提供,
// 两个来源在这里合并成 Provider 协议的 verdict。
module;
#include <cstdio>
#ifndef _WIN32
# include <sys/wait.h>
#endif
#include <stdlib.h> // setenv / _putenv_s
export module d2x.provider.runner;
import std;
namespace fs = std::filesystem;
namespace d2x::runner {
export struct Captured {
int exit_code{};
std::string output;
};
// 只要 stdout:JSON 协议流在 stdout,stderr 的人读错误信息这里不需要
// (包级失败在 stdout 也有 {"error":"package"} 记录)。
//
// 注:旧实现这里要先 unsetenv("LD_LIBRARY_PATH") 绕嵌套 mcpp 的 glibc
// 段错误 —— mcpp 已在上游根治(merged_environ 剥离私有 glibc 条目),
// workaround 随之删除。
//
// on_heartbeat:构建期的「还活着」信号,参数是已经等了多久;不传则完全不介入。
//
// 为什么需要它:`mcpp test --message-format json` 在整个构建期**一个字节都不
// 产出** —— 实测带不带 `-q` 都一样,stdout 只有末尾那两行 JSON、stderr 全空
// (机器可读模式下人读输出被整个收编进 JSON 了)。于是从 d2x 的视角看,Provider
// 从 stage("compile") 之后就彻底沉默,直到构建结束。
//
// 冷机第一次跑要在这段沉默里备工具链与 std 模块,一旦超过 d2x 的活性超时
// (provider_idle_timeout,默认 120s)就会被当成挂死而终止 —— 而且学习者改一次
// 文件就重试一次、每次都在同一处被杀,表现为「怎么改都过不去」。
//
// 心跳同时解决两件事:持续喂活 d2x 的计时器,并让学习者看见首次构建正在进行,
// 而不是对着黑屏怀疑卡死。
export Captured capture_stdout(const std::string& cmd,
const std::function<void(std::chrono::seconds)>& on_heartbeat = {},
std::chrono::seconds heartbeat_every = std::chrono::seconds{20}) {
Captured result;
// 丢弃 stderr 的写法必须分平台:_popen 走的是 cmd.exe,那里没有
// /dev/null —— `2>/dev/null` 会被当成「重定向到 \dev\null 这个路径」,
// 而 \dev 不存在,cmd 直接报 "The system cannot find the path specified."
// 并且整条命令根本不执行。于是 mcpp test 一条 JSON 都吐不出来,判定链
// 退化成「没有该测试的记录」,Windows 上每道题都卡在这里(d2x 的
// Win10/Win11 checker 冒烟实测)。空设备在 cmd 下叫 NUL。
#ifdef _WIN32
std::string full = cmd + " 2>NUL";
#else
std::string full = cmd + " 2>/dev/null";
#endif
#ifdef _WIN32
FILE* pipe = ::_popen(full.c_str(), "r");
#else
FILE* pipe = ::popen(full.c_str(), "r");
#endif
if (!pipe) return {127, std::format("failed to spawn: {}", cmd)};
// 读取交给工作线程,主线程才有机会按节奏发心跳。fgets 是阻塞的,单线程下
// 沉默期内根本回不到我们手里。
std::string collected;
std::atomic<bool> finished{false};
std::thread reader([&] {
char buf[4096];
while (std::fgets(buf, sizeof(buf), pipe)) collected += buf;
finished.store(true, std::memory_order_release);
});
if (on_heartbeat) {
const auto started = std::chrono::steady_clock::now();
auto next = started + heartbeat_every;
while (!finished.load(std::memory_order_acquire)) {
std::this_thread::sleep_for(std::chrono::milliseconds{200});
auto now = std::chrono::steady_clock::now();
if (now >= next) {
on_heartbeat(std::chrono::duration_cast<std::chrono::seconds>(now - started));
next = now + heartbeat_every;
}
}
}
reader.join(); // pipe 归 reader 用,必须先 join 再 pclose
result.output = std::move(collected);
#ifdef _WIN32
int status = ::_pclose(pipe);
result.exit_code = status;
#else
int status = ::pclose(pipe);
// pclose 回的是 wait status,转成真实退出码,否则 exit 1 会变成 256
result.exit_code = (status == -1) ? 127
: WIFEXITED(status) ? WEXITSTATUS(status)
: WIFSIGNALED(status) ? 128 + WTERMSIG(status)
: status;
#endif
return result;
}
// mcpp 的一条 per-test JSON 记录(我们关心的子集)。
export struct TestRecord {
std::string test; // 相对 tests/ 的路径名,如 00-auto-and-decltype/0
std::string status; // pass | compile_fail | run_fail
int exit_code{};
std::string compile_output;
std::string run_output;
};
export struct McppTestResult {
std::optional<TestRecord> record; // 精确匹配 test 名的那条
std::string package_error; // {"error":"package"} 的 compile_output
bool saw_any = false;
};
// 从一行 JSON 里取字段。格式由 mcpp --message-format json 产出:
// 字段固定、无嵌套对象(summary 行不取),不引入 JSON 库。
std::string field(std::string_view line, std::string_view key) {
auto pat = std::format("\"{}\":", key);
auto at = line.find(pat);
if (at == std::string_view::npos) return {};
at += pat.size();
if (at >= line.size()) return {};
if (line[at] == '"') { // 字符串值
++at;
std::string out;
while (at < line.size() && line[at] != '"') {
if (line[at] == '\\' && at + 1 < line.size()) {
++at;
switch (line[at]) {
case 'n': out += '\n'; break;
case 't': out += '\t'; break;
case 'r': out += '\r'; break;
case 'u': { // \u00XX —— mcpp 只对 <0x20 编码
if (at + 4 < line.size()) {
int v = 0;
auto hex = line.substr(at + 1, 4);
std::from_chars(hex.data(), hex.data() + 4, v, 16);
out += static_cast<char>(v);
at += 4;
}
break;
}
default: out += line[at];
}
} else {
out += line[at];
}
++at;
}
return out;
}
auto end = line.find_first_of(",}", at); // 裸值(数字/布尔/null)
return std::string(line.substr(at, end == std::string_view::npos ? end : end - at));
}
// 跑 `mcpp test` 并抽出目标测试的记录。pattern 是子串匹配,可能带出
// 邻居测试(如 …/1 匹配 …/10),所以逐行解析后按 test 名精确挑。
export McppTestResult run_mcpp_test(const std::string& member,
const std::string& test_name,
const fs::path& result_file,
const std::function<void(std::chrono::seconds)>& on_heartbeat = {}) {
std::error_code ec;
fs::remove(result_file, ec); // harness 是追加写的,清掉上一轮残留
fs::create_directories(result_file.parent_path(), ec);
#ifndef _WIN32
::setenv("D2X_RESULT_FILE", result_file.string().c_str(), 1);
#else
::_putenv_s("D2X_RESULT_FILE", result_file.string().c_str());
#endif
auto cmd = std::format("mcpp test -q -p {} {} --message-format json",
member, test_name);
auto cap = capture_stdout(cmd, on_heartbeat);
McppTestResult out;
std::istringstream lines(cap.output);
for (std::string line; std::getline(lines, line); ) {
if (line.empty() || line.front() != '{') continue;
if (line.find("\"error\":\"package\"") != std::string::npos) {
out.package_error = field(line, "compile_output");
out.saw_any = true;
continue;
}
auto name = field(line, "test");
if (name.empty()) continue; // summary 行等
out.saw_any = true;
if (name != test_name) continue;
TestRecord rec;
rec.test = name;
rec.status = field(line, "status");
rec.compile_output = field(line, "compile_output");
rec.run_output = field(line, "run_output");
auto raw = field(line, "exit_code");
std::from_chars(raw.data(), raw.data() + raw.size(), rec.exit_code);
out.record = std::move(rec);
}
return out;
}
// —— 侧信道判定(语义与设计稿 §5 的顺序表一致)——
export enum class Outcome { Pass, Fail, Blocked };
export std::string_view to_string(Outcome o) {
switch (o) {
case Outcome::Pass: return "pass";
case Outcome::Fail: return "fail";
case Outcome::Blocked: return "blocked";
}
return "fail";
}
export struct Failure {
std::string what;
std::string expected;
std::string actual;
std::string file;
int line{};
};
export struct RunReport {
Outcome outcome{Outcome::Pass};
std::vector<Failure> failures;
};
// 判定顺序(与代码一致;wait 必须先于退出码判定——d2x 库在存在未拆除的
// wait 时会将退出码置为 1,若先判退出码,blocked 会被整体误判为 fail):
// 有 ok:false → Fail,每条失败都能转成一个 Diagnostic
// 无失败、有 wait → Blocked(答案已对,只差拆路障)
// 无失败但退出码非 0 → Fail(纯崩溃 / 练习自己 return 非 0)
// 侧信道文件不存在 → 退回「退出码为 0 即通过」
//
// 最后一条让 harness 自动变成可选的:纯观察型练习可以是零依赖的
// 纯 C++ 文件,学习者能原样拷进 Compiler Explorer。
export RunReport judge_run(int exit_code, const fs::path& result_file) {
RunReport report;
std::ifstream in(result_file);
if (!in) {
report.outcome = (exit_code == 0) ? Outcome::Pass : Outcome::Fail;
return report;
}
bool saw_wait = false;
for (std::string line; std::getline(in, line); ) {
auto kind = field(line, "kind");
if (kind == "wait") { saw_wait = true; continue; }
if (kind != "assert") continue;
if (field(line, "ok") == "true") continue;
int line_no = 0;
auto raw = field(line, "line");
std::from_chars(raw.data(), raw.data() + raw.size(), line_no);
report.failures.push_back(Failure{
.what = field(line, "what"),
.expected = field(line, "expected"),
.actual = field(line, "actual"),
.file = field(line, "file"),
.line = line_no,
});
}
if (!report.failures.empty()) report.outcome = Outcome::Fail;
else if (saw_wait) report.outcome = Outcome::Blocked;
else if (exit_code != 0) report.outcome = Outcome::Fail;
else report.outcome = Outcome::Pass;
return report;
}
} // namespace d2x::runner