-
Notifications
You must be signed in to change notification settings - Fork 112
Expand file tree
/
Copy pathrunner.cppm
More file actions
244 lines (214 loc) · 9.09 KB
/
Copy pathrunner.cppm
File metadata and controls
244 lines (214 loc) · 9.09 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
// 调 `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 随之删除。
export Captured capture_stdout(const std::string& cmd) {
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)};
char buf[4096];
while (std::fgets(buf, sizeof(buf), pipe)) result.output += buf;
#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) {
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);
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