feat(app-iced,pages): M100 取证密闭 + harness 参数拆分 + 设置页测试按 section 下放
四件前置事,都是为了设置页正文四条 section 线能并行(不写任何 section 正文):
1. 取证密闭(两条通道各有各的机制,证据分开算)
- 产品 bin:`cli::ForcedState` 加 `Rest`(静息态),给了 `--screenshot` 且没显式
`--force-state` 时默认 `Some(Rest)`;交互模式仍是 `None`(`shell::overridden`
与 `app/view.rs` 的 `model()` 各补一支)。
- harness:截图窗口开 `window::enable_mouse_passthrough`(winit 的
`set_cursor_hittest(false)`)⇒ 窗口收不到指针事件 ⇒ 全树控件恒为 Active。
为什么用通道级而不是逐控件灌 forced:设置页「外观」区的滑块/文本框/裸 button 都不消费
forced,逐控件做要改十几个组件文件,而新产品组件随时会漏。
- harness 同时数指针事件并打进日志(`指针事件=N`):让「这张图见过几个鼠标事件」变成
可读数字,替代「连跑几次 hash 一样」那种恒真的假绿。
2. harness 的外观真状态入口:`--appearance=real` 让设置页走 `appearance: Some(..)`
(工厂态 + `ULL_APPEARANCE_*`),新入口 `pages::app_with_appearance`。
`--screen=settings` 之前就有了,但 `appearance` 恒为 None,D30 要的那一屏根本不存在。
3. `pages/harness.rs` 400 行 → 拆 `pages/harness/request.rs`(`#[path]` 范式),
纯搬移部分 240 行 + 新增密闭代码。拆分等价性:同一 argv 的 PNG sha256 与拆分前一致。
4. `pages/settings/tests.rs` 按 section 下放成 `tests/{general,appearance,java,download}_section.rs`,
mod 层只留跨 section 断言(每条写清守什么、为什么不能下放)。
给某组加一条新断言 = 只改它自己那个文件。
顺带:`shell/mod.rs` 在 HEAD 就 399 行、`cli.rs` 394 行,两处都按同一范式把 tests 搬成
`shell/tests.rs` / `cli/tests.rs`(断言一字不动,逐字节可 diff)。
变异验证:把四处实现各改坏一处(默认 Rest、passthrough 默认值、overridden 的 Rest 分支、
view 的 Rest 映射),对应 4 条断言全红(390 passed / 4 failed),恢复后 394 passed。
This commit is contained in:
1 parent
2ad808d55c
commit
2061296615
16 files changed
+1152
-469
No files matched your search
@@ -3,7 +3,22 @@
|
||||
//! **刻意不接 `cli.rs`**:等 `cli.rs` 具备 `--screen` / `--window-width` 之后,这个 harness 应当
|
||||
//! 删掉,改由正式启动参数出图。参数:`--screen=instances|download|settings|logs`、`--width`、
|
||||
//! `--height`、`--theme` / `--screenshot-theme`(后者优先)、`--screenshot`、`--renderer`、
|
||||
//! `--software-renderer`、`--scale-factor`。没给 `--screenshot` 就只是开窗看。
|
||||
//! `--software-renderer`、`--scale-factor`、`--appearance=real|demo`、`--passthrough=on|off`。
|
||||
//! 解析与默认值在 [`request`](M100 拆出去:本文件当时正好 400 行、零余量);没给 `--screenshot`
|
||||
//! 就只是开窗看。
|
||||
//!
|
||||
//! ## 取证密闭(M100):取证图不许跟着系统指针变
|
||||
//!
|
||||
//! M99 复审实测过:同一个 exe 连跑 8 次,第 6 次给出**第三个 hash**,差异区是某个按钮被画成 Hovered
|
||||
//! ——根因是 harness 开真窗口,`button::Status` 由**系统光标位置**决定,而「连跑两次一样」不是判据。
|
||||
//!
|
||||
//! 所以 `--screenshot` 有一条**通道级**钉死:开窗那一刻调 `window::enable_mouse_passthrough`
|
||||
//! (winit 的 `set_cursor_hittest(false)`)⇒ 窗口从此收不到指针事件 ⇒ 全树控件恒为 `Active`。
|
||||
//! **为什么不是给每个控件灌 forced**:设置页「外观」区的滑块 / 文本框 / 裸 `iced::button` 都不消费
|
||||
//! forced,那样做要改十几个组件文件——为取证去动产品组件,代价与风险都不对,而且新组件随时会漏。
|
||||
//!
|
||||
//! 同时数指针事件(`iced::event::listen_with`)并打进日志:**「这张图收到几个鼠标事件」是可读数字**,
|
||||
//! 于是「hash 稳」不再是「大概没漏」,而是「这张图压根没收到指针事件」。
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p unlucky-app-iced --example pages_demo -- --screen=instances --width=1180 \
|
||||
@@ -12,171 +27,29 @@
|
||||
//!
|
||||
//! 等 3 帧再截图与 `app.rs` 同一口径(M60 坑 #5):第一帧可能是启动时的旧尺寸,字体首次上屏会抖动。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use iced::{window, Element, Subscription, Task};
|
||||
use iced::{window, Element, Point, Subscription, Task};
|
||||
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||
|
||||
use crate::app::components::interaction::Interaction;
|
||||
use crate::cli::Backend;
|
||||
use crate::fonts;
|
||||
use crate::pages::appearance;
|
||||
use crate::probe::{self, Probe};
|
||||
use crate::renderer;
|
||||
use crate::shot;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{app, demo_model, Model, Screen};
|
||||
use super::{app_with_appearance, demo_model, Model};
|
||||
|
||||
/// 截图落盘的帧号(见模块文档);验收窗口尺寸(D30:1180×760、100% DPI)。
|
||||
mod request;
|
||||
|
||||
pub use request::{Request, CAPTURE_POSITION};
|
||||
|
||||
/// 截图落盘的帧号(见模块文档)。
|
||||
const CAPTURE_AT_FRAME: u32 = 3;
|
||||
const DEFAULT_SIZE: (f32, f32) = (1180.0, 760.0);
|
||||
|
||||
/// 一次渲染请求。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Request {
|
||||
/// 渲染哪一页。
|
||||
pub screen: Screen,
|
||||
/// 窗口宽度(**含外壳**:布局档位就是按它判的)。
|
||||
pub width: f32,
|
||||
/// 窗口高度。
|
||||
pub height: f32,
|
||||
/// 主题(`--screenshot-theme` > `--theme` > 浅色)。
|
||||
pub flavor: SchemeFlavor,
|
||||
/// 出图路径(`None` = 只开窗)。
|
||||
pub screenshot: Option<PathBuf>,
|
||||
/// 渲染后端请求。
|
||||
pub backend: Backend,
|
||||
/// 强制缩放因子(截图要 100% DPI)。
|
||||
pub scale_factor: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for Request {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
screen: Screen::Instances,
|
||||
width: DEFAULT_SIZE.0,
|
||||
height: DEFAULT_SIZE.1,
|
||||
flavor: SchemeFlavor::Light,
|
||||
screenshot: None,
|
||||
backend: Backend::Auto,
|
||||
scale_factor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// 从进程参数解析(`argv[1..]`)。未知参数 / 缺值 / 数字非法时返回人话。
|
||||
pub fn parse(args: impl IntoIterator<Item = String>) -> Result<Self, String> {
|
||||
let mut request = Self::default();
|
||||
let mut theme: Option<SchemeFlavor> = None;
|
||||
let mut shot_theme: Option<SchemeFlavor> = None;
|
||||
let mut args = args.into_iter();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
let (key, inline) = match arg.split_once('=') {
|
||||
Some((key, value)) => (key.to_owned(), Some(value.to_owned())),
|
||||
None => (arg.clone(), None),
|
||||
};
|
||||
|
||||
match key.as_str() {
|
||||
"--screen" => {
|
||||
let raw = take(&mut args, inline, "--screen")?;
|
||||
request.screen = Screen::from_slug(&raw).ok_or_else(|| {
|
||||
format!("--screen 只认 instances|download|settings|logs,收到 {raw:?}")
|
||||
})?;
|
||||
}
|
||||
"--width" => {
|
||||
request.width = number(&take(&mut args, inline, "--width")?, "--width")?
|
||||
}
|
||||
"--height" => {
|
||||
request.height = number(&take(&mut args, inline, "--height")?, "--height")?;
|
||||
}
|
||||
"--theme" => theme = Some(flavor(&take(&mut args, inline, "--theme")?)?),
|
||||
"--screenshot-theme" => {
|
||||
shot_theme = Some(flavor(&take(&mut args, inline, "--screenshot-theme")?)?);
|
||||
}
|
||||
"--screenshot" => {
|
||||
request.screenshot =
|
||||
Some(PathBuf::from(take(&mut args, inline, "--screenshot")?));
|
||||
}
|
||||
"--software-renderer" => request.backend = Backend::Cpu,
|
||||
"--renderer" => {
|
||||
request.backend = match take(&mut args, inline, "--renderer")?.as_str() {
|
||||
"auto" => Backend::Auto,
|
||||
"gpu" => Backend::Gpu,
|
||||
"cpu" => Backend::Cpu,
|
||||
other => {
|
||||
return Err(format!("--renderer 只认 auto|gpu|cpu,收到 {other:?}"))
|
||||
}
|
||||
};
|
||||
}
|
||||
"--scale-factor" => {
|
||||
let raw = take(&mut args, inline, "--scale-factor")?;
|
||||
let value = number(&raw, "--scale-factor")?;
|
||||
if value <= 0.0 {
|
||||
return Err(format!("--scale-factor 必须 > 0,收到 {raw:?}"));
|
||||
}
|
||||
request.scale_factor = Some(value);
|
||||
}
|
||||
other => return Err(format!("未知参数 {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
request.flavor = shot_theme.or(theme).unwrap_or(SchemeFlavor::Light);
|
||||
|
||||
if request.width < 320.0 || request.height < 320.0 {
|
||||
return Err(format!(
|
||||
"窗口太小:{}×{}(至少 320×320)",
|
||||
request.width, request.height
|
||||
));
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// 出图文件名片段(`<screen>-<class>-<flavor>-w<宽>`,归档用)。
|
||||
pub fn slug(&self) -> String {
|
||||
let class = super::layout::WidthClass::of(self.width);
|
||||
let flavor = if self.flavor.is_dark() {
|
||||
"dark"
|
||||
} else {
|
||||
"light"
|
||||
};
|
||||
let width = self.width as u32;
|
||||
format!("{}-{}-{flavor}-w{width}", self.screen.slug(), class.name())
|
||||
}
|
||||
}
|
||||
|
||||
fn take(
|
||||
args: &mut impl Iterator<Item = String>,
|
||||
inline: Option<String>,
|
||||
flag: &str,
|
||||
) -> Result<String, String> {
|
||||
inline
|
||||
.or_else(|| args.next())
|
||||
.ok_or_else(|| format!("{flag} 需要一个值"))
|
||||
}
|
||||
|
||||
fn number(raw: &str, flag: &str) -> Result<f32, String> {
|
||||
raw.parse::<f32>()
|
||||
.map_err(|_| format!("{flag} 需要数字,收到 {raw:?}"))
|
||||
.and_then(|value| {
|
||||
if value.is_finite() && value > 0.0 {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(format!("{flag} 必须是正数,收到 {raw:?}"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn flavor(raw: &str) -> Result<SchemeFlavor, String> {
|
||||
match raw {
|
||||
"light" => Ok(SchemeFlavor::Light),
|
||||
"dark" => Ok(SchemeFlavor::Dark),
|
||||
other => Err(format!("主题只认 light|dark,收到 {other:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动(阻塞到出图或关窗)。
|
||||
///
|
||||
@@ -213,6 +86,14 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
size: iced::Size::new(request.width, request.height),
|
||||
decorations: false,
|
||||
resizable: true,
|
||||
// 取证窗口钉在固定落点:这样「把指针放进窗口某个位置」是可复算的(真实矩形由
|
||||
// `Harness` 用 `window::position` / `window::size` 的返回值打日志报出,见 `Message::Sized`)
|
||||
position: match request.screenshot {
|
||||
Some(_) => {
|
||||
window::Position::Specific(Point::new(CAPTURE_POSITION.0, CAPTURE_POSITION.1))
|
||||
}
|
||||
None => window::Position::default(),
|
||||
},
|
||||
..window::Settings::default()
|
||||
};
|
||||
renderer::acceptance::report_window(
|
||||
@@ -222,17 +103,53 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
None,
|
||||
);
|
||||
|
||||
// 外观**真状态**(`--appearance=real`):工厂态 + `ULL_APPEARANCE_*` 覆盖。
|
||||
// 这不是给设置页塞常量:状态从 `State::new` / `apply_env` 来,打日志的是它算出来的生效值。
|
||||
let appearance = request.real_appearance.then(|| {
|
||||
let mut state = appearance::State::new(
|
||||
&AppearancePrefs::factory(),
|
||||
&AppearanceBase::factory(),
|
||||
request.backend,
|
||||
request.flavor,
|
||||
);
|
||||
state.apply_env();
|
||||
eprintln!(
|
||||
"[harness] 外观真状态:window={:.2} panel={:.2}(生效 {:.2})bg={} fill={:?} dim={:.2} \
|
||||
配色来源={:?} 选中分组={}",
|
||||
state.window_opacity,
|
||||
state.panel_alpha,
|
||||
state.effective_panel_alpha(),
|
||||
state.bg_enabled,
|
||||
state.bg_fill,
|
||||
state.bg_dim,
|
||||
state.palette_source,
|
||||
state.selected_group,
|
||||
);
|
||||
state
|
||||
});
|
||||
|
||||
let application = |backend: renderer::Kind| {
|
||||
let boot = request.clone();
|
||||
let appearance = appearance.clone();
|
||||
iced::application(
|
||||
move || {
|
||||
let mut model = demo_model(boot.screen, boot.width);
|
||||
// 截图通道把**消费 forced 的那批控件**(实例页)钉成常态,与产品 `--screenshot`
|
||||
// 的 `ForcedState::Rest` 同一条纪律。它管不到设置页 / 外观区(那边不读 forced),
|
||||
// 那两屏的密闭靠窗口级 passthrough——别把这条当成万能的。
|
||||
if boot.screenshot.is_some() {
|
||||
model.forced = Some(Interaction::Enabled);
|
||||
}
|
||||
(
|
||||
Harness {
|
||||
ui: Ui::new(boot.flavor, families),
|
||||
model: demo_model(boot.screen, boot.width),
|
||||
model,
|
||||
appearance: appearance.clone(),
|
||||
request: boot.clone(),
|
||||
window: None,
|
||||
placed: None,
|
||||
frames: 0,
|
||||
pointer: 0,
|
||||
started,
|
||||
first_frame: None,
|
||||
backend: backend.name(),
|
||||
@@ -305,8 +222,14 @@ struct Harness {
|
||||
ui: Ui,
|
||||
/// 视图模型(演示数据;harness 不接 presenter,只出**单页**图)
|
||||
model: Model,
|
||||
/// 外观运行期状态(`--appearance=real`);`None` = 设置页画演示行。
|
||||
appearance: Option<appearance::State>,
|
||||
window: Option<window::Id>,
|
||||
/// 窗口落点(`window::position` 的**返回值**,不是请求值)。
|
||||
placed: Option<Point>,
|
||||
frames: u32,
|
||||
/// 本张图收到过几个鼠标事件(`Message::Pointer` 的计数;密闭性靠它自证)。
|
||||
pointer: u32,
|
||||
/// 进程起点(`run()` 的开头)——`[perf]` 的首帧口径与产品路径一致:从进 `run()` 算。
|
||||
started: Instant,
|
||||
/// 第一帧到达的时刻(相对 `started`)。`None` = 一帧都没到过。
|
||||
@@ -319,8 +242,14 @@ struct Harness {
|
||||
enum Message {
|
||||
Boot,
|
||||
WindowReady(Option<window::Id>),
|
||||
/// `window::position` 的回值。
|
||||
Placed(Option<Point>),
|
||||
/// `window::size` 的回值。
|
||||
Sized(iced::Size),
|
||||
Frame,
|
||||
Captured(window::Screenshot),
|
||||
/// 收到一个指针事件(只计数,不画;见 [`Harness::subscription`])。
|
||||
Pointer,
|
||||
/// 页面里的动作(骨架期只打印,接线后由 app 层处理)。
|
||||
Page(super::Message),
|
||||
}
|
||||
@@ -331,6 +260,35 @@ impl Harness {
|
||||
Message::Boot => window::latest().map(Message::WindowReady),
|
||||
Message::WindowReady(id) => {
|
||||
self.window = id;
|
||||
// 取证窗口按住**那一刻**就断开指针输入(M100 模块文档「取证密闭」)。
|
||||
// 交互模式走不到这里:`request.passthrough` 在没有 `--screenshot` 时恒为 false
|
||||
// (`Request::parse` 里显式给值会直接报错,不给值就是 false)。
|
||||
let Some(id) = id else {
|
||||
return Task::none();
|
||||
};
|
||||
let mut tasks = Vec::new();
|
||||
if self.request.passthrough {
|
||||
eprintln!("[harness] 指针穿透已开:本窗口不再接收鼠标事件(取证专用)");
|
||||
tasks.push(window::enable_mouse_passthrough(id));
|
||||
}
|
||||
tasks.push(window::position(id).map(Message::Placed));
|
||||
Task::batch(tasks)
|
||||
}
|
||||
Message::Placed(position) => {
|
||||
self.placed = position;
|
||||
match self.window {
|
||||
Some(id) => window::size(id).map(Message::Sized),
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
Message::Sized(size) => {
|
||||
match self.placed {
|
||||
Some(Point { x, y }) => eprintln!(
|
||||
"[harness] 窗口实测矩形:({x:.0}, {y:.0}) {:.0}×{:.0}(window::position/size 的回值)",
|
||||
size.width, size.height
|
||||
),
|
||||
None => eprintln!("[harness] 窗口落点取不到(window::position 回 None)"),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::Frame => {
|
||||
@@ -346,21 +304,24 @@ impl Harness {
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
Message::Pointer => {
|
||||
self.pointer += 1;
|
||||
Task::none()
|
||||
}
|
||||
Message::Captured(screenshot) => {
|
||||
let Some(path) = self.request.screenshot.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
match shot::save(&screenshot, &path) {
|
||||
Ok(summary) => {
|
||||
eprintln!("{}", summary.describe());
|
||||
eprintln!(
|
||||
"[harness] slug={} backend={}(这张图的来源,自证用)",
|
||||
self.request.slug(),
|
||||
self.backend
|
||||
);
|
||||
}
|
||||
Ok(summary) => eprintln!("{}", summary.describe()),
|
||||
Err(error) => eprintln!("[harness] 截图失败:{error}"),
|
||||
}
|
||||
eprintln!(
|
||||
"[harness] slug={} backend={} 指针事件={}(这张图的来源与密闭性,自证用)",
|
||||
self.request.slug(),
|
||||
self.backend,
|
||||
self.pointer
|
||||
);
|
||||
// `[perf]` 两格都是实测:`first_frame` = 首帧事件的时间戳,`fps` = 帧数 / 时长
|
||||
// (**取证窗口**的吞吐,不是长跑稳态帧率——那个看产品路径的 `[tick]`)。
|
||||
renderer::acceptance::report_perf_scene(
|
||||
@@ -379,11 +340,18 @@ impl Harness {
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
if self.request.screenshot.is_some() {
|
||||
window::frames().map(|_| Message::Frame)
|
||||
} else {
|
||||
Subscription::none()
|
||||
if self.request.screenshot.is_none() {
|
||||
return Subscription::none();
|
||||
}
|
||||
// 帧节拍 + **指针事件计数**两样都订阅。后者是 M100 加的:以前「取证密闭」靠
|
||||
// 「同一场景连跑几次 hash 一样」证明,而进程压根收不到鼠标时那条恒真(U5 的同族假绿)。
|
||||
Subscription::batch([
|
||||
window::frames().map(|_| Message::Frame),
|
||||
iced::event::listen_with(|event, _status, _window| match event {
|
||||
iced::Event::Mouse(_) => Some(Message::Pointer),
|
||||
_ => None,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
fn theme(&self) -> iced::Theme {
|
||||
@@ -391,7 +359,7 @@ impl Harness {
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
app(&self.ui, &self.model).map(Message::Page)
|
||||
app_with_appearance(&self.ui, &self.model, self.appearance.as_ref()).map(Message::Page)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user