diff --git a/crates/app-iced/src/platform/win32/corner/tests.rs b/crates/app-iced/src/platform/win32/corner/tests.rs index 68f57d4..1a8acbb 100644 --- a/crates/app-iced/src/platform/win32/corner/tests.rs +++ b/crates/app-iced/src/platform/win32/corner/tests.rs @@ -3,11 +3,20 @@ //! //! 判据与真实读数的出处:`DevDocs/artifacts/m116/probe-table.md`。 //! -//! M182 起这份真值表**覆盖三行平台**:Windows / macOS / Linux。这是 `HostOs` 作为**参数** -//! (而不是 `#[cfg]`)的直接收益——本机只有 Windows,但另外两行的判据照样能被断言证明。 +//! ## M182 的两条纪律(都来自复审 P1-1,别再退回原样) +//! +//! 1. **判据断言一律走 [`plan_corner_for`],平台显式传值**。`plan_corner` 是**产品入口**, +//! 它读的是 `HostOs::current()`——同一个测试在 Windows 上绿、在 ubuntu 上就可能红。 +//! M182 首轮复审实测:本机(Windows)384+ 条全绿,而其中三条断言的是 Windows 的结果, +//! 合进 main 后 `ubuntu-latest` 的 `test` job 会当场红。 +//! 2. **`plan_corner` 只留一条用例**,且那条必须**与宿主无关**: +//! [`the_production_entry_forwards_to_the_pure_judgement`] 两边都用同一个 `HostOs::current()`, +//! 所以它在任何平台上都成立,只证「产品入口确实转发到纯函数」。 +//! //! 「非 Windows」那一组断言住在 [`platforms`](本文件守 400 行上限)。 -use super::{format_line, plan_corner, CornerPlan, CornerSource}; +use super::{format_line, plan_corner, plan_corner_for, CornerExclusion, CornerPlan, CornerSource}; +use crate::platform::host::HostOs; use crate::Backend; use unlucky_presenter::prefs::{WindowCorner, WindowsVersion, WIN11_MIN_BUILD}; @@ -23,13 +32,18 @@ fn win10() -> WindowsVersion { WindowsVersion::new(10, 0, 19_045) } +/// 判据断言用的固定宿主:**永远是 Windows**,与本机实际跑在哪个系统上无关。 +/// +/// 为什么要有这个包装而不是直接写 `HostOs::Windows`:写一次名字就够了,以后想给 Linux / +/// macOS 也加 Windows 专属断言时,改这一处即可。 +fn on_windows(policy: WindowCorner, os: Option, backend: Backend) -> CornerPlan { + plan_corner_for(policy, HostOs::Windows, os, backend) +} + /// **抓得住**:判定链的顺序与每一格的原因码(把 `policy`/`host`/`os`/`backend` 任一条判反都会红)。 /// **抓不住**:DWM 到底有没有真画圆角(那要像素,见 `platform/win32` 的探针输出)。 #[test] fn truth_table_policy_host_os_backend() { - use super::{plan_corner_for, CornerExclusion}; - use crate::platform::host::HostOs; - let cases = [ // policy=Square:任何平台/系统/后端都直角,原因是用户选的。 ( @@ -153,12 +167,12 @@ fn truth_table_policy_host_os_backend() { fn the_win11_boundary_is_inclusive() { let at = WindowsVersion::new(10, 0, WIN11_MIN_BUILD); assert_eq!( - plan_corner(WindowCorner::System, Some(at), Backend::Gpu).source, + on_windows(WindowCorner::System, Some(at), Backend::Gpu).source, CornerSource::Dwm ); let below = WindowsVersion::new(10, 0, WIN11_MIN_BUILD - 1); assert_eq!( - plan_corner(WindowCorner::System, Some(below), Backend::Gpu).source, + on_windows(WindowCorner::System, Some(below), Backend::Gpu).source, CornerSource::Square, "build 21999 = Win10 22H2 家族末尾,不能算 Win11" ); @@ -170,7 +184,7 @@ fn the_win11_boundary_is_inclusive() { /// 少一栏就等于把这条线索丢掉。M182 新增的 `host=` 与「非 Windows 的 `os=n/a`」在这里钉死。 #[test] fn the_log_line_carries_every_field() { - let dwm = plan_corner(WindowCorner::System, Some(win11()), Backend::Gpu) + let dwm = on_windows(WindowCorner::System, Some(win11()), Backend::Gpu) .log_line(WindowCorner::System); assert!(dwm.starts_with("corner=policy=system"), "{dwm}"); assert!(dwm.contains("host=windows"), "{dwm}"); @@ -179,15 +193,17 @@ fn the_log_line_carries_every_field() { assert!(dwm.contains("excluded_reason=none"), "{dwm}"); assert!(dwm.contains("radius=system"), "{dwm}"); - let square = plan_corner(WindowCorner::Square, Some(win11()), Backend::Gpu) + let square = on_windows(WindowCorner::Square, Some(win11()), Backend::Gpu) .log_line(WindowCorner::Square); assert!(square.contains("source=square"), "{square}"); assert!(square.contains("excluded_reason=user"), "{square}"); - let unknown: CornerPlan = plan_corner(WindowCorner::System, None, Backend::Gpu); - assert!(unknown - .log_line(WindowCorner::System) - .contains("os=unknown")); + // Windows 上「版本读不到」:`os=unknown` **配上** `host=windows` 才是完整的真话。 + let unknown = + on_windows(WindowCorner::System, None, Backend::Gpu).log_line(WindowCorner::System); + assert!(unknown.contains("host=windows"), "{unknown}"); + assert!(unknown.contains("os=unknown"), "{unknown}"); + assert!(unknown.contains("excluded_reason=unknown-os"), "{unknown}"); } /// **日志行的形状**,用 M116 探针窗口的**真实读数**构造(`DevDocs/artifacts/m116/probe-table.md`)。 @@ -201,7 +217,7 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() { // 本机实测:Server 2025 / build 26100;读回 `E_INVALIDARG`;产品形状是 `overlapped`, // 而 `layered=on` 只可能来自整窗 α(我们的 `WS_EX_LAYERED`) ⇒ 带 `suppressed_by=`。 let os = WindowsVersion::new(10, 0, 26_100); - let plan: CornerPlan = plan_corner(WindowCorner::System, Some(os), Backend::Gpu); + let plan: CornerPlan = on_windows(WindowCorner::System, Some(os), Backend::Gpu); let line = format_line( "boot", &plan, @@ -223,7 +239,7 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() { // 设置里选「直角」:原因码必须从 `none` 变成 `user`,其余字段不动。 let square = format_line( "change", - &plan_corner(WindowCorner::Square, Some(os), Backend::Gpu), + &on_windows(WindowCorner::Square, Some(os), Backend::Gpu), WindowCorner::Square, "pref=1 readback=? read_hr=0x80070057", "frame=overlapped layered=off", @@ -241,20 +257,80 @@ fn the_log_line_shape_is_pinned_with_the_measured_readings() { // Win10:原因码是 `os-build`(GPU 档)/ `cpu-tier`(CPU 档),来源仍是 square。 let win10 = WindowsVersion::new(10, 0, 19_045); let os_build = - plan_corner(WindowCorner::System, Some(win10), Backend::Gpu).log_line(WindowCorner::System); + on_windows(WindowCorner::System, Some(win10), Backend::Gpu).log_line(WindowCorner::System); assert!( os_build.contains("source=square") && os_build.contains("excluded_reason=os-build"), "{os_build}" ); let cpu = - plan_corner(WindowCorner::System, Some(win10), Backend::Cpu).log_line(WindowCorner::System); + on_windows(WindowCorner::System, Some(win10), Backend::Cpu).log_line(WindowCorner::System); assert!(cpu.contains("excluded_reason=cpu-tier"), "{cpu}"); } +/// **产品入口只有一条用例**,且它**与宿主无关**(复审 P1-1 的要求)。 +/// +/// 断言的是「`plan_corner` 就是 `plan_corner_for(HostOs::current(), …)`」:两边喂同一个 +/// `HostOs::current()`,所以在 Windows / ubuntu / macOS 上恒成立——它证明的是**转发**, +/// 不是某个平台的结果。任何一个平台的结果都由 [`truth_table_policy_host_os_backend`] +/// 显式传 `HostOs` 钉住。 +/// +/// **抓得住**:`plan_corner` 里偷偷改成写死的 `HostOs::Windows`(那样 ubuntu 上这条会红)、 +/// 或者干脆不再转发(判据分叉)。 +#[test] +fn the_production_entry_forwards_to_the_pure_judgement() { + for policy in [WindowCorner::System, WindowCorner::Square] { + for os in [None, Some(win10()), Some(win11())] { + for backend in [Backend::Gpu, Backend::Cpu, Backend::Auto] { + let production = plan_corner(policy, os, backend); + assert_eq!( + production, + plan_corner_for(policy, HostOs::current(), os, backend), + "plan_corner 必须原样转发到纯函数(policy={policy:?} os={os:?} backend={backend:?})" + ); + assert_eq!(production.host, HostOs::current()); + } + } + } +} + +/// 每个原因码都是一个**非空、无空格、互不重复**的稳定串(复审 P2-1:`unknown-os` 此前没有任何断言)。 +/// +/// 为什么值得一条表测试:这些串是产品日志里 `excluded_reason=` 的唯一取值,而它们只在 +/// **构建这样一张表**的时候才会被逐个过一遍——散在断言里时,`unknown-os` 这种「只在一台 +/// Windows 机器、且版本探测失败时才出现」的值最容易一直没有断言(复审就是这么抓到的)。 +/// +/// **抓得住**:两个原因码共用一个串(日志再也分不清原因)、串里混进空格(那一栏会被截断)、 +/// 或者某一条被改成空串。 +#[test] +fn every_exclusion_code_is_a_stable_non_empty_string() { + use std::collections::BTreeSet; + + let codes = [ + (CornerExclusion::UserSquare, "user"), + (CornerExclusion::OsBuild, "os-build"), + (CornerExclusion::UnknownOs, "unknown-os"), + (CornerExclusion::CpuTier, "cpu-tier"), + (CornerExclusion::BorderlessFrame, "borderless"), + (CornerExclusion::ClientSideFrame, "client-side"), + (CornerExclusion::Unsupported, "unsupported"), + ]; + + let mut seen = BTreeSet::new(); + for (reason, code) in codes { + assert_eq!(reason.as_str(), code, "{reason:?} 的日志取值"); + assert!( + !code.is_empty() && !code.contains(char::is_whitespace), + "原因码必须是单个词(日志按空格分栏):{code:?}" + ); + assert!(seen.insert(code), "两个原因码共用了同一个串:{code}"); + } + assert_eq!(seen.len(), 7, "目前共七个原因码,少一个就是漏了断言"); +} + /// **同一策略 N 次只下发 / 只记 1 行**(M116 r1 复审 P2-4 的判据)。 /// /// 为什么要有这条:设置页的整窗 α / 面板 α / 暗化滑条每动一下都会触发一次外观变更, -/// 而圆角与它们无关。没有去重时拖一次滑条会刷出一串 `[corner] change …`, +/// 而圆角与它们无关。没有去重时拖一次滑条就会刷出一串 `[corner] change …`, /// 把「分辨有没有生效」的那唯一一行日志淹掉。 /// /// 抓得住:把去重删掉(`needs_redispatch` 恒 `true` 时这里会数到 N)、或指纹里漏掉某一维 @@ -265,7 +341,7 @@ fn the_same_policy_is_dispatched_only_once() { use super::{needs_redispatch, CornerApplied}; let os = WindowsVersion::new(10, 0, 26_100); - let plan = plan_corner(WindowCorner::System, Some(os), Backend::Gpu); + let plan = on_windows(WindowCorner::System, Some(os), Backend::Gpu); // 模拟「滑条被拖动 20 次」:每次都是同一个策略、同一个系统、句柄一直在。 let mut last: Option = None; @@ -280,7 +356,7 @@ fn the_same_policy_is_dispatched_only_once() { assert_eq!(dispatches, 1, "同一策略连按 20 次只该下发/记 1 次"); // 换档必须重下。 - let square = plan_corner(WindowCorner::Square, Some(os), Backend::Gpu); + let square = on_windows(WindowCorner::Square, Some(os), Backend::Gpu); assert!(needs_redispatch( last.as_ref(), &CornerApplied::new(WindowCorner::Square, &square, true, false) diff --git a/crates/app-iced/src/platform/win32/transparency/tests.rs b/crates/app-iced/src/platform/win32/transparency/tests.rs index 4f55898..fe4f70a 100644 --- a/crates/app-iced/src/platform/win32/transparency/tests.rs +++ b/crates/app-iced/src/platform/win32/transparency/tests.rs @@ -194,11 +194,20 @@ fn dword_error_codes_and_hresults_are_printed_differently() { #[test] #[ignore = "要真窗口:手动跑 cargo test -p unlucky-app-iced --lib m116 -- --ignored --nocapture"] fn m116_corner_log_line_on_real_windows() { - use crate::platform::win32::corner::plan_corner; + use crate::platform::host::HostOs; + use crate::platform::win32::corner::plan_corner_for; use unlucky_presenter::prefs::WindowCorner; + // 平台**显式传 Windows**:这条探针本身就是 `#[cfg(target_os = "windows")]` 的真窗口实验, + // 用宿主绑定的 `plan_corner` 只会让「这台机器是 Windows」变成一个隐式前提(复审 P1-1 的 + // 同类问题:本机绿、别的宿主红)。判据与产品完全同一份,产物那一行逐字不变。 let os = crate::platform::current_windows_version(); - let plan = plan_corner(WindowCorner::System, os, crate::Backend::Gpu); + let plan = plan_corner_for( + WindowCorner::System, + HostOs::Windows, + os, + crate::Backend::Gpu, + ); // 直接遍历 `controlled_experiment` 的**同一份**行定义(含 `product` / `product+alpha`), // 免得这里另抄一份样式表、跟探针悄悄分叉。 for row in super::controlled_experiment() { @@ -225,7 +234,12 @@ fn m116_corner_log_line_on_real_windows() { } // 设置里选「直角」时的取值(`excluded_reason` 必须变成 `user`)。 - let square = plan_corner(WindowCorner::Square, os, crate::Backend::Gpu); + let square = plan_corner_for( + WindowCorner::Square, + HostOs::Windows, + os, + crate::Backend::Gpu, + ); println!( "[corner] policy=square {}", square.log_line(WindowCorner::Square)