Merge pull request #2 from JMR-dev/fix-test-coverage
Test additions
This commit was merged in pull request #2.
This commit is contained in:
@@ -79,3 +79,56 @@ pub fn extract_icons(worker: &ShellWorker, keys: Vec<IconKey>) -> Vec<(IconKey,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ext(name: &str) -> IconKey {
|
||||
IconKey::Ext(name.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_unrequested_returns_each_key_once_across_calls() {
|
||||
let mut cache = IconCache::default();
|
||||
|
||||
// First sight of these keys: all are handed back for extraction.
|
||||
let first = cache.take_unrequested([IconKey::Folder, ext("txt")]);
|
||||
assert_eq!(first, vec![IconKey::Folder, ext("txt")]);
|
||||
|
||||
// They're now in-flight, so asking again yields nothing — each icon is
|
||||
// only extracted once, even before its handle has come back.
|
||||
assert!(
|
||||
cache
|
||||
.take_unrequested([IconKey::Folder, ext("txt")])
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// A genuinely new key still comes through.
|
||||
assert_eq!(cache.take_unrequested([ext("rs")]), vec![ext("rs")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_unrequested_dedupes_within_a_single_call() {
|
||||
let mut cache = IconCache::default();
|
||||
// The same key repeated in one batch is requested only once.
|
||||
let out = cache.take_unrequested([ext("txt"), ext("txt"), IconKey::Folder]);
|
||||
assert_eq!(out, vec![ext("txt"), IconKey::Folder]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_keys_are_not_requested_again() {
|
||||
let mut cache = IconCache::default();
|
||||
let image = IconImage {
|
||||
width: 1,
|
||||
height: 1,
|
||||
rgba: vec![0, 0, 0, 0],
|
||||
};
|
||||
|
||||
assert!(cache.get(&IconKey::Folder).is_none());
|
||||
cache.insert(IconKey::Folder, image);
|
||||
// Once a handle is cached, get() resolves it and take_unrequested skips it.
|
||||
assert!(cache.get(&IconKey::Folder).is_some());
|
||||
assert!(cache.take_unrequested([IconKey::Folder]).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,29 @@ pub fn format_time(time: SystemTime) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use librarian_core::{Attributes, EntryKind};
|
||||
|
||||
fn file_entry(name: &str, size: u64) -> Entry {
|
||||
Entry {
|
||||
name: name.to_string(),
|
||||
path: PathBuf::from(format!(r"C:\dir\{name}")),
|
||||
kind: EntryKind::File,
|
||||
size,
|
||||
modified: None,
|
||||
created: None,
|
||||
attrs: Attributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entry(name: &str) -> Entry {
|
||||
Entry {
|
||||
kind: EntryKind::Directory,
|
||||
..file_entry(name, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_size_scales_units() {
|
||||
@@ -202,4 +225,168 @@ mod tests {
|
||||
assert_eq!(human_size(1024), "1.0 KB");
|
||||
assert_eq!(human_size(1_500_000), "1.4 MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_label_is_folder_or_uppercase_extension() {
|
||||
assert_eq!(type_label(&dir_entry("Photos")), "File folder");
|
||||
assert_eq!(type_label(&file_entry("notes.txt", 0)), "TXT File");
|
||||
// No extension falls back to the bare "File".
|
||||
assert_eq!(type_label(&file_entry("README", 0)), "File");
|
||||
// A leading-dot name (no real extension) is also just "File".
|
||||
assert_eq!(type_label(&file_entry(".gitignore", 0)), "File");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_row_carries_size_type_and_extension_icon() {
|
||||
let row = row_from_entry(&file_entry("notes.txt", 42));
|
||||
assert_eq!(row.label, "notes.txt");
|
||||
assert!(!row.is_container);
|
||||
assert_eq!(row.size, Some(42));
|
||||
assert_eq!(row.type_label, "TXT File");
|
||||
assert_eq!(row.icon, IconKey::Ext("txt".to_string()));
|
||||
assert_eq!(
|
||||
row.target,
|
||||
Location::Path(PathBuf::from(r"C:\dir\notes.txt"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_row_is_a_container_with_no_size() {
|
||||
let row = row_from_entry(&dir_entry("Photos"));
|
||||
assert!(row.is_container);
|
||||
assert_eq!(row.size, None);
|
||||
assert_eq!(row.icon, IconKey::Folder);
|
||||
assert_eq!(row.type_label, "File folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_row_formats_label_and_falls_back_to_kind() {
|
||||
let drive = DriveInfo {
|
||||
letter: 'C',
|
||||
root: PathBuf::from(r"C:\"),
|
||||
label: "Windows".to_string(),
|
||||
kind: DriveKind::Fixed,
|
||||
total_bytes: 0,
|
||||
free_bytes: 0,
|
||||
};
|
||||
let row = row_from_drive(&drive);
|
||||
assert_eq!(row.label, "Windows (C:)");
|
||||
assert_eq!(row.type_label, "Local Disk");
|
||||
assert!(row.is_container);
|
||||
assert_eq!(row.size, None);
|
||||
assert_eq!(row.icon, IconKey::Path(PathBuf::from(r"C:\")));
|
||||
|
||||
// An unlabeled drive shows its kind in place of a name.
|
||||
let unlabeled = DriveInfo {
|
||||
label: String::new(),
|
||||
kind: DriveKind::Removable,
|
||||
..drive
|
||||
};
|
||||
let row = row_from_drive(&unlabeled);
|
||||
assert_eq!(row.label, "Removable Disk (C:)");
|
||||
assert_eq!(row.type_label, "Removable Disk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_kind_names_cover_every_variant() {
|
||||
let cases = [
|
||||
(DriveKind::Fixed, "Local Disk"),
|
||||
(DriveKind::Removable, "Removable Disk"),
|
||||
(DriveKind::Network, "Network Drive"),
|
||||
(DriveKind::CdRom, "CD Drive"),
|
||||
(DriveKind::RamDisk, "RAM Disk"),
|
||||
(DriveKind::Unknown, "Disk"),
|
||||
];
|
||||
for (kind, expected) in cases {
|
||||
assert_eq!(drive_kind_name(kind), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_folder_row_uses_its_own_shell_icon() {
|
||||
let folder = KnownFolder {
|
||||
name: "Downloads",
|
||||
path: PathBuf::from(r"C:\Users\j\Downloads"),
|
||||
};
|
||||
let row = row_from_known(&folder);
|
||||
assert_eq!(row.label, "Downloads");
|
||||
assert_eq!(row.type_label, "File folder");
|
||||
assert!(row.is_container);
|
||||
assert_eq!(
|
||||
row.icon,
|
||||
IconKey::Path(PathBuf::from(r"C:\Users\j\Downloads"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distro_row_targets_the_unc_root_with_the_wsl_icon() {
|
||||
let distro = WslDistro {
|
||||
name: "Ubuntu".to_string(),
|
||||
};
|
||||
let row = row_from_distro(&distro);
|
||||
assert_eq!(row.label, "Ubuntu");
|
||||
assert_eq!(row.icon, IconKey::Wsl);
|
||||
assert_eq!(row.type_label, "Linux distribution");
|
||||
assert_eq!(row.target, Location::Path(distro_unc_path("Ubuntu")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_hit_label_is_relative_to_the_search_root() {
|
||||
let root = Path::new(r"C:\proj");
|
||||
let hit = SearchHit {
|
||||
path: PathBuf::from(r"C:\proj\src\main.rs"),
|
||||
matches: None,
|
||||
is_dir: false,
|
||||
};
|
||||
let row = row_from_hit(&hit, root);
|
||||
// The path is shown relative to the root, like an editor's search panel.
|
||||
assert_eq!(row.label, r"src\main.rs");
|
||||
assert!(!row.is_container);
|
||||
assert_eq!(row.type_label, "RS File");
|
||||
assert_eq!(row.icon, IconKey::Ext("rs".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_hit_directory_navigates() {
|
||||
let root = Path::new(r"C:\proj");
|
||||
let hit = SearchHit {
|
||||
path: PathBuf::from(r"C:\proj\src"),
|
||||
matches: None,
|
||||
is_dir: true,
|
||||
};
|
||||
let row = row_from_hit(&hit, root);
|
||||
assert!(row.is_container);
|
||||
assert_eq!(row.type_label, "File folder");
|
||||
assert_eq!(row.icon, IconKey::Folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contents_hit_reports_match_count_with_pluralization() {
|
||||
let root = Path::new(r"C:\proj");
|
||||
let hit = |n: u64| SearchHit {
|
||||
path: PathBuf::from(r"C:\proj\notes.txt"),
|
||||
matches: Some(n),
|
||||
is_dir: false,
|
||||
};
|
||||
assert_eq!(row_from_hit(&hit(1), root).type_label, "1 match");
|
||||
assert_eq!(row_from_hit(&hit(3), root).type_label, "3 matches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_time_renders_minute_precision() {
|
||||
// Local time varies by host, so assert the shape, not an exact instant:
|
||||
// `YYYY-MM-DD HH:MM`, exactly 16 chars, all fields zero-padded.
|
||||
let t = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
|
||||
let s = format_time(t);
|
||||
assert_eq!(s.len(), 16, "expected `YYYY-MM-DD HH:MM`, got {s:?}");
|
||||
let bytes = s.as_bytes();
|
||||
for (i, b) in bytes.iter().enumerate() {
|
||||
match i {
|
||||
4 | 7 => assert_eq!(*b, b'-'),
|
||||
10 => assert_eq!(*b, b' '),
|
||||
13 => assert_eq!(*b, b':'),
|
||||
_ => assert!(b.is_ascii_digit(), "non-digit at {i} in {s:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,34 @@ pub(crate) fn wide_to_string(buf: &[u16]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf16_lossy(&buf[..len])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_wide_encodes_and_nul_terminates() {
|
||||
// ASCII encodes one u16 per char, with a trailing NUL the FFI needs.
|
||||
assert_eq!(to_wide("Hi"), vec![0x48, 0x69, 0x00]);
|
||||
// The empty string is just the terminator.
|
||||
assert_eq!(to_wide(""), vec![0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_to_string_stops_at_first_nul() {
|
||||
// Bytes past the NUL (here a stray 'X') are ignored — buffers come back
|
||||
// from Win32 padded with garbage after the terminator.
|
||||
assert_eq!(wide_to_string(&[0x48, 0x69, 0x00, 0x58]), "Hi");
|
||||
// No NUL at all: the whole buffer is decoded.
|
||||
assert_eq!(wide_to_string(&[0x48, 0x69]), "Hi");
|
||||
// A leading NUL is the empty string.
|
||||
assert_eq!(wide_to_string(&[0x00, 0x69]), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_utf16_including_non_ascii() {
|
||||
for s in ["", "C:\\Windows", "Ubuntu", "café — résumé", "日本語"] {
|
||||
assert_eq!(wide_to_string(&to_wide(s)), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user