diff --git a/Cargo.lock b/Cargo.lock index 2676bf42..d9b9276d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -341,6 +351,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "foldhash" version = "0.1.5" @@ -517,6 +533,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "lock_api" version = "0.4.13" @@ -603,6 +625,7 @@ dependencies = [ "rayon", "rusqlite", "serde", + "tempfile", "thiserror", "toml", "tracing", @@ -867,6 +890,19 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -989,6 +1025,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.12" diff --git a/Cargo.toml b/Cargo.toml index 5574ae83..4bcb495b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "nyx" version = "0.1.0" edition = "2024" +[dev-dependencies] +tempfile = "3" + [dependencies] directories = "6.0.0" clap = { version = "4.5.40", features = ["derive"] } diff --git a/src/utils/config.rs b/src/utils/config.rs index dfb28cf4..a72f4548 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -282,3 +282,42 @@ fn merge_configs(mut default: Config, user: Config) -> Config { default } + +#[test] +fn merge_configs_dedupes_and_keeps_order() { + let mut default_cfg = Config::default(); + default_cfg.scanner.excluded_extensions = vec!["rs".into(), "toml".into()]; + + let mut user_cfg = Config::default(); + user_cfg.scanner.excluded_extensions = vec!["jpg".into(), "rs".into()]; + + let merged = merge_configs(default_cfg, user_cfg); + + assert_eq!(merged.scanner.excluded_extensions, vec!["jpg", "rs", "toml"]); +} + +#[test] +fn load_creates_example_and_reads_user_overrides() { + let cfg_dir = tempfile::tempdir().unwrap(); + let cfg_path = cfg_dir.path(); + + let user_toml = r#" + [scanner] + one_file_system = true + excluded_extensions = ["foo"] + + [output] + quiet = true + "#; + fs::write(cfg_path.join("nyx.local"), user_toml).unwrap(); + + let cfg = Config::load(cfg_path).expect("Config::load should succeed"); + + assert!(cfg_path.join("nyx.conf").is_file()); + + assert!(cfg.scanner.one_file_system); + assert!(cfg.output.quiet); + assert!(cfg.scanner.excluded_extensions.contains(&"foo".to_string())); + + assert!(!cfg.scanner.follow_symlinks); +} diff --git a/src/utils/ext.rs b/src/utils/ext.rs index 21cee881..11fe5e0d 100644 --- a/src/utils/ext.rs +++ b/src/utils/ext.rs @@ -12,3 +12,19 @@ pub fn lowercase_ext(path: &std::path::Path) -> Option<&'static str> { _ => None, }) } + +#[test] +fn lowercase_ext_recognises_known_extensions() { + let cases = [ + ("file.rs", Some("rs")), + ("FILE.RS", Some("rs")), + ("main.cpp", Some("cpp")), + ("script.PY",Some("py")), + ("index.tsx",Some("ts")), + ("style.css",None), // unsupported + ]; + + for (file, expected) in cases { + assert_eq!(lowercase_ext(std::path::Path::new(file)), expected, "case: {file}"); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b4d2301b..826c3e9c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,5 +4,4 @@ pub mod project; pub(crate) mod query_cache; pub use config::Config; -// Re-export commonly used functions for convenience pub use project::get_project_info; diff --git a/src/utils/project.rs b/src/utils/project.rs index 50024f4e..5bc10074 100644 --- a/src/utils/project.rs +++ b/src/utils/project.rs @@ -28,3 +28,36 @@ pub fn sanitize_project_name(name: &str) -> String { .collect::>() .join("_") } + +#[test] +fn sanitize_project_name_is_idempotent_and_lossless_enough() { + let samples = [ + ("My Project", "my_project"), + ("Hello-World", "hello-world"), + ("mixed_case", "mixed_case"), + ("tabs\tspaces\n", "tabs_spaces"), + (" multiple ", "multiple"), + ("weird@$*chars", "weird_chars"), + ]; + + for (input, expected) in samples { + assert_eq!(sanitize_project_name(input), expected, "input: {}", input); + assert_eq!(sanitize_project_name(expected), expected); + } +} + +#[test] +fn get_project_info_uses_sanitized_name_in_sqlite_path() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + let project_dir = root.join("Example Project"); + std::fs::create_dir(&project_dir).unwrap(); + + let (project_name, db_path) = + get_project_info(&project_dir, root).expect("should detect project"); + + assert_eq!(project_name, "Example Project"); + assert_eq!(db_path, root.join("example_project.sqlite")); +} +