use super::*;
use crate::searcher::{SearchExecutionCoverage, SearchTextExecutionOptions, SearchTextRowMode};
#[test]
fn literal_search_returns_sorted_deterministic_matches() -> FriggResult<()> {
let root_a = temp_workspace_root("literal-search-sort-a");
let root_b = temp_workspace_root("literal-search-sort-b");
prepare_workspace(
&root_a,
&[
("zeta.txt", "needle zeta\n"),
("alpha.txt", "needle alpha\nnext needle\n"),
],
)?;
prepare_workspace(&root_b, &[("beta.txt", "beta needle\n")])?;
let config = FriggConfig::from_workspace_roots(vec![root_b.clone(), root_a.clone()])?;
let searcher = TextSearcher::new(config);
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 100,
};
let first = searcher.search(query.clone())?;
let second = searcher.search(query)?;
assert_eq!(first, second);
assert_eq!(
first,
vec![
text_match("repo-001", "beta.txt", 1, 6, "beta needle"),
text_match("repo-002", "alpha.txt", 1, 1, "needle alpha"),
text_match("repo-002", "alpha.txt", 2, 6, "next needle"),
text_match("repo-002", "zeta.txt", 1, 1, "needle zeta"),
]
);
cleanup_workspace(&root_a);
cleanup_workspace(&root_b);
Ok(())
}
#[test]
fn literal_search_repository_filter_accepts_stable_repository_id_alias() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-stable-repository-filter");
prepare_workspace(
&root,
&[(
"src/main.rs",
"pub fn submissions_open() { println!(\"needle\"); }\n",
)],
)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let matches = searcher.search_literal_with_filters(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters {
repository_id: Some(crate::domain::model::RepositoryId::for_root(&root).0),
language: None,
include_hidden: true,
},
)?;
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].path, "src/main.rs");
assert_eq!(matches[0].repository_id, "repo-001");
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_walk_fallback_respects_gitignored_contract_artifacts() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-gitignored-contracts");
prepare_workspace(&root, &[("contracts/errors.md", "invalid_params\n")])?;
fs::write(root.join(".gitignore"), "contracts\n").map_err(FriggError::Io)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let matches = searcher.search_literal_with_filters(
SearchTextQuery {
query: "invalid_params".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert!(
matches
.iter()
.all(|entry| entry.path != "contracts/errors.md"),
"walk fallback should respect gitignored contract artifacts"
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_can_filter_hidden_paths_before_scan_limit() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-hidden-filter");
prepare_workspace(
&root,
&[
(".hidden/first.txt", "needle hidden\n"),
("src/visible.txt", "needle visible\n"),
],
)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let visible_only = searcher.search_literal_with_filters_diagnostics(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 1,
},
SearchFilters {
include_hidden: false,
..SearchFilters::default()
},
)?;
assert_eq!(visible_only.total_matches, 1);
assert_eq!(visible_only.matches.len(), 1);
assert_eq!(visible_only.matches[0].path, "src/visible.txt");
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_continues_after_invalid_utf8_line() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-invalid-utf8-line");
prepare_workspace(&root, &[("src/placeholder.txt", "placeholder\n")])?;
fs::write(
root.join("src/data.bin"),
b"before\ninvalid \xFF\xFE\nneedle after\n",
)
.map_err(FriggError::Io)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let matches = searcher.search_literal_with_filters(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].path, "src/data.bin");
assert_eq!(matches[0].line, 3);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_scrubs_generic_markdown_leading_comment_metadata() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-markdown-leading-comment");
prepare_workspace(
&root,
&[(
"docs/guide.md",
"<!-- hidden metadata secret-token -->\n# Guide\npublic content\n",
)],
)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let hidden = searcher.search_literal_with_filters(
SearchTextQuery {
query: "secret-token".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert!(
hidden.is_empty(),
"leading markdown comment metadata should not pollute literal search: {:?}",
hidden
);
let public = searcher.search_literal_with_filters(
SearchTextQuery {
query: "public".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert_eq!(public.len(), 1);
assert_eq!(public[0].path, "docs/guide.md");
assert_eq!(public[0].line, 3);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_walk_fallback_excludes_target_artifacts_without_gitignore() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-target-exclusion");
prepare_workspace(
&root,
&[
("src/main.rs", "needle\n"),
("target/debug/app", "needle\n"),
],
)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let matches = searcher.search_literal_with_filters(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert!(
matches
.iter()
.all(|entry| !entry.path.starts_with("target/")),
"walk fallback must not search target artifacts: {matches:?}"
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_walk_fallback_respects_root_ignore_file_for_auxiliary_trees() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-root-ignore");
prepare_workspace(
&root,
&[
("src/main.rs", "needle main\n"),
("auxiliary/embedded-repo/src/lib.rs", "needle auxiliary\n"),
],
)?;
fs::write(root.join(".ignore"), "auxiliary/\n").map_err(FriggError::Io)?;
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let matches = searcher.search_literal_with_filters(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
},
SearchFilters::default(),
)?;
assert_eq!(
matches,
vec![text_match("repo-001", "src/main.rs", 1, 1, "needle main")]
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_applies_path_regex_filter() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-path-filter");
prepare_workspace(
&root,
&[
("src/lib.rs", "needle here\n"),
("README.md", "needle docs\n"),
],
)?;
let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
config.lexical_runtime.backend = LexicalBackendMode::Native;
let searcher = TextSearcher::new(config);
let query =
SearchTextQuery {
query: "needle".to_owned(),
path_regex: Some(Regex::new(r"^src/.*\.rs$").map_err(|err| {
FriggError::InvalidInput(format!("invalid test path regex: {err}"))
})?),
limit: 100,
};
let matches = searcher.search(query)?;
assert_eq!(
matches,
vec![text_match("repo-001", "src/lib.rs", 1, 1, "needle here")]
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_applies_repository_filter_and_limit_after_sorting() -> FriggResult<()> {
let root_a = temp_workspace_root("literal-search-repo-filter-a");
let root_b = temp_workspace_root("literal-search-repo-filter-b");
prepare_workspace(&root_a, &[("a.txt", "needle a\nneedle aa\n")])?;
prepare_workspace(&root_b, &[("b.txt", "needle b\nneedle bb\n")])?;
let config = FriggConfig::from_workspace_roots(vec![root_a.clone(), root_b.clone()])?;
let searcher = TextSearcher::new(config);
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 1,
};
let matches = searcher.search_literal(query, Some("repo-002"))?;
assert_eq!(
matches,
vec![text_match("repo-002", "b.txt", 1, 1, "needle b")]
);
cleanup_workspace(&root_a);
cleanup_workspace(&root_b);
Ok(())
}
#[test]
fn literal_search_small_limit_matches_sorted_prefix_of_full_results() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-small-limit-prefix");
prepare_workspace(
&root,
&[
("z.txt", "needle zeta\n"),
("a.txt", "needle alpha\nneedle again\n"),
("nested/b.txt", "prefix needle\nneedle suffix\n"),
],
)?;
let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
config.lexical_runtime.backend = LexicalBackendMode::Native;
let searcher = TextSearcher::new(config);
let full_query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 100,
};
let limited_query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 3,
};
let full = searcher.search_literal_with_filters(full_query, SearchFilters::default())?;
let first_limited =
searcher.search_literal_with_filters(limited_query.clone(), SearchFilters::default())?;
let second_limited =
searcher.search_literal_with_filters(limited_query, SearchFilters::default())?;
assert_eq!(first_limited, second_limited);
assert_eq!(
first_limited,
full.into_iter().take(3).collect::<Vec<_>>(),
"limited search should match deterministic sorted prefix"
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn diagnostics_literal_search_reports_read_failures_deterministically() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-diagnostics-read-failure");
fs::create_dir_all(root.join("src")).map_err(FriggError::Io)?;
fs::create_dir_all(root.join(".git")).map_err(FriggError::Io)?;
fs::write(
root.join("src/good.rs"),
"pub fn hotspot() { let _ = \"needle_hotspot\"; }\n",
)
.map_err(FriggError::Io)?;
let unreadable_path = root.join("src/bad.rs");
fs::write(&unreadable_path, "pub fn hidden() {}\n").map_err(FriggError::Io)?;
#[cfg(unix)]
{
let mut permissions = fs::metadata(&unreadable_path)
.map_err(FriggError::Io)?
.permissions();
permissions.set_mode(0o000);
fs::set_permissions(&unreadable_path, permissions).map_err(FriggError::Io)?;
}
let config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
let searcher = TextSearcher::new(config);
let query = SearchTextQuery {
query: "needle_hotspot".to_owned(),
path_regex: None,
limit: 20,
};
let first = searcher
.search_literal_with_filters_diagnostics(query.clone(), SearchFilters::default())?;
let second =
searcher.search_literal_with_filters_diagnostics(query, SearchFilters::default())?;
assert_eq!(first.matches, second.matches);
assert_eq!(first.matches.len(), 1);
assert_eq!(first.matches[0].repository_id, "repo-001");
assert_eq!(first.matches[0].path, "src/good.rs");
assert_eq!(first.diagnostics.entries, second.diagnostics.entries);
assert_eq!(first.diagnostics.total_count(), 1);
assert_eq!(
first.diagnostics.count_by_kind(SearchDiagnosticKind::Read),
1
);
assert_eq!(
first.diagnostics.count_by_kind(SearchDiagnosticKind::Walk),
0
);
assert_eq!(first.diagnostics.entries[0].repository_id, "repo-001");
assert_eq!(
first.diagnostics.entries[0].path.as_deref(),
Some("src/bad.rs")
);
assert_eq!(
first.diagnostics.entries[0].kind,
SearchDiagnosticKind::Read
);
assert!(
!first.diagnostics.entries[0].message.is_empty(),
"diagnostic message should be populated for read failures"
);
#[cfg(unix)]
{
let mut permissions = fs::metadata(&unreadable_path)
.map_err(FriggError::Io)?
.permissions();
permissions.set_mode(0o644);
fs::set_permissions(&unreadable_path, permissions).map_err(FriggError::Io)?;
}
cleanup_workspace(&root);
Ok(())
}
#[test]
fn diagnostics_literal_search_reports_manifest_storage_errors() -> FriggResult<()> {
let root = temp_workspace_root("literal-search-diagnostics-manifest-storage-error");
prepare_workspace(
&root,
&[(
"src/lib.rs",
"pub fn fallback_walk_still_finds_needle() {}\n",
)],
)?;
let db_path = resolve_provenance_db_path(&root)?;
fs::create_dir_all(
db_path
.parent()
.expect("resolved provenance path should have a parent"),
)
.map_err(FriggError::Io)?;
let conn = rusqlite::Connection::open(&db_path).map_err(|err| {
FriggError::Internal(format!(
"failed to create incompatible search fixture db: {err}"
))
})?;
conn.execute_batch(
r#"
CREATE TABLE schema_version (
id INTEGER PRIMARY KEY CHECK (id = 1),
version INTEGER NOT NULL,
updated_at TEXT NOT NULL
);
INSERT INTO schema_version (id, version, updated_at)
VALUES (1, 0, CURRENT_TIMESTAMP);
"#,
)
.map_err(|err| {
FriggError::Internal(format!(
"failed to seed incompatible search fixture db: {err}"
))
})?;
drop(conn);
let searcher = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?);
let output = searcher.search_literal_with_filters_diagnostics(
SearchTextQuery {
query: "fallback_walk_still_finds_needle".to_owned(),
path_regex: None,
limit: 20,
},
SearchFilters::default(),
)?;
assert_eq!(output.matches.len(), 1);
assert_eq!(output.matches[0].path, "src/lib.rs");
assert_eq!(
output.diagnostics.count_by_kind(SearchDiagnosticKind::Read),
1
);
let diagnostic = output
.diagnostics
.entries
.iter()
.find(|entry| {
entry
.message
.contains("failed to read validated manifest snapshot from storage")
})
.expect("manifest storage error should be surfaced as a read diagnostic");
assert!(
diagnostic
.message
.contains("storage schema is incompatible"),
"diagnostic should preserve the actionable storage error, got {:?}",
diagnostic.message
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_reuses_validated_manifest_candidates_across_repeated_queries() -> FriggResult<()>
{
let root = temp_workspace_root("literal-search-manifest-cache-hit");
prepare_workspace(
&root,
&[("src/lib.rs", "pub fn cached() { let _ = \"needle\"; }\n")],
)?;
seed_manifest_snapshot(&root, "repo-001", "snapshot-001", &["src/lib.rs"])?;
let cache = Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let searcher = TextSearcher::with_validated_manifest_candidate_cache(
FriggConfig::from_workspace_roots(vec![root.clone()])?,
Arc::clone(&cache),
);
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
};
let first = searcher
.search_literal_with_filters_diagnostics(query.clone(), SearchFilters::default())?;
let second =
searcher.search_literal_with_filters_diagnostics(query, SearchFilters::default())?;
assert_eq!(first.matches, second.matches);
assert_eq!(first.matches.len(), 1);
let stats = cache
.read()
.expect("validated manifest candidate cache should not be poisoned")
.stats();
assert_eq!(stats.dirty_bypasses, 0);
assert!(
cache
.read()
.expect("validated manifest candidate cache should not be poisoned")
.has_entry_for_root(&root)
|| stats.misses >= 1,
"expected a Ready cache entry or at least one miss that attempted population"
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_dirty_validated_manifest_cache_falls_back_to_walk_for_new_files()
-> FriggResult<()> {
let root = temp_workspace_root("literal-search-manifest-cache-dirty");
prepare_workspace(&root, &[("src/lib.rs", "pub fn cached() {}\n")])?;
seed_manifest_snapshot(&root, "repo-001", "snapshot-001", &["src/lib.rs"])?;
let cache = Arc::new(RwLock::new(ValidatedManifestCandidateCache::default()));
let searcher = TextSearcher::with_validated_manifest_candidate_cache(
FriggConfig::from_workspace_roots(vec![root.clone()])?,
Arc::clone(&cache),
);
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 10,
};
let first = searcher
.search_literal_with_filters_diagnostics(query.clone(), SearchFilters::default())?;
assert_eq!(first.matches.len(), 0);
prepare_workspace(
&root,
&[("src/new.rs", "pub fn fresh() { let _ = \"needle\"; }\n")],
)?;
cache
.write()
.expect("validated manifest candidate cache should not be poisoned")
.mark_dirty_root(&root);
let second =
searcher.search_literal_with_filters_diagnostics(query, SearchFilters::default())?;
assert_eq!(second.matches.len(), 1);
assert_eq!(second.matches[0].path, "src/new.rs");
let stats = cache
.read()
.expect("validated manifest candidate cache should not be poisoned")
.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 0);
assert_eq!(stats.dirty_bypasses, 1);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn literal_search_low_limit_large_corpus_matches_sorted_prefix_deterministically() -> FriggResult<()>
{
const FILE_COUNT: usize = 96;
const LIMIT: usize = 5;
let root = temp_workspace_root("literal-search-large-corpus-low-limit");
fs::create_dir_all(root.join("src/nested")).map_err(FriggError::Io)?;
fs::create_dir_all(root.join(".git")).map_err(FriggError::Io)?;
for file_idx in 0..FILE_COUNT {
let relative = if file_idx % 2 == 0 {
format!("src/file_{file_idx:03}.rs")
} else {
format!("src/nested/file_{file_idx:03}.rs")
};
let mut lines = Vec::with_capacity(40);
lines.push(format!(
"// deterministic large-corpus fixture file={file_idx:03}"
));
for line_idx in 0..36 {
if line_idx % 4 == 0 {
lines.push(format!(
"let hotspot_{line_idx:03} = \"needle_hotspot {file_idx} {line_idx}\";"
));
} else {
lines.push(format!(
"let filler_{line_idx:03} = {};",
file_idx + line_idx
));
}
}
fs::write(root.join(relative), lines.join("\n")).map_err(FriggError::Io)?;
}
let config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
let searcher = TextSearcher::new(config);
let full_query = SearchTextQuery {
query: "needle_hotspot".to_owned(),
path_regex: None,
limit: 10_000,
};
let limited_query = SearchTextQuery {
query: "needle_hotspot".to_owned(),
path_regex: None,
limit: LIMIT,
};
let full = searcher.search_literal_with_filters(full_query, SearchFilters::default())?;
let first_limited =
searcher.search_literal_with_filters(limited_query.clone(), SearchFilters::default())?;
let second_limited =
searcher.search_literal_with_filters(limited_query, SearchFilters::default())?;
assert_eq!(first_limited.len(), LIMIT);
assert_eq!(first_limited, second_limited);
assert_eq!(
first_limited,
full.into_iter().take(LIMIT).collect::<Vec<_>>(),
"low-limit search should stay equal to deterministic sorted prefix on large corpus"
);
cleanup_workspace(&root);
Ok(())
}
#[test]
fn exact_execution_filters_before_scan_and_shapes_before_paging_across_backends() -> FriggResult<()>
{
let root = temp_workspace_root("exact-execution-eligibility-and-shaping");
prepare_workspace(
&root,
&[
("src/a.rs", "needle\r\nneedle\r\nneedle\r\n"),
("src/z.rs", "unicode needle caf\u{e9}"),
("src/skip.rs", "needle\nneedle\nneedle\nneedle\n"),
("other/out.txt", "needle"),
],
)?;
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: Some(Regex::new(r"^src/").expect("valid fixture regex")),
limit: 1,
};
let options = SearchTextExecutionOptions {
include_glob: Some(Regex::new(r"^src/[^/]+\.rs$").expect("valid fixture glob")),
exclude_glob: Some(Regex::new(r"^src/skip\.rs$").expect("valid fixture glob")),
row_mode: SearchTextRowMode::UniqueFile,
};
let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
native_config.lexical_runtime.backend = LexicalBackendMode::Native;
let native = TextSearcher::new(native_config)
.search_literal_with_execution_options_diagnostics(
query.clone(),
SearchFilters::default(),
options.clone(),
)?;
assert_eq!(native.total_matches, Some(4));
assert_eq!(native.total_rows, Some(2));
assert_eq!(native.matches.len(), 1);
assert_eq!(native.matches[0].path, "src/a.rs");
assert_eq!(native.coverage, SearchExecutionCoverage::Exact);
let capped = TextSearcher::new(FriggConfig::from_workspace_roots(vec![root.clone()])?)
.search_literal_with_execution_options_diagnostics(
query.clone(),
SearchFilters::default(),
SearchTextExecutionOptions {
row_mode: SearchTextRowMode::PerFileCapped {
max_count_per_file: 1,
},
..options.clone()
},
)?;
assert_eq!(capped.total_matches, Some(4));
assert_eq!(capped.total_rows, Some(2));
assert_eq!(capped.matches[0].path, "src/a.rs");
if std::process::Command::new("rg")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
{
clear_ripgrep_availability_cache();
let mut ripgrep_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
ripgrep_config.lexical_runtime.backend = LexicalBackendMode::Ripgrep;
let ripgrep = TextSearcher::new(ripgrep_config)
.search_literal_with_execution_options_diagnostics(
query,
SearchFilters::default(),
options,
)?;
assert_eq!(ripgrep.total_matches, native.total_matches);
assert_eq!(ripgrep.total_rows, native.total_rows);
assert_eq!(ripgrep.matches, native.matches);
assert_eq!(ripgrep.coverage, native.coverage);
}
cleanup_workspace(&root);
Ok(())
}
#[test]
fn exact_execution_routes_invalid_utf8_candidates_to_native_without_backend_divergence()
-> FriggResult<()> {
let root = temp_workspace_root("exact-execution-invalid-utf8");
prepare_workspace(&root, &[("src/valid.rs", "needle\n")])?;
fs::write(root.join("src/binary.rs"), b"needle\xff\n").map_err(FriggError::Io)?;
let query = SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 20,
};
let mut native_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
native_config.lexical_runtime.backend = LexicalBackendMode::Native;
let native = TextSearcher::new(native_config)
.search_literal_with_execution_options_diagnostics(
query.clone(),
SearchFilters::default(),
SearchTextExecutionOptions::default(),
)?;
assert_eq!(native.total_matches, Some(2));
assert_eq!(native.total_rows, Some(2));
assert_eq!(native.coverage, SearchExecutionCoverage::Exact);
if std::process::Command::new("rg")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
{
clear_ripgrep_availability_cache();
let mut ripgrep_config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
ripgrep_config.lexical_runtime.backend = LexicalBackendMode::Ripgrep;
let ripgrep = TextSearcher::new(ripgrep_config)
.search_literal_with_execution_options_diagnostics(
query,
SearchFilters::default(),
SearchTextExecutionOptions::default(),
)?;
assert_eq!(ripgrep.total_matches, native.total_matches);
assert_eq!(ripgrep.total_rows, native.total_rows);
assert_eq!(ripgrep.coverage, native.coverage);
}
cleanup_workspace(&root);
Ok(())
}
#[cfg(unix)]
#[test]
fn exact_execution_omits_totals_when_a_read_diagnostic_can_hide_matches() -> FriggResult<()> {
use std::os::unix::fs::PermissionsExt;
let root = temp_workspace_root("exact-execution-incomplete-read");
prepare_workspace(
&root,
&[
("src/good.rs", "needle visible\n"),
("src/unreadable.rs", "needle hidden\n"),
],
)?;
let unreadable = root.join("src/unreadable.rs");
let mut permissions = std::fs::metadata(&unreadable)
.map_err(FriggError::Io)?
.permissions();
permissions.set_mode(0o000);
std::fs::set_permissions(&unreadable, permissions).map_err(FriggError::Io)?;
let mut config = FriggConfig::from_workspace_roots(vec![root.clone()])?;
config.lexical_runtime.backend = LexicalBackendMode::Native;
let output = TextSearcher::new(config).search_literal_with_execution_options_diagnostics(
SearchTextQuery {
query: "needle".to_owned(),
path_regex: None,
limit: 20,
},
SearchFilters::default(),
SearchTextExecutionOptions::default(),
)?;
assert_eq!(output.coverage, SearchExecutionCoverage::Incomplete);
assert_eq!(output.total_matches, None);
assert_eq!(output.total_rows, None);
assert_eq!(output.matches.len(), 1);
assert_eq!(output.matches[0].path, "src/good.rs");
let mut restore = std::fs::metadata(&unreadable)
.map_err(FriggError::Io)?
.permissions();
restore.set_mode(0o644);
std::fs::set_permissions(&unreadable, restore).map_err(FriggError::Io)?;
cleanup_workspace(&root);
Ok(())
}