test(app-iced): M102 r2 护栏改成「抓得住页面换摆法」——块偏移/行距/散文接线全部有源码结构断言

r1 复审的两条 P1 都是假护栏:把修复原样回滚(A:frame::body 换回居中 424;
C:内缩 X2→X1;D:行距加回 4px)后测试仍全绿,因为断言只喂 frame_metrics() 与常量重算,
既不观察 frame::body,也不引用页面常量。本轮把「摆法」做成可断言的东西:

- 新增 `pages/layout/block.rs`:body_width(pane 宽封顶 1440)、body_offset(恒 0 = 贴左)、
  prose_width(= metrics.column ≤424)三个纯量 + 单测。frame::body / frame::prose 从它们建盒,
  frame::body_width 改成 pub use(不另算一份)。
- 新增 `pages/layout/source.rs`(cfg(test)):把某个函数的实现文本切出来。iced 的 Element
  没有几何内省,量不到「块摆在哪」,只能断言「照哪个数建的」;仓里已有 no_hardcoded_tokens
  扫源码、download/tests 用 include_str! 扫 key 的先例。
- 新护栏(每条都做过变异实测,见报告):
  · frame_body_is_built_from_the_block_quantities(A 变异红)
  · frame_prose_is_built_from_the_prose_width(E 变异红)
  · list_column_spaces_rows_only_from_the_shared_constant(D 变异红)
  · instances 几何断言改用页面真源 LIST_BOTTOM_INSET.dp() / ROW_SPACING(C 变异红)
  · the_detail_note_goes_through_the_prose_box(散文接线)
- P2:wrap::strip 的「会删原文自带 U+200B」写成限界 + 一条断言;docs 口径从 layout.rs
  挪到 layout/block.rs(layout.rs 399 → 389)。

零像素变化:改后 9 张图与 r1 的 m102-after 逐张 cmp 完全相同;2560×760 新出三张证明块宽封顶
1440(112..1551)且仍贴左。
This commit is contained in:
AskaEth committed 2026-11-04 19:53:41 +08:00
1 parent 89d98184c1
commit 824bb36f8e
11 files changed
+469 -67

No files matched your search

@@ -0,0 +1,39 @@
//! **源码结构断言**的小工具:把某个函数的实现文本切出来(只给测试用)。
//!
//! ## 为什么要读源码(而不是量 Element)
//!
//! iced 的 `Element` 没有几何内省接口(`size()` 只是 `Size<Length>`,取不到布局结果),
//! 所以「页面把块摆在哪 / 有没有在行之间叠间距」在单测里**量不到**。M102 r1 的教训正是这个:
//! 护栏只喂 `frame_metrics()` 与常量重算,把 `frame::body` 换回「居中 424」的实现,
//! 测试照样全绿——钉住的是公式,不是摆法。
//!
//! 本仓已有同一条纪律的先例:`tests/no_hardcoded_tokens.rs` 直接扫 `src/**`,
//! `pages/download/tests.rs::every_download_key_is_referenced_by_the_page` 用 `include_str!`
//! 扫页面源码找 key 引用。这里只是把「切出某个函数」这件小事做成一个共用工具,
//! 让护栏能断言「盒子是不是照那个数建的」。
//!
//! ## 边界(它抓不住什么)
//!
//! 它读的是**文本**:换个写法(先赋值给局部变量再 `.width(...)`)就可能绕过。
//! 所以它只配一条**明确的契约**用:`frame::body` / `frame::prose` / `list_column` 这几处的
//! 写法本身就是契约的一部分(它们各自只有 5~10 行),不是为了抠实现细节。
//! 真要守像素,还是得靠软件渲染金图(`shot::diff`,绑 Windows + 字体栈,属另一个决定)。
/// 把 `source` 里第一个 `signature` 起始、到该函数**顶层**右花括号为止的文本切出来。
///
/// 「顶层」= 行首的 `}`(本仓函数都是顶格收尾,内层块都是缩进收尾),所以不会在内层 `}` 提前断。
///
/// # Panics
/// 签名不存在时 panic(护栏自己的前置条件坏了要立刻红,不能静默放行)。
pub(crate) fn function_body<'a>(source: &'a str, signature: &str) -> &'a str {
let start = source
.find(signature)
.unwrap_or_else(|| panic!("源码里找不到签名 {signature:?}"));
let rest = &source[start..];
let end = rest.find("\n}").map_or(rest.len(), |index| index + 2);
&rest[..end]
}
#[cfg(test)]
mod tests;