From ca5c500e18124bcd3fb4bd11a0979d19cfd54b32 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sun, 5 Jul 2026 21:19:50 +0100 Subject: [PATCH] test(backend): cover storage, indexer, thumbnail, and tag-filter logic storage.rs: thumbnail worker clamping across parallelism levels, the adaptive-profile EMA transitions, and UNC-path fallback detection. indexer.rs: supported-media extension matching plus media-kind and MIME mapping. thumbnail.rs: fit_dimensions aspect-ratio math with extreme ratios and is_jpeg extension checks. ai_tag_filter.rs: padding and mixed-separator normalization edge cases. --- src-tauri/src/ai_tag_filter.rs | 9 ++++++ src-tauri/src/indexer.rs | 39 ++++++++++++++++++++++++++ src-tauri/src/storage.rs | 50 ++++++++++++++++++++++++++++++++++ src-tauri/src/thumbnail.rs | 20 ++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/src-tauri/src/ai_tag_filter.rs b/src-tauri/src/ai_tag_filter.rs index d683235..872996d 100644 --- a/src-tauri/src/ai_tag_filter.rs +++ b/src-tauri/src/ai_tag_filter.rs @@ -42,4 +42,13 @@ mod tests { assert!(!is_removed_ai_tag(tag), "{tag} should be kept"); } } + + #[test] + fn removed_ai_tags_tolerate_padding_and_mixed_separators() { + assert!(is_removed_ai_tag(" 1girl ")); + assert!(is_removed_ai_tag("1_-_girl")); + assert!(is_removed_ai_tag("No Humans")); + assert!(!is_removed_ai_tag("")); + assert!(!is_removed_ai_tag(" ")); + } } diff --git a/src-tauri/src/indexer.rs b/src-tauri/src/indexer.rs index 8e3f1be..0fbd447 100644 --- a/src-tauri/src/indexer.rs +++ b/src-tauri/src/indexer.rs @@ -1995,3 +1995,42 @@ fn process_watcher_rename( Err(e) => log::error!("Watcher rename: post-update fetch error: {e}"), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn supported_media_matches_known_extensions_case_insensitively() { + for path in ["a.jpg", "b.JPEG", "c.PNG", "d.avif", "e.mp4", "f.WEBM"] { + assert!( + is_supported_media(Path::new(path)), + "{path} should be supported" + ); + } + for path in ["notes.txt", "archive.zip", "no_extension", "clip.mkv"] { + assert!( + !is_supported_media(Path::new(path)), + "{path} should be skipped" + ); + } + } + + #[test] + fn media_kind_splits_video_from_image_extensions() { + for ext in ["mp4", "MOV", "m4v", "webm"] { + assert_eq!(media_kind_for_ext(ext), "video"); + } + for ext in ["jpg", "PNG", "webp", "avif"] { + assert_eq!(media_kind_for_ext(ext), "image"); + } + } + + #[test] + fn mime_types_map_per_extension() { + assert_eq!(mime_for_ext("JPG"), "image/jpeg"); + assert_eq!(mime_for_ext("png"), "image/png"); + assert_eq!(mime_for_ext("mov"), "video/quicktime"); + assert_eq!(mime_for_ext("m4v"), "video/mp4"); + } +} diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index bf7f29e..686719c 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -122,3 +122,53 @@ fn fallback_profile_for_path(path: &Path) -> StorageProfile { StorageProfile::Balanced } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn thumbnail_workers_scale_with_parallelism_within_clamps() { + assert_eq!(StorageProfile::Fast.thumbnail_workers(3), 2); + assert_eq!(StorageProfile::Fast.thumbnail_workers(12), 4); + assert_eq!(StorageProfile::Fast.thumbnail_workers(64), 4); + assert_eq!(StorageProfile::Balanced.thumbnail_workers(4), 2); + assert_eq!(StorageProfile::Balanced.thumbnail_workers(12), 3); + assert_eq!(StorageProfile::Balanced.thumbnail_workers(64), 3); + assert_eq!(StorageProfile::Conservative.thumbnail_workers(64), 1); + } + + #[test] + fn adaptive_profile_tracks_scan_speed() { + let mut adaptive = RuntimeAdaptiveProfile::new(StorageProfile::Balanced); + assert_eq!(adaptive.profile(), StorageProfile::Balanced); + + // 1 ms/item → fast storage. + adaptive.observe_scan_batch(10, Duration::from_millis(10)); + assert_eq!(adaptive.profile(), StorageProfile::Fast); + + // A very slow batch drags the EMA over the conservative threshold. + adaptive.observe_scan_batch(1, Duration::from_millis(100)); + assert_eq!(adaptive.profile(), StorageProfile::Conservative); + } + + #[test] + fn adaptive_profile_ignores_empty_batches() { + let mut adaptive = RuntimeAdaptiveProfile::new(StorageProfile::Fast); + adaptive.observe_scan_batch(0, Duration::from_secs(10)); + assert_eq!(adaptive.profile(), StorageProfile::Fast); + } + + #[test] + fn fallback_profile_treats_unc_paths_as_conservative() { + assert_eq!( + fallback_profile_for_path(Path::new("\\\\server\\share\\photos")), + StorageProfile::Conservative + ); + assert_eq!( + fallback_profile_for_path(Path::new("C:\\photos")), + StorageProfile::Balanced + ); + } +} diff --git a/src-tauri/src/thumbnail.rs b/src-tauri/src/thumbnail.rs index af29b9c..1e82858 100644 --- a/src-tauri/src/thumbnail.rs +++ b/src-tauri/src/thumbnail.rs @@ -383,6 +383,26 @@ fn fit_dimensions(width: u32, height: u32, max_size: u32) -> (u32, u32) { mod tests { use super::*; + #[test] + fn fit_dimensions_preserves_aspect_ratio_within_max() { + // Already small enough: unchanged. + assert_eq!(fit_dimensions(100, 50, THUMB_SIZE), (100, 50)); + // Landscape and portrait scale to the max on their long edge. + assert_eq!(fit_dimensions(6400, 3200, THUMB_SIZE), (320, 160)); + assert_eq!(fit_dimensions(3200, 6400, THUMB_SIZE), (160, 320)); + // Extreme ratios never collapse to zero. + assert_eq!(fit_dimensions(1, 100_000, 320), (1, 320)); + assert_eq!(fit_dimensions(100_000, 1, 320), (320, 1)); + } + + #[test] + fn is_jpeg_checks_extension_only() { + assert!(is_jpeg(Path::new("photo.jpg"))); + assert!(is_jpeg(Path::new("photo.JPEG"))); + assert!(!is_jpeg(Path::new("photo.png"))); + assert!(!is_jpeg(Path::new("photo"))); + } + #[test] fn scale_numerator_picks_smallest_sufficient() { assert_eq!(scale_numerator(6000, THUMB_SIZE), 1);