From 433cf605e3384171ace91328205d68089775b193 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Mon, 13 Jan 2025 20:27:58 +0000 Subject: [PATCH 1/4] start adding tests --- Cargo.lock | 191 ++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 ++ src/cli.rs | 50 +++++++++++++ src/main.rs | 58 +++++++++++++++ src/structs.rs | 53 +++++++++++++- 5 files changed, 355 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6641376..79f08ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -14,6 +14,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -90,6 +99,22 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -131,6 +156,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -318,6 +354,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -358,6 +400,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -379,6 +427,31 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -548,6 +621,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "log" version = "0.4.22" @@ -558,6 +637,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" name = "lsplus" version = "0.6.0" dependencies = [ + "assert_cmd", "chrono", "clap", "config", @@ -565,9 +645,11 @@ dependencies = [ "glob", "inline_colorization", "nix", + "predicates", "prettytable", "serde", "strip-ansi-escapes", + "tempfile", "term_size", ] @@ -589,6 +671,12 @@ dependencies = [ "libc", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-traits" version = "0.2.19" @@ -665,6 +753,36 @@ dependencies = [ "sha2", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettytable" version = "0.10.0" @@ -708,6 +826,35 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ron" version = "0.8.1" @@ -731,6 +878,19 @@ dependencies = [ "trim-in-place", ] +[[package]] +name = "rustix" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -827,6 +987,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "term" version = "0.7.0" @@ -848,6 +1022,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -999,6 +1179,15 @@ dependencies = [ "quote", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 9c8f5ca..a22a28f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,8 @@ config = "0.15.4" serde = { version = "1.0.215", features = ["derive"] } dirs-next = "2.0.0" strip-ansi-escapes = "0.2.0" + +[dev-dependencies] +tempfile = "3.8" +assert_cmd = "2.0" +predicates = "3.0" diff --git a/src/cli.rs b/src/cli.rs index a98ba21..addd05e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -94,3 +94,53 @@ pub fn version_info() -> String { version, description, authors ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_flags() { + let args = Flags::parse_from(["lsplus"]); + assert_eq!(args.paths, vec![String::from(".")]); + assert!(!args.show_all); + assert!(!args.almost_all); + assert!(!args.long); + assert!(!args.human_readable); + } + + #[test] + fn test_multiple_paths() { + let args = Flags::parse_from(["lsplus", "path1", "path2"]); + assert_eq!(args.paths, vec![String::from("path1"), String::from("path2")]); + } + + #[test] + fn test_all_flags() { + let args = Flags::parse_from([ + "lsplus", + "-a", + "-A", + "-l", + "-h", + "-p", + "--sort-dirs", + "--no-icons", + "--fuzzy-time", + ]); + assert!(args.show_all); + assert!(args.almost_all); + assert!(args.long); + assert!(args.human_readable); + assert!(args.slash); + assert!(args.dirs_first); + assert!(args.no_icons); + assert!(args.fuzzy_time); + } + + #[test] + fn test_version_flag() { + let args = Flags::parse_from(["lsplus", "--version"]); + assert!(args.version); + } +} diff --git a/src/main.rs b/src/main.rs index 0413b0a..8a08f96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -206,3 +206,61 @@ fn display_short_format( table.printstd(); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_load_config_default() { + // When no config file exists, should return default params + let config = load_config(); + // We can only test that it returns a Params struct + // The actual values might be affected by the user's config file + assert!(matches!(config, Params { .. })); + } + + #[test] + fn test_run_multi() -> io::Result<()> { + let temp_dir = tempdir()?; + + // Create some test files + File::create(temp_dir.path().join("test1.txt"))?; + File::create(temp_dir.path().join("test2.txt"))?; + std::fs::create_dir(temp_dir.path().join("testdir"))?; + + let params = Params::default(); + let patterns = vec![temp_dir.path().to_string_lossy().to_string()]; + + assert!(run_multi(&patterns, ¶ms).is_ok()); + Ok(()) + } + + #[test] + fn test_run_multi_nonexistent() { + let params = Params::default(); + let patterns = vec![String::from("/nonexistent/path")]; + + let result = run_multi(&patterns, ¶ms); + assert!(result.is_ok()); // The function handles errors internally + } + + #[test] + fn test_run_multi_with_glob() -> io::Result<()> { + let temp_dir = tempdir()?; + + // Create test files with different extensions + File::create(temp_dir.path().join("test1.txt"))?; + File::create(temp_dir.path().join("test2.txt"))?; + File::create(temp_dir.path().join("test.rs"))?; + + let params = Params::default(); + let pattern = format!("{}/*.txt", temp_dir.path().to_string_lossy()); + let patterns = vec![pattern]; + + assert!(run_multi(&patterns, ¶ms).is_ok()); + Ok(()) + } +} diff --git a/src/structs.rs b/src/structs.rs index dd3d51b..497d67f 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -15,7 +15,7 @@ macro_rules! config_to_params { }; } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq)] pub struct Params { pub show_all: bool, pub append_slash: bool, @@ -76,3 +76,54 @@ pub struct FileInfo { pub display_name: String, pub full_path: PathBuf, } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn test_default_params() { + let params = Params::default(); + assert!(!params.show_all); + assert!(!params.append_slash); + assert!(!params.dirs_first); + assert!(!params.almost_all); + assert!(!params.long_format); + assert!(!params.human_readable); + assert!(!params.no_icons); + assert!(!params.fuzzy_time); + } + + #[test] + fn test_config_conversion() -> std::io::Result<()> { + let temp_dir = tempdir()?; + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" + show_all = true + append_slash = true + dirs_first = true + long_format = true + human_readable = true + "#; + + fs::write(&config_path, config_content)?; + + let config = config::Config::builder() + .add_source(config::File::from(config_path)) + .build() + .unwrap(); + + let params: Params = config.into(); + + assert!(params.show_all); + assert!(params.append_slash); + assert!(params.dirs_first); + assert!(params.long_format); + assert!(params.human_readable); + + Ok(()) + } +} From ff62fdadf3e02ed4ed86f739d1fcafd19362e1ef Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Mon, 13 Jan 2025 20:46:17 +0000 Subject: [PATCH 2/4] improve tests --- src/cli.rs | 52 +++++- src/main.rs | 221 ++++++++++++++++++++++ src/utils/file.rs | 404 ++++++++++++++++++++++++++++++++++++++++ src/utils/format.rs | 69 +++++++ src/utils/fuzzy_time.rs | 81 ++++++++ src/utils/icons.rs | 65 ++++++- tests/integration.rs | 62 ++++++ 7 files changed, 952 insertions(+), 2 deletions(-) create mode 100644 tests/integration.rs diff --git a/src/cli.rs b/src/cli.rs index addd05e..77a1785 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -102,11 +102,16 @@ mod tests { #[test] fn test_default_flags() { let args = Flags::parse_from(["lsplus"]); - assert_eq!(args.paths, vec![String::from(".")]); assert!(!args.show_all); assert!(!args.almost_all); assert!(!args.long); assert!(!args.human_readable); + assert!(!args.slash); + assert!(!args.dirs_first); + assert!(!args.no_icons); + assert!(!args.version); + assert!(!args.fuzzy_time); + assert_eq!(args.paths, vec![String::from(".")]); } #[test] @@ -143,4 +148,49 @@ mod tests { let args = Flags::parse_from(["lsplus", "--version"]); assert!(args.version); } + + #[test] + fn test_version_info() { + let info = version_info(); + assert!(info.contains("lsplus v")); + assert!(info.contains("Released under the MIT license by")); + assert!(info.contains(env!("CARGO_PKG_AUTHORS"))); + assert!(info.contains(env!("CARGO_PKG_DESCRIPTION"))); + } + + #[test] + fn test_version_info_empty() { + // This test is just to verify the code paths for empty fields + // The actual env vars cannot be modified at runtime + let version_info = version_info(); + assert!(version_info.contains("lsplus v")); + assert!(version_info.contains("Released under the MIT license by")); + } + + #[test] + fn test_version_info_with_empty_env() { + // We can't modify the env vars at compile time, but we can test the format + let info = version_info(); + assert!(info.contains("lsplus v")); + assert!(info.contains("Released under the MIT license by")); + + // Verify the format is correct even if env vars were empty + let formatted = format!( + "lsplus v{}\n\ + \n{}\n\ + \nReleased under the MIT license by {}\n", + env!("CARGO_PKG_VERSION"), + if env!("CARGO_PKG_DESCRIPTION").is_empty() { "No description provided" } else { env!("CARGO_PKG_DESCRIPTION") }, + if env!("CARGO_PKG_AUTHORS").is_empty() { "Unknown" } else { env!("CARGO_PKG_AUTHORS") } + ); + assert_eq!(info, formatted); + } + + #[test] + fn test_help_flag() { + let result = Flags::try_parse_from(["lsplus", "--help"]); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("Usage:")); + } } diff --git a/src/main.rs b/src/main.rs index 8a08f96..858a44b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -211,6 +211,7 @@ fn display_short_format( mod tests { use super::*; use std::fs::File; + use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; #[test] @@ -263,4 +264,224 @@ mod tests { assert!(run_multi(&patterns, ¶ms).is_ok()); Ok(()) } + + #[test] + fn test_display_formats() -> io::Result<()> { + // Create a temporary directory for our test files + let temp_dir = tempfile::tempdir()?; + let test_file = temp_dir.path().join("test.txt"); + File::create(&test_file)?; + + // Create test file info + let params = Params::default(); + let file_info = collect_file_info(&test_file, ¶ms)?; + + // Test long format display + let params = Params { + long_format: true, + fuzzy_time: true, + human_readable: true, + ..Default::default() + }; + display_long_format(&file_info, ¶ms)?; + + // Test long format without fuzzy time and human readable + let params = Params { + long_format: true, + fuzzy_time: false, + human_readable: false, + ..Default::default() + }; + display_long_format(&file_info, ¶ms)?; + + // Test short format display + let params = Params::default(); + display_short_format(&file_info, ¶ms)?; + + Ok(()) + } + + #[test] + fn test_main_flags() { + // Test version flag + let args = cli::Flags { + version: true, + paths: vec![], + show_all: false, + almost_all: false, + slash: false, + dirs_first: false, + long: false, + human_readable: false, + no_icons: false, + fuzzy_time: false, + }; + assert!(cli::version_info().contains("lsplus")); + + // Test empty paths + let args = cli::Flags { + version: false, + paths: vec![], + show_all: false, + almost_all: false, + slash: false, + dirs_first: false, + long: false, + human_readable: false, + no_icons: false, + fuzzy_time: false, + }; + assert_eq!( + if args.paths.is_empty() { + vec![String::from(".")] + } else { + args.paths + }, + vec![String::from(".")] + ); + } + + #[test] + fn test_load_config_error() { + // Test with invalid config file + let mut config_path = PathBuf::new(); + if let Some(home_dir) = home_dir() { + config_path.push(home_dir); + config_path.push(".config/lsplus/config.toml"); + } + + let settings = Config::builder() + .add_source(config::File::new(config_path.to_str().unwrap(), FileFormat::Toml)) + .build(); + + match settings { + Ok(_) => (), + Err(e) => { + if e.to_string().contains("not found") { + assert_eq!(Params::default(), Params::default()); + } else { + assert_eq!(Params::default(), Params::default()); + } + } + } + } + + #[test] + fn test_run_multi_glob_error() { + let params = Params::default(); + let invalid_pattern = vec![String::from("[invalid-glob-pattern")]; + + // This should print an error message but not panic + run_multi(&invalid_pattern, ¶ms).unwrap(); + } + + #[test] + fn test_load_config_error_other() { + // Test with a malformed config file + let temp_dir = tempfile::tempdir().unwrap(); + let config_path = temp_dir.path().join(".config/lsplus/config.toml"); + std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); + std::fs::write(&config_path, "invalid = toml [ content").unwrap(); + + let settings = Config::builder() + .add_source(config::File::new(config_path.to_str().unwrap(), FileFormat::Toml)) + .build(); + + match settings { + Ok(_) => panic!("Expected error"), + Err(e) => { + assert!(!e.to_string().contains("not found")); + let params = Params::default(); + assert_eq!(params.show_all, false); + } + } + } + + #[test] + fn test_main_error_handling() { + // Test with a pattern that will cause an error + let params = Params::default(); + let invalid_pattern = vec![String::from("/nonexistent/path/that/should/not/exist")]; + + let result = run_multi(&invalid_pattern, ¶ms); + assert!(result.is_ok()); // The function handles errors internally + } + + #[test] + fn test_main_config_merge() { + // Test merging of config and CLI args + let config = Params { + show_all: true, + append_slash: true, + dirs_first: false, + almost_all: false, + long_format: true, + human_readable: true, + no_icons: false, + fuzzy_time: false, + }; + + let args = cli::Flags { + version: false, + paths: vec![], + show_all: false, + almost_all: true, + slash: false, + dirs_first: true, + long: false, + human_readable: false, + no_icons: true, + fuzzy_time: true, + }; + + let params = Params { + show_all: args.show_all || config.show_all, + append_slash: args.slash || config.append_slash, + dirs_first: args.dirs_first || config.dirs_first, + almost_all: args.almost_all || config.almost_all, + long_format: args.long || config.long_format, + human_readable: args.human_readable || config.human_readable, + no_icons: args.no_icons || config.no_icons, + fuzzy_time: args.fuzzy_time || config.fuzzy_time, + }; + + // Verify the merging logic + assert!(params.show_all); + assert!(params.append_slash); + assert!(params.dirs_first); + assert!(params.almost_all); + assert!(params.long_format); + assert!(params.human_readable); + assert!(params.no_icons); + assert!(params.fuzzy_time); + } + + #[test] + fn test_run_multi_error_handling() { + // Create a temporary directory and a file with no read permissions + let temp_dir = tempfile::tempdir().unwrap(); + let test_file = temp_dir.path().join("no_read.txt"); + std::fs::write(&test_file, "test").unwrap(); + std::fs::set_permissions(&test_file, std::fs::Permissions::from_mode(0o000)).unwrap(); + + let params = Params::default(); + let pattern = vec![test_file.to_string_lossy().to_string()]; + + // This should print an error message but not panic + let result = run_multi(&pattern, ¶ms); + assert!(result.is_ok()); // The function handles errors internally + + // Clean up by restoring permissions so the file can be deleted + std::fs::set_permissions(&test_file, std::fs::Permissions::from_mode(0o644)).unwrap(); + } + + #[test] + fn test_run_multi_empty_pattern() { + let params = Params::default(); + let pattern = vec![String::from("**/nonexistent_pattern_*.xyz")]; + + // This should handle empty glob results + let result = run_multi(&pattern, ¶ms); + assert!(result.is_ok()); + } } diff --git a/src/utils/file.rs b/src/utils/file.rs index 8a1a1e7..b217ec0 100644 --- a/src/utils/file.rs +++ b/src/utils/file.rs @@ -258,3 +258,407 @@ pub fn check_display_name(info: &FileInfo) -> String { _ => info.display_name.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::path::PathBuf; + use tempfile::tempdir; + + #[test] + fn test_check_display_name() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.txt"); + File::create(&file_path).unwrap(); + + let info = FileInfo { + file_type: String::from("regular file"), + mode: String::from("-rw-r--r--"), + nlink: 1, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("test.txt"), + full_path: file_path, + }; + + let result = check_display_name(&info); + assert_eq!(result, "test.txt"); + + // Test with directory + let dir_path = temp_dir.path().join("testdir"); + fs::create_dir(&dir_path).unwrap(); + + let info = FileInfo { + file_type: String::from("directory"), + mode: String::from("drwxr-xr-x"), + nlink: 2, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("testdir"), + full_path: dir_path, + }; + + let result = check_display_name(&info); + assert_eq!(result, "testdir"); + + // Test with . directory + let dot_info = FileInfo { + file_type: String::from("directory"), + mode: String::from("drwxr-xr-x"), + nlink: 2, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("."), + full_path: temp_dir.path().join("."), + }; + + let result = check_display_name(&dot_info); + assert_eq!(result, format!("{color_blue}.")); + } + + #[test] + fn test_collect_file_info() -> io::Result<()> { + let temp_dir = tempdir()?; + + // Create test files and directories + let file1 = temp_dir.path().join("file1.txt"); + let file2 = temp_dir.path().join("file2.txt"); + let dir1 = temp_dir.path().join("dir1"); + let hidden = temp_dir.path().join(".hidden"); + + File::create(&file1)?; + File::create(&file2)?; + File::create(&hidden)?; + fs::create_dir(&dir1)?; + + // Test default params + let params = Params::default(); + let info = collect_file_info(temp_dir.path(), ¶ms)?; + assert_eq!(info.len(), 3); // 2 files + 1 dir, hidden file not included + + // Test show_all + let params = Params { + show_all: true, + ..Default::default() + }; + let info = collect_file_info(temp_dir.path(), ¶ms)?; + assert_eq!(info.len(), 6); // Including hidden file + . and .. + + // Test dirs_first + let params = Params { + dirs_first: true, + ..Default::default() + }; + let info = collect_file_info(temp_dir.path(), ¶ms)?; + // Find all directories in the list + let dir_count = info.iter().filter(|f| f.file_type == "d").count(); + assert!(dir_count > 0, "Should have at least one directory"); + // Check that all directories come before files + let first_file_idx = info.iter().position(|f| f.file_type != "d"); + if let Some(idx) = first_file_idx { + assert!(info[..idx].iter().all(|f| f.file_type == "d"), + "All items before first file should be directories"); + assert!(info[idx..].iter().all(|f| f.file_type != "d"), + "All items after first file should not be directories"); + } + + Ok(()) + } + + #[test] + fn test_get_file_info() -> io::Result<()> { + let temp_dir = tempdir()?; + let file_path = temp_dir.path().join("test.txt"); + fs::write(&file_path, "test content")?; + + let params = Params::default(); + let info = create_file_info(&file_path, ¶ms)?; + + assert_eq!(info.display_name, format!("{color_reset}test.txt")); + assert!(!info.full_path.is_dir()); + assert_eq!(info.size, 12); // "test content" is 12 bytes + assert!(info.item_icon.is_some()); + + Ok(()) + } + + #[test] + fn test_sort_file_info() { + let mut files = vec![ + FileInfo { + file_type: String::from("regular file"), + mode: String::from("-rw-r--r--"), + nlink: 1, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("b.txt"), + full_path: PathBuf::from("b.txt"), + }, + FileInfo { + file_type: String::from("regular file"), + mode: String::from("-rw-r--r--"), + nlink: 1, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("a.txt"), + full_path: PathBuf::from("a.txt"), + }, + FileInfo { + file_type: String::from("directory"), + mode: String::from("drwxr-xr-x"), + nlink: 2, + user: String::from("user"), + group: String::from("group"), + size: 0, + mtime: SystemTime::now(), + item_icon: None, + display_name: String::from("dir"), + full_path: PathBuf::from("dir"), + }, + ]; + + // Test normal sort + files.sort_by(|a, b| a.display_name.cmp(&b.display_name)); + assert_eq!(files[0].display_name, "a.txt"); + assert_eq!(files[1].display_name, "b.txt"); + assert_eq!(files[2].display_name, "dir"); + + // Test dirs_first + files.sort_by(|a, b| { + if a.file_type == "directory" && b.file_type != "directory" { + std::cmp::Ordering::Less + } else if a.file_type != "directory" && b.file_type == "directory" { + std::cmp::Ordering::Greater + } else { + a.display_name.cmp(&b.display_name) + } + }); + assert_eq!(files[0].display_name, "dir"); + assert_eq!(files[1].display_name, "a.txt"); + assert_eq!(files[2].display_name, "b.txt"); + } + + #[test] + fn test_get_file_details() -> io::Result<()> { + // Create test files with different types + let dir_path = Path::new("test_dir"); + let file_path = Path::new("test_file"); + let symlink_path = Path::new("test_symlink"); + let special_path = Path::new("test_special"); + + fs::create_dir(dir_path)?; + File::create(file_path)?; + std::os::unix::fs::symlink(file_path, symlink_path)?; + + // Test directory + let metadata = fs::metadata(dir_path)?; + let (file_type, _, _, _, _, _, _, _) = get_file_details(&metadata); + assert_eq!(file_type, "d"); + + // Test regular file + let metadata = fs::metadata(file_path)?; + let (file_type, _, _, _, _, _, _, executable) = get_file_details(&metadata); + assert_eq!(file_type, "-"); + assert!(!executable); + + // Test symlink + let metadata = fs::symlink_metadata(symlink_path)?; + let (file_type, _, _, _, _, _, _, _) = get_file_details(&metadata); + assert_eq!(file_type, "l"); + + // Test executable file + std::fs::set_permissions(file_path, fs::Permissions::from_mode(0o755))?; + let metadata = fs::metadata(file_path)?; + let (_, _, _, _, _, _, _, executable) = get_file_details(&metadata); + assert!(executable); + + // Cleanup + fs::remove_dir(dir_path)?; + fs::remove_file(file_path)?; + fs::remove_file(symlink_path)?; + Ok(()) + } + + #[test] + fn test_get_username_groupname() { + // Test existing user + let root_uid = 0; + let username = get_username(root_uid); + assert!(username == "root" || username == "0"); + + // Test non-existent user + let nonexistent_uid = u32::MAX; + let username = get_username(nonexistent_uid); + assert_eq!(username, nonexistent_uid.to_string()); + + // Test existing group + let root_gid = 0; + let groupname = get_groupname(root_gid); + assert!(groupname == "root" || groupname == "0"); + + // Test non-existent group + let nonexistent_gid = u32::MAX; + let groupname = get_groupname(nonexistent_gid); + assert_eq!(groupname, nonexistent_gid.to_string()); + } + + #[test] + fn test_create_file_info_symlinks() -> io::Result<()> { + // Create temporary test directory + let temp_dir = tempfile::tempdir()?; + let file_path = temp_dir.path().join("test_file"); + let valid_symlink = temp_dir.path().join("valid_symlink"); + let broken_symlink = temp_dir.path().join("broken_symlink"); + let unreadable_symlink = temp_dir.path().join("unreadable_symlink"); + + // Create the test file and symlinks + File::create(&file_path)?; + std::os::unix::fs::symlink(&file_path, &valid_symlink)?; + std::os::unix::fs::symlink("nonexistent", &broken_symlink)?; + std::os::unix::fs::symlink("/dev/null", &unreadable_symlink)?; + + // Test valid symlink with long format + let mut params = Params::default(); + params.long_format = true; + let info = create_file_info(&valid_symlink, ¶ms)?; + assert!(info.display_name.contains("->")); + assert!(!info.display_name.contains("[Broken Link]")); + + // Test broken symlink with long format + let info = create_file_info(&broken_symlink, ¶ms)?; + assert!(info.display_name.contains("->")); + assert!(info.display_name.contains("[Broken Link]")); + + // Test symlink without long format but with append_slash + params.long_format = false; + params.append_slash = true; + let info = create_file_info(&valid_symlink, ¶ms)?; + assert!(info.display_name.contains("*")); + + Ok(()) + } + + #[test] + fn test_create_file_info_special_cases() -> io::Result<()> { + // Create temporary test directory + let temp_dir = tempfile::tempdir()?; + let dir_path = temp_dir.path().join("test_dir"); + let file_path = temp_dir.path().join("test_file"); + + fs::create_dir(&dir_path)?; + File::create(&file_path)?; + + // Test directory with append_slash + let mut params = Params::default(); + params.append_slash = true; + let info = create_file_info(&dir_path, ¶ms)?; + assert!(info.display_name.ends_with('/')); + + // Test file with ./ prefix + let info = create_file_info(&file_path, ¶ms)?; + assert!(!info.display_name.starts_with("./")); + + // Test with no_icons parameter + params.no_icons = true; + let info = create_file_info(&file_path, ¶ms)?; + assert!(info.item_icon.is_none()); + + // Test executable file + std::fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755))?; + let info = create_file_info(&file_path, ¶ms)?; + assert!(info.display_name.contains(color_green)); + + Ok(()) + } + + #[test] + fn test_collect_file_names() -> io::Result<()> { + // Create temporary test directory + let temp_dir = tempfile::tempdir()?; + let file1_path = temp_dir.path().join(".hidden_file"); + let file2_path = temp_dir.path().join("visible_file"); + let subdir_path = temp_dir.path().join("subdir"); + + fs::create_dir(&subdir_path)?; + File::create(&file1_path)?; + File::create(&file2_path)?; + + // Test with show_all = false (default) + let params = Params::default(); + let files = collect_file_names(temp_dir.path(), ¶ms)?; + assert!(!files.contains(&".hidden_file".to_string())); + assert!(files.contains(&"visible_file".to_string())); + + // Test with show_all = true + let mut params = Params::default(); + params.show_all = true; + let files = collect_file_names(temp_dir.path(), ¶ms)?; + assert!(files.contains(&".".to_string())); + assert!(files.contains(&"..".to_string())); + assert!(files.contains(&".hidden_file".to_string())); + + // Test with almost_all = true + let mut params = Params::default(); + params.almost_all = true; + let files = collect_file_names(temp_dir.path(), ¶ms)?; + assert!(!files.contains(&".".to_string())); + assert!(!files.contains(&"..".to_string())); + assert!(files.contains(&".hidden_file".to_string())); + + // Test with dirs_first = true + let mut params = Params::default(); + params.dirs_first = true; + let files = collect_file_names(temp_dir.path(), ¶ms)?; + let subdir_idx = files.iter().position(|x| x == "subdir").unwrap(); + let file_idx = files.iter().position(|x| x == "visible_file").unwrap(); + assert!(subdir_idx < file_idx); + + // Test with a regular file (not a directory) + let files = collect_file_names(&file2_path, ¶ms)?; + assert_eq!(files, vec!["visible_file"]); + + Ok(()) + } + + #[test] + fn test_create_file_info_edge_cases() -> io::Result<()> { + // Create temporary test directory + let temp_dir = tempfile::tempdir()?; + let file_path = temp_dir.path().join("test_file"); + let symlink_path = temp_dir.path().join("test_symlink"); + + // Create test file and symlink + File::create(&file_path)?; + std::os::unix::fs::symlink("nonexistent", &symlink_path)?; + + // Test broken symlink with long format + let mut params = Params::default(); + params.long_format = true; + let info = create_file_info(&symlink_path, ¶ms)?; + assert!(info.display_name.contains("[Broken Link]")); + + // Test symlink with append_slash but not long_format + params.long_format = false; + params.append_slash = true; + let info = create_file_info(&symlink_path, ¶ms)?; + assert!(info.display_name.contains("*")); + + Ok(()) + } +} diff --git a/src/utils/format.rs b/src/utils/format.rs index 51a961e..467d6a1 100644 --- a/src/utils/format.rs +++ b/src/utils/format.rs @@ -48,3 +48,72 @@ pub fn show_size(size: u64, human_readable: bool) -> (String, &'static str) { (size.to_string(), "") } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_size() { + let (size, unit) = human_readable_format(0); + assert_eq!(format!("{:.1} {}", size, unit), "0.0 B"); + + let (size, unit) = human_readable_format(1023); + assert_eq!(format!("{:.1} {}", size, unit), "1023.0 B"); + + let (size, unit) = human_readable_format(1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 KB"); + + let (size, unit) = human_readable_format(1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 MB"); + + let (size, unit) = human_readable_format(1024 * 1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 GB"); + + let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 TB"); + } + + #[test] + fn test_format_size_partial() { + let (size, unit) = human_readable_format(1536); + assert_eq!(format!("{:.1} {}", size, unit), "1.5 KB"); + + let (size, unit) = human_readable_format(1024 * 1024 * 3 / 2); + assert_eq!(format!("{:.1} {}", size, unit), "1.5 MB"); + + let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 5 / 2); + assert_eq!(format!("{:.1} {}", size, unit), "2.5 GB"); + + // Test show_size with human readable format + let (size, unit) = show_size(2560, true); + assert_eq!(size, "2.5"); + assert_eq!(unit, "KB"); + + let (size, unit) = show_size(1024, true); + assert_eq!(size, "1"); + assert_eq!(unit, "KB"); + + // Test non-human readable format + let (size, unit) = show_size(2560, false); + assert_eq!(size, "2560"); + assert_eq!(unit, ""); + } + + #[test] + fn test_format_mode() { + assert_eq!(mode_to_rwx(0o755), "rwxr-xr-x"); + assert_eq!(mode_to_rwx(0o644), "rw-r--r--"); + assert_eq!(mode_to_rwx(0o777), "rwxrwxrwx"); + } + + #[test] + fn test_format_mode_permissions() { + // Test no permissions + assert_eq!(mode_to_rwx(0o000), "---------"); + // Test all permissions + assert_eq!(mode_to_rwx(0o777), "rwxrwxrwx"); + // Test mixed permissions + assert_eq!(mode_to_rwx(0o750), "rwxr-x---"); + } +} diff --git a/src/utils/fuzzy_time.rs b/src/utils/fuzzy_time.rs index 66d8446..3babb7d 100644 --- a/src/utils/fuzzy_time.rs +++ b/src/utils/fuzzy_time.rs @@ -83,3 +83,84 @@ pub fn fuzzy_time(time: SystemTime) -> String { .unwrap_or_else(|_| Duration::from_secs(0)); get_fuzzy_time(duration).to_string() } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, SystemTime}; + + fn get_test_time(seconds_ago: u64) -> SystemTime { + SystemTime::now() + .checked_sub(Duration::from_secs(seconds_ago)) + .unwrap() + } + + #[test] + fn test_fuzzy_time_seconds() { + let time = get_test_time(30); + assert_eq!(fuzzy_time(time), "30 seconds ago"); + } + + #[test] + fn test_fuzzy_time_minutes() { + let time = get_test_time(5 * 60); + assert_eq!(fuzzy_time(time), "5 minutes ago"); + + let time = get_test_time(60); + assert_eq!(fuzzy_time(time), "1 minute ago"); + } + + #[test] + fn test_fuzzy_time_hours() { + let time = get_test_time(2 * 60 * 60); + assert_eq!(fuzzy_time(time), "2 hours ago"); + + let time = get_test_time(60 * 60); + assert_eq!(fuzzy_time(time), "1 hour ago"); + } + + #[test] + fn test_fuzzy_time_days() { + let time = get_test_time(2 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "2 days ago"); + + let time = get_test_time(24 * 60 * 60); + assert_eq!(fuzzy_time(time), "yesterday"); + } + + #[test] + fn test_fuzzy_time_weeks() { + let time = get_test_time(2 * 7 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "2 weeks ago"); + + let time = get_test_time(7 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "last week"); + } + + #[test] + fn test_fuzzy_time_months() { + let time = get_test_time(60 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "2 months ago"); + + let time = get_test_time(32 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "last month"); + } + + #[test] + fn test_fuzzy_time_years() { + let time = get_test_time(2 * 365 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "2 years ago"); + + let time = get_test_time(366 * 24 * 60 * 60); + assert_eq!(fuzzy_time(time), "last year"); + } + + #[test] + fn test_fuzzy_time_future() { + // Create a time in the future + let future = SystemTime::now() + .checked_add(Duration::from_secs(3600)) + .unwrap(); + assert_eq!(fuzzy_time(future), "0 seconds ago"); + } +} diff --git a/src/utils/icons.rs b/src/utils/icons.rs index abc8cb9..c6abaf7 100644 --- a/src/utils/icons.rs +++ b/src/utils/icons.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::sync::OnceLock; use std::{fmt, fs}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Icon { // we define all the possible icons we can use. This will be a growing // list as we decode more file types. @@ -307,4 +307,67 @@ mod tests { assert!(has_extension("$^#@.bin", "bin")); assert!(has_extension("spaces in name.doc", "doc")); } + + #[test] + fn test_get_item_icon() { + use std::fs::Metadata; + use std::os::unix::fs::MetadataExt; + use std::os::unix::fs::FileTypeExt; + + // Create a mock metadata for a file + let metadata = fs::metadata("Cargo.toml").unwrap(); + + // Test unknown file type returns generic icon + let icon = get_item_icon(&metadata, "test.unknown"); + assert_eq!(icon, Icon::GenericFile); + + // Test with a known file type + let icon = get_item_icon(&metadata, "test.rs"); + assert_eq!(icon, Icon::RustFile); + + // Test with a known filename + let icon = get_item_icon(&metadata, "Cargo.toml"); + assert_eq!(icon, Icon::TomlFile); + + // Test with a directory + let metadata = fs::metadata(".").unwrap(); + let icon = get_item_icon(&metadata, "test_dir"); + assert_eq!(icon, Icon::Folder); + + // Test with a symlink + // Create a temporary symlink for testing + let _ = std::os::unix::fs::symlink("test_target", "test_link"); + if let Ok(metadata) = fs::symlink_metadata("test_link") { + let icon = get_item_icon(&metadata, "test_link"); + assert_eq!(icon, Icon::Symlink); + } + // Clean up the symlink + let _ = fs::remove_file("test_link"); + } + + #[test] + fn test_get_filename_icon() { + // Test known filenames from file_name_icons + let icons = file_name_icons(); + assert_eq!(icons.get("swapfile"), Some(&Icon::SwapFile)); + assert_eq!(icons.get("docker-compose.yml"), Some(&Icon::DockerFile)); + assert_eq!(icons.get("Dockerfile"), Some(&Icon::DockerFile)); + assert_eq!(icons.get("LICENSE"), Some(&Icon::TextFile)); + assert_eq!(icons.get("Rakefile"), Some(&Icon::RubyFile)); + assert_eq!(icons.get("Gemfile"), Some(&Icon::RubyFile)); + + // Test unknown filename + assert_eq!(icons.get("unknown.txt"), None); + } + + #[test] + fn test_folder_icons() { + // Test known folder names + assert_eq!(folder_icons().get(".git"), Some(&Icon::GitFile)); + assert_eq!(folder_icons().get("node_modules"), Some(&Icon::NodeModulesFolder)); + assert_eq!(folder_icons().get(".vscode"), Some(&Icon::VsCodeFolder)); + + // Test unknown folder name + assert_eq!(folder_icons().get("unknown_folder"), None); + } } diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..70a6c66 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,62 @@ +use assert_cmd::Command; +use std::fs; +use std::path::PathBuf; +use tempfile::tempdir; + +#[test] +fn test_version_flag() { + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.arg("--version") + .assert() + .success() + .stdout(predicates::str::contains("lsplus")); +} + +#[test] +fn test_invalid_path() { + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.arg("/path/that/does/not/exist") + .assert() + .success() // The program handles errors internally + .stderr(predicates::str::contains("No such file or directory")); +} + +#[test] +fn test_list_current_directory() { + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.assert().success(); +} + +#[test] +fn test_config_file() { + // Create a temporary directory and config file + let temp_dir = tempdir().unwrap(); + let config_dir = temp_dir.path().join(".config").join("lsplus"); + fs::create_dir_all(&config_dir).unwrap(); + let config_file = config_dir.join("config.toml"); + + // Write an invalid config file + fs::write(&config_file, "invalid = toml [ content").unwrap(); + + // Set the home directory environment variable + std::env::set_var("HOME", temp_dir.path()); + + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.assert().success(); // Should use default params when config is invalid +} + +#[test] +fn test_long_format() { + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.arg("-l") + .assert() + .success(); +} + +#[test] +fn test_multiple_paths() { + let mut cmd = Command::cargo_bin("lsp").unwrap(); + cmd.args([".", "Cargo.toml"]) + .assert() + .success(); +} From 664e7fa3ddcc697bc5f8c625d8c979b0c60e42f2 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Mon, 13 Jan 2025 21:58:46 +0000 Subject: [PATCH 3/4] add some more edge cases --- Cargo.lock | 23 ++++++ Cargo.toml | 7 +- src/utils/file.rs | 42 +++++++++- src/utils/format.rs | 39 +++++++++ src/utils/fuzzy_time.rs | 176 ++++++++++++++++++++++++++++++++++++++-- src/utils/icons.rs | 4 - tests/integration.rs | 1 - 7 files changed, 277 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79f08ab..1ef842d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -619,6 +631,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags", "libc", + "redox_syscall", ] [[package]] @@ -642,6 +655,7 @@ dependencies = [ "clap", "config", "dirs-next", + "filetime", "glob", "inline_colorization", "nix", @@ -815,6 +829,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index a22a28f..39d68f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ dirs-next = "2.0.0" strip-ansi-escapes = "0.2.0" [dev-dependencies] -tempfile = "3.8" -assert_cmd = "2.0" -predicates = "3.0" +tempfile = "3.8.1" +assert_cmd = "2.0.12" +predicates = "3.0.4" +filetime = "0.2.23" diff --git a/src/utils/file.rs b/src/utils/file.rs index b217ec0..6e53ea6 100644 --- a/src/utils/file.rs +++ b/src/utils/file.rs @@ -460,7 +460,6 @@ mod tests { let dir_path = Path::new("test_dir"); let file_path = Path::new("test_file"); let symlink_path = Path::new("test_symlink"); - let special_path = Path::new("test_special"); fs::create_dir(dir_path)?; File::create(file_path)?; @@ -661,4 +660,45 @@ mod tests { Ok(()) } + + #[test] + fn test_large_file_size() -> io::Result<()> { + let temp_dir = tempdir()?; + let file_path = temp_dir.path().join("large_file"); + let file = File::create(&file_path)?; + + // Set file size to 5GB using seek + // Note: This doesn't actually allocate disk space + let size = 5 * 1024 * 1024 * 1024; + file.set_len(size)?; + + let params = Params { + human_readable: true, + ..Params::default() + }; + let info = create_file_info(&file_path, ¶ms)?; + + // Check the actual size field + assert_eq!(info.size, size); + Ok(()) + } + + #[test] + fn test_circular_symlink() -> io::Result<()> { + let temp_dir = tempdir()?; + let link1_path = temp_dir.path().join("link1"); + let link2_path = temp_dir.path().join("link2"); + + std::os::unix::fs::symlink(&link2_path, &link1_path)?; + std::os::unix::fs::symlink(&link1_path, &link2_path)?; + + let mut params = Params::default(); + params.long_format = true; // Need long format to see the symlink target + let info = create_file_info(&link1_path, ¶ms)?; + + // Should handle circular symlinks gracefully + assert_eq!(info.file_type, "l"); + assert!(info.display_name.contains("->")); + Ok(()) + } } diff --git a/src/utils/format.rs b/src/utils/format.rs index 467d6a1..e9a9707 100644 --- a/src/utils/format.rs +++ b/src/utils/format.rs @@ -100,6 +100,30 @@ mod tests { assert_eq!(unit, ""); } + #[test] + fn test_format_size_extreme() { + // Test extremely large sizes + let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 TB"); + + let (size, unit) = human_readable_format(1024 * 1024 * 1024 * 1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 PB"); + + // Test exact boundary cases + let (size, unit) = human_readable_format(1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 KB"); + + let (size, unit) = human_readable_format(1024 * 1024); + assert_eq!(format!("{:.1} {}", size, unit), "1.0 MB"); + + // Test just under boundary cases + let (size, unit) = human_readable_format(1023); + assert_eq!(format!("{:.1} {}", size, unit), "1023.0 B"); + + let (size, unit) = human_readable_format(1024 * 1024 - 1); + assert_eq!(format!("{:.1} {}", size, unit), "1024.0 KB"); + } + #[test] fn test_format_mode() { assert_eq!(mode_to_rwx(0o755), "rwxr-xr-x"); @@ -116,4 +140,19 @@ mod tests { // Test mixed permissions assert_eq!(mode_to_rwx(0o750), "rwxr-x---"); } + + #[test] + fn test_mode_to_rwx_edge_cases() { + // Test no permissions + assert_eq!(mode_to_rwx(0o0000), "---------"); + + // Test all permissions + assert_eq!(mode_to_rwx(0o0777), "rwxrwxrwx"); + + // Test write-only (unusual case) + assert_eq!(mode_to_rwx(0o0222), "-w--w--w-"); + + // Test execute-only (unusual case) + assert_eq!(mode_to_rwx(0o0111), "--x--x--x"); + } } diff --git a/src/utils/fuzzy_time.rs b/src/utils/fuzzy_time.rs index 3babb7d..3bab11e 100644 --- a/src/utils/fuzzy_time.rs +++ b/src/utils/fuzzy_time.rs @@ -156,11 +156,175 @@ mod tests { } #[test] - fn test_fuzzy_time_future() { - // Create a time in the future - let future = SystemTime::now() - .checked_add(Duration::from_secs(3600)) - .unwrap(); - assert_eq!(fuzzy_time(future), "0 seconds ago"); + fn test_fuzzy_time_boundary_cases() { + let now = SystemTime::now(); + + // Test exactly 59 seconds + let time = now.checked_sub(Duration::from_secs(59)).unwrap(); + assert_eq!(fuzzy_time(time), "59 seconds ago"); + + // Test exactly 60 seconds (should show as 1 minute) + let time = now.checked_sub(Duration::from_secs(60)).unwrap(); + assert_eq!(fuzzy_time(time), "1 minute ago"); + + // Test 1 minute and 59 seconds (should show as 1 minute) + let time = now.checked_sub(Duration::from_secs(119)).unwrap(); + assert_eq!(fuzzy_time(time), "1 minute ago"); + + // Test exactly 2 minutes + let time = now.checked_sub(Duration::from_secs(120)).unwrap(); + assert_eq!(fuzzy_time(time), "2 minutes ago"); + + // Test exactly 23 hours + let time = now.checked_sub(Duration::from_secs(23 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "23 hours ago"); + + // Test exactly 24 hours (should show as "yesterday") + let time = now.checked_sub(Duration::from_secs(24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "yesterday"); + + // Test 6 days + let time = now.checked_sub(Duration::from_secs(6 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "6 days ago"); + + // Test 7 days (should show as "last week") + let time = now.checked_sub(Duration::from_secs(7 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last week"); + } + + #[test] + fn test_fuzzy_time_month_boundaries() { + let now = SystemTime::now(); + + // Test 29 days (should still show weeks) + let time = now.checked_sub(Duration::from_secs(29 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "4 weeks ago"); + + // Test 30 days (should show "last month") + let time = now.checked_sub(Duration::from_secs(30 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last month"); + + // Test 45 days (should show "last month") + let time = now.checked_sub(Duration::from_secs(45 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last month"); + + // Test 60 days (should show "2 months ago") + let time = now.checked_sub(Duration::from_secs(60 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 months ago"); + } + + #[test] + fn test_fuzzy_time_year_boundaries() { + let now = SystemTime::now(); + + // Test 364 days (should show months) + let time = now.checked_sub(Duration::from_secs(364 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "12 months ago"); + + // Test 365 days (should show "last year") + let time = now.checked_sub(Duration::from_secs(365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last year"); + + // Test 729 days (should still show "last year") + let time = now.checked_sub(Duration::from_secs(729 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last year"); + + // Test 730 days (should show "2 years ago") + let time = now.checked_sub(Duration::from_secs(730 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 years ago"); + } + + #[test] + fn test_fuzzy_time_zero_duration() { + let now = SystemTime::now(); + assert_eq!(fuzzy_time(now), "0 seconds ago"); + } + + #[test] + fn test_fuzzy_time_week_boundaries() { + let now = SystemTime::now(); + + // Test 13 days (should still show "last week") + let time = now.checked_sub(Duration::from_secs(13 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last week"); + + // Test 14 days (should show "2 weeks ago") + let time = now.checked_sub(Duration::from_secs(14 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 weeks ago"); + + // Test 20 days (should show "2 weeks ago") + let time = now.checked_sub(Duration::from_secs(20 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 weeks ago"); + + // Test 21 days (should show "3 weeks ago") + let time = now.checked_sub(Duration::from_secs(21 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "3 weeks ago"); + } + + #[test] + fn test_fuzzy_time_month_edge_cases() { + let now = SystemTime::now(); + + // Test 59 days (should still show "last month") + let time = now.checked_sub(Duration::from_secs(59 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "last month"); + + // Test 61 days (should show "2 months ago") + let time = now.checked_sub(Duration::from_secs(61 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 months ago"); + + // Test 89 days (should show "2 months ago") + let time = now.checked_sub(Duration::from_secs(89 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 months ago"); + + // Test 90 days (should show "3 months ago") + let time = now.checked_sub(Duration::from_secs(90 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "3 months ago"); + } + + #[test] + fn test_fuzzy_time_very_old_files() { + let now = SystemTime::now(); + + // Test 2 years exactly + let time = now.checked_sub(Duration::from_secs(2 * 365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 years ago"); + + // Test 5 years + let time = now.checked_sub(Duration::from_secs(5 * 365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "5 years ago"); + + // Test 10 years + let time = now.checked_sub(Duration::from_secs(10 * 365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "10 years ago"); + + // Test 20 years + let time = now.checked_sub(Duration::from_secs(20 * 365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "20 years ago"); + + // Test 50 years (like old Unix timestamps) + let time = now.checked_sub(Duration::from_secs(50 * 365 * 24 * 60 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "50 years ago"); + } + + #[test] + fn test_fuzzy_time_hour_edge_cases() { + let now = SystemTime::now(); + + // Test 59 minutes (should still show minutes) + let time = now.checked_sub(Duration::from_secs(59 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "59 minutes ago"); + + // Test 61 minutes (should show "1 hour ago") + let time = now.checked_sub(Duration::from_secs(61 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "1 hour ago"); + + // Test 119 minutes (should show "1 hour ago") + let time = now.checked_sub(Duration::from_secs(119 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "1 hour ago"); + + // Test 120 minutes (should show "2 hours ago") + let time = now.checked_sub(Duration::from_secs(120 * 60)).unwrap(); + assert_eq!(fuzzy_time(time), "2 hours ago"); } } diff --git a/src/utils/icons.rs b/src/utils/icons.rs index c6abaf7..43a4985 100644 --- a/src/utils/icons.rs +++ b/src/utils/icons.rs @@ -310,10 +310,6 @@ mod tests { #[test] fn test_get_item_icon() { - use std::fs::Metadata; - use std::os::unix::fs::MetadataExt; - use std::os::unix::fs::FileTypeExt; - // Create a mock metadata for a file let metadata = fs::metadata("Cargo.toml").unwrap(); diff --git a/tests/integration.rs b/tests/integration.rs index 70a6c66..db36dc5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,5 @@ use assert_cmd::Command; use std::fs; -use std::path::PathBuf; use tempfile::tempdir; #[test] From add10228250392d54779b7e6b1d6cefc2e58cc65 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Mon, 13 Jan 2025 22:04:51 +0000 Subject: [PATCH 4/4] remove unused var --- src/main.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 858a44b..3356207 100644 --- a/src/main.rs +++ b/src/main.rs @@ -304,18 +304,6 @@ mod tests { #[test] fn test_main_flags() { // Test version flag - let args = cli::Flags { - version: true, - paths: vec![], - show_all: false, - almost_all: false, - slash: false, - dirs_first: false, - long: false, - human_readable: false, - no_icons: false, - fuzzy_time: false, - }; assert!(cli::version_info().contains("lsplus")); // Test empty paths