mod support;
use predicates::prelude::*;
use support::command;
fn explain_scores(args: &[&str], stdin: &str) -> Vec<(String, i64)> {
let output = command()
.args(["--ignore-case", "--lang", "plain", "--explain"])
.args(args)
.write_stdin(stdin.to_string())
.output()
.expect("yuru ran");
assert!(output.status.success(), "yuru {args:?} failed");
let stdout = String::from_utf8(output.stdout).expect("UTF-8 output");
let mut ranked: Vec<(String, i64)> = Vec::new();
for line in stdout.lines() {
match line.strip_prefix(" score: ") {
Some(score) => {
ranked.last_mut().expect("a record before its score").1 =
score.parse().expect("an integer score");
}
None if !line.starts_with(" ") => ranked.push((line.to_string(), 0)),
None => {}
}
}
ranked
}
#[test]
fn cli_ignore_case_matches_mixed_case_candidate() {
command()
.args(["--filter", "abc", "--ignore-case"])
.write_stdin("ABC\n")
.assert()
.success()
.stdout(predicate::eq("ABC\n"));
}
#[test]
fn cli_ignore_case_survives_literal() {
command()
.args(["--filter", "abc", "--ignore-case", "--literal"])
.write_stdin("ABC\n")
.assert()
.success()
.stdout(predicate::eq("ABC\n"));
}
#[test]
fn cli_ignore_case_survives_literal_for_uppercase_query() {
command()
.args(["--filter", "ABC", "--ignore-case", "--literal"])
.write_stdin("abc\n")
.assert()
.success()
.stdout(predicate::eq("abc\n"));
}
#[test]
fn cli_no_ignore_case_stays_case_sensitive() {
command()
.args(["--filter", "abc", "--no-ignore-case"])
.write_stdin("ABC\nabc\n")
.assert()
.success()
.stdout(predicate::eq("abc\n"));
}
#[test]
fn cli_no_ignore_case_stays_case_sensitive_with_literal() {
command()
.args(["--filter", "abc", "--no-ignore-case", "--literal"])
.write_stdin("ABC\nabc\n")
.assert()
.success()
.stdout(predicate::eq("abc\n"));
}
#[test]
fn cli_smart_case_treats_uppercase_query_as_case_sensitive() {
command()
.args(["--filter", "ABC"])
.write_stdin("ABC\nabc\n")
.assert()
.success()
.stdout(predicate::eq("ABC\n"));
}
#[test]
fn cli_smart_case_treats_lowercase_query_as_case_insensitive() {
command()
.args(["--filter", "abc"])
.write_stdin("ABC\nxyz\n")
.assert()
.success()
.stdout(predicate::eq("ABC\n"));
}
#[test]
fn cli_exact_mode_ignores_case_by_default() {
command()
.args(["--filter", "abc", "--exact"])
.write_stdin("xxABCxx\nnope\n")
.assert()
.success()
.stdout(predicate::eq("xxABCxx\n"));
}
#[test]
fn cli_exact_mode_ignores_case_with_literal() {
command()
.args(["--filter", "abc", "--exact", "--ignore-case", "--literal"])
.write_stdin("xxABCxx\nnope\n")
.assert()
.success()
.stdout(predicate::eq("xxABCxx\n"));
}
#[test]
fn cli_exact_mode_honours_no_ignore_case() {
command()
.args(["--filter", "abc", "--exact", "--no-ignore-case"])
.write_stdin("xxABCxx\nxxabcxx\n")
.assert()
.success()
.stdout(predicate::eq("xxabcxx\n"));
}
#[test]
fn cli_extended_terms_ignore_case_with_literal() {
command()
.args(["--filter", "'abc !xyz", "--ignore-case", "--literal"])
.write_stdin("xxABCxx\nABC-XYZ\n")
.assert()
.success()
.stdout(predicate::eq("xxABCxx\n"));
}
#[test]
fn cli_ignore_case_survives_literal_for_non_ascii_text() {
command()
.args(["--filter", "éa", "--ignore-case", "--literal"])
.write_stdin("ÉA.txt\n")
.assert()
.success()
.stdout(predicate::eq("ÉA.txt\n"));
}
#[test]
fn cli_extended_exact_term_ranks_the_literal_spelling_first() {
for stdin in ["FOO\nfoo\n", "foo\nFOO\n"] {
command()
.args(["--filter", "'foo", "--ignore-case"])
.write_stdin(stdin)
.assert()
.success()
.stdout(predicate::eq("foo\nFOO\n"));
}
}
#[test]
fn cli_extended_exact_term_orders_case_variants_like_the_global_exact_path() {
for stdin in ["README.md\nreadme.md\n", "readme.md\nREADME.md\n"] {
for args in [
["--filter", "'readme"].as_slice(),
["--filter", "readme", "--exact"].as_slice(),
] {
command()
.args(args)
.args(["--ignore-case", "--tiebreak=index", "--limit", "1"])
.write_stdin(stdin)
.assert()
.success()
.stdout(predicate::eq("readme.md\n"));
}
}
}
#[test]
fn cli_exact_ignore_case_does_not_match_across_a_combining_lowercase_tail() {
command()
.args([
"--filter",
"ia",
"--ignore-case",
"--exact",
"--lang",
"plain",
])
.write_stdin("İa\n")
.assert()
.code(1)
.stdout(predicate::eq(""));
}
#[test]
fn cli_exact_ignore_case_does_not_match_across_a_combining_lowercase_tail_with_literal() {
command()
.args([
"--filter",
"ia",
"--ignore-case",
"--exact",
"--literal",
"--lang",
"plain",
])
.write_stdin("İa\n")
.assert()
.code(1)
.stdout(predicate::eq(""));
}
#[test]
fn cli_ignore_case_survives_literal_for_a_multi_char_lowercase_mapping() {
for (query, stdin) in [
("i\u{307}stanbul", "İstanbul.txt\n"),
("İstanbul", "i\u{307}stanbul.txt\n"),
] {
for mode in [&["--exact"][..], &[]] {
command()
.args(["--filter", query, "--ignore-case", "--literal"])
.args(mode)
.args(["--lang", "plain"])
.write_stdin(stdin)
.assert()
.success()
.stdout(predicate::eq(stdin));
}
}
}
#[test]
fn cli_ignore_case_ranks_the_spelling_the_query_was_typed_with_first() {
let stdin = "i\u{307}\nİ\n";
for (query, expected, literal) in [
("İ", "İ", &["--literal"][..]),
("i\u{307}", "i\u{307}", &["--literal"][..]),
("i\u{307}", "i\u{307}", &[][..]),
] {
for (filter, algo, decided_by_score) in [
(query.to_string(), &[][..], true),
(query.to_string(), &["--exact"][..], true),
(query.to_string(), &["--algo", "nucleo"][..], true),
(format!("'{query}"), &["--extended"][..], query != "İ"),
] {
let mut args = vec!["--filter", filter.as_str()];
args.extend(literal);
args.extend(algo);
let ranked = explain_scores(&args, stdin);
let context = format!("{filter:?} {literal:?} {algo:?}");
assert_eq!(
ranked.first().map(|(text, _)| text.as_str()),
Some(expected)
);
if let Some((runner_up, score)) = ranked.get(1) {
if decided_by_score {
assert!(
ranked[0].1 > *score,
"{context}: {expected:?} must outscore {runner_up:?}, got {} vs {score}",
ranked[0].1
);
} else {
assert_eq!(ranked[0].1, *score, "{context}: expected a scoring tie");
}
}
}
}
}
#[test]
fn cli_ignore_case_exact_term_bonus_ignores_folding_elsewhere_in_the_candidate() {
let ranked = explain_scores(&["--filter", "'a", "--literal"], "İ a\ni\u{307} a\nİ A\n");
let score = |text: &str| {
ranked
.iter()
.find(|(display, _)| display == text)
.unwrap_or_else(|| panic!("{text:?} matched"))
.1
};
assert_eq!(
score("İ a"),
score("i\u{307} a"),
"the two spellings fold to the same text and the term matches neither of them"
);
assert!(
score("İ a") > score("İ A"),
"and both collect the bonus rather than both losing it"
);
let controls = explain_scores(&["--filter", "'a", "--literal"], "X a\n\u{212a} a\n");
assert_eq!(controls[0].1, controls[1].1, "KELVIN SIGN folds 1:1");
}