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 开发中。
224 lines
5.6 KiB
Rust
224 lines
5.6 KiB
Rust
//! 断点续传保守判定的单测:矩阵覆盖 + 416 处理。
|
|
|
|
use unlucky_core::download::resume::{
|
|
decide, on_range_unsatisfiable, RestartReason, ResumeDecision, ResumeMeta, Validator,
|
|
};
|
|
use unlucky_core::download::types::{HashAlgo, ResumePolicy};
|
|
|
|
const URL: &str = "https://bmclapi2.bangbang93.com/versions/1.20.1/client.jar";
|
|
const HASH: &str = "0123456789abcdef0123456789abcdef01234567";
|
|
|
|
fn meta(url: &str) -> ResumeMeta {
|
|
ResumeMeta {
|
|
url: url.to_string(),
|
|
etag: Some("\"abc\"".to_string()),
|
|
last_modified: None,
|
|
total: Some(1_000),
|
|
algo: HashAlgo::Sha1,
|
|
hash: HASH.to_string(),
|
|
saved_at_ms: 1,
|
|
}
|
|
}
|
|
|
|
fn resume_ok(part_len: u64, meta: &ResumeMeta) -> ResumeDecision {
|
|
decide(
|
|
ResumePolicy::Conservative,
|
|
part_len,
|
|
Some(meta),
|
|
URL,
|
|
Some((HashAlgo::Sha1, HASH)),
|
|
Some(1_000),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn resumes_when_everything_matches() {
|
|
let decision = resume_ok(400, &meta(URL));
|
|
assert_eq!(
|
|
decision,
|
|
ResumeDecision::Resume {
|
|
offset: 400,
|
|
validator: Validator {
|
|
etag: Some("\"abc\"".to_string()),
|
|
last_modified: None,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn never_policy_and_missing_files_restart() {
|
|
let meta = meta(URL);
|
|
assert_eq!(
|
|
decide(
|
|
ResumePolicy::Never,
|
|
400,
|
|
Some(&meta),
|
|
URL,
|
|
Some((HashAlgo::Sha1, HASH)),
|
|
Some(1_000)
|
|
),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::PolicyNever
|
|
}
|
|
);
|
|
assert_eq!(
|
|
resume_ok(0, &meta),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::EmptyPart
|
|
}
|
|
);
|
|
// 没有元数据:无法证明中间文件是同一次下载的产物。
|
|
assert_eq!(
|
|
decide(
|
|
ResumePolicy::Conservative,
|
|
400,
|
|
None,
|
|
URL,
|
|
Some((HashAlgo::Sha1, HASH)),
|
|
Some(1_000)
|
|
),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::NoMeta
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changed_url_or_hash_restarts() {
|
|
assert_eq!(
|
|
decide(
|
|
ResumePolicy::Conservative,
|
|
400,
|
|
Some(&meta("https://other.example/client.jar")),
|
|
URL,
|
|
Some((HashAlgo::Sha1, HASH)),
|
|
Some(1_000)
|
|
),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::UrlChanged
|
|
}
|
|
);
|
|
assert_eq!(
|
|
decide(
|
|
ResumePolicy::Conservative,
|
|
400,
|
|
Some(&meta(URL)),
|
|
URL,
|
|
Some((HashAlgo::Sha1, "ffffffffffffffffffffffffffffffffffffffff")),
|
|
Some(1_000)
|
|
),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::HashChanged
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_total_or_oversize_restarts() {
|
|
let mut unknown = meta(URL);
|
|
unknown.total = None;
|
|
assert_eq!(
|
|
decide(
|
|
ResumePolicy::Conservative,
|
|
400,
|
|
Some(&unknown),
|
|
URL,
|
|
None,
|
|
None
|
|
),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::TotalUnknown
|
|
}
|
|
);
|
|
|
|
assert_eq!(
|
|
resume_ok(2_000, &meta(URL)),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::Oversize
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_part_skips_transfer() {
|
|
assert_eq!(
|
|
resume_ok(1_000, &meta(URL)),
|
|
ResumeDecision::Complete { size: 1_000 }
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_validator_restarts() {
|
|
let mut weak = meta(URL);
|
|
weak.etag = None;
|
|
weak.last_modified = None;
|
|
assert_eq!(
|
|
resume_ok(400, &weak),
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::NoValidator
|
|
}
|
|
);
|
|
// Last-Modified 单独也能当校验器。
|
|
weak.last_modified = Some("Wed, 21 Oct 2015 07:28:00 GMT".to_string());
|
|
assert!(matches!(
|
|
resume_ok(400, &weak),
|
|
ResumeDecision::Resume { offset: 400, .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn expected_size_can_stand_in_for_missing_total() {
|
|
let mut unknown = meta(URL);
|
|
unknown.total = None;
|
|
let decision = decide(
|
|
ResumePolicy::Conservative,
|
|
400,
|
|
Some(&unknown),
|
|
URL,
|
|
Some((HashAlgo::Sha1, HASH)),
|
|
Some(1_000),
|
|
);
|
|
assert!(matches!(
|
|
decision,
|
|
ResumeDecision::Resume { offset: 400, .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn range_not_satisfiable_handling() {
|
|
// 已下满:直接进入校验,不要白删一个好文件。
|
|
assert_eq!(
|
|
on_range_unsatisfiable(1_000, Some(1_000)),
|
|
ResumeDecision::Complete { size: 1_000 }
|
|
);
|
|
// 没下满却 416:中间文件不可信,丢掉重来。
|
|
let decision = on_range_unsatisfiable(400, Some(1_000));
|
|
assert_eq!(
|
|
decision,
|
|
ResumeDecision::Restart {
|
|
reason: RestartReason::RangeUnsatisfiable
|
|
}
|
|
);
|
|
assert!(decision.should_drop_part());
|
|
// 不允许丢掉「已完成」的中间文件。
|
|
assert!(!ResumeDecision::Complete { size: 1 }.should_drop_part());
|
|
}
|
|
|
|
#[test]
|
|
fn resume_meta_roundtrip_and_removal() {
|
|
let dir = std::env::temp_dir().join(format!("ull-resume-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("client.jar.ullpart.ullmeta.json");
|
|
|
|
let original = meta(URL);
|
|
original.save(&path).expect("写元数据失败");
|
|
assert_eq!(ResumeMeta::load(&path), Some(original));
|
|
|
|
ResumeMeta::remove(&path);
|
|
assert!(ResumeMeta::load(&path).is_none());
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|