代码复审 r3 的四条 p2,修三条、登记一条(P2-4 页脚余量 13px,后置)。 **P2-1(回归修复)**:r3 把归属判据写成「有选中就只认选中」,把**导入崩溃文件**与 `open_session` 这两条路径的诊断一并关掉了——`559ba20` 是**无条件**画的,所以这是 r3 引入的**代码级回归**, 不是既有缺口(今天 iced 侧没有 dispatch 站点,那些还在遗留的 Slint shell 里,故复审未判 p1)。 判据改成三分支(`app/view/logs.rs`): | live_session | selected_session | 报告算数的条件 | |---|---|---| | Some(s) | 任意 | `report.session == s`(只有它在线时日志行是它在追加,别人的报告挂上来就是串场) | | None | Some(s) | `report.session == s` **或** 报告不属于任何已知会话(导入用 `file:<文件名>`,永不在索引里) | | None | None | 全部算数(此时日志行只可能来自 `open_session`/导入,与报告同一次调用发出) | 新增两条测试(都走真实 reducer):`an_imported_crash_file_still_shows_its_findings`、 `a_live_run_says_it_is_running_instead_of_nothing_runs`。 残留(今天不可达,已登记):`open_session(A)` 在 `selected_session` 仍停在上场 B 时会被挡掉—— 真要修得让 presenter 在 `LogLinesChanged` 记下「日志行属于哪一场」。 **P2-2 置信度档(视觉变更)**:`Severity`(借 core 的 `critical/warning/notice` 词汇)→ `Confidence`(`High/Medium/Low`)+ 新 key `LogsConfidenceHigh/Medium/Low`(中英齐,标题明确写 「高/中/低置信度」)。阈值仍是 80/50,但它现在是**档位定义**而不是「严重度代理」。 摘要里的 `findings=rule:high(85)` 同步换档位 slug。DTO 补齐 `severity` 之后应换回真严重度(finding)。 **P2-3 运行中的页头**:session 索引只在**结束**时追加,运行中的那一场不在 `log_sessions` 里 ⇒ 拿不到实例名与结局 ⇒ r3 之前页头在日志滚动时一直说「当前没有运行中的实例」(demo 伪造了身份, 截图照不出来)。`Logs` 新增 `live: bool`(`live_session.is_some()`),`with_status` 三级回退: 有实例+结局 → `实例 · 时间 · 结局`;运行中但没结局 → 新 key `LogsSubtitleLive`(「Running now — logs stream as they arrive」);都没有 → `LogsSubtitleIdle`。 新增 `a_live_run_says_it_is_running_instead_of_nothing_runs`(真实 reducer:只有 `LogAppended(running=true)`,断言副标题不是 Idle 文案且含 `Running now`)。 新增 demo 状态 `ULL_LOGS_FIXTURE=running` 用于出图。 **变异实测**(每条:回退 → 红 → 放回 → 绿):见报告。 验证:`fmt --check` 通过;`clippy -p unlucky-app-iced -p unlucky-i18n --all-targets -D warnings` 干净; `cargo test` **280 + 6 + 23 passed / 0 failed**;行数最大 `pages/mod.rs` 367、`app/view.rs` 324、 `app/view/logs.rs` 337。
183 lines
7.3 KiB
Rust
183 lines
7.3 KiB
Rust
//! 日志页演示数据:**只给 `pages_demo` 出图与单测**,产品运行时的数据来自 presenter 快照
|
||
//! (`app/view.rs::logs_model`)。页面任何路径都不读盘:日志文本要么来自快照,要么来自
|
||
//! 本文件 `include_str!` 内嵌的 [`SAMPLE`](编译期进二进制,运行时零 I/O)。
|
||
//!
|
||
//! ## 为什么用环境变量挑状态
|
||
//!
|
||
//! 三种状态(空态 / 有日志 / 有诊断)要分别出图,而 `pages_demo` 的 `--screen` 只有一个
|
||
//! `logs`。环境变量是这个 crate 已有的 harness 口径(`ULL_GALLERY_SHOT` / `ULL_FEEDBACK_PART`
|
||
//! 都是这么挑的,见 `components/gallery/mod.rs` 的说明):既能被测试直接调纯函数覆盖,
|
||
//! 又不用跟别人的 CLI 解析抢同一个文件。
|
||
//!
|
||
//! ```text
|
||
//! ULL_LOGS_FIXTURE=empty pages_demo --screen=logs --screenshot=logs-empty-light.png
|
||
//! ```
|
||
//!
|
||
//! 取值:`empty`(空态)/ `live`(有日志、无诊断)/ 其它或未设 = `crash`(日志 + 三条诊断)。
|
||
//! 默认取 `crash`:一屏就能看见页面的全部结构,reviewer 不必先猜环境变量。
|
||
//!
|
||
//! ## 样例内容的出处
|
||
//!
|
||
//! [`SAMPLE`] 是按 `core/diagnose/rules.d/samples/graphics/graphics_driver.log` 与
|
||
//! `integrity/unsatisfied_link.log` 的形状写的(含中文 + 堆栈 + 200 字符以上的长行),
|
||
//! 诊断结论的 `rule` / `title` / `hint` 抄自规则表 `rules.d/integrity.yaml` 与 `graphics.yaml`。
|
||
//! **中文在这里是「规则表的输出(数据)」,不是界面文案**:界面文案(标题 / 按钮 / 分组名 /
|
||
//! 空态)全部走 `unlucky_i18n` 的 key,见 `logs.rs`。
|
||
|
||
use unlucky_i18n::Lang;
|
||
|
||
use super::model::{Confidence, Finding, Level, Line, Logs, Outcome};
|
||
|
||
/// 挑演示状态的环境变量。
|
||
pub const STATE_ENV: &str = "ULL_LOGS_FIXTURE";
|
||
|
||
/// 内嵌的崩溃日志片段(见模块文档的出处)。
|
||
const SAMPLE: &str = include_str!("sample_crash.log");
|
||
|
||
/// 内嵌的**正常跑完**一场的日志片段(`live` 状态用;见 `sample_live.log` 的文件头)。
|
||
const LIVE_SAMPLE: &str = include_str!("sample_live.log");
|
||
|
||
/// 演示状态。
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum State {
|
||
/// 空态:没有输出,也没有诊断。
|
||
Empty,
|
||
/// 有日志、无诊断(正常跑着的一场)。
|
||
Live,
|
||
/// 崩溃现场:日志 + 诊断结论。
|
||
Crash,
|
||
/// **正在跑**的一场(页头走「正在运行」那条文案;session 索引里还没有它)。
|
||
Running,
|
||
}
|
||
|
||
impl State {
|
||
/// 从环境变量取值解析(未设 / 不认识一律 `Crash`)。
|
||
///
|
||
/// 取值:`empty`(空态)/ `live`(正常跑完的一场)/ `running`(**正在跑**:页头走「正在运行」)
|
||
/// / 其余 = `crash`。
|
||
#[must_use]
|
||
pub fn of_env_value(raw: Option<&str>) -> Self {
|
||
match raw {
|
||
Some("empty") => Self::Empty,
|
||
Some("live") => Self::Live,
|
||
Some("running") => Self::Running,
|
||
_ => Self::Crash,
|
||
}
|
||
}
|
||
|
||
/// 造出这个状态的视图模型(`lang` 用于页头副标题的 i18n)。
|
||
#[must_use]
|
||
pub fn build(self, lang: Lang) -> Logs {
|
||
match self {
|
||
Self::Empty => Logs::default().with_status(lang),
|
||
// 正常跑完的一场用**自己的**日志样例:页头写「正常结束」却配崩溃日志尾行是自己打自己
|
||
// (视觉复审 r2 的 p3)。
|
||
Self::Live => Logs {
|
||
instance: Some("开发测试".to_owned()),
|
||
when: Some("11-02 19:22".to_owned()),
|
||
outcome: Some(Outcome::Ok),
|
||
lines: parse(LIVE_SAMPLE),
|
||
..Logs::default()
|
||
}
|
||
.with_status(lang),
|
||
// 运行中:还没有结局、也不在 session 索引里(`host::logs::sessions` 只在结束时追加),
|
||
// 所以既没有实例名也没有 outcome —— 页头只能走「正在运行」那条中性文案。
|
||
Self::Running => Logs {
|
||
live: true,
|
||
lines: parse(LIVE_SAMPLE),
|
||
..Logs::default()
|
||
}
|
||
.with_status(lang),
|
||
Self::Crash => crash().with_status(lang),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// harness 入口:按 [`STATE_ENV`] 挑状态。
|
||
#[must_use]
|
||
pub fn demo(lang: Lang) -> Logs {
|
||
State::of_env_value(std::env::var(STATE_ENV).ok().as_deref()).build(lang)
|
||
}
|
||
|
||
/// 崩溃现场(日志 + 三条诊断,覆盖 critical / warning 两个分组)。
|
||
///
|
||
/// 不带 `status`(副标题要 i18n,见 [`Logs::with_status`]):[`State::build`] 会补上。
|
||
#[must_use]
|
||
pub fn crash() -> Logs {
|
||
Logs {
|
||
instance: Some("开发测试".to_owned()),
|
||
when: Some("11-02 19:40".to_owned()),
|
||
outcome: Some(Outcome::Crashed),
|
||
lines: parse(SAMPLE),
|
||
findings: findings(),
|
||
..Logs::default()
|
||
}
|
||
}
|
||
|
||
/// 解析内嵌样例:`#` 注释与空行跳过,首字符是等级前缀。
|
||
pub(super) fn parse(text: &str) -> Vec<Line> {
|
||
text.lines()
|
||
.filter_map(|raw| {
|
||
let raw = raw.trim_end();
|
||
if raw.is_empty() || raw.starts_with('#') {
|
||
return None;
|
||
}
|
||
|
||
// 按**字符**取等级前缀,不 `split_at(1)`:后者在非 ASCII 首字符上会切到
|
||
// 多字节序列中间直接 panic(视觉复审 r2 的 p3)。
|
||
let mut chars = raw.chars();
|
||
let level = match chars.next() {
|
||
Some('D') => Level::Debug,
|
||
Some('I') => Level::Info,
|
||
Some('W') => Level::Warn,
|
||
Some('E') => Level::Error,
|
||
_ => return None,
|
||
};
|
||
|
||
Some(Line {
|
||
text: chars.as_str().trim_start().to_owned(),
|
||
level,
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// 三条诊断(`rule` / `title` / `hint` 抄自规则表;置信度档走 [`Confidence::of`],
|
||
/// 也就是 DTO 缺 `severity` 时产品路径用的同一条档位定义)。
|
||
fn findings() -> Vec<Finding> {
|
||
vec![
|
||
finding(
|
||
"integrity.unsatisfied_link",
|
||
85,
|
||
"找不到原生库(natives):lwjgl_opengl.dll 没解压出来或被杀软删了",
|
||
"重装组件(重新解压 natives);Windows 上检查杀软隔离区",
|
||
Some("reinstall natives"),
|
||
),
|
||
finding(
|
||
"graphics.gl_operation_failure",
|
||
70,
|
||
"某个 mod/光影包做了非法的 OpenGL 操作(GL_INVALID_OPERATION)",
|
||
"换掉最近装的光影/资源包,或把资源包分辨率降下来",
|
||
Some("change_graphics.renderer = OpenGL 兼容模式"),
|
||
),
|
||
finding(
|
||
"integrity.no_class_def_found",
|
||
60,
|
||
"NoClassDefFoundError:缺少类文件(库没下全 / jar 被解压 / mod 不配套)",
|
||
"先检查 libraries 是否完整,再排查 mods 里的解压目录",
|
||
Some("reinstall libraries"),
|
||
),
|
||
]
|
||
}
|
||
|
||
fn finding(rule: &str, confidence: u8, title: &str, hint: &str, action: Option<&str>) -> Finding {
|
||
Finding {
|
||
rule: rule.to_owned(),
|
||
confidence_band: Confidence::of(confidence),
|
||
title: title.to_owned(),
|
||
hint: hint.to_owned(),
|
||
confidence,
|
||
action: action.map(str::to_owned),
|
||
}
|
||
}
|