use cs::{
run_search, CodeReference, ReferenceTreeBuilder, SearchQuery, SearchResult, TranslationEntry,
TreeFormatter,
};
use proptest::prelude::*;
use std::path::PathBuf;
#[test]
fn test_format_search_results() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
println!("\n{}", output);
assert!(output.contains("'add new'"));
assert!(output.contains("search query"));
assert!(output.contains("invoice.labels.add_new"));
assert!(output.contains("Key:"));
}
#[test]
fn test_format_with_custom_width() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::with_width(120);
let output = formatter.format(&tree);
println!("\n=== Wide Format (120 columns) ===\n{}", output);
assert!(output.contains("'add new'"));
}
#[test]
fn test_format_no_results() {
let query = SearchQuery::new("nonexistent xyz".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
println!("\n=== No Results ===\n{}", output);
assert!(output.contains("'nonexistent xyz'"));
assert!(output.contains("search query"));
assert_eq!(output.lines().count(), 1);
}
#[test]
fn test_format_tree_structure() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
assert!(output.contains("└─>") || output.contains("├─>"));
let lines: Vec<&str> = output.lines().collect();
assert!(lines.len() > 1, "Should have multiple lines");
}
#[test]
fn test_end_to_end_with_formatter() {
println!("\n=== End-to-End with Formatter ===\n");
let search_text = "add new";
println!("Searching for: '{}'", search_text);
let query = SearchQuery::new(search_text.to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
println!("1. Running search...");
let result = run_search(query).expect("Search should succeed");
println!(
" Found {} translations, {} code references",
result.translation_entries.len(),
result.code_references.len()
);
println!("2. Building tree...");
let tree = ReferenceTreeBuilder::build(&result);
println!(
" Tree has {} nodes, depth {}",
tree.node_count(),
tree.max_depth()
);
println!("3. Formatting output...\n");
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
println!("{}", output);
println!("\n✅ End-to-end workflow complete!");
assert!(!output.is_empty());
assert!(output.contains("'add new'"));
}
#[test]
fn test_format_multiple_translations() {
let query = SearchQuery::new("invoice".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
println!("\n=== Multiple Translations ===\n{}", output);
assert!(output.contains("invoice"));
let yml_count = output.lines().filter(|line| line.contains(".yml")).count();
assert!(
yml_count > 0,
"Should have at least one translation file reference"
);
}
#[test]
fn test_format_shows_locations() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
assert!(output.contains(".yml:"));
assert!(output.contains(".ts:") || output.contains(".tsx:"));
}
#[test]
fn test_format_readable_output() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
let formatter = TreeFormatter::new();
let output = formatter.format(&tree);
println!("\n=== Readable Output Test ===\n{}", output);
assert!(output.contains("Key:"), "Should label key paths");
assert!(output.contains("search query"), "Should label root");
assert!(
!output.contains("...") || output.len() > 500,
"Short content should not be truncated"
);
}
#[test]
fn test_format_comparison() {
let query = SearchQuery::new("add new".to_string())
.with_base_dir(PathBuf::from("tests/fixtures/rails-app"));
let result = run_search(query).expect("Search should succeed");
let tree = ReferenceTreeBuilder::build(&result);
println!("\n=== Format Comparison ===\n");
println!("--- 80 columns ---");
let formatter_80 = TreeFormatter::with_width(80);
let output_80 = formatter_80.format(&tree);
println!("{}", output_80);
println!("\n--- 120 columns ---");
let formatter_120 = TreeFormatter::with_width(120);
let output_120 = formatter_120.format(&tree);
println!("{}", output_120);
assert_eq!(output_80.lines().count(), output_120.lines().count());
}
proptest! {
#[test]
fn test_simple_format_consistency(
translation_key in "[a-zA-Z][a-zA-Z0-9_.]{0,50}",
translation_value in "[^\\n\\r]{0,100}",
file_path in "[a-zA-Z0-9_/.-]{1,50}\\.(yml|ts|js)",
line_num in 1u32..1000u32,
code_content in "[^\\n\\r]{0,200}"
) {
let mut result = SearchResult {
query: "test".to_string(),
translation_entries: vec![],
code_references: vec![],
};
result.translation_entries.push(TranslationEntry {
key: translation_key.clone(),
value: translation_value.clone(),
line: line_num as usize,
file: PathBuf::from(&file_path),
});
result.code_references.push(CodeReference {
file: PathBuf::from(&file_path),
line: line_num as usize,
pattern: "test".to_string(),
context: code_content.clone(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
});
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
for line in output.lines() {
if !line.trim().is_empty() {
let parts: Vec<&str> = line.splitn(3, ':').collect();
prop_assert_eq!(parts.len(), 3, "Line should have exactly 3 parts separated by colons: {}", line);
prop_assert!(!parts[0].is_empty(), "File path should not be empty: {}", line);
prop_assert!(parts[1].parse::<u32>().is_ok(), "Line number should be numeric: {}", line);
prop_assert!(!parts[2].contains("├─>"), "Content should not contain tree characters: {}", line);
prop_assert!(!parts[2].contains("└─>"), "Content should not contain tree characters: {}", line);
prop_assert!(!parts[2].contains("\x1b["), "Content should not contain ANSI codes: {}", line);
prop_assert!(!parts[2].contains('\n'), "Content should not contain newlines: {}", line);
prop_assert!(!parts[2].contains('\r'), "Content should not contain carriage returns: {}", line);
}
}
}
}
#[test]
fn test_simple_format_empty_results() {
let result = SearchResult {
query: "nonexistent".to_string(),
translation_entries: vec![],
code_references: vec![],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
assert_eq!(output, "", "Empty results should produce empty output");
}
#[test]
fn test_simple_format_single_translation() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![TranslationEntry {
key: "test.key".to_string(),
value: "test value".to_string(),
line: 5,
file: PathBuf::from("test.yml"),
}],
code_references: vec![],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
assert_eq!(output, "test.yml:5:test.key: test value\n");
}
#[test]
fn test_simple_format_single_code_reference() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![],
code_references: vec![CodeReference {
file: PathBuf::from("test.ts"),
line: 10,
pattern: "test".to_string(),
context: "const x = test();".to_string(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
}],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
assert_eq!(output, "test.ts:10:const x = test();\n");
}
#[test]
fn test_simple_format_multiple_results() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![
TranslationEntry {
key: "test.key1".to_string(),
value: "value1".to_string(),
line: 1,
file: PathBuf::from("en.yml"),
},
TranslationEntry {
key: "test.key2".to_string(),
value: "value2".to_string(),
line: 2,
file: PathBuf::from("en.yml"),
},
],
code_references: vec![
CodeReference {
file: PathBuf::from("app.ts"),
line: 5,
pattern: "test".to_string(),
context: "I18n.t('test.key1')".to_string(),
key_path: "test.key1".to_string(),
context_before: vec![],
context_after: vec![],
},
CodeReference {
file: PathBuf::from("app.ts"),
line: 10,
pattern: "test".to_string(),
context: "I18n.t('test.key2')".to_string(),
key_path: "test.key2".to_string(),
context_before: vec![],
context_after: vec![],
},
],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
let expected = "en.yml:1:test.key1: value1\n\
en.yml:2:test.key2: value2\n\
app.ts:5:I18n.t('test.key1')\n\
app.ts:10:I18n.t('test.key2')\n";
assert_eq!(output, expected);
}
#[test]
fn test_simple_format_special_characters_in_paths() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![TranslationEntry {
key: "test.key".to_string(),
value: "test value".to_string(),
line: 1,
file: PathBuf::from("path with spaces/file:name.yml"),
}],
code_references: vec![],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
assert_eq!(
output,
"path with spaces/file\\:name.yml:1:test.key: test value\n"
);
}
#[test]
fn test_simple_format_special_characters_in_content() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![TranslationEntry {
key: "test.key".to_string(),
value: "value with\nnewlines\rand\ttabs".to_string(),
line: 1,
file: PathBuf::from("test.yml"),
}],
code_references: vec![CodeReference {
file: PathBuf::from("test.ts"),
line: 5,
pattern: "test".to_string(),
context: " code with\n newlines ".to_string(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
}],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("test.key: value with newlines and tabs"));
assert!(!lines[0].contains('\n'));
assert!(!lines[0].contains('\r'));
assert!(lines[1].contains("code with newlines"));
assert!(!lines[1].contains('\n'));
assert!(!lines[1].contains('\r'));
}
#[test]
fn test_simple_format_ansi_codes_stripped() {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![],
code_references: vec![CodeReference {
file: PathBuf::from("test.ts"),
line: 5,
pattern: "test".to_string(),
context: "\x1b[31mred text\x1b[0m normal text".to_string(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
}],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
assert_eq!(output, "test.ts:5:red text normal text\n");
assert!(!output.contains("\x1b["));
}
#[test]
fn test_simple_format_different_search_types() {
let translation_result = SearchResult {
query: "test".to_string(),
translation_entries: vec![TranslationEntry {
key: "test.key".to_string(),
value: "test value".to_string(),
line: 1,
file: PathBuf::from("en.yml"),
}],
code_references: vec![],
};
let exact_match_result = SearchResult {
query: "test".to_string(),
translation_entries: vec![],
code_references: vec![CodeReference {
file: PathBuf::from("app.ts"),
line: 5,
pattern: "test".to_string(),
context: "const test = 'value';".to_string(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
}],
};
let formatter = TreeFormatter::new().with_simple_format(true);
let translation_output = formatter.format_result(&translation_result);
let exact_match_output = formatter.format_result(&exact_match_result);
assert_eq!(translation_output, "en.yml:1:test.key: test value\n");
assert_eq!(exact_match_output, "app.ts:5:const test = 'value';\n");
let translation_parts: Vec<&str> = translation_output.trim().splitn(3, ':').collect();
let exact_match_parts: Vec<&str> = exact_match_output.trim().splitn(3, ':').collect();
assert_eq!(translation_parts.len(), 3);
assert_eq!(exact_match_parts.len(), 3);
}
proptest! {
#[test]
fn test_special_character_handling(
file_base in "[a-zA-Z0-9_-]{1,20}",
file_ext in prop::sample::select(vec!["yml", "ts", "js", "json"]),
path_special_chars in prop::collection::vec(
prop::sample::select(vec![" ", ".", "-", "_", "(", ")", "[", "]", "&", "$", "!", "@"]),
0..3
),
content_base in "[a-zA-Z0-9 ]{0,50}",
unicode_chars in prop::collection::vec(
prop::sample::select(vec!["é", "ñ", "ä¸", "🚀", "ü", "ß", "ø", "λ"]),
0..3
),
shell_metacharacters in prop::collection::vec(
prop::sample::select(vec!["$", "|", "&", ";", "(", ")", "<", ">", "`", "\"", "'", "\\", "*", "?"]),
0..5
),
line_num in 1u32..1000u32,
translation_key in "[a-zA-Z][a-zA-Z0-9_.]{0,30}",
translation_value_base in "[a-zA-Z0-9 ]{0,30}"
) {
let mut file_path = file_base.clone();
for special_char in &path_special_chars {
file_path.push_str(special_char);
}
file_path.push('.');
file_path.push_str(file_ext);
let mut content = content_base.clone();
for unicode_char in &unicode_chars {
content.push_str(unicode_char);
}
for meta_char in &shell_metacharacters {
content.push_str(meta_char);
}
let mut translation_value = translation_value_base.clone();
for unicode_char in &unicode_chars {
translation_value.push_str(unicode_char);
}
let mut result = SearchResult {
query: "test".to_string(),
translation_entries: vec![],
code_references: vec![],
};
result.translation_entries.push(TranslationEntry {
key: translation_key.clone(),
value: translation_value.clone(),
line: line_num as usize,
file: PathBuf::from(&file_path),
});
result.code_references.push(CodeReference {
file: PathBuf::from(&file_path),
line: line_num as usize,
pattern: "test".to_string(),
context: content.clone(),
key_path: "".to_string(),
context_before: vec![],
context_after: vec![],
});
let formatter = TreeFormatter::new().with_simple_format(true);
let output = formatter.format_result(&result);
for line in output.lines() {
if !line.trim().is_empty() {
let parts: Vec<&str> = line.splitn(3, ':').collect();
prop_assert_eq!(parts.len(), 3, "Line should have exactly 3 parts separated by colons even with special characters: {}", line);
prop_assert!(!parts[0].is_empty(), "File path should not be empty: {}", line);
prop_assert!(parts[1].parse::<u32>().is_ok(), "Line number should be numeric: {}", line);
prop_assert!(!parts[2].contains('\n'), "Content should not contain unescaped newlines: {}", line);
prop_assert!(!parts[2].contains('\r'), "Content should not contain unescaped carriage returns: {}", line);
prop_assert!(!parts[2].contains("├─>"), "Content should not contain tree characters: {}", line);
prop_assert!(!parts[2].contains("└─>"), "Content should not contain tree characters: {}", line);
prop_assert!(!parts[2].contains("\x1b["), "Content should not contain ANSI codes: {}", line);
if file_path.contains(':') {
prop_assert!(parts[0].contains("%3A"), "File path with colons should be URL-encoded: {}", line);
}
}
}
for unicode_char in &unicode_chars {
if !unicode_char.is_empty() && (content.contains(unicode_char) || translation_value.contains(unicode_char)) {
prop_assert!(output.contains(unicode_char), "Unicode character '{}' should be preserved in output", unicode_char);
}
}
let lines: Vec<&str> = output.lines().filter(|line| !line.trim().is_empty()).collect();
prop_assert_eq!(lines.len(), 2, "Should have exactly 2 output lines (translation + code reference)");
}
}
proptest! {
#[test]
fn test_translation_vs_code_context_separation(
translation_key in "[a-zA-Z][a-zA-Z0-9_.]{0,30}",
translation_value in "[^\\n\\r]{0,50}",
code_content in "[^\\n\\r]{0,100}",
file_path in "[a-zA-Z][a-zA-Z0-9_/.]{0,29}\\.(yml|ts|js)",
line_num in 1u32..100u32
) {
let result = SearchResult {
query: "test".to_string(),
translation_entries: vec![TranslationEntry {
key: translation_key.clone(),
value: translation_value.clone(),
line: line_num as usize,
file: PathBuf::from(&file_path),
}],
code_references: vec![CodeReference {
file: PathBuf::from(&file_path),
line: (line_num + 10) as usize,
pattern: "test".to_string(),
context: code_content.clone(),
key_path: translation_key.clone(),
context_before: vec!["context before line 1".to_string(), "context before line 2".to_string()],
context_after: vec!["context after line 1".to_string(), "context after line 2".to_string()],
}],
};
let formatter = TreeFormatter::new().with_simple_format(false);
let output = formatter.format_result(&result);
let translation_section_lines: Vec<&str> = output
.lines()
.skip_while(|line| !line.contains("=== Translation Files ==="))
.take_while(|line| !line.contains("=== Code References ==="))
.collect();
let translation_line = translation_section_lines
.iter()
.skip(1) .find(|line| {
line.contains(&file_path) && line.contains(&format!(":{}", line_num))
});
prop_assert!(translation_line.is_some(),
"Should find translation line with file '{}' and line '{}' in section: {:?}",
file_path, line_num, translation_section_lines);
if let Some(trans_line) = translation_line {
let expected_format = format!("{}:{}", file_path, line_num);
prop_assert!(trans_line.contains(&expected_format),
"Translation line should contain '{}' but was: {}", expected_format, trans_line);
prop_assert!(trans_line.contains(&translation_key));
let context_format = format!("{}-", file_path);
prop_assert!(!trans_line.contains(&context_format),
"Translation line should not contain context indicator '{}' but was: {}", context_format, trans_line);
}
let code_section_lines: Vec<&str> = output
.lines()
.skip_while(|line| !line.contains("=== Code References ==="))
.collect();
let context_indicator = format!("{}-", file_path);
let has_context_lines = code_section_lines
.iter()
.any(|line| line.contains(&context_indicator));
let match_indicator = format!("{}:{}", file_path, line_num + 10);
let has_match_line = code_section_lines
.iter()
.any(|line| line.contains(&match_indicator));
prop_assert!(has_match_line, "Code reference should have match line");
if has_context_lines {
if !code_content.trim().is_empty() {
let context_lines: Vec<&str> = code_section_lines
.iter()
.filter(|line| line.contains(&context_indicator))
.cloned()
.collect();
for context_line in context_lines {
if code_content.len() > 2 {
prop_assert!(!context_line.contains(&code_content),
"Context lines should not contain the match content: {}", context_line);
}
}
}
}
}
}
proptest! {
#[test]
fn test_rg_compatibility(
search_text in "[a-zA-Z][a-zA-Z0-9]{1,10}",
file_content in prop::collection::vec("[a-zA-Z0-9 ]{0,50}", 3..10),
file_path in "[a-zA-Z][a-zA-Z0-9_]{3,15}\\.(ts|js|yml|json)"
) {
if search_text.trim().is_empty() {
return Ok(());
}
let temp_dir = tempfile::TempDir::new().unwrap();
let test_file_path = temp_dir.path().join(&file_path);
if let Some(parent) = test_file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let mut full_content = Vec::new();
let mut has_matches = false;
for (i, content_line) in file_content.iter().enumerate() {
let line_content = if i % 3 == 0 {
let embedded_content = format!("prefix {} suffix", search_text);
has_matches = true;
embedded_content
} else {
content_line.clone()
};
full_content.push(line_content);
}
if !has_matches {
return Ok(());
}
std::fs::write(&test_file_path, full_content.join("\n")).unwrap();
let cs_query = cs::SearchQuery::new(search_text.clone())
.with_base_dir(temp_dir.path().to_path_buf())
.with_case_sensitive(true)
.with_quiet(true);
let cs_result = cs::run_search(cs_query);
prop_assert!(cs_result.is_ok(), "cs search should succeed");
let cs_result = cs_result.unwrap();
let formatter = cs::TreeFormatter::new().with_simple_format(true);
let cs_output = formatter.format_result(&cs_result);
let cs_matches: Vec<(String, usize, String)> = cs_output
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(3, ':').collect();
if parts.len() == 3 {
if let Ok(line_num) = parts[1].parse::<usize>() {
Some((parts[0].to_string(), line_num, parts[2].to_string()))
} else {
None
}
} else {
None
}
})
.collect();
if !cs_matches.is_empty() {
for line in cs_output.lines() {
if !line.trim().is_empty() {
let parts: Vec<&str> = line.splitn(3, ':').collect();
prop_assert_eq!(parts.len(), 3,
"cs output should be parseable in rg-compatible format: {}", line);
prop_assert!(!parts[0].is_empty(), "File path should not be empty");
prop_assert!(parts[1].parse::<u32>().is_ok(),
"Line number should be numeric: {}", parts[1]);
prop_assert!(parts[2].contains(&search_text),
"Match content should contain search text: {} in {}", search_text, parts[2]);
}
}
let unique_files: std::collections::HashSet<_> = cs_matches.iter().map(|(f, _, _)| f).collect();
prop_assert_eq!(unique_files.len(), 1, "Should find matches in exactly one file");
let found_lines: std::collections::HashSet<_> = cs_matches.iter().map(|(_, l, _)| *l).collect();
prop_assert!(!found_lines.is_empty(), "Should find at least one match");
for (_, _line_num, content) in &cs_matches {
prop_assert!(content.contains(&search_text),
"Match content should contain search text: '{}' in '{}'",
search_text, content);
}
}
}
}