mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-28 01:32:27 +01:00
@@ -166,8 +166,8 @@ mod tests {
|
||||
|
||||
fn create_test_file(name: &str) -> ScannedMediaFile {
|
||||
ScannedMediaFile {
|
||||
path: PathBuf::from(format!("/test/{}", name)),
|
||||
file_path: format!("/test/{}", name),
|
||||
path: PathBuf::from(format!("/test/{name}")),
|
||||
file_path: format!("/test/{name}"),
|
||||
file_name: name.to_string(),
|
||||
extension: "mkv".to_string(),
|
||||
size_bytes: 1024,
|
||||
@@ -186,8 +186,7 @@ mod tests {
|
||||
let resolver = MetadataResolver::from_config(&config, client, MetadataStorage::new(PathBuf::from("/tmp")));
|
||||
let file = create_test_file("The.Matrix.1999.1080p.mkv");
|
||||
let metadata = match MediaClassifier::classify(&file) {
|
||||
MediaClassification::Movie { metadata, .. } => metadata,
|
||||
MediaClassification::Series { metadata, .. } => metadata,
|
||||
MediaClassification::Movie { metadata, .. } | MediaClassification::Series { metadata, .. } => metadata,
|
||||
};
|
||||
let group = MediaGroup::Movie { file, metadata: Box::new(metadata) };
|
||||
|
||||
@@ -215,8 +214,7 @@ mod tests {
|
||||
let resolver = MetadataResolver::from_config(&config, client, MetadataStorage::new(PathBuf::from("/tmp")));
|
||||
let file = create_test_file("343jfkjh4789dkjfh934z3.Movie.mkv");
|
||||
let metadata = match MediaClassifier::classify(&file) {
|
||||
MediaClassification::Movie { metadata, .. } => metadata,
|
||||
MediaClassification::Series { metadata, .. } => metadata,
|
||||
MediaClassification::Movie { metadata, .. } | MediaClassification::Series { metadata, .. } => metadata,
|
||||
};
|
||||
let group = MediaGroup::Movie { file, metadata: Box::new(metadata) };
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ mod tests {
|
||||
let entry = MetadataCacheEntry::new(
|
||||
"/test/movie.mp4".to_string(),
|
||||
1024,
|
||||
1234567890,
|
||||
1_234_567_890,
|
||||
MediaMetadata::Movie(MovieMetadata {
|
||||
title: "Test Movie".to_string(),
|
||||
year: Some(2020),
|
||||
|
||||
@@ -281,8 +281,8 @@ mod tests {
|
||||
assert_eq!(movie.year, Some(1999));
|
||||
assert_eq!(movie.imdb_id, Some("tt0133093".to_string()));
|
||||
assert_eq!(movie.tmdb_id, Some(603));
|
||||
assert_eq!(movie.genres.as_ref().map(|g| g.len()).unwrap_or_default(), 2);
|
||||
assert_eq!(movie.directors.as_ref().map(|g| g.len()).unwrap_or_default(), 2);
|
||||
assert_eq!(movie.genres.as_ref().map(Vec::len).unwrap_or_default(), 2);
|
||||
assert_eq!(movie.directors.as_ref().map(Vec::len).unwrap_or_default(), 2);
|
||||
} else {
|
||||
panic!("Expected movie metadata");
|
||||
}
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
assert_eq!(series.imdb_id, Some("tt0903747".to_string()));
|
||||
assert_eq!(series.tmdb_id, Some(1396));
|
||||
assert_eq!(series.tvdb_id, Some(81189));
|
||||
assert_eq!(series.genres.as_ref().map(|g| g.len()).unwrap_or_default(), 3);
|
||||
assert_eq!(series.genres.as_ref().map(Vec::len).unwrap_or_default(), 3);
|
||||
assert_eq!(series.status, Some("Ended".to_string()));
|
||||
} else {
|
||||
panic!("Expected series metadata");
|
||||
|
||||
+15
-14
@@ -281,28 +281,29 @@ async fn resolve_template<'a>(app_config: &'a Arc<AppConfig>, http_client: &'a r
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arc_swap::ArcSwap;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use crate::model::ProcessingStats;
|
||||
use super::*;
|
||||
use shared::model::{ConfigPaths};
|
||||
use crate::utils::FileLockManager;
|
||||
|
||||
fn create_app_config() -> Arc<AppConfig> {
|
||||
Arc::new(AppConfig {
|
||||
config: Arc::new(Default::default()),
|
||||
sources: Arc::new(Default::default()),
|
||||
hdhomerun: Arc::new(Default::default()),
|
||||
api_proxy: Arc::new(Default::default()),
|
||||
file_locks: Arc::new(Default::default()),
|
||||
config: Arc::new(ArcSwap::default()),
|
||||
sources: Arc::new(ArcSwap::default()),
|
||||
hdhomerun: Arc::new(ArcSwapOption::default()),
|
||||
api_proxy: Arc::new(ArcSwapOption::default()),
|
||||
file_locks: Arc::new(FileLockManager::default()),
|
||||
paths: Arc::new(ArcSwap::from_pointee(ConfigPaths {
|
||||
config_path: "".to_string(),
|
||||
config_file_path: "".to_string(),
|
||||
sources_file_path: "".to_string(),
|
||||
config_path: String::new(),
|
||||
config_file_path: String::new(),
|
||||
sources_file_path: String::new(),
|
||||
mapping_file_path: None,
|
||||
mapping_files_used: None,
|
||||
api_proxy_file_path: "".to_string(),
|
||||
api_proxy_file_path: String::new(),
|
||||
custom_stream_response_path: None,
|
||||
})),
|
||||
custom_stream_response: Arc::new(Default::default()),
|
||||
custom_stream_response: Arc::new(ArcSwapOption::default()),
|
||||
access_token_secret: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],
|
||||
encrypt_secret: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
|
||||
})
|
||||
@@ -459,7 +460,7 @@ mod tests {
|
||||
let app_cfg = create_app_config();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let template = r#"
|
||||
let template = r"
|
||||
*🔄 Playlist Update Report*
|
||||
|
||||
{{#each stats}}
|
||||
@@ -484,10 +485,10 @@ mod tests {
|
||||
{{/if}}
|
||||
|
||||
_Timestamp: {{timestamp}}_
|
||||
"#;
|
||||
";
|
||||
let output = render_template(&app_cfg, &client, Some(template), &content).await;
|
||||
|
||||
println!("Telegram Output:\n{}", output);
|
||||
println!("Telegram Output:\n{output}");
|
||||
|
||||
assert!(output.contains("🔄 Playlist Update Report"));
|
||||
assert!(output.contains("Telegram Input"));
|
||||
|
||||
@@ -617,7 +617,6 @@ mod tests {
|
||||
use crate::model::{EpgSmartMatchConfig, PersistedEpgSource, TVGuide};
|
||||
use crate::processing::parser::xmltv::normalize_channel_name;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
@@ -636,9 +635,9 @@ mod tests {
|
||||
}
|
||||
|
||||
|
||||
#[ignore]
|
||||
#[ignore = "requires a local XMLTV fixture under /tmp"]
|
||||
#[test]
|
||||
fn parse_test() -> io::Result<()> {
|
||||
fn parse_test() {
|
||||
let run_test = async move || {
|
||||
//let file_path = PathBuf::from("/tmp/epg.xml.gz");
|
||||
let file_path = PathBuf::from("/tmp/invalid_epg.xml");
|
||||
@@ -652,19 +651,18 @@ mod tests {
|
||||
|
||||
let channel_ids = HashSet::from([342u32.intern()]);
|
||||
match tv_guide.filter(&mut id_cache).await {
|
||||
None => assert!(false, "No epg filtered"),
|
||||
None => panic!("No epg filtered"),
|
||||
Some(epgs) => {
|
||||
for epg in epgs {
|
||||
assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match")
|
||||
assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let _result = tokio::runtime::Runtime::new()
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(run_test());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -725,4 +723,4 @@ mod tests {
|
||||
println!("{}", metaphone.encode(&normalize_channel_name("BU | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_smart_cfg)));
|
||||
println!("{}", metaphone.encode(&normalize_channel_name("BG | ODISEA ᵁᴴᴰ ³⁸⁴⁰ᴾ", &epg_smart_cfg)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,11 +387,10 @@ mod tests {
|
||||
let file_content = fs::read_to_string("/tmp/series-info.json").expect("Unable to read file");
|
||||
match serde_json::from_str::<XtreamSeriesInfo>(&file_content) {
|
||||
Ok(series_info) => {
|
||||
println!("{:#?}", series_info);
|
||||
assert!(true);
|
||||
println!("{series_info:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Failed to parse json file: {err}");
|
||||
panic!("Failed to parse json file: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,14 +401,13 @@ mod tests {
|
||||
if fs::exists("/tmp/vod_streams.json").unwrap_or(false) {
|
||||
let reader = Box::pin(async_file_reader(tokio::fs::File::open("/tmp/vod_streams.json").await?));
|
||||
match map_to_xtream_streams(XtreamCluster::Video, reader, &"test".intern()).await {
|
||||
Ok(_streams) => {
|
||||
println!("{:?}", _streams.get(1));
|
||||
println!("{:?}", _streams.get(100));
|
||||
println!("{:?}", _streams.get(200));
|
||||
assert!(true);
|
||||
Ok(streams) => {
|
||||
println!("{:?}", streams.get(1));
|
||||
println!("{:?}", streams.get(100));
|
||||
println!("{:?}", streams.get(200));
|
||||
}
|
||||
Err(err) => {
|
||||
assert!(false, "Failed to parse json file: {err}");
|
||||
panic!("Failed to parse json file: {err}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ mod tests {
|
||||
.map(|(i, (name, quality))| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: format!("Chanel {name} [{quality}]").into(),
|
||||
source_ordinal: i as u32,
|
||||
source_ordinal: u32::try_from(i).unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
@@ -313,10 +313,10 @@ mod tests {
|
||||
let ord =
|
||||
playlist_comparator(channel_sort.sequence.as_ref(), channel_sort.order, va, vb);
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
ord
|
||||
} else {
|
||||
if ord == Ordering::Equal {
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
} else {
|
||||
ord
|
||||
}
|
||||
});
|
||||
|
||||
@@ -376,7 +376,7 @@ mod tests {
|
||||
.map(|(i, name)| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: name.to_string().into(),
|
||||
source_ordinal: i as u32,
|
||||
source_ordinal: u32::try_from(i).unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
@@ -407,10 +407,10 @@ mod tests {
|
||||
&b.header.title,
|
||||
);
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
ord
|
||||
} else {
|
||||
if ord == Ordering::Equal {
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
} else {
|
||||
ord
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4146,7 +4146,7 @@ mod tests {
|
||||
id: record.id,
|
||||
data: format!("{content} {}", record.id + 9000),
|
||||
};
|
||||
tree_update.update(&i, new_record).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, new_record).map_err(BPlusTreeError::to_io)?;
|
||||
} else {
|
||||
panic!("{content} {i} not found");
|
||||
}
|
||||
@@ -4256,7 +4256,7 @@ mod tests {
|
||||
assert_eq!(update.len().expect("Update len failed"), test_size as usize);
|
||||
|
||||
// Update existing key - length should stay same
|
||||
update.update(&1, Record { id: 1, data: "updated".to_string() }).map_err(|e| e.to_io())?;
|
||||
update.update(&1, Record { id: 1, data: "updated".to_string() }).map_err(BPlusTreeError::to_io)?;
|
||||
assert_eq!(update.len().expect("Update len failed after update"), test_size as usize);
|
||||
|
||||
// Insert new key - length should increase
|
||||
@@ -4313,7 +4313,7 @@ mod tests {
|
||||
let mut tree_update = BPlusTreeUpdate::<u32, Record>::try_new(&filepath)?;
|
||||
for i in 0u32..100 {
|
||||
if i % 2 == 0 {
|
||||
tree_update.update(&i, Record { id: i, data: format!("UpdatedContent {i}") }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, Record { id: i, data: format!("UpdatedContent {i}") }).map_err(BPlusTreeError::to_io)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4357,7 +4357,7 @@ mod tests {
|
||||
let padding: String = generate_random_string(400);
|
||||
for i in 0u32..10 {
|
||||
// Each record has unique data to prevent compression
|
||||
tree.insert(i, Record { id: i, data: format!("{}{}", padding, i) });
|
||||
tree.insert(i, Record { id: i, data: format!("{padding}{i}") });
|
||||
}
|
||||
tree.store(&filepath)?;
|
||||
|
||||
@@ -4367,7 +4367,7 @@ mod tests {
|
||||
let mut tree_update = BPlusTreeUpdate::<u32, Record>::try_new(&filepath)?;
|
||||
let same_size_padding: String = generate_random_string(400);
|
||||
for i in 0u32..10 {
|
||||
tree_update.update(&i, Record { id: i, data: format!("{}{}", same_size_padding, i) }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, Record { id: i, data: format!("{same_size_padding}{i}") }).map_err(BPlusTreeError::to_io)?;
|
||||
}
|
||||
|
||||
let size_after_same_update = std::fs::metadata(&filepath)?.len();
|
||||
@@ -4388,7 +4388,7 @@ mod tests {
|
||||
let mut tree_update = BPlusTreeUpdate::<u32, Record>::try_new(&filepath)?;
|
||||
let smaller_padding: String = generate_random_string(200);
|
||||
for i in 0u32..10 {
|
||||
tree_update.update(&i, Record { id: i, data: format!("{}{}", smaller_padding, i) }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, Record { id: i, data: format!("{smaller_padding}{i}") }).map_err(BPlusTreeError::to_io)?;
|
||||
}
|
||||
|
||||
let size_after_smaller_update = std::fs::metadata(&filepath)?.len();
|
||||
@@ -4398,7 +4398,7 @@ mod tests {
|
||||
// 5000 chars is much larger than the original ~400 byte allocation
|
||||
let larger_padding: String = generate_random_string(5000);
|
||||
for i in 0u32..1 {
|
||||
tree_update.update(&i, Record { id: i, data: format!("{}{}", larger_padding, i) }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, Record { id: i, data: format!("{larger_padding}{i}") }).map_err(BPlusTreeError::to_io)?;
|
||||
}
|
||||
|
||||
let size_after_larger_update = std::fs::metadata(&filepath)?.len();
|
||||
@@ -4444,7 +4444,7 @@ mod tests {
|
||||
|
||||
// 2. Multiple Updates (COW)
|
||||
for i in (0..test_size).step_by(10) {
|
||||
tree_update.update(&i, Record { id: i, data: format!("UpdatedContent {i}") }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&i, Record { id: i, data: format!("UpdatedContent {i}") }).map_err(BPlusTreeError::to_io)?;
|
||||
}
|
||||
|
||||
// 3. Verify Query Integrity (Must return NEW values)
|
||||
@@ -4497,7 +4497,7 @@ mod tests {
|
||||
assert_eq!(tree_update.query_le(&5).unwrap().unwrap().id, 0);
|
||||
|
||||
// 2. COW Update
|
||||
tree_update.update(&10, Record { id: 10, data: "NewVal".to_string() }).map_err(|e| e.to_io())?;
|
||||
tree_update.update(&10, Record { id: 10, data: "NewVal".to_string() }).map_err(BPlusTreeError::to_io)?;
|
||||
|
||||
// 3. Verify LE returns the LATEST value
|
||||
let val = tree_update.query_le(&15).expect("Query failed").expect("Should find LE key after COW update");
|
||||
@@ -4574,7 +4574,7 @@ mod tests {
|
||||
.map(|(k, v)| (k, v))
|
||||
.collect();
|
||||
|
||||
tree_update.update_batch(&update_refs).map_err(|e| e.to_io())?;
|
||||
tree_update.update_batch(&update_refs).map_err(BPlusTreeError::to_io)?;
|
||||
drop(tree_update);
|
||||
|
||||
// Verify all updates
|
||||
@@ -4612,7 +4612,7 @@ mod tests {
|
||||
|
||||
// Test empty batch - should be no-op
|
||||
let empty_batch: Vec<(&u32, &Record)> = vec![];
|
||||
let result = tree_update.update_batch(&empty_batch).map_err(|e| e.to_io())?;
|
||||
let result = tree_update.update_batch(&empty_batch).map_err(BPlusTreeError::to_io)?;
|
||||
|
||||
assert_eq!(result, initial_root, "Empty batch should not change root offset");
|
||||
|
||||
@@ -4656,7 +4656,7 @@ mod tests {
|
||||
.collect();
|
||||
|
||||
// Perform batch update
|
||||
tree_update.update_batch(&update_refs).map_err(|e| e.to_io())?;
|
||||
tree_update.update_batch(&update_refs).map_err(BPlusTreeError::to_io)?;
|
||||
drop(tree_update);
|
||||
|
||||
// Verify all updates via iterator
|
||||
@@ -4704,7 +4704,7 @@ mod tests {
|
||||
.map(|(k, v)| (k, v))
|
||||
.collect();
|
||||
|
||||
tree_update.update_batch(&update_refs).map_err(|e| e.to_io())?;
|
||||
tree_update.update_batch(&update_refs).map_err(BPlusTreeError::to_io)?;
|
||||
|
||||
let size_before_compact = std::fs::metadata(&filepath)?.len();
|
||||
|
||||
@@ -4756,9 +4756,9 @@ mod tests {
|
||||
// Verify all data is present in the NEW file
|
||||
let mut tree_check = BPlusTreeQuery::<u32, Record>::try_new(&filepath)?;
|
||||
|
||||
assert!(tree_check.query(&1).map_err(|e| e.to_io())?.is_some(), "Should have key 1");
|
||||
assert!(tree_check.query(&2).map_err(|e| e.to_io())?.is_some(), "Should have key 2");
|
||||
assert!(tree_check.query(&3).map_err(|e| e.to_io())?.is_some(), "Should have key 3 - if missing, file handle wasn't updated");
|
||||
assert!(tree_check.query(&1).map_err(BPlusTreeError::to_io)?.is_some(), "Should have key 1");
|
||||
assert!(tree_check.query(&2).map_err(BPlusTreeError::to_io)?.is_some(), "Should have key 2");
|
||||
assert!(tree_check.query(&3).map_err(BPlusTreeError::to_io)?.is_some(), "Should have key 3 - if missing, file handle wasn't updated");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4935,10 +4935,11 @@ mod tests {
|
||||
// Expected size without packing:
|
||||
// 1000 items * 4096 bytes/block = 4,096,000 bytes (~4MB)
|
||||
// Plus internal nodes
|
||||
let unpacked_size_estimate = count as u64 * super::PAGE_SIZE_USIZE as u64;
|
||||
let unpacked_size_estimate =
|
||||
u64::from(count) * u64::try_from(super::PAGE_SIZE_USIZE).unwrap();
|
||||
|
||||
println!("File size with packing: {} bytes", file_size);
|
||||
println!("Estimated unpacked size: {} bytes", unpacked_size_estimate);
|
||||
println!("File size with packing: {file_size} bytes");
|
||||
println!("Estimated unpacked size: {unpacked_size_estimate} bytes");
|
||||
|
||||
// We expect significant savings.
|
||||
// 1000 items * ~60 bytes / 4096 bytes/block ~= 15 blocks
|
||||
@@ -5008,7 +5009,7 @@ mod tests {
|
||||
let val1 = "A".repeat(10000);
|
||||
let val2 = "B".repeat(10000);
|
||||
|
||||
let updates = vec![
|
||||
let updates = [
|
||||
(1, val1.clone()),
|
||||
(2, val2.clone()),
|
||||
];
|
||||
@@ -5072,7 +5073,7 @@ mod tests {
|
||||
let mut tree_update = BPlusTreeUpdate::<u32, String>::try_new(&filepath)?;
|
||||
|
||||
// Batch contains same key multiple times
|
||||
let updates = vec![
|
||||
let updates = [
|
||||
(1, "First".to_string()),
|
||||
(1, "Second".to_string()),
|
||||
(2, "Two".to_string()),
|
||||
@@ -5117,7 +5118,7 @@ mod tests {
|
||||
|
||||
let size_before = std::fs::metadata(&filepath)?.len();
|
||||
// Each individual update writes a full path, increasing file size significantly.
|
||||
assert!(size_before > count as u64 * 4000);
|
||||
assert!(size_before > u64::from(count) * 4000);
|
||||
|
||||
// Now Compact
|
||||
tree_update.compact(&filepath)?;
|
||||
@@ -5127,7 +5128,7 @@ mod tests {
|
||||
// 200 items * 100 bytes = 20KB payload.
|
||||
// Should pack into ~5-6 blocks (4KB each).
|
||||
|
||||
println!("Size before: {}, Size after: {}", size_before, size_after);
|
||||
println!("Size before: {size_before}, Size after: {size_after}");
|
||||
assert!(size_after < size_before / 10, "Compaction should pack values");
|
||||
assert!(size_after < 100 * 1024, "File should be small"); // < 100KB
|
||||
|
||||
@@ -5208,7 +5209,7 @@ mod tests {
|
||||
let keys: Vec<u32> = (0..count).collect();
|
||||
let value_info: Vec<ValueInfo> = (0..count)
|
||||
.map(|i| ValueInfo {
|
||||
mode: ValueStorageMode::Packed(i as u64 * 4096, (i % 16) as u16),
|
||||
mode: ValueStorageMode::Packed(u64::from(i) * 4096, (i % 16) as u16),
|
||||
length: 100,
|
||||
cache: Mutex::new(None),
|
||||
})
|
||||
@@ -5277,7 +5278,7 @@ mod tests {
|
||||
|
||||
// File size should have grown slightly (promoted values written at EOF)
|
||||
// but not dramatically since most values are still packed
|
||||
println!("Size initial: {}, Size after: {}", size_initial, size_after);
|
||||
println!("Size initial: {size_initial}, Size after: {size_after}");
|
||||
assert!(size_after >= size_initial, "File should not shrink");
|
||||
|
||||
// Verify all data is correct
|
||||
@@ -5303,7 +5304,7 @@ mod tests {
|
||||
|
||||
let mut tree = BPlusTree::<u32, String>::new();
|
||||
for i in 0..10 {
|
||||
tree.insert(i, format!("value_{}", i));
|
||||
tree.insert(i, format!("value_{i}"));
|
||||
}
|
||||
tree.store(&filepath)?;
|
||||
|
||||
@@ -5312,12 +5313,12 @@ mod tests {
|
||||
// Test None policy - should not error
|
||||
update.flush_policy = super::FlushPolicy::None;
|
||||
for i in 0..5 {
|
||||
update.update(&i, format!("new_{}", i)).map_err(super::BPlusTreeError::to_io)?;
|
||||
update.update(&i, format!("new_{i}")).map_err(super::BPlusTreeError::to_io)?;
|
||||
}
|
||||
|
||||
// Verify values within same session
|
||||
for i in 0..5 {
|
||||
assert_eq!(update.query(&i).map_err(super::BPlusTreeError::to_io)?.unwrap(), format!("new_{}", i));
|
||||
assert_eq!(update.query(&i).map_err(super::BPlusTreeError::to_io)?.unwrap(), format!("new_{i}"));
|
||||
}
|
||||
|
||||
// Test Batch policy
|
||||
@@ -5424,8 +5425,8 @@ mod tests {
|
||||
let page = SlottedPage::new(&mut data, PageType::Leaf).expect("Init failed");
|
||||
assert_eq!(page.header.page_type, PageType::Leaf);
|
||||
assert_eq!(page.header.cell_count, 0);
|
||||
assert_eq!(page.header.free_start, PAGE_HEADER_SIZE as u16);
|
||||
assert_eq!(page.header.free_end, PAGE_SIZE as u16);
|
||||
assert_eq!(page.header.free_start, PAGE_HEADER_SIZE);
|
||||
assert_eq!(page.header.free_end, PAGE_SIZE);
|
||||
assert_eq!(page.free_space(), PAGE_SIZE_USIZE - PAGE_HEADER_SIZE_USIZE);
|
||||
}
|
||||
|
||||
@@ -5439,11 +5440,11 @@ mod tests {
|
||||
|
||||
// Insert length-prefixed for test realism
|
||||
let mut cell1 = Vec::new();
|
||||
cell1.extend_from_slice(&(val1.len() as u32).to_le_bytes());
|
||||
cell1.extend_from_slice(&u32::try_from(val1.len()).unwrap().to_le_bytes());
|
||||
cell1.extend_from_slice(val1);
|
||||
|
||||
let mut cell2 = Vec::new();
|
||||
cell2.extend_from_slice(&(val2.len() as u32).to_le_bytes());
|
||||
cell2.extend_from_slice(&u32::try_from(val2.len()).unwrap().to_le_bytes());
|
||||
cell2.extend_from_slice(val2);
|
||||
|
||||
page.insert_at_index(0, &cell1).unwrap();
|
||||
@@ -5465,7 +5466,7 @@ mod tests {
|
||||
|
||||
let payload = vec![0xAAu8; 500];
|
||||
let mut cell = Vec::new();
|
||||
cell.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
cell.extend_from_slice(&u32::try_from(payload.len()).unwrap().to_le_bytes());
|
||||
cell.extend_from_slice(&payload);
|
||||
|
||||
for i in 0..6 {
|
||||
@@ -5496,7 +5497,7 @@ mod tests {
|
||||
// Case 1: Split single item page -> Should return None (no-op)
|
||||
let val = b"item";
|
||||
let mut cell = Vec::new();
|
||||
cell.extend_from_slice(&(val.len() as u32).to_le_bytes());
|
||||
cell.extend_from_slice(&u32::try_from(val.len()).unwrap().to_le_bytes());
|
||||
cell.extend_from_slice(val);
|
||||
page.insert_at_index(0, &cell).unwrap();
|
||||
|
||||
@@ -5506,7 +5507,7 @@ mod tests {
|
||||
assert_eq!(page.header.cell_count, 1); // Original page untouched
|
||||
}
|
||||
Ok(Some(_)) => panic!("Split of single item should result in None"),
|
||||
Err(e) => panic!("Split of single item should result in no-op, not error: {:?}", e),
|
||||
Err(e) => panic!("Split of single item should result in no-op, not error: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5535,4 +5536,3 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::bplustree::{BPlusTree, BPlusTreeUpdate, BPlusTreeQuery};
|
||||
use rand::prelude::*;
|
||||
use rand::distr::Alphanumeric;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
@@ -15,12 +16,17 @@ fn random_string(len: usize) -> String {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::too_many_lines
|
||||
)]
|
||||
fn stress_test_bplustree() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let filepath = temp_file.path().to_path_buf();
|
||||
let log_path = std::path::Path::new("/tmp/stress_results.txt");
|
||||
let mut log_file = std::fs::File::create(log_path).unwrap();
|
||||
use std::io::Write;
|
||||
|
||||
// Config
|
||||
let num_items = 500_000;
|
||||
@@ -29,7 +35,7 @@ fn stress_test_bplustree() {
|
||||
let large_val_len = 500; // Larger than packed limit (256)
|
||||
|
||||
writeln!(log_file, "=== B+Tree Stress Test & Performance Analysis ===").unwrap();
|
||||
writeln!(log_file, "Dataset: {} items", num_items).unwrap();
|
||||
writeln!(log_file, "Dataset: {num_items} items").unwrap();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 1: Batch Insert (Sequential Keys)
|
||||
@@ -50,7 +56,7 @@ fn stress_test_bplustree() {
|
||||
tree.insert(k, v);
|
||||
}
|
||||
let insert_duration = start.elapsed();
|
||||
writeln!(log_file, "Insert Time: {:.2?}", insert_duration).unwrap();
|
||||
writeln!(log_file, "Insert Time: {insert_duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", num_items as f64 / insert_duration.as_secs_f64()).unwrap();
|
||||
|
||||
// Prepare query keys
|
||||
@@ -61,13 +67,13 @@ fn stress_test_bplustree() {
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 1b: Memory-Only Random Query (Before storing)
|
||||
// ----------------------------------------------------------------
|
||||
writeln!(log_file, "\n[Phase 1b] Memory-Only Random Query ({} items)...", query_count).unwrap();
|
||||
writeln!(log_file, "\n[Phase 1b] Memory-Only Random Query ({query_count} items)...").unwrap();
|
||||
let start = Instant::now();
|
||||
for k in query_subset_mem {
|
||||
let _ = tree.query(k);
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Time: {duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", query_count as f64 / duration.as_secs_f64()).unwrap();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -78,21 +84,21 @@ fn stress_test_bplustree() {
|
||||
tree.store(&filepath).unwrap();
|
||||
drop(tree);
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Write Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Write Time: {duration:.2?}").unwrap();
|
||||
let size_phase1 = std::fs::metadata(&filepath).unwrap().len();
|
||||
writeln!(log_file, "File Size: {:.2} MB", size_phase1 as f64 / 1024.0 / 1024.0).unwrap();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 2: Random Query (Disk-based)
|
||||
// ----------------------------------------------------------------
|
||||
writeln!(log_file, "\n[Phase 2] Random Query Disk-based ({} items)...", query_count).unwrap();
|
||||
writeln!(log_file, "\n[Phase 2] Random Query Disk-based ({query_count} items)...").unwrap();
|
||||
let mut query = BPlusTreeQuery::<u32, String>::try_new(&filepath).unwrap();
|
||||
let start = Instant::now();
|
||||
for k in query_subset_mem {
|
||||
let _ = query.query_zero_copy(k).unwrap();
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Time: {duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", query_count as f64 / duration.as_secs_f64()).unwrap();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
@@ -105,11 +111,11 @@ fn stress_test_bplustree() {
|
||||
.map(|&k| (k, random_string(small_val_len)))
|
||||
.collect();
|
||||
let update_refs: Vec<(&u32, &String)> = updates.iter().map(|(k,v)| (k,v)).collect();
|
||||
let mut updater = BPlusTreeUpdate::<u32, String>::try_new(&filepath).unwrap();
|
||||
let mut tree_updater = BPlusTreeUpdate::<u32, String>::try_new(&filepath).unwrap();
|
||||
let start = Instant::now();
|
||||
updater.update_batch(&update_refs).unwrap();
|
||||
tree_updater.update_batch(&update_refs).unwrap();
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Time: {duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", update_count as f64 / duration.as_secs_f64()).unwrap();
|
||||
let size_phase3 = std::fs::metadata(&filepath).unwrap().len();
|
||||
writeln!(log_file, "File Size: {:.2} MB", size_phase3 as f64 / 1024.0 / 1024.0).unwrap();
|
||||
@@ -123,26 +129,32 @@ fn stress_test_bplustree() {
|
||||
.collect();
|
||||
let update_refs_prom: Vec<(&u32, &String)> = updates_prom.iter().map(|(k,v)| (k,v)).collect();
|
||||
let start = Instant::now();
|
||||
updater.update_batch(&update_refs_prom).unwrap();
|
||||
tree_updater.update_batch(&update_refs_prom).unwrap();
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Time: {duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", update_count as f64 / duration.as_secs_f64()).unwrap();
|
||||
let size_phase4 = std::fs::metadata(&filepath).unwrap().len();
|
||||
writeln!(log_file, "File Size: {:.2} MB", size_phase4 as f64 / 1024.0 / 1024.0).unwrap();
|
||||
drop(updater);
|
||||
drop(tree_updater);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 5: Compaction
|
||||
// ----------------------------------------------------------------
|
||||
writeln!(log_file, "\n[Phase 5] Compaction...").unwrap();
|
||||
let mut updater = BPlusTreeUpdate::<u32, String>::try_new(&filepath).unwrap();
|
||||
let mut tree_updater = BPlusTreeUpdate::<u32, String>::try_new(&filepath).unwrap();
|
||||
let start = Instant::now();
|
||||
updater.compact(&filepath).unwrap();
|
||||
tree_updater.compact(&filepath).unwrap();
|
||||
let duration = start.elapsed();
|
||||
writeln!(log_file, "Time: {:.2?}", duration).unwrap();
|
||||
writeln!(log_file, "Time: {duration:.2?}").unwrap();
|
||||
let size_phase5 = std::fs::metadata(&filepath).unwrap().len();
|
||||
writeln!(log_file, "File Size: {:.2} MB (Reduction: {:.2} MB)", size_phase5 as f64 / 1024.0 / 1024.0, (size_phase4 as i64 - size_phase5 as i64) as f64 / 1024.0 / 1024.0).unwrap();
|
||||
drop(updater);
|
||||
writeln!(
|
||||
log_file,
|
||||
"File Size: {:.2} MB (Reduction: {:.2} MB)",
|
||||
size_phase5 as f64 / 1024.0 / 1024.0,
|
||||
(size_phase4 as i64 - size_phase5 as i64) as f64 / 1024.0 / 1024.0
|
||||
)
|
||||
.unwrap();
|
||||
drop(tree_updater);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 6: Full Tree Load and In-Memory Query
|
||||
@@ -151,13 +163,13 @@ fn stress_test_bplustree() {
|
||||
let start = Instant::now();
|
||||
let tree_mem = BPlusTree::<u32, String>::load(&filepath).unwrap();
|
||||
let load_duration = start.elapsed();
|
||||
writeln!(log_file, "Load Time: {:.2?}", load_duration).unwrap();
|
||||
writeln!(log_file, "Load Time: {load_duration:.2?}").unwrap();
|
||||
|
||||
let start = Instant::now();
|
||||
for k in query_subset_mem {
|
||||
let _ = tree_mem.query(k);
|
||||
}
|
||||
let query_duration = start.elapsed();
|
||||
writeln!(log_file, "In-Memory Query Time: {:.2?}", query_duration).unwrap();
|
||||
writeln!(log_file, "In-Memory Query Time: {query_duration:.2?}").unwrap();
|
||||
writeln!(log_file, "Throughput: {:.0} ops/sec", query_count as f64 / query_duration.as_secs_f64()).unwrap();
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@ mod tests {
|
||||
assert_eq!(offset, 100);
|
||||
assert_eq!(length, 50);
|
||||
}
|
||||
_ => panic!("Expected Single location"),
|
||||
ValueLocation::Packed { .. } => panic!("Expected Single location"),
|
||||
}
|
||||
|
||||
assert_eq!(entries[1].sort_key, "banana".to_string());
|
||||
@@ -675,7 +675,7 @@ mod tests {
|
||||
assert_eq!(index, 3);
|
||||
assert_eq!(length, 75);
|
||||
}
|
||||
_ => panic!("Expected Packed location"),
|
||||
ValueLocation::Single { .. } => panic!("Expected Packed location"),
|
||||
}
|
||||
|
||||
assert_eq!(entries[2].sort_key, "cherry".to_string());
|
||||
@@ -685,7 +685,7 @@ mod tests {
|
||||
assert_eq!(offset, 300);
|
||||
assert_eq!(length, 100);
|
||||
}
|
||||
_ => panic!("Expected Single location"),
|
||||
ValueLocation::Packed { .. } => panic!("Expected Single location"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,47 +117,47 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_timeshift() {
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("2"))), EpgTimeShift::Fixed(120)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("-1:30"))), EpgTimeShift::Fixed(-90)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("+0:15"))), EpgTimeShift::Fixed(15)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("1:45"))), EpgTimeShift::Fixed(105)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from(":45"))), EpgTimeShift::Fixed(45)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("-:45"))), EpgTimeShift::Fixed(-45)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("0:30"))), EpgTimeShift::Fixed(30)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from(":3"))), EpgTimeShift::Fixed(3)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("2:"))), EpgTimeShift::Fixed(120)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("+2:00"))), EpgTimeShift::Fixed(120)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("-0:10"))), EpgTimeShift::Fixed(-10)));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("invalid"))), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(Some(&String::from("+abc"))), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(Some(&String::new())), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(Some("-1:30")), EpgTimeShift::Fixed(-90)));
|
||||
assert!(matches!(parse_timeshift(Some("+0:15")), EpgTimeShift::Fixed(15)));
|
||||
assert!(matches!(parse_timeshift(Some("1:45")), EpgTimeShift::Fixed(105)));
|
||||
assert!(matches!(parse_timeshift(Some(":45")), EpgTimeShift::Fixed(45)));
|
||||
assert!(matches!(parse_timeshift(Some("-:45")), EpgTimeShift::Fixed(-45)));
|
||||
assert!(matches!(parse_timeshift(Some("0:30")), EpgTimeShift::Fixed(30)));
|
||||
assert!(matches!(parse_timeshift(Some(":3")), EpgTimeShift::Fixed(3)));
|
||||
assert!(matches!(parse_timeshift(Some("2:")), EpgTimeShift::Fixed(120)));
|
||||
assert!(matches!(parse_timeshift(Some("+2:00")), EpgTimeShift::Fixed(120)));
|
||||
assert!(matches!(parse_timeshift(Some("-0:10")), EpgTimeShift::Fixed(-10)));
|
||||
assert!(matches!(parse_timeshift(Some("invalid")), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(Some("+abc")), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(Some("")), EpgTimeShift::None));
|
||||
assert!(matches!(parse_timeshift(None), EpgTimeShift::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timezone() {
|
||||
// Check timezone parsing creates the correct variant
|
||||
let amterdam = parse_timeshift(Some(&"Europe/Amsterdam".to_string()));
|
||||
let amterdam = parse_timeshift(Some("Europe/Amsterdam"));
|
||||
if let EpgTimeShift::TimeZone(tz) = amterdam {
|
||||
assert_eq!(tz.name(), "Europe/Amsterdam");
|
||||
} else {
|
||||
panic!("Expected TimeZone for Europe/Amsterdam");
|
||||
}
|
||||
|
||||
let new_york = parse_timeshift(Some(&"America/New_York".to_string()));
|
||||
let new_york = parse_timeshift(Some("America/New_York"));
|
||||
if let EpgTimeShift::TimeZone(tz) = new_york {
|
||||
assert_eq!(tz.name(), "America/New_York");
|
||||
} else {
|
||||
panic!("Expected TimeZone for America/New_York");
|
||||
}
|
||||
|
||||
let tokyo = parse_timeshift(Some(&"Asia/Tokyo".to_string()));
|
||||
let tokyo = parse_timeshift(Some("Asia/Tokyo"));
|
||||
if let EpgTimeShift::TimeZone(tz) = tokyo {
|
||||
assert_eq!(tz.name(), "Asia/Tokyo");
|
||||
} else {
|
||||
panic!("Expected TimeZone for Asia/Tokyo");
|
||||
}
|
||||
|
||||
let utc = parse_timeshift(Some(&"UTC".to_string()));
|
||||
let utc = parse_timeshift(Some("UTC"));
|
||||
if let EpgTimeShift::TimeZone(tz) = utc {
|
||||
assert_eq!(tz.name(), "UTC");
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::str::FromStr;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::{NameStep, Panel, PlaylistAssistantContext, TypeStep};
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum PlaylistAssistantStep {
|
||||
Name,
|
||||
Type,
|
||||
@@ -58,4 +59,4 @@ pub fn PlaylistAssistant() -> Html {
|
||||
</div>
|
||||
</ContextProvider<PlaylistAssistantContext >>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use yew::UseStateHandle;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PlaylistAssistantContext {
|
||||
pub custom_class: UseStateHandle<String>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,7 +718,7 @@ mod tests {
|
||||
match get_filter(flt, None) {
|
||||
Ok(filter) => {
|
||||
assert_eq!(format!("{filter}"), flt);
|
||||
let channels = vec![
|
||||
let channels = [
|
||||
create_mock_pli("24/7: Cars", "FR Channels"),
|
||||
create_mock_pli("24/7: Cars", "US Channels"),
|
||||
create_mock_pli("Entertainment", "US Channels"),
|
||||
@@ -768,7 +768,7 @@ mod tests {
|
||||
match get_filter(flt, None) {
|
||||
Ok(filter) => {
|
||||
assert_eq!(format!("{filter}"), flt);
|
||||
let channels = vec![
|
||||
let channels = [
|
||||
create_mock_pli("NA", "GA"),
|
||||
create_mock_pli("NB", "GB"),
|
||||
create_mock_pli("NA", "GB"),
|
||||
@@ -833,7 +833,7 @@ mod tests {
|
||||
match get_filter(flt, None) {
|
||||
Ok(filter) => {
|
||||
assert_eq!(format!("{filter}"), flt);
|
||||
let channels = vec![
|
||||
let channels = [
|
||||
create_mock_pli("24/7: Cars", "FR Channels"),
|
||||
create_mock_pli("24/7: Cars", "US Channels"),
|
||||
create_mock_pli("Entertainment", "US Channels"),
|
||||
|
||||
Reference in New Issue
Block a user