use tempfile::TempDir;
use syntext::__internal::regex_decompose::decompose;
use syntext::__internal::{is_literal, literal_grams, route_query, GramQuery, QueryRoute};
use syntext::index::Index;
use syntext::Config;
#[test]
fn simplify_removes_all_from_and() {
let q = GramQuery::And(vec![
GramQuery::All,
GramQuery::Grams(vec![1, 2]),
GramQuery::All,
]);
match q.simplify() {
GramQuery::Grams(hashes) => assert_eq!(hashes, vec![1, 2]),
other => panic!("expected Grams, got {:?}", other),
}
}
#[test]
fn simplify_all_dominates_or() {
let q = GramQuery::Or(vec![
GramQuery::Grams(vec![1]),
GramQuery::All,
GramQuery::Grams(vec![2]),
]);
assert!(matches!(q.simplify(), GramQuery::All));
}
#[test]
fn simplify_empty_and_becomes_all() {
let q = GramQuery::And(vec![]);
assert!(matches!(q.simplify(), GramQuery::All));
}
#[test]
fn simplify_empty_or_becomes_none() {
let q = GramQuery::Or(vec![]);
assert!(matches!(q.simplify(), GramQuery::None));
}
#[test]
fn simplify_single_child_and_unwraps() {
let q = GramQuery::And(vec![GramQuery::Grams(vec![42])]);
match q.simplify() {
GramQuery::Grams(hashes) => assert_eq!(hashes, vec![42]),
other => panic!("expected Grams, got {:?}", other),
}
}
#[test]
fn simplify_single_child_or_unwraps() {
let q = GramQuery::Or(vec![GramQuery::Grams(vec![99])]);
match q.simplify() {
GramQuery::Grams(hashes) => assert_eq!(hashes, vec![99]),
other => panic!("expected Grams, got {:?}", other),
}
}
#[test]
fn simplify_and_of_all_becomes_all() {
let q = GramQuery::And(vec![GramQuery::All, GramQuery::All]);
assert!(matches!(q.simplify(), GramQuery::All));
}
#[test]
fn decompose_literal_falls_back_to_scan() {
let q = decompose("parse_query", false).unwrap().simplify();
assert!(matches!(q, GramQuery::All), "expected All, got {:?}", q);
}
#[test]
fn decompose_dot_star_between_literals() {
let q = decompose("foo.*bar", false).unwrap().simplify();
assert!(
matches!(q, GramQuery::All),
"foo.*bar should fall to full scan with forced boundaries, got {:?}",
q
);
}
#[test]
fn decompose_alternation() {
let q = decompose("foo|bar", false).unwrap().simplify();
assert!(matches!(q, GramQuery::All), "expected All, got {:?}", q);
}
#[test]
fn decompose_exact_literal_alternation_with_shared_prefix() {
let q = decompose("LanguageServer(Id|InstallationStatus)", false)
.unwrap()
.simplify();
assert!(
matches!(q, GramQuery::Or(_) | GramQuery::Grams(_)),
"expected indexed grams for exact literal alternation, got {:?}",
q
);
}
#[test]
fn decompose_required_repetition() {
let q = decompose("(parse_query)+", false).unwrap().simplify();
assert!(
matches!(q, GramQuery::All),
"(parse_query)+ should fall to full scan with forced boundaries, got {:?}",
q
);
}
#[test]
fn decompose_dot_star_alone_is_all() {
let q = decompose(".*", false).unwrap().simplify();
assert!(matches!(q, GramQuery::All), "expected All, got {:?}", q);
}
#[test]
fn optional_prefix_does_not_contribute_grams() {
let q = decompose("(foo)?bar", false).unwrap().simplify();
match &q {
GramQuery::Grams(_) => {
}
GramQuery::And(children) => {
panic!(
"optional prefix produced And node (may cause false negatives): {:?}",
children
);
}
GramQuery::All => {
}
other => panic!("unexpected query type for (foo)?bar: {:?}", other),
}
}
#[test]
fn is_literal_no_metacharacters() {
assert!(is_literal("parse_query"));
assert!(is_literal("hello world"));
assert!(is_literal("foo_bar_baz"));
assert!(is_literal("some::path::to::function"));
}
#[test]
fn is_literal_with_metacharacters() {
assert!(!is_literal("foo.*bar"));
assert!(!is_literal("foo?bar"));
assert!(!is_literal("[abc]"));
assert!(!is_literal("(foo)+"));
assert!(!is_literal("^start"));
assert!(!is_literal("end$"));
assert!(!is_literal("foo\\d"));
}
#[test]
fn route_literal_pattern() {
let route = route_query("parse_query", false).unwrap();
assert!(
matches!(route, QueryRoute::Literal),
"expected Literal route, got {:?}",
route
);
}
#[test]
fn route_regex_without_interior_grams_is_fullscan() {
let route = route_query("parse_query|process_batch", false).unwrap();
assert!(
matches!(route, QueryRoute::FullScan),
"expected FullScan for regex without interior grams, got {:?}",
route
);
}
#[test]
fn route_regex_with_extractable_grams_is_indexed() {
let route = route_query("(fn_parse_filter_query)+", false).unwrap();
assert!(
matches!(route, QueryRoute::IndexedRegex(_)),
"expected IndexedRegex for extractable regex grams, got {:?}",
route
);
}
#[test]
fn route_exact_literal_alternation_with_shared_prefix_is_indexed() {
let route = route_query("LanguageServer(Id|InstallationStatus)", false).unwrap();
assert!(
matches!(route, QueryRoute::IndexedRegex(_)),
"expected IndexedRegex for exact literal alternation, got {:?}",
route
);
}
#[test]
fn route_full_scan_for_dot_star() {
let route = route_query(".*", false).unwrap();
assert!(
matches!(route, QueryRoute::FullScan),
"expected FullScan for .*, got {:?}",
route
);
}
#[test]
fn route_case_insensitive_literal_not_literal_route() {
let route = route_query("_parse_query_", true).unwrap();
assert!(
matches!(route, QueryRoute::IndexedRegex(_)),
"case-insensitive anchored long literal should use IndexedRegex, got {:?}",
route
);
}
#[test]
fn literal_grams_returns_none_for_short_pattern() {
assert!(literal_grams("ab").is_none());
assert!(literal_grams("x").is_none());
}
#[test]
fn literal_grams_returns_some_for_long_pattern() {
assert!(literal_grams("parse_query").is_some());
}
#[test]
fn route_case_insensitive_short_literal_falls_back_to_full_scan() {
let route = route_query("ab", true).unwrap();
assert!(
matches!(route, QueryRoute::FullScan),
"short case-insensitive literal should fall back to FullScan, got {:?}",
route
);
}
#[test]
fn route_case_insensitive_optional_only_literal_is_full_scan() {
let route = route_query("parse", true).unwrap();
assert!(
matches!(route, QueryRoute::FullScan),
"case-insensitive 'parse' (optional-only grams) must route to FullScan, got {:?}",
route
);
}
#[test]
fn route_backslash_n_is_not_a_newline() {
assert!(
route_query(r"foo\\nbar", false).is_ok(),
"escaped-backslash-then-n must not be treated as a literal newline"
);
assert!(
route_query(r"C:\\new", false).is_ok(),
"regex-escaped `C:\\new` must route, not error"
);
assert!(
route_query("a\nb", false).is_err(),
"a raw newline byte must still be rejected"
);
assert!(
route_query(r"a\nb", false).is_err(),
"a `\\n` regex newline escape must still be rejected"
);
}
#[test]
fn eh0001_literal_parse_finds_subtoken_reparse() {
let repo = TempDir::new().unwrap();
let index_dir = TempDir::new().unwrap();
std::fs::write(repo.path().join("a.rs"), "fn reparse() {}\n").unwrap();
std::fs::write(repo.path().join("b.rs"), "fn parse() {}\n").unwrap();
let config = Config {
index_dir: index_dir.path().to_path_buf(),
repo_root: repo.path().to_path_buf(),
..Config::default()
};
let index = Index::build(config).unwrap();
let results = index.search("parse", &Default::default()).unwrap();
let mut paths: Vec<String> = results
.iter()
.map(|m| m.path.file_name().unwrap().to_string_lossy().to_string())
.collect();
paths.sort();
paths.dedup();
assert_eq!(
paths,
vec!["a.rs", "b.rs"],
"literal 'parse' must find both 'reparse' (a.rs) and 'parse' (b.rs)"
);
drop(index);
}
#[test]
fn eh0001_literal_parse_insertion_order_independent() {
for first in &["reparse", "parse"] {
let repo = TempDir::new().unwrap();
let index_dir = TempDir::new().unwrap();
let content_a = if *first == "reparse" {
"fn reparse() {}\nfn parse_it() {}\n"
} else {
"fn parse() {}\nfn reparsing() {}\n"
};
std::fs::write(repo.path().join("lib.rs"), content_a).unwrap();
let config = Config {
index_dir: index_dir.path().to_path_buf(),
repo_root: repo.path().to_path_buf(),
..Config::default()
};
let index = Index::build(config).unwrap();
let results = index.search("parse", &Default::default()).unwrap();
assert!(
!results.is_empty(),
"parse must produce results regardless of insertion order (tried {first} first)"
);
drop(index);
}
}
#[test]
fn eh0001_build_covering_classifies_parse_as_optional_only() {
let covering = syntext::__internal::build_covering(b"parse").unwrap();
assert!(
covering.required.is_empty(),
"parse has no interior boundaries: all grams must be optional"
);
assert_eq!(
covering.optional.len(),
1,
"parse must produce exactly one optional gram"
);
}
#[test]
fn eh0001_build_covering_classifies_parse_query_as_optional() {
let covering = syntext::__internal::build_covering(b"parse_query").unwrap();
assert!(
covering.required.is_empty(),
"parse_query has synthetic edges; grams must be optional"
);
assert_eq!(
covering.optional.len(),
2,
"parse_query must produce exactly two optional grams"
);
}
#[test]
fn eh0001_build_covering_classifies_anchored_parse_query_as_required() {
let covering = syntext::__internal::build_covering(b"_parse_query_").unwrap();
assert_eq!(
covering.required.len(),
2,
"anchored parse_query must produce required grams"
);
assert!(
covering.optional.is_empty(),
"anchored parse_query should not have optional grams"
);
}
#[test]
fn eh0001_case_insensitive_literal_finds_subtoken_reparse() {
let repo = TempDir::new().unwrap();
let index_dir = TempDir::new().unwrap();
std::fs::write(repo.path().join("a.rs"), "fn reparse() {}\n").unwrap();
std::fs::write(repo.path().join("b.rs"), "fn PARSE() {}\n").unwrap();
let config = Config {
index_dir: index_dir.path().to_path_buf(),
repo_root: repo.path().to_path_buf(),
..Config::default()
};
let index = Index::build(config).unwrap();
let mut opts = syntext::SearchOptions::default();
opts.case_insensitive = true;
let results = index.search("parse", &opts).unwrap();
let mut paths: Vec<String> = results
.iter()
.map(|m| m.path.file_name().unwrap().to_string_lossy().to_string())
.collect();
paths.sort();
paths.dedup();
assert_eq!(
paths,
vec!["a.rs", "b.rs"],
"case-insensitive 'parse' must find both reparse (a.rs) and PARSE (b.rs)"
);
drop(index);
}
#[test]
fn route_query_rejects_newlines() {
assert_eq!(
route_query("foo\nbar", false).unwrap_err(),
"literal \\n not allowed".to_string()
);
assert_eq!(
route_query("foo\\nbar", false).unwrap_err(),
"literal \\n not allowed".to_string()
);
assert_eq!(
route_query("foo\\x0abar", false).unwrap_err(),
"literal \\n not allowed".to_string()
);
assert_eq!(
route_query("foo\\u000abar", false).unwrap_err(),
"literal \\n not allowed".to_string()
);
assert_eq!(
route_query("foo\\u{a}bar", false).unwrap_err(),
"literal \\n not allowed".to_string()
);
}