Skip to content

Commit 23dfa5d

Browse files
committed
test: add unit tests for file extension filtering and path conversion
- Introduced tests to verify that excluded file extensions are correctly identified as irrelevant. - Added tests to ensure that multi-dot file extensions are handled properly, only matching the last segment. - Implemented a test to confirm that paths are converted from Windows to Unix format while preserving drive letters. - Added a test to check that symlink loops do not cause infinite recursion during directory scanning.
1 parent f42fea7 commit 23dfa5d

File tree

6 files changed

+163
-0
lines changed

6 files changed

+163
-0
lines changed

tests/fs_event_filtering_ext.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use std::collections::HashSet;
2+
use stitch::core::is_event_path_relevant;
3+
use tempfile::TempDir;
4+
5+
#[test]
6+
fn excluded_extension_is_not_relevant() {
7+
let tmp = TempDir::new().unwrap();
8+
let root = tmp.path().to_path_buf();
9+
10+
let p = root.join("note.LOG");
11+
12+
let include_exts: HashSet<String> = HashSet::new();
13+
let exclude_exts: HashSet<String> = std::iter::once(String::from(".log")).collect();
14+
let exclude_dirs: HashSet<String> = HashSet::new();
15+
let exclude_files: HashSet<String> = HashSet::new();
16+
17+
assert!(
18+
!is_event_path_relevant(
19+
&root,
20+
&p,
21+
&include_exts,
22+
&exclude_exts,
23+
&exclude_dirs,
24+
&exclude_files
25+
),
26+
"excluded extension should not be relevant (case-insensitive)"
27+
);
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::collections::HashSet;
2+
use std::fs;
3+
use stitch::core::scan_dir_to_node;
4+
use tempfile::TempDir;
5+
6+
#[test]
7+
fn exclude_multidot_matches_last_segment_only() {
8+
let tmp = TempDir::new().unwrap();
9+
let root = tmp.path();
10+
fs::write(root.join("archive.tar.gz"), "x").unwrap();
11+
12+
let include: HashSet<String> = HashSet::new();
13+
let exclude_tar_gz: HashSet<String> = std::iter::once(String::from(".tar.gz")).collect();
14+
let exclude_gz: HashSet<String> = std::iter::once(String::from(".gz")).collect();
15+
let nodirs = HashSet::new();
16+
let nofiles = HashSet::new();
17+
18+
// Excluding ".tar.gz" should NOT hide it if only the last segment is considered.
19+
let tree1 = scan_dir_to_node(root, &include, &exclude_tar_gz, &nodirs, &nofiles);
20+
assert!(tree1.children.iter().any(|n| n.name == "archive.tar.gz"));
21+
22+
// Excluding ".gz" should hide it.
23+
let tree2 = scan_dir_to_node(root, &include, &exclude_gz, &nodirs, &nofiles);
24+
assert!(!tree2.children.iter().any(|n| n.name == "archive.tar.gz"));
25+
}

tests/path_to_unix_windows.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#[cfg(windows)]
2+
#[test]
3+
fn path_to_unix_converts_backslashes_and_keeps_drive() {
4+
use std::path::PathBuf;
5+
use stitch::core::path_to_unix;
6+
7+
let p = PathBuf::from(r"C:\projects\stitch\src\lib.rs");
8+
let s = path_to_unix(&p);
9+
assert!(
10+
s.contains("C:"),
11+
"drive letter should be preserved in lossy path"
12+
);
13+
assert!(
14+
s.ends_with("projects/stitch/src/lib.rs"),
15+
"backslashes should become slashes"
16+
);
17+
}

tests/profile_load_precedence.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use stitch::core::{
2+
Profile, ProfileScope, RustOptions, SlintOptions, WorkspaceSettings, load_profile, save_profile,
3+
};
4+
use tempfile::TempDir;
5+
6+
const fn ws() -> WorkspaceSettings {
7+
WorkspaceSettings {
8+
version: 1,
9+
ext_filter: String::new(),
10+
exclude_dirs: String::new(),
11+
exclude_files: String::new(),
12+
remove_prefix: String::new(),
13+
remove_regex: String::new(),
14+
hierarchy_only: false,
15+
dirs_only: false,
16+
rust: RustOptions {
17+
rust_remove_inline_comments: false,
18+
rust_remove_doc_comments: false,
19+
rust_function_signatures_only: false,
20+
rust_signatures_only_filter: String::new(),
21+
},
22+
slint: SlintOptions {
23+
slint_remove_line_comments: false,
24+
slint_remove_block_comments: false,
25+
},
26+
}
27+
}
28+
29+
#[test]
30+
fn load_profile_prefers_local_over_shared() {
31+
let tmp = TempDir::new().unwrap();
32+
let root = tmp.path();
33+
34+
let mut shared = Profile {
35+
name: "same".into(),
36+
settings: ws(),
37+
explicit: vec![],
38+
};
39+
shared.settings.ext_filter = ".rs".into();
40+
save_profile(root, &shared, ProfileScope::Shared).unwrap();
41+
42+
let mut local = Profile {
43+
name: "same".into(),
44+
settings: ws(),
45+
explicit: vec![],
46+
};
47+
local.settings.ext_filter = ".md".into();
48+
save_profile(root, &local, ProfileScope::Local).unwrap();
49+
50+
let (loaded, scope) = load_profile(root, "same").expect("load");
51+
assert_eq!(scope, ProfileScope::Local);
52+
assert_eq!(loaded.settings.ext_filter, ".md");
53+
}

tests/render_without_root.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use stitch::core::render_unicode_tree_from_paths;
2+
3+
#[test]
4+
fn render_without_root_has_no_top_line() {
5+
let paths = vec!["a/b.rs".into(), "a/c.rs".into(), "d.txt".into()];
6+
let out = render_unicode_tree_from_paths(&paths, None);
7+
// Just ensure it starts with the first top-level dir/file and ends with a newline.
8+
assert!(out.starts_with("├── a\n") || out.starts_with("└── d.txt\n"));
9+
assert!(out.ends_with('\n'));
10+
}

tests/scan_symlink_loop.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#[cfg(unix)]
2+
#[test]
3+
fn scanner_does_not_follow_symlink_loops() {
4+
use std::collections::HashSet;
5+
use std::fs;
6+
use std::os::unix::fs::symlink;
7+
use stitch::core::scan_dir_to_node;
8+
use tempfile::TempDir;
9+
10+
let tmp = TempDir::new().unwrap();
11+
let root = tmp.path();
12+
13+
fs::create_dir_all(root.join("real")).unwrap();
14+
fs::write(root.join("real/file.txt"), "x").unwrap();
15+
// Create loop: real/loop -> real
16+
symlink(root.join("real"), root.join("real/loop")).unwrap();
17+
18+
let h = HashSet::new();
19+
let tree = scan_dir_to_node(root, &h, &h, &h, &h);
20+
21+
// Expect: "real" present, "file.txt" present, but not an endlessly nested "loop" chain.
22+
let real = tree.children.iter().find(|n| n.name == "real").unwrap();
23+
assert!(real.children.iter().any(|n| n.name == "file.txt"));
24+
// Either "loop" is omitted, or included once but not infinitely expanded.
25+
let loop_count = real.children.iter().filter(|n| n.name == "loop").count();
26+
assert!(
27+
loop_count <= 1,
28+
"symlink loop must not cause unbounded descent"
29+
);
30+
}

0 commit comments

Comments
 (0)