use super::*;
fn argv(parts: &[&str]) -> Vec<Vec<u8>> {
parts.iter().map(|p| p.as_bytes().to_vec()).collect()
}
#[test]
fn nothing_is_reserved_and_the_surface_is_complete() {
assert!(NOT_YET.is_empty(), "the last reserved clause has shipped");
for c in ["IN", "FILTER", "FACET", "SORT", "DISTINCT", "HIGHLIGHT"] {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", c, "x"]);
assert!(
!matches!(MatchArgs::parse_terminal(&a), MatchParse::NotYet(_)),
"{c} executes now"
);
}
}
#[test]
fn highlight_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hello"]);
assert!(matches!(MatchArgs::parse_terminal(&none),
MatchParse::Ok(q) if q.highlight.is_none()));
let all = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "HIGHLIGHT"]);
assert!(matches!(MatchArgs::parse_terminal(&all),
MatchParse::Ok(q) if q.highlight == Some(vec![])));
let some = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "HIGHLIGHT", "title", "body"]);
assert!(matches!(MatchArgs::parse_terminal(&some),
MatchParse::Ok(q) if q.highlight == Some(vec![b"title".to_vec(), b"body".to_vec()])));
let both = argv(&["IDX.QUERY", "idx", "MATCH", "hi", "FIELDS", "a", "HIGHLIGHT", "b"]);
match MatchArgs::parse_terminal(&both) {
MatchParse::Ok(q) => {
assert_eq!(q.fields, vec![b"a".to_vec()]);
assert_eq!(q.highlight, Some(vec![b"b".to_vec()]));
}
_ => panic!("FIELDS and HIGHLIGHT must coexist"),
}
}
#[test]
fn the_shipped_surface_still_parses() {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "LIMIT", "5", "FIELDS", "title"]);
match MatchArgs::parse_terminal(&a) {
MatchParse::Ok(q) => {
assert_eq!(q.text, b"hello");
assert_eq!(q.limit, 5);
assert_eq!(q.fields, vec![b"title".to_vec()]);
}
_ => panic!("the existing surface must keep working unchanged"),
}
}
#[test]
fn genuine_nonsense_is_still_bad_args() {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "WOBBLE"]);
assert!(matches!(MatchArgs::parse_terminal(&a), MatchParse::BadArgs));
}
#[test]
fn a_reserved_word_as_the_query_text_is_not_a_clause() {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "FILTER"]);
assert!(matches!(MatchArgs::parse_terminal(&a), MatchParse::Ok(_)));
}
#[test]
fn typo_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hello"]);
assert!(matches!(MatchArgs::parse_terminal(&none), MatchParse::Ok(q) if q.typo == 0));
for (v, want) in [("0", 0u32), ("1", 1), ("2", 2)] {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "TYPO", v]);
assert!(
matches!(MatchArgs::parse_terminal(&a), MatchParse::Ok(q) if q.typo == want),
"TYPO {v}"
);
}
for bad in ["AUTO", "3", "x"] {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "TYPO", bad]);
assert!(matches!(MatchArgs::parse_terminal(&a), MatchParse::BadArgs), "TYPO {bad}");
}
let both = argv(&["IDX.QUERY", "idx", "MATCH", "hi", "TYPO", "1", "FIELDS", "a"]);
match MatchArgs::parse_terminal(&both) {
MatchParse::Ok(q) => {
assert_eq!(q.typo, 1);
assert_eq!(q.fields, vec![b"a".to_vec()]);
}
_ => panic!("TYPO and FIELDS must coexist"),
}
}
#[test]
fn offset_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hello"]);
assert!(matches!(MatchArgs::parse_terminal(&none), MatchParse::Ok(q) if q.offset == 0));
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "LIMIT", "5", "OFFSET", "10"]);
match MatchArgs::parse_terminal(&a) {
MatchParse::Ok(q) => {
assert_eq!(q.limit, 5);
assert_eq!(q.offset, 10);
}
_ => panic!("LIMIT and OFFSET must coexist"),
}
let bad = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "OFFSET", "x"]);
assert!(matches!(MatchArgs::parse_terminal(&bad), MatchParse::BadArgs));
}
#[test]
fn scope_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hello"]);
assert!(matches!(MatchArgs::parse_terminal(&none), MatchParse::Ok(q) if q.scope.is_empty()));
let one = argv(&["IDX.QUERY", "idx", "MATCH", "hello", "IN", "title"]);
match MatchArgs::parse_terminal(&one) {
MatchParse::Ok(q) => assert_eq!(q.scope, vec![b"title".to_vec()]),
_ => panic!("IN <field> should parse"),
}
let many = argv(&[
"IDX.QUERY",
"idx",
"MATCH",
"hi",
"IN",
"title",
"body",
"LIMIT",
"3",
"TYPO",
"1",
]);
match MatchArgs::parse_terminal(&many) {
MatchParse::Ok(q) => {
assert_eq!(q.scope, vec![b"title".to_vec(), b"body".to_vec()]);
assert_eq!(q.limit, 3);
assert_eq!(q.typo, 1);
}
_ => panic!("IN must stop at the next keyword"),
}
let empty = argv(&["IDX.QUERY", "idx", "MATCH", "hi", "IN"]);
assert!(matches!(MatchArgs::parse_terminal(&empty), MatchParse::BadArgs));
}
#[test]
fn pass_two_reparses_every_clause() {
let argv2 = argv(&[
"MATCH.SCORE",
"idx",
"hello",
"LIMIT=7",
"<gstats>",
"FIELDS",
"a",
"HIGHLIGHT",
"TYPO",
"2",
"OFFSET",
"4",
"IN",
"title",
"body",
]);
let q = crate::cmd_index_query::parse_match_score(&argv2).expect("pass-2 argv parses");
assert_eq!(q.name, b"idx".to_vec());
assert_eq!(q.text, b"hello".to_vec());
assert_eq!(q.limit, 7);
assert_eq!(q.fields, vec![b"a".to_vec()]);
assert_eq!(q.highlight, Some(Vec::new()), "bare HIGHLIGHT = every field");
assert_eq!(q.typo, 2);
assert_eq!(q.offset, 4);
assert_eq!(q.scope, vec![b"title".to_vec(), b"body".to_vec()]);
}
#[test]
fn filter_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hello"]);
assert!(matches!(MatchArgs::parse_terminal(&none), MatchParse::Ok(q) if q.filters.is_empty()));
let one = argv(&["IDX.QUERY", "idx", "MATCH", "hi", "FILTER", "price", "RANGE", "10", "100"]);
match MatchArgs::parse_terminal(&one) {
MatchParse::Ok(q) => {
assert_eq!(q.filters.len(), 1);
assert_eq!(q.filters[0].field, b"price".to_vec());
assert_eq!(
q.filters[0].shape,
FilterShape::Range { min: b"10".to_vec(), max: b"100".to_vec() }
);
}
_ => panic!("FILTER … RANGE should parse"),
}
let many = argv(&[
"IDX.QUERY",
"idx",
"MATCH",
"hi",
"FILTER",
"price",
"RANGE",
"1",
"9",
"IN",
"title",
"FILTER",
"status",
"EQ",
"live",
"LIMIT",
"3",
]);
match MatchArgs::parse_terminal(&many) {
MatchParse::Ok(q) => {
assert_eq!(q.filters.len(), 2, "both predicates kept");
assert_eq!(q.filters[1].shape, FilterShape::Eq { value: b"live".to_vec() });
assert_eq!(q.scope, vec![b"title".to_vec()]);
assert_eq!(q.limit, 3);
}
_ => panic!("FILTER must compose with the other clauses"),
}
for bad in [
vec!["IDX.QUERY", "idx", "MATCH", "hi", "FILTER"],
vec!["IDX.QUERY", "idx", "MATCH", "hi", "FILTER", "price"],
vec!["IDX.QUERY", "idx", "MATCH", "hi", "FILTER", "price", "NEAR", "5"],
vec!["IDX.QUERY", "idx", "MATCH", "hi", "FILTER", "price", "RANGE", "10"],
] {
let a = argv(&bad);
assert!(matches!(MatchArgs::parse_terminal(&a), MatchParse::BadArgs), "{bad:?}");
}
}
#[test]
fn sort_parses() {
let none = argv(&["IDX.QUERY", "idx", "MATCH", "hi"]);
assert!(matches!(MatchArgs::parse_terminal(&none), MatchParse::Ok(q) if q.sort.is_none()));
for (dir, desc) in [("ASC", false), ("DESC", true), ("desc", true)] {
let a = argv(&["IDX.QUERY", "idx", "MATCH", "hi", "SORT", "price", dir]);
match MatchArgs::parse_terminal(&a) {
MatchParse::Ok(q) => assert_eq!(q.sort, Some((b"price".to_vec(), desc)), "{dir}"),
_ => panic!("SORT {dir} should parse"),
}
}
let full = argv(&[
"IDX.QUERY",
"idx",
"MATCH",
"hi",
"SORT",
"price",
"ASC",
"FILTER",
"price",
"RANGE",
"1",
"9",
"LIMIT",
"5",
]);
match MatchArgs::parse_terminal(&full) {
MatchParse::Ok(q) => {
assert_eq!(q.sort, Some((b"price".to_vec(), false)));
assert_eq!(q.filters.len(), 1);
assert_eq!(q.limit, 5);
}
_ => panic!("SORT must compose"),
}
for bad in [
vec!["IDX.QUERY", "idx", "MATCH", "hi", "SORT"],
vec!["IDX.QUERY", "idx", "MATCH", "hi", "SORT", "price"],
vec!["IDX.QUERY", "idx", "MATCH", "hi", "SORT", "price", "SIDEWAYS"],
] {
let a = argv(&bad);
assert!(matches!(MatchArgs::parse_terminal(&a), MatchParse::BadArgs), "{bad:?}");
}
}