fix(instances): M302 r1 P2 —— 冷启动首帧三态(占位 / 真空态 / 有实例)+ N1~N4
复审 P2-1:扫盘搬后台之后,`App::dispatch(RefreshAll)` 返回时快照还没到(`pump` 只抽队列、
不 `tick`),**首帧必然**在快照之前。改前那一帧画的是「计数 0 + 还没有实例 + 实心导入 CTA」
——一句假话,而且 CTA 能点。改后三态分开:
- 真源放 presenter:`dto::InstancePhase { Scanning, Stalled, Ready }`,`AppState::instances_phase`
在 `CoreEvent::InstancesChanged` 落地时置 `Ready`;**不从 `instances.is_empty()` 推**(那就是 bug 形态)。
- `Scanning` = 首个快照未到:详情列画「正在扫描实例…」占位(新 key `InstancesScanning`,golden 两语),
列头**不写计数**(藏起来,不写 0 也不写「—」),**没有**空态 CTA。
- `Stalled` = 有界兜底:`host::game::SCAN_STALL = 15 s`(= 仓库内最坏实测 1.35 s 的约 11 倍,
理由在常量文档里)。超时只**报**一次、不掐线程;迟到的结果照样落地(代次没变)。
那一屏给一枚「重新扫描」→ `InstanceMessage::RetryScan` → 新命令 `AppCommand::RefreshInstances`
(只重扫实例目录,不重拉版本清单)。那一支在 `selected_instance_id()` 守卫**之前**——冷启动没有选中项。
- `Ready` = 收到过快照(空列表也算):这时才轮到 M273 的两种空态(没选中 / 一个都没有)。
- 已就绪时**不发** `Scanning`:否则每次刷新列表都退回占位(把卡顿换成闪屏)。
判据(都能红,变异原文见报告):
- `pages/instances/tests/cold_boot.rs`:真控件树(未就绪 7 枚按钮 = 有实例那一屏,零实例 = 8 枚,
多的那枚就是 CTA;每行 2 条文字节点的差分数行数)+ 源码结构(`Scanning`/`Stalled` 两支不碰
`detail_empty` 与空态文案、超时那支接 `RetryScan`、三个 key 真的被读)。
- `app/update/instances/scan_phase.rs`:首帧 = `Scanning`、空快照 = `Ready`、`Stalled` 经重试回到
`Scanning`、重试只派 `RefreshInstances`。夹具 `ULL_INSTANCE_PHASE` 只给出图(与 `ULL_SAVES_FIXTURE` 同一条纪律)。
顺手收 N1~N4(全是措辞 / 观测口径,不动行为):
- N1 两处 docstring 不再声称「`admit` 写成 `>=` 会红」(实测等价),改成「去掉或恒真才红」;
- N2 收尾用例那条 Idle 断言的失败消息改准(它不区分新旧快照,区分靠条数/代次断言);
- N3 boot 双刷新必然丢 1 份 ⇒ 丢弃日志分成「此前没有快照落地(启动期,非交错)」与「已有落地(真交错)」,
`Scan::dropped` 的文档登记这条已知噪音;
- N4 app 用例 docstring 收窄成「一拍无事件不清表、不钉时钟」(它绕过 `App::dispatch`,抓不住那 5 行)。
拆文件守 400 行:`detail/scan.rs`(分派 + 占位)、`refresh_stall_tests.rs`(空根 / 超时)、
`app/update/instances/scan_phase.rs`(app 级三态用例);`i18n` 的 `PAGE_SOURCES` 同步加上新页面文件。
This commit is contained in:
1 parent
a0413a5f94
commit
f03411d58b
26 files changed
+1010
-70
No files matched your search
@@ -16,11 +16,10 @@
|
||||
//! | 启动收尾触发的第三次刷新 | [`a_launch_end_refresh_beats_the_two_that_were_in_flight`] | 收尾后列表不许被扫描期间的旧结果盖回「运行中」 |
|
||||
//!
|
||||
//! 三份都是**真构造**:真临时数据根、真 `profiles.json` 写入、真后台线程跑真 `game::instances`。
|
||||
//! 只有**交付顺序**是人为摆的(把 `rx` 里的任务按住、按想验的顺序喂给 host)——那正是要验的
|
||||
//! 「先发后到」,靠调度器碰运气是测不出确定结论的。
|
||||
//! 只有**交付顺序**是人为摆的(把 `rx` 里的任务按住、按想验的顺序喂给 host)——那正是要验的「先发后到」。
|
||||
//!
|
||||
//! 每条用例的「抓不住」写在各自文档里;共同的一条:**它们不量真实耗时**(扫盘在进程内可能只有
|
||||
//! 几百微秒),也不证明用户能感知的快慢——那要真盘上的 236 ms / 1.35 s 场景,见 M297 的实测口径。
|
||||
//! 每条用例的「抓不住」写在各自文档里;共同的一条:**不量真实耗时**(进程内可能只有几百微秒),
|
||||
//! 也不证明用户能感知的快慢——那要真盘上的 236 ms / 1.35 s 场景(M297 的实测口径)。
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -35,13 +34,12 @@ use crate::host::{CoreHost, HostEnv, HostTask, LiveLaunch};
|
||||
|
||||
use super::Scan;
|
||||
|
||||
/// 取任务的等待上限(真线程 + 临时根,毫秒级就回来;超时说明真的坏了)。
|
||||
/// 取任务的等待上限(真线程 + 临时根,毫秒级就回来;超时说明真的坏了)
|
||||
const WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
// ------------------------------------------------------------------ 夹具
|
||||
|
||||
/// 临时数据根守卫:测试结束(含断言 panic)时删掉目录。
|
||||
struct TempRoot(std::path::PathBuf);
|
||||
pub(super) struct TempRoot(std::path::PathBuf);
|
||||
|
||||
impl Drop for TempRoot {
|
||||
fn drop(&mut self) {
|
||||
@@ -97,7 +95,7 @@ fn ids(count: usize) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// 建好 `count` 条档案的 host(headless:不跑 `refresh_all`,不起版本清单网络线程)。
|
||||
fn host_with(label: &str, count: usize) -> (CoreHost, HostEnv, Vec<String>, TempRoot) {
|
||||
pub(super) fn host_with(label: &str, count: usize) -> (CoreHost, HostEnv, Vec<String>, TempRoot) {
|
||||
let (env, guard) = root(label);
|
||||
let ids = ids(count);
|
||||
seed(&env, &ids);
|
||||
@@ -107,7 +105,7 @@ fn host_with(label: &str, count: usize) -> (CoreHost, HostEnv, Vec<String>, Temp
|
||||
// ------------------------------------------------------------------ 取件
|
||||
|
||||
/// 从通道里等下一份扫盘任务(**不经过 `tick`**:这里要自己按住顺序)。
|
||||
fn next_scan(host: &CoreHost) -> HostTask {
|
||||
pub(super) fn next_scan(host: &CoreHost) -> HostTask {
|
||||
host.rx.recv_timeout(WAIT).expect("扫盘任务要在 5 s 内回来")
|
||||
}
|
||||
|
||||
@@ -132,7 +130,7 @@ fn ids_of(task: &HostTask) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// 事件队列里所有 `InstancesChanged` 快照(按先后)。
|
||||
fn snapshots(events: Vec<AppEvent>) -> Vec<Vec<InstanceSummary>> {
|
||||
pub(super) fn snapshots(events: Vec<AppEvent>) -> Vec<Vec<InstanceSummary>> {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|event| match event {
|
||||
@@ -240,8 +238,10 @@ fn two_refreshes_in_flight_publish_only_the_newest() {
|
||||
/// → #2 先到、#1 后到。没有代次判据时,`InstancesChanged` 的整体替换会把被删的磁贴**放回来**
|
||||
/// (点进去必然报错,用户以为删失败了)。
|
||||
///
|
||||
/// 抓得住:去掉/写错 `admit`(例如判成 `epoch >= self.epoch`),这条红;把丢掉旧代次那一步做成
|
||||
/// 「发一条空快照」也红(那正是任务里禁止的「清空再重填」)。
|
||||
/// 抓得住:把 `admit` 去掉(或改成恒真)——这条红,因为旧代次那份会被采纳、把被删的磁贴放回来;
|
||||
/// 把丢掉旧代次那一步做成「发一条空快照」也红(那正是任务里禁止的「清空再重填」)。
|
||||
/// **不**抓:把 `admit` 的判据从 `==` 改成 `>=` —— 代次只增不减、在飞任务不可能带着**未来**代次,
|
||||
/// 两种写法行为等价(复审 r1 实测变异③全绿,这条 docstring 原先声称会红,是错的)。
|
||||
/// 抓不住:刷新 #1 与删除动作**同时**发生的窄窗口——本用例把两份扫描都等回来了再摆交付顺序,
|
||||
/// 所以它验的是「顺序」,不是「磁盘并发」。
|
||||
#[test]
|
||||
@@ -350,7 +350,9 @@ fn a_launch_end_refresh_beats_the_two_that_were_in_flight() {
|
||||
assert_eq!(
|
||||
tile.runtime,
|
||||
InstanceRuntime::Idle,
|
||||
"收尾之后的快照必须是 Idle(旧结果若盖回去会带着「运行中」的外观)"
|
||||
"收尾之后磁贴必须是 Idle:运行态是**落地那刻**按 host 当前表盖的,收尾已清表,\
|
||||
所以哪怕是过期快照落地也会是 Idle —— 这条断言只验「盖的结果对」,\
|
||||
新旧之分靠上面的条数与代次断言(`published.len() == 1` / `adopted.epoch` / `dropped`)"
|
||||
);
|
||||
assert_eq!(
|
||||
host.instances_scan.adopted().map(|adopted| adopted.epoch),
|
||||
@@ -364,7 +366,8 @@ fn a_launch_end_refresh_beats_the_two_that_were_in_flight() {
|
||||
|
||||
/// `Scan` 的账目:换代 → 只认当代次 → 旧代次计数 → 记账。
|
||||
///
|
||||
/// 抓得住:把 `admit` 写成 `>=`、把丢弃计数漏掉、把 `adopt` 记到别的代次上。
|
||||
/// 抓得住:把 `admit` 去掉(或改成恒真)、把丢弃计数漏掉、把 `adopt` 记到别的代次上。
|
||||
/// **不**抓:把 `==` 写成 `>=`(代次只增不减 ⇒ 两种写法等价,复审 r1 变异③实测全绿)。
|
||||
/// 抓不住:真实交错的时序(那是上面三条用例的事)。
|
||||
#[test]
|
||||
fn the_ledger_only_admits_the_current_epoch() {
|
||||
|
||||
Reference in new issue
Block a user