use std::path::Path;
use crate::error::Result;
use crate::executor::{Executor, Match, QueryOptions, QueryStats};
use crate::planner::Planner;
use crate::reader::Reader;
pub fn execute(
reader: &Reader,
pattern: &str,
planner_opts: crate::planner::QueryOptions,
exec_opts: &QueryOptions,
delta_path: Option<&Path>,
) -> Result<(Vec<Match>, QueryStats)> {
let plan = Planner::plan_with_options(pattern, planner_opts)?;
let mut executor = Executor::new(reader);
if let Some(dp) = delta_path {
executor.set_delta_path(dp.to_path_buf());
}
executor.execute(&plan, exec_opts)
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use crate::builder::Builder;
use crate::executor;
use crate::planner;
use crate::reader::Reader;
fn build_index(root: &std::path::Path, filename: &str, content: &str) -> Reader {
fs::write(root.join(filename), content).unwrap();
let mut builder = Builder::new(root).unwrap();
builder.build().unwrap();
let index_path = root.join(".ix").join("shard.ix");
Reader::open(&index_path).unwrap()
}
#[test]
fn execute_literal_search() {
let dir = tempdir().unwrap();
let root = dir.path();
let reader = build_index(root, "hello.txt", "hello world\nneedle in haystack\n");
let (matches, stats) = super::execute(
&reader,
"needle",
planner::QueryOptions::default(),
&executor::QueryOptions::default(),
None,
)
.unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(stats.total_matches, 1);
assert!(stats.files_verified >= 1);
assert!(matches[0].line_content.contains("needle"));
assert!(
matches[0]
.file_path
.to_string_lossy()
.ends_with("hello.txt")
);
}
#[test]
fn execute_regex_search() {
let dir = tempdir().unwrap();
let root = dir.path();
let reader = build_index(root, "data.txt", "foo\nbar\nbaz\n");
let (matches, _) = super::execute(
&reader,
r"ba[rz]",
planner::QueryOptions {
is_regex: true,
..Default::default()
},
&executor::QueryOptions::default(),
None,
)
.unwrap();
assert_eq!(
matches.len(),
2,
"regex ba[rz] should match 'bar' and 'baz'"
);
let lines: Vec<&str> = matches.iter().map(|m| m.line_content.trim()).collect();
assert!(lines.contains(&"bar"));
assert!(lines.contains(&"baz"));
}
#[test]
fn execute_ignore_case() {
let dir = tempdir().unwrap();
let root = dir.path();
let reader = build_index(root, "case.txt", "UPPERCASE\nlowercase\nMixedCase\n");
let (matches, _) = super::execute(
&reader,
"mixedcase",
planner::QueryOptions {
ignore_case: true,
..Default::default()
},
&executor::QueryOptions::default(),
None,
)
.unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].line_content.trim(), "MixedCase");
}
#[test]
fn execute_no_matches() {
let dir = tempdir().unwrap();
let root = dir.path();
let reader = build_index(root, "only.txt", "only this content exists\n");
let (matches, stats) = super::execute(
&reader,
"nonexistent",
planner::QueryOptions::default(),
&executor::QueryOptions::default(),
None,
)
.unwrap();
assert!(matches.is_empty());
assert_eq!(stats.total_matches, 0);
}
#[test]
fn execute_with_context_lines() {
let dir = tempdir().unwrap();
let root = dir.path();
let reader = build_index(root, "ctx.txt", "before1\nbefore2\nHIT\nafter1\nafter2\n");
let (matches, _) = super::execute(
&reader,
"HIT",
planner::QueryOptions::default(),
&executor::QueryOptions {
context_lines: 2,
..Default::default()
},
None,
)
.unwrap();
assert_eq!(matches.len(), 1);
let m = &matches[0];
assert_eq!(m.line_content.trim(), "HIT");
assert_eq!(
m.context_before,
vec!["before1".to_string(), "before2".to_string()]
);
assert!(!m.context_after.is_empty());
assert_eq!(m.context_after[0], "after1");
}
#[test]
fn execute_max_results() {
let dir = tempdir().unwrap();
let root = dir.path();
let content = "match\nmatch\nmatch\nmatch\nmatch\n";
let reader = build_index(root, "caps.txt", content);
let (matches, stats) = super::execute(
&reader,
"match",
planner::QueryOptions::default(),
&executor::QueryOptions {
max_results: 3,
..Default::default()
},
None,
)
.unwrap();
assert_eq!(matches.len(), 3, "should cap at 3 results");
assert_eq!(stats.total_matches, 3);
}
}