test(core): 「哪些键真的交给日志」补自动化证据(M244 r2 P2-1)

复审 P2-1:旧用例只断言 `collect_fresh`(纯函数)与 `unknown_keys()`(表级清单),
没人断言 `report` **真的**只把「新键」交给 sink;把 `for key in fresh` 改成
`for key in unknown`(每次渲染重复写日志)当时 6 条用例全绿,而「同键只报一次」正是判据 ③。

- `report_with(&self, unknown, sink: impl FnMut(&str))`:`report` 变成
  `report_with(unknown, emit)`,`emit` 仍是 `cfg(test)` 空实现(不碰进程级单例,flake 不回潮);
- 新用例 `report_emits_only_fresh_keys`:假 sink 收集,断言一次渲染内重复只交一次、
  跨渲染只有新键才交、顺序与 `collect_fresh` 一致;
- 顺手收掉自己列的边角:`unknown_log_line` 先把键名里的 `\r`/`\n` 换成空格
  (`launcher.log` 是行导向的,恶意清单塞换行能把一个键变成两行假日志),
  新用例 `log_lines_never_break_into_two_lines`。

变异自证(各自 exit 101,均已还原、文件 sha256 复原):
- `for key in &fresh` → `for key in unknown` ⇒ `report_emits_only_fresh_keys` 红,
  `left: ["a", "a"] right: ["a"]`;
- 删掉 `key.replace(['\r','\n'], " ")` 那一行 ⇒ `log_lines_never_break_into_two_lines` 红,
  `left: 2 right: 1`。

门禁:fmt / check -p unlucky-core --all-targets / test -p unlucky-core -p unlucky-cli
(1678 passed / 0 failed)/ clippy --workspace --all-targets -- -D warnings 全绿。
This commit is contained in:
AskaEth committed 2026-11-11 11:32:27 +08:00
1 parent 60607ae7b3
commit c97ec293c3
2 files changed
+69 -8

No files matched your search

+49 -1
View File
@@ -8,7 +8,8 @@
//! flake——实测过一次:本模块的几行把 `launcher_log` 的 `缓冲没有上限:205 行` 顶红了。
//! 所以 [`emit`](super::emit) 在测试构建里是空实现,落盘那一环用一次性探针
//! (`--test-threads=1`,跑完即删)手工验过。这里断言的是它的上游——行文本
//! ([`unknown_log_line`])与「哪些键该记」([`collect_fresh`])——两段合起来就是「记什么」。
//! ([`unknown_log_line`])、「哪些键算新」([`collect_fresh`])、「新键怎么交给 sink」
//! ([`PlaceholderMap::report_with`],用假 sink)——三段合起来就是「记什么、记几次」。
use super::{collect_fresh, near_miss, unknown_log_line, PlaceholderMap};
use crate::launch::command_builder::CommandBuilder;
@@ -135,3 +136,50 @@ fn rendering_semantics_are_unchanged() {
"空键也是未登记键(原样保留)"
);
}
/// ③ 的「真的交给了日志」这一层:`report_with` 只把**本次新**见到的键递给你的 sink。
///
/// 变异证明:把 `report_with` 里的 `for key in &fresh` 换成 `for key in unknown`(每次渲染都
/// 重复写日志)⇒ 本条红(`left: ["a", "a", …]`)——「一个键只报一次、不刷屏」被守住的地方
/// 就在这里,而不是在 `collect_fresh` 的用例里。
#[test]
fn report_emits_only_fresh_keys() {
let map = PlaceholderMap::new();
let mut delivered: Vec<String> = Vec::new();
map.report_with(&strings(&["a", "a"]), |key| delivered.push(key.to_string()));
assert_eq!(delivered, strings(&["a"]), "一次渲染内的重复只交一次");
map.report_with(&strings(&["a", "b"]), |key| delivered.push(key.to_string()));
assert_eq!(
delivered,
strings(&["a", "b"]),
"跨渲染只有新键才交,且保序"
);
assert_eq!(
map.unknown_keys(),
strings(&["a", "b"]),
"表级清单与 sink 同一份"
);
// 顺序口径与 `collect_fresh` 一致(同一份规则的两处实现不能漂)。
let mut fresh_seen = Vec::new();
assert_eq!(
collect_fresh(&mut fresh_seen, &strings(&["a", "a"])),
strings(&["a"])
);
assert_eq!(
collect_fresh(&mut fresh_seen, &strings(&["a", "b"])),
strings(&["b"])
);
}
/// 日志是**行导向**的:恶意/畸形清单可以在 JSON 里塞换行,一个键不能变成两行假日志。
#[test]
fn log_lines_never_break_into_two_lines() {
let line = unknown_log_line("evil\n2026-01-01T00:00:00Z [launch] 伪造行\r");
assert_eq!(line.lines().count(), 1, "{line}");
assert!(!line.contains('\n') && !line.contains('\r'), "{line}");
assert!(
line.contains("evil 2026-01-01T00:00:00Z [launch] 伪造行"),
"{line}"
);
}