Skip to content

Commit eda843b

Browse files
committed
test: Add comprehensive tests for hierarchical .ckignore support
Adds 7 new tests (300+ lines) to verify the hierarchical .ckignore behavior added in the previous commits: Integration tests (ck-cli): - test_no_ckignore_flag_disables_hierarchical_ignore: Verifies --no-ckignore CLI flag properly disables .ckignore filtering in regex search mode Engine tests (ck-engine): - test_multiple_ckignore_files_merge_correctly: Verifies multiple .ckignore files at parent, subdir, and deeper levels all apply correctly Index tests (ck-index): - test_ckignore_works_without_gitignore: Verifies .ckignore works independently when respect_gitignore=false Core tests (ck-core): - test_build_exclude_patterns_with_defaults: Tests pattern merging with defaults enabled - test_build_exclude_patterns_without_defaults: Tests pattern handling with defaults disabled - test_build_exclude_patterns_empty_additional: Tests behavior with empty additional patterns - test_read_ckignore_edge_cases: Tests edge cases including empty files, comments only, whitespace, and mixed content All tests pass. Total test count now 149 (was 142).
1 parent b6d5c35 commit eda843b

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

ck-cli/tests/integration_tests.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,3 +821,66 @@ fn test_add_file_with_relative_path() {
821821
let stdout = String::from_utf8(output.stdout).unwrap();
822822
assert!(stdout.contains("Relative path content"));
823823
}
824+
825+
#[test]
826+
#[serial]
827+
fn test_no_ckignore_flag_disables_hierarchical_ignore() {
828+
let temp_dir = TempDir::new().unwrap();
829+
let parent = temp_dir.path();
830+
let subdir = parent.join("subdir");
831+
fs::create_dir(&subdir).unwrap();
832+
833+
// Create .ckignore at parent level excluding *.tmp files
834+
fs::write(parent.join(".ckignore"), "*.tmp\n").unwrap();
835+
836+
// Create test files with easily searchable pattern
837+
fs::write(parent.join("test.txt"), "FINDME_TEXT").unwrap();
838+
fs::write(parent.join("ignored.tmp"), "FINDME_TMP").unwrap();
839+
fs::write(subdir.join("nested.txt"), "FINDME_TEXT").unwrap();
840+
fs::write(subdir.join("also_ignored.tmp"), "FINDME_TMP").unwrap();
841+
842+
// Test WITH --no-ckignore flag - .tmp files should be INCLUDED
843+
// Using -r for recursive grep-style search (no indexing needed)
844+
let output = Command::new(ck_binary())
845+
.args(["-r", "--no-ckignore", "FINDME", "."])
846+
.current_dir(parent)
847+
.output()
848+
.expect("Failed to run ck search --no-ckignore");
849+
850+
assert!(
851+
output.status.success(),
852+
"Search with --no-ckignore failed: {}",
853+
String::from_utf8_lossy(&output.stderr)
854+
);
855+
let stdout = String::from_utf8(output.stdout).unwrap();
856+
857+
// With --no-ckignore, should find .tmp files
858+
assert!(
859+
stdout.contains("ignored.tmp") || stdout.contains("also_ignored.tmp"),
860+
"Should find .tmp files when --no-ckignore is used. Output: {}",
861+
stdout
862+
);
863+
864+
// Test WITHOUT --no-ckignore flag (default behavior) - .tmp files should be EXCLUDED
865+
let output = Command::new(ck_binary())
866+
.args(["-r", "FINDME", "."])
867+
.current_dir(parent)
868+
.output()
869+
.expect("Failed to run ck search");
870+
871+
assert!(output.status.success());
872+
let stdout = String::from_utf8(output.stdout).unwrap();
873+
874+
// Without --no-ckignore (default), should NOT find .tmp files
875+
assert!(
876+
!stdout.contains("ignored.tmp") && !stdout.contains("also_ignored.tmp"),
877+
"Should NOT find .tmp files when .ckignore is active (default). Output: {}",
878+
stdout
879+
);
880+
881+
// Should still find .txt files
882+
assert!(
883+
stdout.contains("test.txt") || stdout.contains("nested.txt"),
884+
"Should find .txt files even with .ckignore active"
885+
);
886+
}

ck-core/src/lib.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,4 +1411,94 @@ mod tests {
14111411
// Check for issue reference
14121412
assert!(content.contains("issue #27"));
14131413
}
1414+
1415+
#[test]
1416+
fn test_build_exclude_patterns_with_defaults() {
1417+
// Test with defaults enabled
1418+
let additional = vec!["*.custom".to_string(), "temp/".to_string()];
1419+
let patterns = build_exclude_patterns(&additional, true);
1420+
1421+
// Should include additional patterns
1422+
assert!(patterns.contains(&"*.custom".to_string()));
1423+
assert!(patterns.contains(&"temp/".to_string()));
1424+
1425+
// Should include default patterns (from get_default_exclude_patterns)
1426+
assert!(patterns.iter().any(|p| p.contains(".git")));
1427+
assert!(patterns.iter().any(|p| p.contains("node_modules")));
1428+
1429+
// Additional patterns should come before defaults
1430+
let custom_idx = patterns.iter().position(|p| p == "*.custom").unwrap();
1431+
let default_idx = patterns.iter().position(|p| p.contains(".git")).unwrap();
1432+
assert!(custom_idx < default_idx);
1433+
}
1434+
1435+
#[test]
1436+
fn test_build_exclude_patterns_without_defaults() {
1437+
// Test with defaults disabled
1438+
let additional = vec!["*.custom".to_string(), "temp/".to_string()];
1439+
let patterns = build_exclude_patterns(&additional, false);
1440+
1441+
// Should include additional patterns
1442+
assert!(patterns.contains(&"*.custom".to_string()));
1443+
assert!(patterns.contains(&"temp/".to_string()));
1444+
1445+
// Should NOT include default patterns
1446+
assert!(!patterns.iter().any(|p| p.contains(".git")));
1447+
assert!(!patterns.iter().any(|p| p.contains("node_modules")));
1448+
1449+
// Should only have the 2 additional patterns
1450+
assert_eq!(patterns.len(), 2);
1451+
}
1452+
1453+
#[test]
1454+
fn test_build_exclude_patterns_empty_additional() {
1455+
// Test with empty additional patterns and defaults enabled
1456+
let patterns = build_exclude_patterns(&[], true);
1457+
1458+
// Should only have default patterns
1459+
assert!(patterns.iter().any(|p| p.contains(".git")));
1460+
assert!(!patterns.is_empty());
1461+
1462+
// Test with empty additional patterns and defaults disabled
1463+
let patterns = build_exclude_patterns(&[], false);
1464+
1465+
// Should be empty
1466+
assert!(patterns.is_empty());
1467+
}
1468+
1469+
#[test]
1470+
fn test_read_ckignore_edge_cases() {
1471+
let temp_dir = TempDir::new().unwrap();
1472+
let test_path = temp_dir.path();
1473+
1474+
// Test 1: Empty .ckignore file
1475+
let ckignore_path = test_path.join(".ckignore");
1476+
fs::write(&ckignore_path, "").unwrap();
1477+
let patterns = read_ckignore_patterns(test_path).unwrap();
1478+
assert_eq!(patterns.len(), 0);
1479+
1480+
// Test 2: .ckignore with only comments
1481+
fs::write(&ckignore_path, "# Comment 1\n# Comment 2\n# Comment 3\n").unwrap();
1482+
let patterns = read_ckignore_patterns(test_path).unwrap();
1483+
assert_eq!(patterns.len(), 0);
1484+
1485+
// Test 3: .ckignore with only whitespace
1486+
fs::write(&ckignore_path, " \n\t\n \t \n").unwrap();
1487+
let patterns = read_ckignore_patterns(test_path).unwrap();
1488+
assert_eq!(patterns.len(), 0);
1489+
1490+
// Test 4: .ckignore with mixed content
1491+
fs::write(
1492+
&ckignore_path,
1493+
"# Comment\n\n \n*.tmp \n *.log\n\n# Another comment\n",
1494+
)
1495+
.unwrap();
1496+
let patterns = read_ckignore_patterns(test_path).unwrap();
1497+
assert_eq!(patterns.len(), 2);
1498+
assert!(patterns.contains(&"*.tmp".to_string()));
1499+
assert!(patterns.contains(&"*.log".to_string()));
1500+
// Patterns should be trimmed
1501+
assert!(!patterns.iter().any(|p| p.starts_with(' ')));
1502+
assert!(!patterns.iter().any(|p| p.ends_with(' ')));
1503+
}
14141504
}

ck-engine/src/lib.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,4 +1785,91 @@ mod tests {
17851785
"Should not create .ck directory in subdirectory"
17861786
);
17871787
}
1788+
1789+
#[tokio::test]
1790+
async fn test_multiple_ckignore_files_merge_correctly() {
1791+
// Test that multiple .ckignore files in the hierarchy are all applied
1792+
use std::fs;
1793+
use tempfile::TempDir;
1794+
1795+
let temp_dir = TempDir::new().unwrap();
1796+
let parent = temp_dir.path();
1797+
let subdir = parent.join("subdir");
1798+
let deeper = subdir.join("deeper");
1799+
fs::create_dir(&subdir).unwrap();
1800+
fs::create_dir(&deeper).unwrap();
1801+
1802+
// Create hierarchical .ckignore files
1803+
fs::write(parent.join(".ckignore"), "*.log\n").unwrap();
1804+
fs::write(subdir.join(".ckignore"), "*.tmp\n").unwrap();
1805+
fs::write(deeper.join(".ckignore"), "*.cache\n").unwrap();
1806+
1807+
// Create test files at each level
1808+
fs::write(parent.join("root.txt"), "searchable").unwrap();
1809+
fs::write(parent.join("root.log"), "should be ignored").unwrap();
1810+
1811+
fs::write(subdir.join("mid.txt"), "searchable").unwrap();
1812+
fs::write(subdir.join("mid.log"), "should be ignored by parent").unwrap();
1813+
fs::write(subdir.join("mid.tmp"), "should be ignored by local").unwrap();
1814+
1815+
fs::write(deeper.join("deep.txt"), "searchable").unwrap();
1816+
fs::write(deeper.join("deep.log"), "should be ignored by grandparent").unwrap();
1817+
fs::write(deeper.join("deep.tmp"), "should be ignored by parent").unwrap();
1818+
fs::write(deeper.join("deep.cache"), "should be ignored by local").unwrap();
1819+
1820+
// Index from parent
1821+
let parent_options = SearchOptions {
1822+
mode: SearchMode::Semantic,
1823+
query: "searchable".to_string(),
1824+
path: parent.to_path_buf(),
1825+
top_k: Some(20),
1826+
threshold: Some(0.1),
1827+
..Default::default()
1828+
};
1829+
1830+
let _ = search(&parent_options).await;
1831+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1832+
1833+
// Search from deeper directory - should respect ALL three .ckignore files
1834+
let deeper_options = SearchOptions {
1835+
mode: SearchMode::Semantic,
1836+
query: "ignored".to_string(),
1837+
path: deeper.clone(),
1838+
top_k: Some(20),
1839+
threshold: Some(0.1),
1840+
..Default::default()
1841+
};
1842+
1843+
let results = search(&deeper_options).await.unwrap();
1844+
1845+
// All ignored files should be excluded
1846+
let has_log = results
1847+
.iter()
1848+
.any(|r| r.file.to_string_lossy().ends_with(".log"));
1849+
let has_tmp = results
1850+
.iter()
1851+
.any(|r| r.file.to_string_lossy().ends_with(".tmp"));
1852+
let has_cache = results
1853+
.iter()
1854+
.any(|r| r.file.to_string_lossy().ends_with(".cache"));
1855+
1856+
assert!(
1857+
!has_log,
1858+
"*.log files should be excluded by parent .ckignore"
1859+
);
1860+
assert!(
1861+
!has_tmp,
1862+
"*.tmp files should be excluded by subdir .ckignore"
1863+
);
1864+
assert!(
1865+
!has_cache,
1866+
"*.cache files should be excluded by deeper .ckignore"
1867+
);
1868+
1869+
// Should still find .txt files
1870+
let has_txt = results
1871+
.iter()
1872+
.any(|r| r.file.to_string_lossy().ends_with(".txt"));
1873+
assert!(has_txt, "Should find .txt files (not ignored)");
1874+
}
17881875
}

ck-index/src/lib.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,6 +2014,78 @@ mod tests {
20142014
assert!(!test_path.join("level1").join("level2").exists());
20152015
assert!(!test_path.join("level1").exists());
20162016
}
2017+
2018+
#[test]
2019+
fn test_ckignore_works_without_gitignore() {
2020+
// Test that .ckignore is respected even when respect_gitignore is false
2021+
let temp_dir = TempDir::new().unwrap();
2022+
let test_path = temp_dir.path();
2023+
2024+
// Create .gitignore and .ckignore with different patterns
2025+
fs::write(test_path.join(".gitignore"), "*.git\n").unwrap();
2026+
fs::write(test_path.join(".ckignore"), "*.ck\n").unwrap();
2027+
2028+
// Create test files
2029+
fs::write(test_path.join("normal.txt"), "normal content").unwrap();
2030+
fs::write(test_path.join("ignored_by_git.git"), "git ignored").unwrap();
2031+
fs::write(test_path.join("ignored_by_ck.ck"), "ck ignored").unwrap();
2032+
2033+
// Test with respect_gitignore=false, use_ckignore=true
2034+
let options = ck_core::FileCollectionOptions {
2035+
respect_gitignore: false,
2036+
use_ckignore: true,
2037+
exclude_patterns: vec![],
2038+
};
2039+
2040+
let files = collect_files(test_path, &options).unwrap();
2041+
let file_names: Vec<String> = files
2042+
.iter()
2043+
.filter_map(|p| p.file_name())
2044+
.map(|n| n.to_string_lossy().to_string())
2045+
.collect();
2046+
2047+
// Should find normal.txt
2048+
assert!(
2049+
file_names.contains(&"normal.txt".to_string()),
2050+
"Should find normal.txt"
2051+
);
2052+
2053+
// Should find .git file (gitignore not respected)
2054+
assert!(
2055+
file_names.contains(&"ignored_by_git.git".to_string()),
2056+
"Should find .git file when respect_gitignore=false"
2057+
);
2058+
2059+
// Should NOT find .ck file (ckignore is respected)
2060+
assert!(
2061+
!file_names.contains(&"ignored_by_ck.ck".to_string()),
2062+
"Should NOT find .ck file when use_ckignore=true"
2063+
);
2064+
2065+
// Test with both disabled
2066+
let options_both_disabled = ck_core::FileCollectionOptions {
2067+
respect_gitignore: false,
2068+
use_ckignore: false,
2069+
exclude_patterns: vec![],
2070+
};
2071+
2072+
let files_all = collect_files(test_path, &options_both_disabled).unwrap();
2073+
let file_names_all: Vec<String> = files_all
2074+
.iter()
2075+
.filter_map(|p| p.file_name())
2076+
.map(|n| n.to_string_lossy().to_string())
2077+
.collect();
2078+
2079+
// Should find ALL files when both are disabled
2080+
assert!(
2081+
file_names_all.contains(&"ignored_by_git.git".to_string()),
2082+
"Should find .git file"
2083+
);
2084+
assert!(
2085+
file_names_all.contains(&"ignored_by_ck.ck".to_string()),
2086+
"Should find .ck file when use_ckignore=false"
2087+
);
2088+
}
20172089
}
20182090

20192091
// ============================================================================

0 commit comments

Comments
 (0)