feat(acceptance): M98 —— 八类诊断日志 + 一次运行的全覆盖验收矩阵
验收要的是「一次运行、多角度覆盖、输出能直接判读」,而不是把朋友当测试机反复问 7 个问题。 这一轮把产品进程能**自己观测**的那部分日志补齐,并把人工问题压缩到 3 条。 新增 `renderer/acceptance/`(三块,全部 ≤400 行): - `mod.rs`:`[backend]`(plan = 判据,final = **结果**:`result=started|failed` + 具名错误 + `via=respawn|refuse|none`)、`[perf]`(boot 三段实测 / session / scene 帧率只算首帧之后的间隔)、 `[theme]`(来源 = 哪个参数决定,系统那条写 `pending` 等异步真值)、`[window]`(**请求值**, 字段名自证 `requested` / `scale_requested`);开关 `ULL_ACCEPTANCE=1`、尺寸覆盖 `ULL_WINDOW_SIZE=1520x900` 与它的解析/夹取(含 `size_source=` 四态)。 - `locale.rs`:`[env]`(真适配器枚举 + 硬件判定;显示器写 `needs-window` 不猜)、`[i18n]` (四级链每一级的原始值 + 结论; macOS 走 `defaults read -g AppleLanguages`)。 - `visual.rs`:`[font] chain`(令牌声明的回退链,与已有的实际落点 `[font] platform=` 互补)、 `[caps]`(`bundle` / `menu_bar_name` 从可执行文件路径**读**出来;`controls=self-drawn` 由 `decorations=false` 推出)。 接线:`app/boot.rs` 在正确的时刻把真值喂进去(探测产物 / 窗口设置 / 实测 `Instant`), `pages/harness.rs` 同一套前缀(含 `[perf] scene`),`main.rs` 用法里写出两个开关。 反模式的防线(M97 那类「在需要结果的地方打印请求」):每一格都有能改坏的断言 (`acceptance/tests.rs` + `locale_tests.rs`,共 20 条新增),失败路径的 `error="…"` 会把 wgpu 自带的双引号换成单引号,保证单行可解析。 本机(Windows)实测:auto → `[backend] final=wgpu result=started` + `Selected: AdapterInfo`; cpu → `tiny-skia`;`WGPU_BACKEND=metal` 负例 → exit 1 + 无图 + `via=refuse`;宽屏覆盖生效。 macOS 分支(Metal / AppleLanguages / 菜单栏名)需真机复核,期望字符串见 `renderer/mod.rs` 文档。 验收脚本不入库(用户要求):落在 gitignore 的 `DevDocs/ops/macos-acceptance.sh`, 由 tower 拷到分发目录。
This commit is contained in:
1 parent
18e36c6514
commit
f986981c12
9 files changed
+1429
-129
No files matched your search
@@ -1,24 +1,16 @@
|
||||
//! 截图 harness:把**一页**按给定窗口宽度 / 主题单独渲染并出图。
|
||||
//! 截图 harness:把**一页**按给定窗口宽度 / 主题单独渲染并出图(`examples/pages_demo.rs` 的入口)。
|
||||
//!
|
||||
//! 它是 `examples/pages_demo.rs` 的入口(该 example 只有三行)。**刻意不接 `cli.rs`**:
|
||||
//! `cli.rs` 归 M75 改(它正在加 `--screen`),本 mission 不碰;等 `cli.rs` 具备 `--screen` /
|
||||
//! `--window-width` 之后,这个 harness 应当删掉,改由正式启动参数出图。
|
||||
//!
|
||||
//! ## 用法
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run -p unlucky-app-iced --example pages_demo -- \
|
||||
//! --screen=instances --width=1180 --height=760 \
|
||||
//! --screenshot=out.png --screenshot-theme=light --software-renderer
|
||||
//! ```
|
||||
//!
|
||||
//! 参数:`--screen=instances|download|settings|logs`、`--width`、`--height`、
|
||||
//! `--theme` / `--screenshot-theme`(后者优先)、`--screenshot`、`--renderer`、
|
||||
//! **刻意不接 `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` 就只是开窗看。
|
||||
//!
|
||||
//! ## 为什么还要等 3 帧
|
||||
//! ```text
|
||||
//! cargo run -p unlucky-app-iced --example pages_demo -- --screen=instances --width=1180 \
|
||||
//! --height=760 --screenshot=out.png --screenshot-theme=light --software-renderer
|
||||
//! ```
|
||||
//!
|
||||
//! 与 `app.rs` 同一口径(M60 坑 #5):第一帧可能是启动时的旧尺寸,且字体首次上屏会抖动。
|
||||
//! 等 3 帧再截图与 `app.rs` 同一口径(M60 坑 #5):第一帧可能是启动时的旧尺寸,字体首次上屏会抖动。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
@@ -36,9 +28,8 @@ use crate::theme::Ui;
|
||||
|
||||
use super::{app, demo_model, Model, Screen};
|
||||
|
||||
/// 截图落盘的帧号(见模块文档)。
|
||||
/// 截图落盘的帧号(见模块文档);验收窗口尺寸(D30:1180×760、100% DPI)。
|
||||
const CAPTURE_AT_FRAME: u32 = 3;
|
||||
/// 验收窗口尺寸(D30:1180×760、100% DPI)。
|
||||
const DEFAULT_SIZE: (f32, f32) = (1180.0, 760.0);
|
||||
|
||||
/// 一次渲染请求。
|
||||
@@ -75,10 +66,7 @@ impl Default for Request {
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// 从进程参数解析(`argv[1..]`)。
|
||||
///
|
||||
/// # Errors
|
||||
/// 未知参数、缺值、数字非法时返回人话。
|
||||
/// 从进程参数解析(`argv[1..]`)。未知参数 / 缺值 / 数字非法时返回人话。
|
||||
pub fn parse(args: impl IntoIterator<Item = String>) -> Result<Self, String> {
|
||||
let mut request = Self::default();
|
||||
let mut theme: Option<SchemeFlavor> = None;
|
||||
@@ -147,7 +135,7 @@ impl Request {
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// 出图文件名片段(`<screen>-<class>-<flavor>`,归档用)。
|
||||
/// 出图文件名片段(`<screen>-<class>-<flavor>-w<宽>`,归档用)。
|
||||
pub fn slug(&self) -> String {
|
||||
let class = super::layout::WidthClass::of(self.width);
|
||||
let flavor = if self.flavor.is_dark() {
|
||||
@@ -155,12 +143,8 @@ impl Request {
|
||||
} else {
|
||||
"light"
|
||||
};
|
||||
format!(
|
||||
"{}-{}-{flavor}-w{}",
|
||||
self.screen.slug(),
|
||||
class.name(),
|
||||
self.width as u32
|
||||
)
|
||||
let width = self.width as u32;
|
||||
format!("{}-{}-{flavor}-w{width}", self.screen.slug(), class.name())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,19 +180,16 @@ fn flavor(raw: &str) -> Result<SchemeFlavor, String> {
|
||||
|
||||
/// 启动(阻塞到出图或关窗)。
|
||||
///
|
||||
/// ## 取证通道的硬规则(M97)
|
||||
///
|
||||
/// **宁可红着退出,也不能悄悄换一种后端继续出图。** 以前这条路径在 wgpu 挂掉时会静默降级到
|
||||
/// tiny-skia,然后照样落一张 PNG,而报告上写的是「GPU 渲染截图」——所有视觉结论的可信度都被
|
||||
/// 悄悄打折,输出上看不出来。现在:① 决策被 [`renderer::strict`] 剥掉兜底;② 起不来就带着
|
||||
/// **具名**错误返回;③ 每张图的日志里都带 `backend=`,图能自证来源。
|
||||
/// **取证通道的硬规则(M97)**:宁可红着退出,也不能悄悄换一种后端继续出图(以前 wgpu 挂掉时
|
||||
/// 会静默降级 tiny-skia 照样落 PNG,而报告上写着「GPU 渲染截图」)。所以这里 ① 决策被
|
||||
/// [`renderer::strict`] 剥掉兜底、② 起不来带**具名**错误返回、③ 每张图日志都带 `backend=`。
|
||||
///
|
||||
/// # Errors
|
||||
/// 后端探测/字体求值失败、**渲染后端起不来(含 iced 的具名图形错误)**、截图落盘失败时返回人话。
|
||||
pub fn run(request: Request) -> Result<(), String> {
|
||||
let started = Instant::now();
|
||||
|
||||
let needs_probe = request.backend == Backend::Auto;
|
||||
let needs_probe = request.backend == Backend::Auto || renderer::acceptance::enabled();
|
||||
let probe = if needs_probe {
|
||||
Probe::run()
|
||||
} else {
|
||||
@@ -221,6 +202,14 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
decision.primary.name(),
|
||||
decision.reason
|
||||
);
|
||||
renderer::acceptance::report_backend_plan(&decision, request.backend);
|
||||
renderer::acceptance::report_window(
|
||||
(request.width, request.height),
|
||||
None,
|
||||
request.scale_factor,
|
||||
"pages_demo",
|
||||
None,
|
||||
);
|
||||
|
||||
let resolved = fonts::resolve();
|
||||
eprintln!("{}", resolved.describe());
|
||||
@@ -237,6 +226,8 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
request: boot.clone(),
|
||||
window: None,
|
||||
frames: 0,
|
||||
started,
|
||||
first_frame: None,
|
||||
backend: backend.name(),
|
||||
},
|
||||
Task::done(Message::Boot),
|
||||
@@ -270,21 +261,34 @@ pub fn run(request: Request) -> Result<(), String> {
|
||||
|
||||
// 兜底已被 `strict` 剥掉,所以这条最多跑一次;具名错误的口径与 App 启动一致,
|
||||
// 但**取证通道不许降级**:出不了图就红着退出,不换后端(那是另一处 iced 行为)。
|
||||
renderer::run_once(decision, |backend| {
|
||||
let session_started = Instant::now();
|
||||
let result = renderer::run_once(decision, |backend| {
|
||||
std::env::set_var("ICED_BACKEND", backend.env_value());
|
||||
let application = application(backend);
|
||||
match request.scale_factor {
|
||||
Some(scale) => application.scale_factor(move |_| scale).run(),
|
||||
None => application.run(),
|
||||
}
|
||||
})
|
||||
.map_err(|error| match renderer::named_graphics_error(&error) {
|
||||
});
|
||||
let session = session_started.elapsed();
|
||||
|
||||
let outcome = result.map_err(|error| match renderer::named_graphics_error(&error) {
|
||||
Some(named) => format!(
|
||||
"渲染后端 {} 初始化失败(取证通道无回退,未出图):{named}",
|
||||
decision.primary.name()
|
||||
),
|
||||
None => format!("iced 启动失败:{error}"),
|
||||
})?;
|
||||
});
|
||||
|
||||
renderer::acceptance::report_outcome(
|
||||
&decision,
|
||||
outcome.as_ref().err().map(String::as_str),
|
||||
"refuse",
|
||||
"screenshot",
|
||||
session,
|
||||
);
|
||||
|
||||
outcome?;
|
||||
|
||||
eprintln!(
|
||||
"[harness] 用时 {:.0} ms",
|
||||
@@ -300,6 +304,10 @@ struct Harness {
|
||||
model: Model,
|
||||
window: Option<window::Id>,
|
||||
frames: u32,
|
||||
/// 进程起点(`run()` 的开头)——`[perf]` 的首帧口径与产品路径一致:从进 `run()` 算。
|
||||
started: Instant,
|
||||
/// 第一帧到达的时刻(相对 `started`)。`None` = 一帧都没到过。
|
||||
first_frame: Option<std::time::Duration>,
|
||||
/// 出这张图的后端(`wgpu` / `tiny-skia`)。图要能**自证来源**——以前它证不了(M97)。
|
||||
backend: &'static str,
|
||||
}
|
||||
@@ -323,6 +331,9 @@ impl Harness {
|
||||
Task::none()
|
||||
}
|
||||
Message::Frame => {
|
||||
if self.frames == 0 {
|
||||
self.first_frame = Some(self.started.elapsed());
|
||||
}
|
||||
self.frames += 1;
|
||||
if self.frames < CAPTURE_AT_FRAME || self.request.screenshot.is_none() {
|
||||
return Task::none();
|
||||
@@ -347,6 +358,14 @@ impl Harness {
|
||||
}
|
||||
Err(error) => eprintln!("[harness] 截图失败:{error}"),
|
||||
}
|
||||
// `[perf]` 两格都是实测:`first_frame` = 首帧事件的时间戳,`fps` = 帧数 / 时长
|
||||
// (**取证窗口**的吞吐,不是长跑稳态帧率——那个看产品路径的 `[tick]`)。
|
||||
renderer::acceptance::report_perf_scene(
|
||||
&self.request.slug(),
|
||||
self.frames,
|
||||
self.first_frame,
|
||||
self.started.elapsed(),
|
||||
);
|
||||
iced::exit()
|
||||
}
|
||||
Message::Page(message) => {
|
||||
|
||||
Reference in new issue
Block a user