CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
150 lines
5.1 KiB
Rust
150 lines
5.1 KiB
Rust
//! 内容寻址缓存的单测:两级分片、记录信任(命中不重算)、损坏检测、索引持久化。
|
|
|
|
mod support;
|
|
|
|
use std::time::Duration;
|
|
|
|
use unlucky_core::download::cache::ContentCache;
|
|
use unlucky_core::download::types::{HashAlgo, Integrity};
|
|
|
|
const NOW: u64 = 5_000_000;
|
|
|
|
fn cache_dir(tag: &str) -> std::path::PathBuf {
|
|
support::temp_dir(tag)
|
|
}
|
|
|
|
#[test]
|
|
fn sharded_path_follows_layout() {
|
|
let dir = cache_dir("cache-path");
|
|
let cache = ContentCache::open(&dir);
|
|
let integrity = Integrity::sha1("abcdef0123456789abcdef0123456789abcdef01");
|
|
let path = cache.path_for(integrity.algo, &integrity.hex);
|
|
assert!(path.ends_with("sha1/ab/abcdef0123456789abcdef0123456789abcdef01"));
|
|
assert!(path.starts_with(cache.root()));
|
|
// SHA-256 单独一棵子树。
|
|
let sha256_path = cache.path_for(HashAlgo::Sha256, "00112233445566778899aabbccddeeff");
|
|
assert!(sha256_path.ends_with("sha256/00/00112233445566778899aabbccddeeff"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_then_lookup_hits_without_rehashing() {
|
|
let dir = cache_dir("cache-hit");
|
|
let cache = ContentCache::open(&dir);
|
|
|
|
let payload = b"hello unlucky launcher".to_vec();
|
|
let integrity = Integrity::sha1(support::sha1_hex(&payload));
|
|
let source = dir.join("download.bin");
|
|
std::fs::write(&source, &payload).unwrap();
|
|
|
|
let cached = cache
|
|
.insert(&source, &integrity, NOW)
|
|
.expect("写入缓存失败");
|
|
assert!(cached.is_file());
|
|
assert_eq!(cache.len(), 1);
|
|
|
|
// 第二次命中:size/mtime 未变 → 记录信任,直接返回(不重算摘要)。
|
|
let hit = cache.lookup(&integrity, NOW + 10).expect("应命中缓存");
|
|
assert_eq!(hit, cached);
|
|
|
|
// 文件被删 → 记录作废。
|
|
std::fs::remove_file(&cached).unwrap();
|
|
assert!(cache.lookup(&integrity, NOW + 20).is_none());
|
|
assert_eq!(cache.len(), 0);
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_cache_file_is_a_miss() {
|
|
let dir = cache_dir("cache-truncate");
|
|
let cache = ContentCache::open(&dir);
|
|
|
|
let payload = vec![7_u8; 256];
|
|
let integrity = Integrity::sha1(support::sha1_hex(&payload));
|
|
let source = dir.join("payload.bin");
|
|
std::fs::write(&source, &payload).unwrap();
|
|
let cached = cache.insert(&source, &integrity, NOW).unwrap();
|
|
|
|
// 半损坏(长度变了)必须立刻视为未命中。
|
|
std::fs::write(&cached, &payload[..128]).unwrap();
|
|
assert!(cache.lookup(&integrity, NOW + 1).is_none());
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_change_forces_a_rehash() {
|
|
let dir = cache_dir("cache-rehash");
|
|
let cache = ContentCache::open(&dir);
|
|
|
|
let payload = vec![3_u8; 128];
|
|
let integrity = Integrity::sha1(support::sha1_hex(&payload));
|
|
let source = dir.join("data.bin");
|
|
std::fs::write(&source, &payload).unwrap();
|
|
let cached = cache.insert(&source, &integrity, NOW).unwrap();
|
|
// 硬链接/复制都可能让源文件与缓存同 inode,先断开再改缓存内容。
|
|
let _ = std::fs::remove_file(&source);
|
|
|
|
// 等 mtime 变化(部分文件系统时间戳精度 10ms)。
|
|
std::thread::sleep(Duration::from_millis(30));
|
|
let mut tampered = payload.clone();
|
|
tampered[0] = 9;
|
|
std::fs::write(&cached, &tampered).unwrap();
|
|
|
|
// 内容变了 → 摘要不符 → 记录作废。
|
|
assert!(cache.lookup(&integrity, NOW + 100).is_none());
|
|
assert_eq!(cache.len(), 0);
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_verified_rejects_wrong_content_with_same_size() {
|
|
let dir = cache_dir("cache-verified");
|
|
let cache = ContentCache::open(&dir);
|
|
|
|
let payload = vec![1_u8; 64];
|
|
let integrity = Integrity::sha1(support::sha1_hex(&payload));
|
|
// 直接放到缓存位但内容是错的(模拟外部篡改 / 镜像写坏)。
|
|
let path = cache.path_for(integrity.algo, &integrity.hex);
|
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
std::fs::write(&path, vec![2_u8; 64]).unwrap();
|
|
cache.register(&path, &integrity, NOW).unwrap();
|
|
|
|
assert!(
|
|
cache.lookup_verified(&integrity, NOW + 1).is_none(),
|
|
"强制校验必须发现内容不符"
|
|
);
|
|
|
|
// 内容对了就能通过。
|
|
std::fs::write(&path, &payload).unwrap();
|
|
cache.register(&path, &integrity, NOW).unwrap();
|
|
assert!(cache.lookup_verified(&integrity, NOW + 2).is_some());
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn index_persists_across_reopen() {
|
|
let dir = cache_dir("cache-persist");
|
|
let payload = vec![5_u8; 32];
|
|
let integrity = Integrity::sha1(support::sha1_hex(&payload));
|
|
|
|
{
|
|
let cache = ContentCache::open(&dir);
|
|
let source = dir.join("src.bin");
|
|
std::fs::write(&source, &payload).unwrap();
|
|
cache.insert(&source, &integrity, NOW).unwrap();
|
|
cache.persist().expect("持久化索引失败");
|
|
assert_eq!(cache.stats().entries, 1);
|
|
assert_eq!(cache.stats().total_bytes, 32);
|
|
}
|
|
|
|
let reopened = ContentCache::open(&dir);
|
|
assert_eq!(reopened.len(), 1);
|
|
assert!(reopened.lookup(&integrity, NOW + 1).is_some());
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|