use roxmltree::{Document, Node};
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn extract_xml_from_output(output: &str) -> &str {
if let Some(start_index) = output.find("<?xml") {
&output[start_index..]
} else {
output
}
}
fn create_test_file(dir: &TempDir, filename: &str, content: &str) -> PathBuf {
let file_path = dir.path().join(filename);
let parent_dir = file_path.parent().unwrap();
fs::create_dir_all(parent_dir).expect("Failed to create parent directories");
let mut file = File::create(&file_path).expect("Failed to create test file");
file.write_all(content.as_bytes())
.expect("Failed to write test content");
file_path
}
fn create_test_directory_structure(root_dir: &TempDir) {
let src_dir = root_dir.path().join("src");
fs::create_dir(&src_dir).expect("Failed to create src directory");
let rust_content = r#"
fn hello_world() {
println!("Hello, world!");
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
"#;
create_test_file(root_dir, "src/functions.rs", rust_content);
let js_content = r#"
function greet(name) {
return `Hello, ${name}!`;
}
const multiply = (a, b) => a * b;
"#;
create_test_file(root_dir, "src/functions.js", js_content);
let special_chars_content = r#"
// This file contains special characters: "quotes", 'apostrophes', <tags>, &ersands
function escapeTest(input) {
return input.replace(/[<>&"']/g, function(c) {
return {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
"'": '''
}[c];
});
}
"#;
create_test_file(root_dir, "src/special_chars.js", special_chars_content);
let python_content = r#"
def calculate_sum(numbers):
"""Calculate the sum of a list of numbers."""
return sum(numbers)
def process_data(data, callback):
"""Process data using the provided callback function."""
return callback(data)
"#;
create_test_file(root_dir, "src/functions.py", python_content);
}
#[test]
fn test_query_xml_output_rust_functions() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_test_directory_structure(&temp_dir);
let output = Command::new("cargo")
.args([
"run",
"--",
"query",
"fn $NAME($$$PARAMS) $$$BODY", temp_dir.path().to_str().unwrap(),
"--language",
"rust",
"--format",
"xml",
])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let xml_str = extract_xml_from_output(&stdout);
let doc = Document::parse(xml_str).expect("Failed to parse XML output");
let root = doc.root_element();
assert_eq!(
root.tag_name().name(),
"probe_results",
"Root element should be 'probe_results'"
);
let matches: Vec<Node> = root
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "result")
.collect();
assert!(
!matches.is_empty(),
"Should have at least one match element"
);
assert_eq!(matches.len(), 2, "Should find 2 Rust functions");
let summary = root
.children()
.find(|n| n.is_element() && n.tag_name().name() == "summary");
assert!(summary.is_some(), "Should have a summary element");
if let Some(summary) = summary {
let count = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "count");
assert!(count.is_some(), "Summary should have a count element");
let total_bytes = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "total_bytes");
assert!(
total_bytes.is_some(),
"Summary should have a total_bytes element"
);
let total_tokens = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "total_tokens");
assert!(
total_tokens.is_some(),
"Summary should have a total_tokens element"
);
if let Some(count) = count {
let count_value = count.text().unwrap_or("0").parse::<usize>().unwrap_or(0);
assert_eq!(
count_value,
matches.len(),
"Count should match the number of matches"
);
}
}
for match_node in matches {
let file = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "file");
assert!(file.is_some(), "Each match should have a file element");
let lines = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "lines");
assert!(lines.is_some(), "Each result should have a lines element");
let node_type = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "node_type");
assert!(
node_type.is_some(),
"Each result should have a node_type element"
);
let column_start = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "column_start");
assert!(
column_start.is_some(),
"Each result should have a column_start element"
);
let column_end = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "column_end");
assert!(
column_end.is_some(),
"Each result should have a column_end element"
);
let code = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code");
assert!(code.is_some(), "Each result should have a code element");
if let Some(code) = code {
let code_content = code.text();
assert!(
code_content.is_some(),
"Code element should have text content"
);
assert!(
code_content.unwrap().starts_with("fn "),
"Function code should start with 'fn '"
);
}
}
}
#[test]
fn test_query_xml_output_javascript_functions() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_test_directory_structure(&temp_dir);
let output = Command::new("cargo")
.args([
"run",
"--",
"query",
"function $NAME($$$PARAMS) $$$BODY", temp_dir.path().to_str().unwrap(),
"--language",
"javascript",
"--format",
"xml",
])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let xml_str = extract_xml_from_output(&stdout);
let doc = Document::parse(xml_str).expect("Failed to parse XML output");
let root = doc.root_element();
let matches: Vec<Node> = root
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "result")
.collect();
assert!(
!matches.is_empty(),
"Should have at least one result element"
);
let has_greet = matches.iter().any(|m| {
if let Some(code) = m
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code")
{
if let Some(content) = code.text() {
return content.contains("greet");
}
}
false
});
assert!(has_greet, "Should find the 'greet' function");
}
#[test]
fn test_query_xml_output_with_special_characters() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_test_directory_structure(&temp_dir);
let output = Command::new("cargo")
.args([
"run",
"--",
"query",
"function escapeTest", temp_dir.path().to_str().unwrap(),
"--language",
"javascript",
"--format",
"xml",
])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let xml_str = extract_xml_from_output(&stdout);
let doc = Document::parse(xml_str).expect("Failed to parse XML output");
let root = doc.root_element();
let matches: Vec<Node> = root
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "result")
.collect();
assert!(
!matches.is_empty(),
"Should have at least one result element"
);
let escape_match = matches.iter().find(|&m| {
if let Some(code) = m
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code")
{
if let Some(content) = code.text() {
return content.contains("escapeTest");
}
}
false
});
assert!(
escape_match.is_some(),
"Should find the 'escapeTest' function"
);
if let Some(match_node) = escape_match {
if let Some(code) = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code")
{
if let Some(content) = code.text() {
assert!(content.contains("<"), "Should contain '<'");
assert!(content.contains(">"), "Should contain '>'");
assert!(content.contains("&"), "Should contain '&'");
assert!(content.contains("""), "Should contain '"'");
assert!(content.contains("'"), "Should contain '''");
} else {
panic!("Code element should have content");
}
} else {
panic!("Result should have a code element");
}
if let Some(file) = match_node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "file")
{
if let Some(text) = file.text() {
assert!(
text.contains("special_chars.js"),
"File path should be correct"
);
} else {
panic!("File element should have text content");
}
} else {
panic!("Result should have a file element");
}
}
}
#[test]
fn test_query_xml_output_with_multiple_languages() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_test_directory_structure(&temp_dir);
let output = Command::new("cargo")
.args([
"run",
"--",
"query",
"def $NAME($$$PARAMS): $$$BODY", temp_dir.path().to_str().unwrap(),
"--language",
"python",
"--format",
"xml",
])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let xml_str = extract_xml_from_output(&stdout);
let doc = Document::parse(xml_str).expect("Failed to parse XML output");
let root = doc.root_element();
let matches: Vec<Node> = root
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "result")
.collect();
assert!(
!matches.is_empty(),
"Should have at least one result element"
);
let has_calculate_sum = matches.iter().any(|m| {
if let Some(code) = m
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code")
{
if let Some(content) = code.text() {
return content.contains("calculate_sum");
}
}
false
});
let has_process_data = matches.iter().any(|m| {
if let Some(code) = m
.children()
.find(|n| n.is_element() && n.tag_name().name() == "code")
{
if let Some(content) = code.text() {
return content.contains("process_data");
}
}
false
});
assert!(
has_calculate_sum,
"Should find the 'calculate_sum' function"
);
assert!(has_process_data, "Should find the 'process_data' function");
}
#[test]
fn test_query_xml_output_with_no_results() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_test_directory_structure(&temp_dir);
let output = Command::new("cargo")
.args([
"run",
"--",
"query",
"class $NAME { $$$METHODS }", temp_dir.path().to_str().unwrap(),
"--format",
"xml",
])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let xml_str = extract_xml_from_output(&stdout);
let doc = Document::parse(xml_str).expect("Failed to parse XML output");
let root = doc.root_element();
assert_eq!(
root.tag_name().name(),
"probe_results",
"Root element should be 'probe_results'"
);
let matches: Vec<Node> = root
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "result")
.collect();
assert!(matches.is_empty(), "Should have no result elements");
let summary = root
.children()
.find(|n| n.is_element() && n.tag_name().name() == "summary");
assert!(summary.is_some(), "Should have a summary element");
if let Some(summary) = summary {
if let Some(count) = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "count")
{
let count_value = count.text().unwrap_or("0").parse::<usize>().unwrap_or(0);
assert_eq!(count_value, 0, "Count should be 0");
} else {
panic!("Summary should have a count element");
}
if let Some(total_bytes) = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "total_bytes")
{
let total_bytes_value = total_bytes
.text()
.unwrap_or("0")
.parse::<usize>()
.unwrap_or(0);
assert_eq!(total_bytes_value, 0, "Total bytes should be 0");
} else {
panic!("Summary should have a total_bytes element");
}
if let Some(total_tokens) = summary
.children()
.find(|n| n.is_element() && n.tag_name().name() == "total_tokens")
{
let total_tokens_value = total_tokens
.text()
.unwrap_or("0")
.parse::<usize>()
.unwrap_or(0);
assert_eq!(total_tokens_value, 0, "Total tokens should be 0");
} else {
panic!("Summary should have a total_tokens element");
}
}
}