Files
aria2-rust-pro/crates/aria2-rust-pro-storage/src/control/tests.rs
T

202 lines
7.5 KiB
Rust

use std::{
collections::BTreeMap,
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
checksum::Checksum,
model::{DownloadFile, PieceIndex, PieceState},
};
use super::*;
fn temp_control_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be monotonic enough for test naming")
.as_nanos();
std::env::temp_dir().join(format!("aria2-rust-pro-storage-{name}-{nanos}.aria2"))
}
fn upstream_binary_fixture(version: u16) -> Vec<u8> {
let mut bytes = Vec::new();
match version {
0 => {
bytes.extend_from_slice(&0_u16.to_le_bytes());
bytes.extend_from_slice(&0_u32.to_le_bytes());
bytes.extend_from_slice(&0_u32.to_le_bytes());
bytes.extend_from_slice(&1024_u32.to_le_bytes());
bytes.extend_from_slice(&81_920_u64.to_le_bytes());
bytes.extend_from_slice(&0_u64.to_le_bytes());
bytes.extend_from_slice(&10_u32.to_le_bytes());
bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]);
bytes.extend_from_slice(&2_u32.to_le_bytes());
bytes.extend_from_slice(&1_u32.to_le_bytes());
bytes.extend_from_slice(&1024_u32.to_le_bytes());
bytes.extend_from_slice(&1_u32.to_le_bytes());
bytes.push(0x00);
bytes.extend_from_slice(&2_u32.to_le_bytes());
bytes.extend_from_slice(&512_u32.to_le_bytes());
bytes.extend_from_slice(&1_u32.to_le_bytes());
bytes.push(0x00);
}
1 => {
bytes.extend_from_slice(&1_u16.to_be_bytes());
bytes.extend_from_slice(&0_u32.to_be_bytes());
bytes.extend_from_slice(&0_u32.to_be_bytes());
bytes.extend_from_slice(&1024_u32.to_be_bytes());
bytes.extend_from_slice(&81_920_u64.to_be_bytes());
bytes.extend_from_slice(&0_u64.to_be_bytes());
bytes.extend_from_slice(&10_u32.to_be_bytes());
bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]);
bytes.extend_from_slice(&2_u32.to_be_bytes());
bytes.extend_from_slice(&1_u32.to_be_bytes());
bytes.extend_from_slice(&1024_u32.to_be_bytes());
bytes.extend_from_slice(&1_u32.to_be_bytes());
bytes.push(0x00);
bytes.extend_from_slice(&2_u32.to_be_bytes());
bytes.extend_from_slice(&512_u32.to_be_bytes());
bytes.extend_from_slice(&1_u32.to_be_bytes());
bytes.push(0x00);
}
other => panic!("unexpected test fixture version: {other}"),
}
bytes
}
fn piece_states_by_index(metadata: &ControlMetadata) -> BTreeMap<u32, PieceState> {
metadata
.piece_states
.iter()
.map(|(index, state)| (index.0, *state))
.collect()
}
#[test]
fn control_metadata_roundtrip_preserves_runtime_fields() {
let metadata = ControlMetadata {
version: ControlFileVersion::CURRENT,
files: vec![DownloadFile {
path: PathBuf::from("D:/downloads/a.bin"),
length: 1024,
piece_length: 256,
}],
checksums: vec![Checksum::Sha256("abc123".to_owned())],
piece_states: vec![
(PieceIndex(0), PieceState::Verified),
(PieceIndex(1), PieceState::InFlight),
],
completed_length: 512,
retry_count: 3,
last_error: Some("timeout".to_owned()),
last_error_at_unix_ms: Some(1_700_000_000_001),
last_retry_at_unix_ms: Some(1_700_000_000_010),
next_retry_at_unix_ms: Some(1_700_000_000_020),
consecutive_failure_count: Some(2),
active_segment_count: Some(4),
resume_verified_at_unix_ms: Some(1_700_000_000_100),
resume_generation: Some(7),
};
let encoded = encode_control_metadata(&metadata);
let decoded = decode_control_metadata(&encoded).unwrap();
assert_eq!(decoded, metadata);
}
#[test]
fn control_metadata_decode_keeps_backward_compat_defaults() {
let raw = "version=1.0\nfiles=0\nchecksums=0\ncompleted_length=12\nretry_count=1\npieces=0";
let decoded = decode_control_metadata(raw).unwrap();
assert_eq!(decoded.completed_length, 12);
assert_eq!(decoded.retry_count, 1);
assert_eq!(decoded.last_error, None);
assert_eq!(decoded.last_error_at_unix_ms, None);
assert_eq!(decoded.active_segment_count, None);
assert_eq!(decoded.resume_generation, None);
}
#[test]
fn binary_control_reader_loads_upstream_v1_fixture() {
let path = temp_control_path("binary-v1-fixture.bin");
fs::write(&path, upstream_binary_fixture(1)).unwrap();
let loaded = read_aria2_binary_control_file(&path).unwrap();
let states = piece_states_by_index(&loaded);
let file = loaded
.files
.first()
.expect("binary control fixture should contain one file");
assert_eq!(loaded.files.len(), 1);
assert_eq!(file.path, path.with_extension(""));
assert_eq!(file.length, 81_920);
assert_eq!(file.piece_length, 1_024);
assert_eq!(loaded.completed_length, 80_896);
assert_eq!(states.get(&0), Some(&PieceState::Verified));
assert_eq!(states.get(&1), Some(&PieceState::InFlight));
assert_eq!(states.get(&2), Some(&PieceState::InFlight));
assert_eq!(states.get(&78), Some(&PieceState::Verified));
assert_eq!(states.get(&79), None);
let _ = fs::remove_file(path);
}
#[test]
fn control_file_reader_auto_detects_upstream_v0_binary_fixture() {
let path = temp_control_path("binary-v0-fixture.bin");
fs::write(&path, upstream_binary_fixture(0)).unwrap();
let loaded = read_aria2_control_file(&path).unwrap();
let states = piece_states_by_index(&loaded);
let file = loaded
.files
.first()
.expect("binary control fixture should contain one file");
assert_eq!(loaded.files.len(), 1);
assert_eq!(file.path, path.with_extension(""));
assert_eq!(file.length, 81_920);
assert_eq!(file.piece_length, 1_024);
assert_eq!(loaded.completed_length, 80_896);
assert_eq!(states.get(&0), Some(&PieceState::Verified));
assert_eq!(states.get(&1), Some(&PieceState::InFlight));
assert_eq!(states.get(&2), Some(&PieceState::InFlight));
let _ = fs::remove_file(path);
}
#[test]
fn control_file_roundtrip_preserves_multiline_runtime_error() {
let metadata = ControlMetadata {
version: ControlFileVersion::CURRENT,
files: vec![DownloadFile {
path: PathBuf::from("D:/downloads/a.bin"),
length: 1024,
piece_length: 256,
}],
checksums: Vec::new(),
piece_states: vec![(PieceIndex(0), PieceState::Verified)],
completed_length: 128,
retry_count: 2,
last_error: Some("timeout\nmirror=2;retry".to_owned()),
last_error_at_unix_ms: Some(1_700_000_001_111),
last_retry_at_unix_ms: Some(1_700_000_001_222),
next_retry_at_unix_ms: Some(1_700_000_001_333),
consecutive_failure_count: Some(4),
active_segment_count: Some(6),
resume_verified_at_unix_ms: Some(1_700_000_001_444),
resume_generation: Some(12),
};
let path = temp_control_path("roundtrip");
write_aria2_control_file(&path, &metadata).unwrap();
let raw = fs::read(&path).unwrap();
assert!(
raw.starts_with(&[0x00, 0x01]),
"control file should start with the upstream binary v0001 header"
);
let loaded = read_aria2_control_file(&path).unwrap();
assert_eq!(loaded, metadata);
let _ = fs::remove_file(path);
}