Skip to content

Commit 2a50a49

Browse files
committed
Add comprehensive test framework with real directory testing
- Create TestFileBuilder utility for building test file structures - Implement tests for the recent fixes (T-shape/L-shape connectors, no collapse for single items) - Test gitignore pattern handling with realistic directory structures - Add integration tests with real filesystem directories - Move tempfile from dev-dependencies to regular dependencies
1 parent 21f7ffd commit 2a50a49

File tree

3 files changed

+309
-1
lines changed

3 files changed

+309
-1
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ categories = ["command-line-utilities", "filesystem"]
1313

1414
[dev-dependencies]
1515
pretty_assertions = "1.4"
16-
tempfile = "3.8"
16+
1717
[dependencies]
1818
clap = { version = "4.4", features = ["derive"] }
1919
anyhow = "1.0"
2020
log = "0.4"
2121
env_logger = "0.10"
2222
glob = "0.3"
2323
colored = "2.0"
24+
tempfile = "3.8"

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod display;
44
mod gitignore;
55
mod log_macros;
66
mod scanner;
7+
mod tests;
78
mod types;
89

910
// Re-export public items

src/tests/mod.rs

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
//! Integration and system tests for Smart Tree
2+
//! This module contains comprehensive tests that create real directory structures
3+
//! and run the application against them.
4+
5+
use std::fs::{self, File};
6+
use std::io::Write;
7+
use std::path::{Path, PathBuf};
8+
use tempfile::TempDir;
9+
10+
/// A utility struct for building test file structures
11+
pub struct TestFileBuilder {
12+
/// The root directory for this test
13+
pub root_dir: TempDir,
14+
/// Track created files for verification
15+
pub created_files: Vec<PathBuf>,
16+
/// Track created directories for verification
17+
pub created_dirs: Vec<PathBuf>,
18+
}
19+
20+
impl TestFileBuilder {
21+
/// Create a new test file builder with a temporary root directory
22+
pub fn new() -> Self {
23+
let root_dir = tempfile::tempdir().expect("Failed to create temp directory");
24+
Self {
25+
root_dir,
26+
created_files: Vec::new(),
27+
created_dirs: Vec::new(),
28+
}
29+
}
30+
31+
/// Get the root path
32+
pub fn root_path(&self) -> &Path {
33+
self.root_dir.path()
34+
}
35+
36+
/// Create a directory at the given path relative to the root
37+
pub fn create_dir(&mut self, rel_path: &str) -> &mut Self {
38+
let path = self.root_dir.path().join(rel_path);
39+
40+
// Create parent directories if they don't exist
41+
if let Some(parent) = path.parent() {
42+
fs::create_dir_all(parent).expect("Failed to create parent directory");
43+
}
44+
45+
fs::create_dir_all(&path).expect("Failed to create directory");
46+
self.created_dirs.push(path);
47+
self
48+
}
49+
50+
/// Create a file with the given content at the given path relative to the root
51+
pub fn create_file(&mut self, rel_path: &str, content: &str) -> &mut Self {
52+
let path = self.root_dir.path().join(rel_path);
53+
54+
// Create parent directories if they don't exist
55+
if let Some(parent) = path.parent() {
56+
fs::create_dir_all(parent).expect("Failed to create parent directory");
57+
// Add parent to created_dirs if not already present
58+
if !self.created_dirs.contains(&parent.to_path_buf()) {
59+
self.created_dirs.push(parent.to_path_buf());
60+
}
61+
}
62+
63+
let mut file = File::create(&path).expect("Failed to create file");
64+
file.write_all(content.as_bytes()).expect("Failed to write file content");
65+
self.created_files.push(path);
66+
self
67+
}
68+
69+
/// Create a .gitignore file with the given patterns at the given path relative to the root
70+
pub fn create_gitignore(&mut self, rel_dir: &str, patterns: &[&str]) -> &mut Self {
71+
let content = patterns.join("\n");
72+
let gitignore_path = if rel_dir.is_empty() { ".gitignore".to_string() } else { format!("{rel_dir}/.gitignore") };
73+
self.create_file(&gitignore_path, &content)
74+
}
75+
76+
/// Create a git-like directory structure (to test system directory handling)
77+
pub fn create_git_dir(&mut self, rel_path: &str) -> &mut Self {
78+
// Create basic .git structure
79+
let git_path = if rel_path.is_empty() { ".git".to_string() } else { format!("{rel_path}/.git") };
80+
81+
self.create_dir(&git_path)
82+
.create_dir(&format!("{}/objects", git_path))
83+
.create_dir(&format!("{}/refs", git_path))
84+
.create_file(&format!("{}/HEAD", git_path), "ref: refs/heads/main\n")
85+
.create_file(&format!("{}/config", git_path), "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n")
86+
}
87+
88+
/// Create a node_modules-like directory with many files
89+
pub fn create_node_modules(&mut self, rel_path: &str) -> &mut Self {
90+
let node_modules_path = if rel_path.is_empty() {
91+
"node_modules".to_string()
92+
} else {
93+
format!("{rel_path}/node_modules")
94+
};
95+
96+
self.create_dir(&node_modules_path)
97+
.create_dir(&format!("{}/lodash", node_modules_path))
98+
.create_file(&format!("{}/lodash/package.json", node_modules_path), "{}")
99+
.create_dir(&format!("{}/react", node_modules_path))
100+
.create_file(&format!("{}/react/package.json", node_modules_path), "{}")
101+
}
102+
103+
/// Create a nested project structure with multiple .gitignore files
104+
pub fn create_nested_project(&mut self) -> &mut Self {
105+
// Root project
106+
self.create_file("README.md", "# Root Project")
107+
.create_file("package.json", "{}")
108+
.create_gitignore("", &["*.log", "dist/", "build/"])
109+
.create_git_dir("")
110+
.create_node_modules("")
111+
112+
// Main source code
113+
.create_dir("src")
114+
.create_file("src/main.js", "console.log('Hello');")
115+
.create_file("src/index.js", "import './main.js';")
116+
117+
// Nested project with its own .gitignore
118+
.create_dir("projects/webapp")
119+
.create_file("projects/webapp/README.md", "# Web App")
120+
.create_gitignore("projects/webapp", &["*.tmp", "node_modules/"])
121+
.create_git_dir("projects/webapp")
122+
.create_node_modules("projects/webapp")
123+
.create_file("projects/webapp/app.js", "// Main app")
124+
125+
// Another nested project
126+
.create_dir("projects/api")
127+
.create_file("projects/api/README.md", "# API")
128+
.create_gitignore("projects/api", &["*.bak", "logs/"])
129+
.create_git_dir("projects/api")
130+
.create_file("projects/api/server.js", "// API server")
131+
132+
// Create some log files that should be ignored
133+
.create_file("error.log", "Error log content")
134+
.create_file("projects/webapp/debug.tmp", "Temp file")
135+
.create_dir("projects/api/logs")
136+
.create_file("projects/api/logs/api.log", "API log content")
137+
}
138+
}
139+
140+
#[cfg(test)]
141+
mod integration_tests {
142+
use super::*;
143+
use crate::gitignore::GitIgnore;
144+
use crate::scanner::scan_directory;
145+
use crate::types::{DisplayConfig, SortBy, ColorTheme};
146+
use crate::format_tree;
147+
148+
/// Test for correctly marking system directories as gitignored
149+
#[test]
150+
fn test_system_dir_marking() {
151+
let mut builder = TestFileBuilder::new();
152+
builder.create_nested_project();
153+
154+
let root_path = builder.root_path();
155+
let gitignore = GitIgnore::load(root_path).unwrap();
156+
157+
let root = scan_directory(root_path, &gitignore, usize::MAX).unwrap();
158+
159+
// Find .git directory in the scanned result
160+
let git_dir = root.children.iter()
161+
.find(|c| c.name == ".git")
162+
.expect(".git directory should be in the result");
163+
164+
// Check that it's marked as gitignored
165+
assert!(git_dir.is_gitignored, ".git should be marked as gitignored");
166+
167+
// Check that node_modules is also marked
168+
let node_modules = root.children.iter()
169+
.find(|c| c.name == "node_modules")
170+
.expect("node_modules directory should be in the result");
171+
172+
assert!(node_modules.is_gitignored, "node_modules should be marked as gitignored");
173+
}
174+
175+
/// Test for correctly handling gitignore patterns
176+
#[test]
177+
fn test_gitignore_patterns() {
178+
let mut builder = TestFileBuilder::new();
179+
builder.create_nested_project();
180+
181+
let root_path = builder.root_path();
182+
let gitignore = GitIgnore::load(root_path).unwrap();
183+
184+
// Test root gitignore patterns
185+
assert!(gitignore.is_ignored(&root_path.join("error.log")), "*.log should be ignored");
186+
assert!(!gitignore.is_ignored(&root_path.join("README.md")), "README.md should not be ignored");
187+
188+
// Note: In the current implementation, nested .gitignore files are not loaded
189+
// This test verifies current behavior - will need to be updated once we implement
190+
// recursive gitignore handling
191+
assert!(!gitignore.is_ignored(&root_path.join("projects/webapp/debug.tmp")),
192+
"Currently nested .gitignore files are not loaded, so .tmp files are not ignored");
193+
}
194+
195+
/// Test for the folding of single items
196+
#[test]
197+
fn test_no_collapse_single_item() {
198+
let mut builder = TestFileBuilder::new();
199+
200+
// Create a directory with 3 files, where we'll hide one of them
201+
builder.create_dir("test_dir")
202+
.create_file("test_dir/file1.txt", "File 1")
203+
.create_file("test_dir/file2.txt", "File 2")
204+
.create_file("test_dir/file3.txt", "File 3");
205+
206+
let root_path = builder.root_path().join("test_dir");
207+
let gitignore = GitIgnore::load(&root_path).unwrap();
208+
let root = scan_directory(&root_path, &gitignore, usize::MAX).unwrap();
209+
210+
// Configure to only show 2 items in directory (2 lines + collapsed indicator)
211+
let config = DisplayConfig {
212+
max_lines: 5,
213+
dir_limit: 2,
214+
sort_by: SortBy::Name,
215+
dirs_first: false,
216+
use_colors: false,
217+
color_theme: ColorTheme::None,
218+
use_emoji: false,
219+
size_colorize: false,
220+
date_colorize: false,
221+
detailed_metadata: false,
222+
};
223+
224+
let output = format_tree(&root, &config).unwrap();
225+
226+
// We should NOT see a "1 item hidden" message, since it doesn't save space
227+
assert!(!output.contains("1 item hidden"),
228+
"Should not show '1 item hidden' message since it doesn't save space");
229+
230+
// Now create a directory with 4 files to verify proper collapse of multiple items
231+
let mut builder = TestFileBuilder::new();
232+
builder.create_dir("test_dir2")
233+
.create_file("test_dir2/file1.txt", "File 1")
234+
.create_file("test_dir2/file2.txt", "File 2")
235+
.create_file("test_dir2/file3.txt", "File 3")
236+
.create_file("test_dir2/file4.txt", "File 4");
237+
238+
let root_path = builder.root_path().join("test_dir2");
239+
let gitignore = GitIgnore::load(&root_path).unwrap();
240+
let root = scan_directory(&root_path, &gitignore, usize::MAX).unwrap();
241+
242+
let output = format_tree(&root, &config).unwrap();
243+
244+
// We SHOULD see an "items hidden" message
245+
assert!(output.contains("items hidden"),
246+
"Should show 'items hidden' message when multiple items are hidden");
247+
}
248+
249+
/// Test for tree connector shapes
250+
#[test]
251+
fn test_tree_connectors() {
252+
let mut builder = TestFileBuilder::new();
253+
254+
// Create a simple directory structure
255+
builder.create_dir("test_dir")
256+
.create_file("test_dir/file1.txt", "File 1")
257+
.create_file("test_dir/file2.txt", "File 2")
258+
.create_file("test_dir/file3.txt", "File 3");
259+
260+
let root_path = builder.root_path().join("test_dir");
261+
let gitignore = GitIgnore::load(&root_path).unwrap();
262+
let root = scan_directory(&root_path, &gitignore, usize::MAX).unwrap();
263+
264+
let config = DisplayConfig {
265+
max_lines: 10,
266+
dir_limit: 10,
267+
sort_by: SortBy::Name,
268+
dirs_first: false,
269+
use_colors: false,
270+
color_theme: ColorTheme::None,
271+
use_emoji: false,
272+
size_colorize: false,
273+
date_colorize: false,
274+
detailed_metadata: false,
275+
};
276+
277+
let output = format_tree(&root, &config).unwrap();
278+
279+
// Check that the last file uses L-shape connector
280+
let lines: Vec<_> = output.lines().collect();
281+
282+
// Find the line with file3.txt
283+
let last_file_line = lines.iter()
284+
.find(|l| l.contains("file3.txt"))
285+
.expect("Should find file3.txt in output");
286+
287+
// Check that it has the corner (L-shape) connector
288+
assert!(
289+
last_file_line.contains("└──"),
290+
"Last item should use L-shape connector: {}",
291+
last_file_line
292+
);
293+
294+
// Find the line with file2.txt (middle item)
295+
let middle_file_line = lines.iter()
296+
.find(|l| l.contains("file2.txt"))
297+
.expect("Should find file2.txt in output");
298+
299+
// Check that it has the T-shape connector
300+
assert!(
301+
middle_file_line.contains("├──"),
302+
"Middle item should use T-shape connector: {}",
303+
middle_file_line
304+
);
305+
}
306+
}

0 commit comments

Comments
 (0)