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 \t repositoryformatversion = 0\n \t filemode = 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