test(presenter)+fix(i18n): 淘汰策略与共享缓存解耦;注释里的 ASCII 引号
- prune() 抽成独立函数:全局缓存被所有用例共享,谁往里面灌 240+ 条都会顺手清掉 别的用例刚缓存的键(实测:并行跑时 the_second_identical_refresh_... 假红)。淘汰 策略改在本地 Cache 上测,两条判据各自确定; - clippy -D warnings:from_ref 代替 &[x.clone()]; - model.rs 注释里的 "原因 + OS 原文" 用 ASCII 引号 → 页面字面量白名单守门报红,改 中文引号;core/events 的三处文档失真登记为 TowerFinding(scope 外); - 出处文案口径:TooLarge 的 detail 只留数字(presenter 不产人读文案)。
This commit is contained in:
1 parent
0b6b8f0a62
commit
dd9962e610
11 files changed
+127
-64
No files matched your search
@@ -41,9 +41,7 @@ use crate::dto::InstanceId;
|
||||
|
||||
mod preview;
|
||||
|
||||
pub use preview::{
|
||||
ScreenshotPreview, ScreenshotThumbFailure, ScreenshotThumbKind, ThumbnailView,
|
||||
};
|
||||
pub use preview::{ScreenshotPreview, ScreenshotThumbFailure, ScreenshotThumbKind, ThumbnailView};
|
||||
|
||||
/// app → host 的存档指令。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -135,7 +133,10 @@ pub enum SavesOped {
|
||||
pub struct WorldView {
|
||||
/// 磁盘上的目录名(这一版它就是展示名:`LevelName` 在 `level.dat` 里,本版不解析 NBT)。
|
||||
pub dir: String,
|
||||
/// 有没有 `icon.png`(只判存在,不解码——缩略图墙不在 U3 范围内)。
|
||||
/// 有没有 `icon.png`(只判存在,不解码)。
|
||||
///
|
||||
/// M251 之后**世界图标仍未解**:缩略图墙([`ScreenshotPreview`])只覆盖 `screenshots/`,
|
||||
/// 这是 D54 的范围。这行字眼原来写的是「缩略图墙不在 U3 范围内」,那句话已失真。
|
||||
pub icon: bool,
|
||||
/// 目录 mtime(UTC 毫秒;`None` = 取不到)。
|
||||
pub modified_ms: Option<u64>,
|
||||
|
||||
@@ -86,4 +86,3 @@ pub enum ScreenshotThumbKind {
|
||||
/// 读到了但解不出来(坏 PNG / 不支持的格式)。
|
||||
Decode,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ fn attach_with(
|
||||
if bytes > MAX_SOURCE_BYTES {
|
||||
views[index].preview = ScreenshotPreview::Failed(ScreenshotThumbFailure {
|
||||
kind: ScreenshotThumbKind::TooLarge,
|
||||
detail: format!("{bytes} > {MAX_SOURCE_BYTES} 字节"),
|
||||
detail: format!("{bytes} > {MAX_SOURCE_BYTES}"),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -140,6 +140,15 @@ fn attach_with(
|
||||
};
|
||||
}
|
||||
// ⑤ 有界:只留这一拍碰到的键(旧的一律掉)。
|
||||
prune(&mut store, &touched);
|
||||
}
|
||||
|
||||
/// 淘汰策略:超过 [`MAX_CACHE_ENTRIES`] 时只留这一拍碰到的键。
|
||||
///
|
||||
/// 单独成函数是为了**能在本地 `Cache` 上测**:全局缓存被所有用例共享,谁往它里面灌 240+ 条
|
||||
/// 就会顺手把别的用例刚缓存的键清掉(M251 实测:并行跑时 `the_second_identical_refresh_...`
|
||||
/// 会假红)。策略与「共享状态」解耦之后,两条判据各自确定。
|
||||
fn prune(store: &mut Cache, touched: &[Key]) {
|
||||
if store.len() > MAX_CACHE_ENTRIES {
|
||||
store.retain(|key, _| touched.contains(key));
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ use super::*;
|
||||
const PNG_8X6: &[u8] = &[
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 8, 0, 0, 0, 6, 8, 6, 0,
|
||||
0, 0, 254, 5, 223, 251, 0, 0, 0, 86, 73, 68, 65, 84, 120, 218, 21, 202, 33, 1, 195, 64, 12, 0,
|
||||
192, 87, 50, 92, 17, 47, 162, 56, 74, 2, 171, 34, 184, 34, 34, 98, 56, 134, 182, 123, 112,
|
||||
236, 214, 90, 207, 239, 195, 38, 72, 138, 102, 88, 235, 18, 216, 4, 73, 209, 204, 117, 194,
|
||||
45, 176, 9, 146, 162, 153, 251, 132, 20, 216, 4, 73, 209, 76, 158, 240, 10, 108, 130, 164,
|
||||
104, 230, 61, 225, 43, 176, 9, 146, 162, 25, 254, 48, 255, 110, 65, 247, 55, 89, 206, 0, 0, 0,
|
||||
0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
192, 87, 50, 92, 17, 47, 162, 56, 74, 2, 171, 34, 184, 34, 34, 98, 56, 134, 182, 123, 112, 236,
|
||||
214, 90, 207, 239, 195, 38, 72, 138, 102, 88, 235, 18, 216, 4, 73, 209, 204, 117, 194, 45, 176,
|
||||
9, 146, 162, 153, 251, 132, 20, 216, 4, 73, 209, 76, 158, 240, 10, 108, 130, 164, 104, 230, 61,
|
||||
225, 43, 176, 9, 146, 162, 25, 254, 48, 255, 110, 65, 247, 55, 89, 206, 0, 0, 0, 0, 73, 69, 78,
|
||||
68, 174, 66, 96, 130,
|
||||
];
|
||||
|
||||
/// 一次性临时目录(本 crate 不引第三方 tempfile)。
|
||||
@@ -41,8 +41,7 @@ fn temp_dir(tag: &str) -> TempDir {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ull-thumbs-{tag}-{}-{seq}", std::process::id()));
|
||||
let path = std::env::temp_dir().join(format!("ull-thumbs-{tag}-{}-{seq}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||
TempDir(path)
|
||||
@@ -73,7 +72,11 @@ fn shot(file: &str, bytes: u64, modified_ms: Option<u64>) -> ScreenshotView {
|
||||
fn a_real_png_becomes_a_thumbnail() {
|
||||
let dir = temp_dir("ready");
|
||||
let bytes = write_png(&dir.0, "2024-05-01_10.00.00.png");
|
||||
let mut views = [shot("2024-05-01_10.00.00.png", bytes, Some(1_714_557_600_000))];
|
||||
let mut views = [shot(
|
||||
"2024-05-01_10.00.00.png",
|
||||
bytes,
|
||||
Some(1_714_557_600_000),
|
||||
)];
|
||||
let calls = std::cell::Cell::new(0u32);
|
||||
let mut counting = |path: &Path, size: u64| {
|
||||
calls.set(calls.get() + 1);
|
||||
@@ -93,7 +96,9 @@ fn a_real_png_becomes_a_thumbnail() {
|
||||
fn a_broken_file_is_a_visible_reason() {
|
||||
let dir = temp_dir("broken");
|
||||
std::fs::write(dir.0.join("bad.png"), b"\x89PNG\r\n\x1a\n garbage").expect("写坏文件");
|
||||
let bytes = std::fs::metadata(dir.0.join("bad.png")).expect("元数据").len();
|
||||
let bytes = std::fs::metadata(dir.0.join("bad.png"))
|
||||
.expect("元数据")
|
||||
.len();
|
||||
let mut views = [shot("bad.png", bytes, Some(1))];
|
||||
let calls = std::cell::Cell::new(0u32);
|
||||
let mut counting = |path: &Path, size: u64| {
|
||||
@@ -165,7 +170,11 @@ fn the_sixty_first_screenshot_is_not_decoded() {
|
||||
};
|
||||
attach_with(&dir.0, &mut views, &mut counting);
|
||||
|
||||
assert_eq!(calls.get(), MAX_PER_REFRESH as u32, "只该解前 {MAX_PER_REFRESH} 张");
|
||||
assert_eq!(
|
||||
calls.get(),
|
||||
MAX_PER_REFRESH as u32,
|
||||
"只该解前 {MAX_PER_REFRESH} 张"
|
||||
);
|
||||
assert!(
|
||||
views[..MAX_PER_REFRESH]
|
||||
.iter()
|
||||
@@ -257,28 +266,44 @@ fn an_empty_list_is_a_no_op() {
|
||||
assert_eq!(calls.get(), 0);
|
||||
}
|
||||
|
||||
/// 缓存有界:跑一轮超过 [`MAX_CACHE_ENTRIES`] 的键之后,缓存不会无限长。
|
||||
/// 缓存有界:**策略函数单独测**(不往全局缓存里灌 240 条——那会把并行用例的键清掉)。
|
||||
///
|
||||
/// 抓得住:淘汰那段被删掉(`store.len()` 会一路涨到 keys 数)。本用例只断言"有界",
|
||||
/// **不**断言淘汰了哪一条(那是实现细节,钉死会让下一版换 LRU 时无谓地红)。
|
||||
/// 抓得住:淘汰那段被删掉(250 条 → 还是 250 条);
|
||||
/// 抓不住:淘汰顺序(这里只留「本拍碰到的」,没有 LRU 语义,也不该有)。
|
||||
#[test]
|
||||
fn the_cache_stays_bounded() {
|
||||
let dir = temp_dir("bounded");
|
||||
let bytes = write_png(&dir.0, "one.png");
|
||||
let calls = std::cell::Cell::new(0u32);
|
||||
let mut counting = |path: &Path, size: u64| {
|
||||
calls.set(calls.get() + 1);
|
||||
decode(path, size)
|
||||
};
|
||||
// 同一张图、每次一个新 mtime ⇒ 每次一个新键。
|
||||
let mut store: Cache = HashMap::new();
|
||||
let touched: Key = ("/x/keep.png".to_string(), Some(1), 10);
|
||||
for round in 0..(MAX_CACHE_ENTRIES + 10) {
|
||||
let mut views = [shot("one.png", bytes, Some(round as u64))];
|
||||
attach_with(&dir.0, &mut views, &mut counting);
|
||||
store.insert(
|
||||
(format!("/x/{round}.png"), Some(round as u64), 10),
|
||||
Ok(sample()),
|
||||
);
|
||||
}
|
||||
let store = cache().lock().unwrap_or_else(|poison| poison.into_inner());
|
||||
assert!(
|
||||
store.len() <= MAX_CACHE_ENTRIES,
|
||||
"缓存长了 {} 条,上限是 {MAX_CACHE_ENTRIES}",
|
||||
store.insert(touched.clone(), Ok(sample()));
|
||||
assert!(store.len() > MAX_CACHE_ENTRIES);
|
||||
|
||||
prune(&mut store, std::slice::from_ref(&touched));
|
||||
assert_eq!(
|
||||
store.len(),
|
||||
1,
|
||||
"超上限之后只该留这一拍碰到的键(实际 {})",
|
||||
store.len()
|
||||
);
|
||||
assert!(store.contains_key(&touched), "本拍碰到的键被自己清了");
|
||||
|
||||
// 没超上限时一条都不许掉。
|
||||
let mut small: Cache = HashMap::new();
|
||||
small.insert(touched.clone(), Ok(sample()));
|
||||
prune(&mut small, std::slice::from_ref(&touched));
|
||||
assert_eq!(small.len(), 1);
|
||||
}
|
||||
|
||||
/// 一张可复用的小缩略图(`tests` 里只用来占位,不看内容)。
|
||||
fn sample() -> ThumbnailView {
|
||||
ThumbnailView {
|
||||
width: 1,
|
||||
height: 1,
|
||||
pixels: vec![0, 0, 0, 255],
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user