| 1259 |
1259 |
|
.await);
|
| 1260 |
1260 |
|
};
|
| 1261 |
1261 |
|
|
|
1262 |
+ |
// Docs integrity, first and cheapest: run the staged binary's DB-free
|
|
1263 |
+ |
// `MNW_CHECK_DOCS` mode before creating the throwaway DB. A broken internal
|
|
1264 |
+ |
// docs link (a `[..](x.md)` resolving to a slug no page serves) fails here
|
|
1265 |
+ |
// in well under a second instead of after a full migrate+seed+boot, and a
|
|
1266 |
+ |
// rotted link never reaches prod as a live 404. Collisions are reported by
|
|
1267 |
+ |
// the check but do not fail it; only broken links do.
|
|
1268 |
+ |
if let Some(outcome) = code_smoke_docs_check(ctx, &bin, &mut log_buf).await {
|
|
1269 |
+ |
return Ok(finish(outcome.with_log_ref(log_ref), log_buf).await);
|
|
1270 |
+ |
}
|
|
1271 |
+ |
|
| 1262 |
1272 |
|
let dbname = code_smoke_db_name(&ctx.version);
|
| 1263 |
1273 |
|
let maintenance_url = pg_url_with_dbname(scratch_url, "postgres");
|
| 1264 |
1274 |
|
let throwaway_url = pg_url_with_dbname(scratch_url, &dbname);
|
| 1289 |
1299 |
|
Ok(finish(outcome.with_log_ref(log_ref), log_buf).await)
|
| 1290 |
1300 |
|
}
|
| 1291 |
1301 |
|
|
|
1302 |
+ |
/// Run the staged binary's DB-free docs integrity check (`MNW_CHECK_DOCS=1`).
|
|
1303 |
+ |
///
|
|
1304 |
+ |
/// Returns `Some(failed)` if the check reports broken links, cannot be spawned,
|
|
1305 |
+ |
/// or overruns its ceiling; `None` when the docs are clean. Output is appended
|
|
1306 |
+ |
/// to `log_buf` either way. The 60s ceiling backstops the case where the staged
|
|
1307 |
+ |
/// binary predates the flag and would fall through to a normal (DB-needing)
|
|
1308 |
+ |
/// boot and hang.
|
|
1309 |
+ |
async fn code_smoke_docs_check(
|
|
1310 |
+ |
ctx: &GateCtx,
|
|
1311 |
+ |
bin: &str,
|
|
1312 |
+ |
log_buf: &mut Vec<u8>,
|
|
1313 |
+ |
) -> Option<GateOutcome> {
|
|
1314 |
+ |
let server_dir = ctx.worktree.join("server");
|
|
1315 |
+ |
log_buf.extend_from_slice(b"---- docs check (MNW_CHECK_DOCS) ----\n");
|
|
1316 |
+ |
let mut cmd = tokio::process::Command::new(bin);
|
|
1317 |
+ |
cmd.env("MNW_CHECK_DOCS", "1")
|
|
1318 |
+ |
.current_dir(&server_dir)
|
|
1319 |
+ |
.stdout(std::process::Stdio::piped())
|
|
1320 |
+ |
.stderr(std::process::Stdio::piped())
|
|
1321 |
+ |
.kill_on_drop(true);
|
|
1322 |
+ |
let out = match tokio::time::timeout(std::time::Duration::from_secs(60), cmd.output()).await {
|
|
1323 |
+ |
Ok(Ok(out)) => out,
|
|
1324 |
+ |
Ok(Err(e)) => {
|
|
1325 |
+ |
log_buf.extend_from_slice(format!("docs check spawn failed: {e}\n").as_bytes());
|
|
1326 |
+ |
return Some(GateOutcome::failed(GateFailure::SpawnFailed {
|
|
1327 |
+ |
message: e.to_string(),
|
|
1328 |
+ |
}));
|
|
1329 |
+ |
}
|
|
1330 |
+ |
Err(_elapsed) => {
|
|
1331 |
+ |
let reason =
|
|
1332 |
+ |
"docs check timed out after 60s (staged binary may predate MNW_CHECK_DOCS)"
|
|
1333 |
+ |
.to_string();
|
|
1334 |
+ |
log_buf.extend_from_slice(reason.as_bytes());
|
|
1335 |
+ |
log_buf.push(b'\n');
|
|
1336 |
+ |
return Some(GateOutcome::failed(GateFailure::CodeSmokeSetup { reason }));
|
|
1337 |
+ |
}
|
|
1338 |
+ |
};
|
|
1339 |
+ |
log_buf.extend_from_slice(&out.stdout);
|
|
1340 |
+ |
log_buf.extend_from_slice(&out.stderr);
|
|
1341 |
+ |
if out.status.success() {
|
|
1342 |
+ |
return None;
|
|
1343 |
+ |
}
|
|
1344 |
+ |
Some(GateOutcome::failed(GateFailure::CodeSmokeDocs {
|
|
1345 |
+ |
broken: parse_check_docs_broken_count(&out.stdout),
|
|
1346 |
+ |
}))
|
|
1347 |
+ |
}
|
|
1348 |
+ |
|
|
1349 |
+ |
/// Best-effort parse of the broken-link count from the `MNW_CHECK_DOCS` sentinel
|
|
1350 |
+ |
/// line (`MNW_CHECK_DOCS: N broken link(s)`). Returns 0 if absent — the failure
|
|
1351 |
+ |
/// still stands, only the summary count is unknown.
|
|
1352 |
+ |
fn parse_check_docs_broken_count(stdout: &[u8]) -> u32 {
|
|
1353 |
+ |
let text = String::from_utf8_lossy(stdout);
|
|
1354 |
+ |
for line in text.lines() {
|
|
1355 |
+ |
if let Some(rest) = line.strip_prefix("MNW_CHECK_DOCS:") {
|
|
1356 |
+ |
for tok in rest.split_whitespace() {
|
|
1357 |
+ |
if let Ok(n) = tok.parse::<u32>() {
|
|
1358 |
+ |
return n;
|
|
1359 |
+ |
}
|
|
1360 |
+ |
}
|
|
1361 |
+ |
}
|
|
1362 |
+ |
}
|
|
1363 |
+ |
0
|
|
1364 |
+ |
}
|
|
1365 |
+ |
|
| 1292 |
1366 |
|
/// The createdb-to-dropdb interior of `code_smoke`: migrate+seed, then boot and
|
| 1293 |
1367 |
|
/// probe. Returns the outcome without a `log_ref` (the caller attaches it after
|
| 1294 |
1368 |
|
/// teardown). Never returns `Err` — spawn/child failures map to typed outcomes.
|
| 1956 |
2030 |
|
}
|
| 1957 |
2031 |
|
|
| 1958 |
2032 |
|
#[test]
|
|
2033 |
+ |
fn parse_check_docs_broken_count_reads_the_sentinel() {
|
|
2034 |
+ |
// The failing sentinel carries the count.
|
|
2035 |
+ |
assert_eq!(
|
|
2036 |
+ |
parse_check_docs_broken_count(b"some log\nMNW_CHECK_DOCS: 3 broken link(s)\n"),
|
|
2037 |
+ |
3
|
|
2038 |
+ |
);
|
|
2039 |
+ |
// Count with a preceding log line still parses (first bare int wins).
|
|
2040 |
+ |
assert_eq!(
|
|
2041 |
+ |
parse_check_docs_broken_count(
|
|
2042 |
+ |
b" broken link: a -> b\nMNW_CHECK_DOCS: 1 broken link(s)\n"
|
|
2043 |
+ |
),
|
|
2044 |
+ |
1
|
|
2045 |
+ |
);
|
|
2046 |
+ |
// The parser only runs on failure; the "ok" sentinel is never fed to it,
|
|
2047 |
+ |
// and its "(2" token is not a bare int, so it yields 0 harmlessly.
|
|
2048 |
+ |
assert_eq!(
|
|
2049 |
+ |
parse_check_docs_broken_count(b"MNW_CHECK_DOCS: ok (2 collision(s) reported)\n"),
|
|
2050 |
+ |
0
|
|
2051 |
+ |
);
|
|
2052 |
+ |
// Absent sentinel -> 0; the failure still stands, only the count is lost.
|
|
2053 |
+ |
assert_eq!(parse_check_docs_broken_count(b"unrelated output"), 0);
|
|
2054 |
+ |
}
|
|
2055 |
+ |
|
|
2056 |
+ |
#[test]
|
| 1959 |
2057 |
|
fn name_target_points_a_test_failure_at_its_crate() {
|
| 1960 |
2058 |
|
// With one target the crate was implicit; with fifteen the operator
|
| 1961 |
2059 |
|
// needs the summary to say which one broke.
|