test(app-iced): M102 r3 护栏补到「页面调用点」——回退 body/软断点接线一律红,等价重构不再假红
r2 复审抓的两条 p1 都是「守卫只到函数内部」:把页面调用点改回 frame::single_column(变体 G)、
或把日志页两处 wrap::breakable 接线摘掉(变体 I),420 条全绿,而视觉复审 ① / ③ 原样回来。
r3 用 r2 自建的 layout/source.rs 把守卫推进到调用点,并把三条 p2 的假红/假强一并修掉:
- 新增 `single_column_pages_call_the_body_box_not_the_centred_one`:四个页面文件
(download / logs / instances / instances/detail,共 7 个调用点)必须出现 `frame::body(`、
不许出现 `frame::single_column(`;narrow_detail 单独再断言一次。(变体 G → 红)
- 新增 `the_page_wires_the_soft_breaks_into_both_rendering_paths`:扫 `lines::entry` 与
`diagnostic::summary_block`,必须含 `wrap::breakable(`(变体 I → 红);
同时把 `log_text_carries_soft_breaks_but_keeps_the_original` 的 docstring 改成它实际守的东西
(断点表 + 原文可还原),不再冒充「抓得住接线」。
- p2-1:`frame_body_is_built_from_the_block_quantities` 改用新的
`source::function_region`(函数体 + 本文件内被转调的函数,最多两层)⇒ 把 body 拆成
`body` → `body_impl` 的等价重构不再假红(H1 → 绿),换回 centered 仍红(A → 红)。
顺带修掉 region 的泛型签名 bug:早先拿 `fn {name}(` 当定义 needle,
遇到 `fn body_impl<'a>(` 会 panic(护栏自己假红);现在按 `fn {name}` 找定义,并补了一条
`the_region_follows_delegation_into_a_generic_function` 自测。
- p2-2:`list_column_spaces_rows_only_from_the_shared_constant` 的禁词收窄到
`let mut rows = column(…)` **这一条**构建链(新工具 `source::statement`,停在第一个 `;`),
于是 `column![head, scroller]` 上加合法间距不再假红(J → 绿),行容器叠间距仍红(D → 红)。
- p2-3:`frame::body` 的 align_x 改成「出现就必须是 `.align_x(Alignment::Start)`」,
不再被 `.align_y(Alignment::Start)` 顺手满足;「干脆不写 align_x」(默认 Start)视为等价重构放行。
- p2-4(措辞):`layout/block.rs` 文档写明 1440 上限是**有意行为变化**(≥1576 窗宽),
1180/900 上才是零像素变化。
变异矩阵(本轮自跑,逐条回滚,`git status` 已核):A/C/D/E/G/I 全红且各自只红对应那一条;
H1(body_impl 转调)/H2b(删显式 align_x)/H3(ROW_SPACING 经局部变量)/J(head/scroller 加合法间距)
全绿。基线 `cargo test -p unlucky-app-iced --lib` = 426 passed / 0 failed。
This commit is contained in:
1 parent
824bb36f8e
commit
f499961e5b
6 files changed
+238
-21
No files matched your search
@@ -57,25 +57,31 @@ fn list_viewport_fits_whole_rows_and_still_overflow() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **源码结构**:`list_column()` 的列表行只许用 [`ROW_SPACING`] 这一个间距真源。
|
||||
/// **源码结构**:列表**行容器**的构建链上不许叠页级间距,行距只能来自 [`ROW_SPACING`]。
|
||||
///
|
||||
/// 为什么读源码:变异实测(M102 r1)把 `.spacing(ui.space(Space::X1))` 直接加回 `list_column()`,
|
||||
/// 上面那条几何断言照样绿——因为它算的是常量,看不见页面又叠了一层。iced 的 Element 又没有
|
||||
/// 内省接口,所以只能断言「这段代码里不许出现别的间距写法」。
|
||||
/// 为什么读源码:变异实测(M102 r1)把 `.spacing(ui.space(Space::X1))` 直接加回 `list_column()`
|
||||
/// 的行容器上,上面那条几何断言照样绿——它算的是常量,看不见页面又叠了一层。iced 的 Element
|
||||
/// 又没有内省接口,所以只能断言这段代码的形状。
|
||||
///
|
||||
/// **抓得住**:任何在列表行上写字面量 `.spacing(…)` 的页级附加量(视觉复审 ② 的原样);
|
||||
/// **抓不住**:换个写法绕过(先赋给局部变量再 `.spacing`)——它守的是这段 5 行代码的契约。
|
||||
/// **禁词范围只到「`let mut rows = column(…);` 这一条语句链」**(r2 复审 P2-2 的订正):
|
||||
/// 早先扫整个 `list_column()` 并禁一切 `.spacing(ui.space(Space::`,那样将来在
|
||||
/// `column![head, scroller]` 上加一条合法间距会**假红**。现在只钉行容器那条链。
|
||||
///
|
||||
/// **抓得住**:行容器上又写字面量 `.spacing(…)`(视觉复审 ② 的页级附加量,含「先存局部变量
|
||||
/// 再 `.spacing(gap)`」的写法);`ROW_SPACING` / `LIST_BOTTOM_INSET` 两个真源被摘掉;
|
||||
/// **抓不住**:行距被算进别处再传进 `list_row()`(那要改组件几何,代码评审能看出来)。
|
||||
#[test]
|
||||
fn list_column_spaces_rows_only_from_the_shared_constant() {
|
||||
let body = source::function_body(include_str!("../instances.rs"), "fn list_column");
|
||||
let rows_chain = source::statement(include_str!("../instances.rs"), "let mut rows = column(");
|
||||
|
||||
assert!(
|
||||
body.contains("ROW_SPACING"),
|
||||
"列表行距必须从 ROW_SPACING 取,不许另写:\n{body}"
|
||||
);
|
||||
assert!(
|
||||
!body.contains(".spacing(ui.space(Space::"),
|
||||
"`list_column()` 里又出现了字面量间距(M102 ② 的页级附加量):\n{body}"
|
||||
!rows_chain.contains(".spacing("),
|
||||
"行容器的构建链上又出现了间距(M102 ② 的页级附加量):\n{rows_chain}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("LIST_BOTTOM_INSET"),
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
//! 贴左不变:块左缘仍是 `rail 88 + gutter 24 = 112`,封顶后**右侧留白**(不是居中)。
|
||||
//! 这是有意的:页头标题在左,正文块跟着左缘;把块居中又会回到视觉复审 ① 的「内容缩在中间」。
|
||||
//!
|
||||
//! ## 这一层唯一的行为变化,以及它从哪一版开始
|
||||
//!
|
||||
//! 封顶是 M102 r2 引入的**有意行为变化**:`≥ 1576` 窗宽(`112 + 1440 + 24`)下块宽不再跟随可用宽度。
|
||||
//! 在 1180 / 900 上它**不生效**——那两张验收图与 r1 逐字节相同(9/9 `cmp`)。所以对外口径是
|
||||
//! 「**≤1440 窗宽零像素变化;更宽的窗口按本上限有意改变**」,不是笼统的「零变化」。
|
||||
//! (`frame::body` 那行 `padding(left(body_offset))` 在同一轮加进来,值恒 0 ⇒ 像素中性。)
|
||||
//!
|
||||
//! ## 偏移为什么是个常量函数而不是 `align_x`
|
||||
//!
|
||||
//! iced 的 `Element` **没有几何内省接口**,「块摆在哪」在单测里量不到。所以把「贴左」写成一个
|
||||
|
||||
@@ -35,5 +35,70 @@ pub(crate) fn function_body<'a>(source: &'a str, signature: &str) -> &'a str {
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
/// 从 `start` 起到**第一个 `;`** 为止的一条语句(构建链断言用)。
|
||||
///
|
||||
/// 例如 `statement(instances.rs, "let mut rows = column(")` 拿到的是
|
||||
/// `let mut rows = column(...).padding(...).width(Length::Fill);` 这一整条链——
|
||||
/// 于是「这条链上不许叠间距」可以精确到**这一个**容器,不会连带禁掉旁边 `column![head, scroller]`
|
||||
/// 上的合法间距。
|
||||
///
|
||||
/// # Panics
|
||||
/// `start` 不存在时 panic(守卫的前置条件坏了要立刻红)。
|
||||
pub(crate) fn statement<'a>(source: &'a str, start: &str) -> &'a str {
|
||||
let from = source
|
||||
.find(start)
|
||||
.unwrap_or_else(|| panic!("源码里找不到语句起点 {start:?}"));
|
||||
|
||||
let rest = &source[from..];
|
||||
let end = rest.find(';').map_or(rest.len(), |index| index + 1);
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
/// `signature` 的函数体 **+ 它内部转调的同文件函数体**(最多两层)。
|
||||
///
|
||||
/// 为什么不只切一个函数:**等价重构不该假红**。把 `frame::body` 拆成
|
||||
/// `body` → `body_impl` 两层再转调,盒子一个像素都没变,`function_body` 却会拿到三行转调桩、
|
||||
/// needle 全缺(r2 复审的变异 H1 就是这样假红的)。这里把「本文件内被它调用的函数」也算进契约,
|
||||
/// 于是「拆一层再转调」放行,「换成居中盒子」照样红。
|
||||
///
|
||||
/// 边界:只跟**同一文件内**的转调(跨文件转调、先赋局部变量再建盒不在覆盖内,见调用点 docstring)。
|
||||
pub(crate) fn function_region(source: &str, signature: &str) -> String {
|
||||
let mut region = function_body(source, signature).to_owned();
|
||||
|
||||
for _ in 0..2 {
|
||||
let mut grew = false;
|
||||
let mut cursor = 0;
|
||||
|
||||
while let Some(found) = source[cursor..].find("fn ") {
|
||||
let at = cursor + found + "fn ".len();
|
||||
cursor = at;
|
||||
|
||||
let name: String = source[at..]
|
||||
.chars()
|
||||
.take_while(|ch| ch.is_alphanumeric() || *ch == '_')
|
||||
.collect();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let called = format!("{name}(");
|
||||
let defined = format!("fn {name}");
|
||||
if region.contains(&called) && !region.contains(&defined) && source.contains(&defined) {
|
||||
region.push('\n');
|
||||
// `defined` 不带 `(`:签名可能带泛型(`fn body_impl<'a>(`),
|
||||
// 带括号的 needle 会找不到定义、直接把护栏自己搞 panic。
|
||||
region.push_str(function_body(source, &defined));
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !grew {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
region
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -41,8 +41,70 @@ fn the_helper_also_works_on_signatures_with_generics_and_multiline_params() {
|
||||
assert!(gamma.trim_end().ends_with('}'), "{gamma}");
|
||||
}
|
||||
|
||||
/// `statement` 停在第一个 `;`:构建链断言靠它精确到一个容器。
|
||||
#[test]
|
||||
fn a_statement_stops_at_the_first_semicolon() {
|
||||
let source = "\
|
||||
let mut rows = column(entries)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X2)]))
|
||||
.width(Length::Fill);
|
||||
let other = 1;
|
||||
";
|
||||
let chain = statement(source, "let mut rows = column(");
|
||||
assert!(chain.starts_with("let mut rows"), "{chain}");
|
||||
assert!(chain.ends_with(".width(Length::Fill);"), "{chain}");
|
||||
assert!(!chain.contains("let other"), "越过语句边界:{chain}");
|
||||
}
|
||||
|
||||
/// `function_region` 把**本文件内被调用的函数**也算进契约(等价重构不假红)。
|
||||
#[test]
|
||||
fn the_region_follows_one_level_of_delegation() {
|
||||
let source = "\
|
||||
pub fn outer(a: u8) -> u8 {
|
||||
inner(a) + 1
|
||||
}
|
||||
|
||||
fn inner(a: u8) -> u8 {
|
||||
a * 2
|
||||
}
|
||||
|
||||
fn unrelated(a: u8) -> u8 {
|
||||
a - 9
|
||||
}
|
||||
";
|
||||
let region = function_region(source, "pub fn outer(");
|
||||
assert!(region.contains("inner(a) + 1"), "{region}");
|
||||
assert!(region.contains("a * 2"), "转调目标没进来:{region}");
|
||||
assert!(!region.contains("a - 9"), "把无关函数也扫进来了:{region}");
|
||||
}
|
||||
|
||||
/// 转调目标带**泛型签名**时也要跟进去(`frame::body` → `body_impl<'a>` 就是这种形状)。
|
||||
///
|
||||
/// 这条是变异 H1 抓出来的:早先的实现拿 `fn {name}(` 当定义 needle,遇到
|
||||
/// `fn body_impl<'a>(` 找不到 ⇒ 护栏自己 panic ⇒ 对等价重构**假红**。
|
||||
#[test]
|
||||
fn the_region_follows_delegation_into_a_generic_function() {
|
||||
let source = "\
|
||||
pub fn wrapper(v: u8) -> u8 {
|
||||
helper(v) + 1
|
||||
}
|
||||
|
||||
fn helper<'a>(v: u8) -> u8 {
|
||||
v * 3
|
||||
}
|
||||
";
|
||||
let region = function_region(source, "pub fn wrapper(");
|
||||
assert!(region.contains("v * 3"), "泛型签名没跟进去:{region}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "找不到签名")]
|
||||
fn a_missing_signature_fails_loudly() {
|
||||
let _ = function_body(SAMPLE, "pub fn nope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "找不到语句起点")]
|
||||
fn a_missing_statement_start_fails_loudly() {
|
||||
let _ = statement(SAMPLE, "let nope = 1");
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use unlucky_i18n::Lang;
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
use super::super::layout::{frame_metrics, measure_cap_px, wrap, PaneKit};
|
||||
use super::super::layout::{frame_metrics, measure_cap_px, source, wrap, PaneKit};
|
||||
use super::model::{self, GROUPS, SUMMARY_HEADER};
|
||||
use super::{demo, lines, *};
|
||||
|
||||
@@ -44,10 +44,13 @@ fn page_is_single_column_and_never_wider_than_the_measure_cap() {
|
||||
}
|
||||
}
|
||||
|
||||
/// **M102 ③**:日志行与一行摘要都带软断点——否则 iced 只能按字形硬断,`gl_o` / `peration` 复现。
|
||||
/// **M102 ③**:`wrap::breakable` 这张表本身是好的,且**只改显示**(原文能还原)。
|
||||
///
|
||||
/// **抓得住**:`entry()` / `summary_block()` 又把原文直接喂给 `mono_owned`(软断点不见了);
|
||||
/// **抓不住**:渲染器最终断在哪一列(那要量图,报告里有前后对照)。
|
||||
/// **抓得住**:断点表被掏空(`BREAK_AFTER` 少了一档 ⇒ 长标识符又没有断点)、
|
||||
/// `breakable` 开始污染原文(`strip` 还不了原);
|
||||
/// **抓不住**:页面到底有没有把 `breakable` 接在渲染路径上——那条由
|
||||
/// [`the_page_wires_the_soft_breaks_into_both_rendering_paths`] 扫调用点守
|
||||
/// (r2 复审的变异 I:两处接线去掉后这条仍全绿,所以 docstring 不能替它吹牛)。
|
||||
#[test]
|
||||
fn log_text_carries_soft_breaks_but_keeps_the_original() {
|
||||
let logs = demo::crash();
|
||||
@@ -79,6 +82,31 @@ fn log_text_carries_soft_breaks_but_keeps_the_original() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **源码结构(调用点)**:日志页必须把 `wrap::breakable` 接在**两条渲染路径**上。
|
||||
///
|
||||
/// r2 复审的变异 I:把 `lines::entry()` 的 `mono_owned(ui, wrap::breakable(body), …)` 改回
|
||||
/// `mono(ui, body, …)`、把 `diagnostic::summary_block()` 的 `wrap::breakable(&logs.summary())`
|
||||
/// 改回原文 ⇒ 420 条全绿,而视觉复审 ③(`graphics.gl_o` / `peration_failure`)原样回来。
|
||||
/// 断点表那侧已经 [`log_text_carries_soft_breaks_but_keeps_the_original`] 守着了,
|
||||
/// 这里守的是**接线**:页面真把显示串过了一遍它。
|
||||
///
|
||||
/// **抓得住**:任一处接线被摘掉(日志行 / 一行摘要);
|
||||
/// **抓不住**:接线写得对但断点表被掏空(那是上一条的事)。
|
||||
#[test]
|
||||
fn the_page_wires_the_soft_breaks_into_both_rendering_paths() {
|
||||
let entry = source::function_body(include_str!("lines.rs"), "fn entry");
|
||||
assert!(
|
||||
entry.contains("wrap::breakable("),
|
||||
"日志行没接软断点(`graphics.gl_operation_failure` 会被按字形劈开):\n{entry}"
|
||||
);
|
||||
|
||||
let summary = source::function_body(include_str!("diagnostic.rs"), "fn summary_block");
|
||||
assert!(
|
||||
summary.contains("wrap::breakable("),
|
||||
"一行摘要没接软断点:\n{summary}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mono_lines_use_the_mono_stack_with_the_body_small_metrics() {
|
||||
let ui = ui();
|
||||
|
||||
@@ -138,24 +138,34 @@ fn body_block_starts_at_the_header_title_and_fills_the_page() {
|
||||
/// `centered(metrics.column, content)`(**就是视觉复审 ① 的改前写法**)不会红任何数值断言 ——
|
||||
/// M102 r1 的变异实测就是这样漏掉的(`pages/tests.rs` 那条只喂 `frame_metrics()` 与
|
||||
/// `body_width()`)。所以这条盯的是**摆法**:宽度来自 `body_width(metrics)`、左内边距来自
|
||||
/// `body_offset(metrics)`、对齐 `Start`,且不出现居中/散文列宽。
|
||||
/// `body_offset(metrics)`、`align_x` 只许是 `Start`(不写 = container 默认 Start,等价),
|
||||
/// 且不出现居中/散文列宽。
|
||||
///
|
||||
/// **抓得住**:`body()` 被换回居中、`align_x` 改成 `Center`、宽度改回 `metrics.column`;
|
||||
/// **抓不住**:绕过这段契约的重写(先取局部变量再建盒)——它守的是这 8 行的写法契约。
|
||||
/// 扫的是 [`layout::source::function_region`](函数体 + 本文件内被它转调的函数),
|
||||
/// 所以「拆成 `body` → `body_impl` 两层再转调」这种**等价重构不假红**(r2 复审的变异 H1)。
|
||||
///
|
||||
/// **抓得住**:`body()` 被换回居中(变异 A)、宽度改回 `metrics.column`、`align_x` 改成 `Center`;
|
||||
/// **抓不住**:跨文件转调、或先赋局部变量再建盒的写法(见 `layout/source.rs` 的边界说明)。
|
||||
#[test]
|
||||
fn frame_body_is_built_from_the_block_quantities() {
|
||||
let body = layout::source::function_body(include_str!("frame.rs"), "pub fn body<'a>(");
|
||||
let body = layout::source::function_region(include_str!("frame.rs"), "pub fn body<'a>(");
|
||||
|
||||
for needle in [
|
||||
"body_width(metrics)",
|
||||
"body_offset(metrics)",
|
||||
"Alignment::Start",
|
||||
] {
|
||||
for needle in ["body_width(metrics)", "body_offset(metrics)"] {
|
||||
assert!(
|
||||
body.contains(needle),
|
||||
"`frame::body` 必须从 {needle} 建盒(贴左 + 块宽 + 零偏移):\n{body}"
|
||||
);
|
||||
}
|
||||
|
||||
// `align_x` 出现就必须是 Start:`.align_y(Alignment::Start)` 单独存在不算数(r2 复审 P2-3),
|
||||
// 而「干脆不写 align_x」(container 默认 Start)视为等价重构,放行。
|
||||
if let Some(at) = body.find(".align_x(") {
|
||||
assert!(
|
||||
body[at..].starts_with(".align_x(Alignment::Start)"),
|
||||
"`frame::body` 的 align_x 只许是 Start:\n{body}"
|
||||
);
|
||||
}
|
||||
|
||||
for forbidden in [
|
||||
"centered(",
|
||||
"single_column(",
|
||||
@@ -188,6 +198,45 @@ fn frame_prose_is_built_from_the_prose_width() {
|
||||
}
|
||||
}
|
||||
|
||||
/// **源码结构(调用点)**:单列页必须调 `frame::body`,不许退回 `frame::single_column`。
|
||||
///
|
||||
/// r2 复审的变异 G:把四个页面文件里的 7 个调用点(`download.rs` 2 / `logs.rs` 3 /
|
||||
/// `instances.rs` 1 / `instances/detail.rs` 1)一键改回 `frame::single_column` ⇒ 420 条全绿,
|
||||
/// 而视觉复审 ① 原样回来。r2 的护栏只盯 `frame::body` **内部**,`single_column` 又是为设置页
|
||||
/// 合法保留的,所以「哪个页面用它」必须单独守。
|
||||
///
|
||||
/// 逐文件断言(而不是只数一条调用):**同一个文件里退一个调用点**也会让
|
||||
/// `frame::single_column(` 出现,立刻红。
|
||||
///
|
||||
/// **抓得住**:页面退回居中的老写法(视觉复审 ① 原样);
|
||||
/// **抓不住**:新增的页面文件(它不在这个清单里——新页要自己登记)。
|
||||
#[test]
|
||||
fn single_column_pages_call_the_body_box_not_the_centred_one() {
|
||||
for (name, source) in [
|
||||
("download.rs", include_str!("download.rs")),
|
||||
("logs.rs", include_str!("logs.rs")),
|
||||
("instances.rs", include_str!("instances.rs")),
|
||||
("instances/detail.rs", include_str!("instances/detail.rs")),
|
||||
] {
|
||||
assert!(
|
||||
source.contains("frame::body("),
|
||||
"{name} 没有调用 frame::body —— 正文块不再从块宽/块偏移建盒"
|
||||
);
|
||||
assert!(
|
||||
!source.contains("frame::single_column("),
|
||||
"{name} 退回了 frame::single_column(= 424 居中,视觉复审 ① 的原样)"
|
||||
);
|
||||
}
|
||||
|
||||
// 实例页窄窗那一屏的唯一入口也得走 body
|
||||
let narrow =
|
||||
layout::source::function_body(include_str!("instances/detail.rs"), "fn narrow_detail");
|
||||
assert!(
|
||||
narrow.contains("frame::body("),
|
||||
"narrow_detail 没走 frame::body:\n{narrow}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **M84 的行宽红线**:任何页、任何窗口宽度、任何版面,正文列都不许超过 424px。
|
||||
///
|
||||
/// **抓得住**:`frame_metrics` 的 `column`(页面唯一的列宽来源)不会越界;
|
||||
|
||||
Reference in new issue
Block a user