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
@@ -0,0 +1,232 @@
|
||||
//! `[env]` 与 `[i18n]`:**本机是什么**(系统 / arch / 核数 / GPU / 时区)与**为什么是这个语言**。
|
||||
//!
|
||||
//! ## 语言链为什么在这里而不是 `platform/`
|
||||
//!
|
||||
//! `platform::locale::SystemLocale` 是 **Windows 注册表**那一路(`reg.exe`,零 FFI)。macOS 的
|
||||
//! 真值源是 `defaults read -g AppleLanguages`,而 M98 的 scope 没有 `platform/**`,所以
|
||||
//! [`AppleLanguages`] 先落在这里。**它该搬去 `platform/locale.rs`**——两处并存会漂(见交接报告)。
|
||||
|
||||
use unlucky_i18n::lang::{self, Detection, LocaleSource};
|
||||
|
||||
use crate::Probe;
|
||||
|
||||
// ---------------------------------------------------------------- [env]
|
||||
|
||||
/// `[env]`:系统 / arch / 核数 / 语言 / 时区 / 缩放请求 / GPU。
|
||||
///
|
||||
/// `gpu` 与 `hardware` **来自真实适配器枚举**([`Probe`]),不是请求值;枚举不到就写
|
||||
/// `<无适配器>` 并把 `hardware=false` 一起打出来——「没有适配器」与「没枚举」是两件事。
|
||||
///
|
||||
/// `display=` 恒为 `needs-window`:显示器尺寸只有拿到窗口句柄才问得到,而这一行在开窗之前就打了。
|
||||
/// 真值由验收脚本用 `system_profiler SPDisplaysDataType` 采进 `logs/00-env.txt`——**写个猜的值
|
||||
/// 比留白更坏**(那会让「Monitor 4K / 无外接」这种结论变成猜测)。
|
||||
#[must_use]
|
||||
pub fn env_line(probe: &Probe, language: &str, scale: Option<f32>, tz: Option<&str>) -> String {
|
||||
format!(
|
||||
"[env] os={} arch={} cores={} language={language} tz={} scale_requested={} display=needs-window \
|
||||
adapters={} hardware={} gpu=\"{}\"",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH,
|
||||
std::thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get),
|
||||
tz.unwrap_or("<未设置>"),
|
||||
scale.map_or_else(|| "system".to_owned(), |value| value.to_string()),
|
||||
probe.adapters.len(),
|
||||
probe.hardware,
|
||||
gpu_summary(probe),
|
||||
)
|
||||
}
|
||||
|
||||
/// 枚举结果里**最该被当成 GPU** 的那个适配器的摘要(硬件优先,其次任何适配器)。
|
||||
///
|
||||
/// 与 `Probe::describe` 的分工:那个给全量清单(日志一行),这个给「一句话结论」(报告表格用)。
|
||||
/// 判据同源:`device_type` 不是 `Cpu` / `Other` 才算硬件(WARP 排在第一是本机实测的形状)。
|
||||
#[must_use]
|
||||
pub fn gpu_summary(probe: &Probe) -> String {
|
||||
probe
|
||||
.adapters
|
||||
.iter()
|
||||
.find(|adapter| !matches!(adapter.device_type, "Cpu" | "Other"))
|
||||
.or_else(|| probe.adapters.first())
|
||||
.map_or_else(
|
||||
|| "<无适配器>".to_owned(),
|
||||
|adapter| format!("{} [{}]", adapter.name, adapter.device_type),
|
||||
)
|
||||
}
|
||||
|
||||
/// 时区(`TZ` 环境变量)。`std` 拿不到系统时区,所以这里**只报环境变量**,没有就写未设置。
|
||||
///
|
||||
/// macOS 上的真值时区由脚本用 `date +%Z%z` 采(那份是观测值,这个只是进程视角)。
|
||||
#[must_use]
|
||||
pub fn tz() -> Option<String> {
|
||||
std::env::var("TZ")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- [i18n]
|
||||
|
||||
/// 语言决策链的四级(顺序即优先级:显式值 > 平台源 > 环境变量 > 英文)。
|
||||
///
|
||||
/// 每一级的**原始值**都留着,日志才能解释「为什么是英文」——只打结论的话,macOS 上「英文」
|
||||
/// 到底是「用户就是英文」还是「一个源都没取到」根本分不出来。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Chain {
|
||||
/// `ui.language` 的显式值(当前没有读取点,恒 `None`)。
|
||||
pub setting: Option<String>,
|
||||
/// (源自称的名字, 原始 locale)。
|
||||
pub platform: (&'static str, Option<String>),
|
||||
/// (源自称的名字, 原始 locale)。
|
||||
pub env: (&'static str, Option<String>),
|
||||
/// 结论(由 [`lang::resolve`] 算,不在本模块重写一遍优先级)。
|
||||
pub detection: Detection,
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
/// 用三个源探测一次。**每个源只读一次**(`defaults` / `reg.exe` 都是子进程)。
|
||||
#[must_use]
|
||||
pub fn read(
|
||||
setting: Option<&str>,
|
||||
platform: &dyn LocaleSource,
|
||||
env: &dyn LocaleSource,
|
||||
) -> Self {
|
||||
let platform = Captured {
|
||||
label: platform.label(),
|
||||
raw: platform.locale(),
|
||||
};
|
||||
let env = Captured {
|
||||
label: env.label(),
|
||||
raw: env.locale(),
|
||||
};
|
||||
// 优先级仍由 `resolve` 单点决定:这里把**已读到的值**包成同标签的源喂回去,
|
||||
// 不复制一遍 if/else(复制就会漂)。
|
||||
let detection = lang::resolve(setting, &platform, &env);
|
||||
Self {
|
||||
setting: setting.map(str::to_owned),
|
||||
platform: (platform.label, platform.raw),
|
||||
env: (env.label, env.raw),
|
||||
detection,
|
||||
}
|
||||
}
|
||||
|
||||
/// 本机(平台源 + 环境变量;配置未接线)。
|
||||
#[must_use]
|
||||
pub fn host() -> Self {
|
||||
Self::read(None, platform_locale_source(), &lang::EnvLocale)
|
||||
}
|
||||
|
||||
/// 结论语言标签(`zh-CN` / `en`)。
|
||||
#[must_use]
|
||||
pub fn language(&self) -> &'static str {
|
||||
self.detection.lang.tag()
|
||||
}
|
||||
|
||||
/// `[i18n]` 一行:四级链 + 结论。
|
||||
///
|
||||
/// `ui_text=pinned-en` 是**今天的事实**:`pages/mod.rs::render` 里下载页固定传 `Lang::En`、
|
||||
/// 日志页传 `model.lang`(默认也是 `En`),也就是**文案还没接这条结论**。不写这一格的话,
|
||||
/// 日志会让人以为「语言已经是中文了」——那是新的假绿。
|
||||
#[must_use]
|
||||
pub fn line(&self) -> String {
|
||||
format!(
|
||||
"[i18n] setting={} platform={}:{} env={}:{} -> {} ui_text=pinned-en",
|
||||
show(self.setting.as_deref()),
|
||||
self.platform.0,
|
||||
show(self.platform.1.as_deref()),
|
||||
self.env.0,
|
||||
show(self.env.1.as_deref()),
|
||||
self.detection.describe()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn show(value: Option<&str>) -> String {
|
||||
value.map_or_else(|| "<未设置>".to_owned(), |raw| raw.to_owned())
|
||||
}
|
||||
|
||||
/// 把值钉住的源(为了「读一次、按真优先级算」)。
|
||||
struct Captured {
|
||||
label: &'static str,
|
||||
raw: Option<String>,
|
||||
}
|
||||
|
||||
impl LocaleSource for Captured {
|
||||
fn locale(&self) -> Option<String> {
|
||||
self.raw.clone()
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
self.label
|
||||
}
|
||||
}
|
||||
|
||||
/// 本平台的「系统语言」真值源。
|
||||
///
|
||||
/// - Windows:`reg.exe` 读注册表([`crate::platform::locale::SystemLocale`]);
|
||||
/// - macOS:[`AppleLanguages`](`defaults read -g AppleLanguages`);
|
||||
/// - 其它:没有平台源(`resolve` 会继续往环境变量走)。
|
||||
///
|
||||
/// 为什么 macOS 不复用 `SystemLocale`:它 `cfg!(windows)` 之外恒返回 `None`,在 Mac 上等于
|
||||
/// 「没有平台源」——于是语言只剩环境变量一级,而 GUI 程序从 Finder 启动时 `LANG` 通常是空的。
|
||||
#[must_use]
|
||||
pub fn platform_locale_source() -> &'static dyn LocaleSource {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
&AppleLanguages
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
&crate::platform::locale::SystemLocale
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 的系统语言源:`defaults read -g AppleLanguages` 的第一项。
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct AppleLanguages;
|
||||
|
||||
impl LocaleSource for AppleLanguages {
|
||||
fn locale(&self) -> Option<String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let output = std::process::Command::new("defaults")
|
||||
.args(["read", "-g", "AppleLanguages"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
parse_apple_languages(&String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
"applelanguages"
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 `defaults` 的输出,取**第一条**语言(真机格式见测试夹具)。
|
||||
///
|
||||
/// ```text
|
||||
/// (
|
||||
/// "zh-Hans-CN",
|
||||
/// "en-US"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// 首项是用户的**首选语言**;后面几项是「语言与地区」里的备选,与本 crate「只看主语言子标签」
|
||||
/// 的口径无关。
|
||||
#[must_use]
|
||||
pub fn parse_apple_languages(text: &str) -> Option<String> {
|
||||
let start = text.find('"')?;
|
||||
let rest = &text[start + 1..];
|
||||
let end = rest.find('"')?;
|
||||
let value = rest[..end].trim();
|
||||
(!value.is_empty()).then(|| value.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "locale_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user