use std::ffi::OsString;
use std::io::{self, Read, Seek};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use arc_swap::ArcSwapOption;
use crate::tools::path::shell_quote;
use crate::util::UnwrapPoison;
use super::super::ShellPlatform;
use super::{
EngineSpec, MatchMode, Output, OutputSink, ParsedGrep, PipelineCtx, StdoutDest, build_matcher,
expand_glob, has_unquoted_glob, serve_into, serve_one_grep,
};
const BATTERY_SPAWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const BATTERY_STDOUT_CAP: u64 = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Need {
Plain,
General,
Glob,
}
impl Need {
const fn capability(self) -> &'static str {
match self {
Need::Plain => "literal matching",
Need::General => "pattern matching",
Need::Glob => "glob expansion",
}
}
}
#[must_use]
pub(super) fn needs(parsed: &ParsedGrep, platform: ShellPlatform) -> Vec<Need> {
let mut needs = Vec::with_capacity(2);
needs.push(if is_byte_literal(parsed) {
Need::Plain
} else {
Need::General
});
if has_glob_surface(parsed, platform) {
needs.push(Need::Glob);
}
needs
}
fn is_byte_literal(parsed: &ParsedGrep) -> bool {
if parsed.flags.i || parsed.flags.w {
return false;
}
parsed.mode == MatchMode::Fixed
|| parsed.patterns.iter().all(|pattern| {
!pattern.contains([
'.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
])
})
}
fn has_glob_surface(parsed: &ParsedGrep, platform: ShellPlatform) -> bool {
!parsed.filters.is_empty()
|| !parsed.exclude_dir.is_empty()
|| parsed
.operand_tokens
.iter()
.any(|tok| has_unquoted_glob(tok, platform))
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct Established {
plain: bool,
general: bool,
glob: bool,
}
impl Established {
const fn all() -> Self {
Established {
plain: true,
general: true,
glob: true,
}
}
const fn covers(self, need: Need) -> bool {
match need {
Need::Plain => self.plain,
Need::General => self.general,
Need::Glob => self.glob,
}
}
}
static ESTABLISHED: ArcSwapOption<Established> = ArcSwapOption::const_empty();
#[cfg(test)]
thread_local! {
static TEST_VERDICT: std::cell::Cell<Option<Established>> =
const { std::cell::Cell::new(None) };
}
fn established() -> Established {
#[cfg(test)]
if let Some(verdict) = TEST_VERDICT.with(std::cell::Cell::get) {
return verdict;
}
ESTABLISHED.load().as_deref().copied().unwrap_or_default()
}
fn publish(verdict: Established) {
ESTABLISHED.store(Some(Arc::new(verdict)));
}
thread_local! {
static MEASURING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
struct Measuring;
impl Measuring {
fn enter() -> Self {
MEASURING.with(|flag| flag.set(true));
Measuring
}
}
impl Drop for Measuring {
fn drop(&mut self) {
MEASURING.with(|flag| flag.set(false));
}
}
static LAST_MEASURED: Mutex<Option<EnvKey>> = Mutex::new(None);
#[derive(PartialEq, Eq)]
struct EnvKey(Vec<(OsString, OsString)>);
impl EnvKey {
fn new(env: &[(OsString, OsString)]) -> Self {
let mut pairs = env.to_vec();
pairs.sort();
EnvKey(pairs)
}
}
pub(super) fn serve_allowed(needs: &[Need], platform: ShellPlatform) -> Result<(), &'static str> {
if platform == ShellPlatform::Windows {
return Ok(());
}
if MEASURING.with(std::cell::Cell::get) {
return Ok(());
}
let verdict = established();
needs
.iter()
.find(|need| !verdict.covers(**need))
.map_or(Ok(()), |need| Err(need.capability()))
}
pub(crate) fn refresh_if_environment_changed(env: &[(OsString, OsString)]) -> bool {
if cfg!(windows) {
return false;
}
if crate::shutdown::aborting() {
return ESTABLISHED.load().is_some();
}
let key = EnvKey::new(env);
if LAST_MEASURED.lock().unwrap_poison().as_ref() == Some(&key) {
return true;
}
ESTABLISHED.store(None);
record(key, measure(env))
}
pub(crate) fn invalidate_verdict_for(env: &[(OsString, OsString)]) {
if cfg!(windows) {
return;
}
let key = EnvKey::new(env);
let mut last = LAST_MEASURED.lock().unwrap_poison();
if last.as_ref() == Some(&key) {
return;
}
ESTABLISHED.store(None);
*last = None;
}
fn record(key: EnvKey, verdict: Option<Established>) -> bool {
let mut last = LAST_MEASURED.lock().unwrap_poison();
if let Some(verdict) = verdict {
publish(verdict);
*last = Some(key);
true
} else {
ESTABLISHED.store(None);
*last = None;
false
}
}
#[cfg(test)]
pub(super) fn publish_for_test(verdict: (bool, bool, bool)) {
TEST_VERDICT.with(|cell| {
cell.set(Some(Established {
plain: verdict.0,
general: verdict.1,
glob: verdict.2,
}));
});
}
#[cfg(feature = "grep-engine-e2e")]
pub(super) fn publish_all_for_harness() {
publish(Established::all());
}
#[derive(Default)]
struct Battery {
verdict: Option<Established>,
rows: usize,
compared: usize,
skipped: usize,
unmeasured: usize,
real_search: Probe,
}
#[derive(Default)]
struct Group {
compared: usize,
failed: bool,
}
impl Battery {
fn fold(&mut self, plain: &Group, general: &Group, glob: &Group) {
if self.unmeasured > 0 {
return;
}
self.verdict = Some(Established {
plain: plain.established(),
general: general.established(),
glob: glob.established(),
});
}
}
#[derive(Clone, Copy)]
enum RowOutcome {
Compared(bool),
Skipped,
Unmeasured,
}
impl Group {
fn record(&mut self, battery: &mut Battery, outcome: RowOutcome) {
battery.rows += 1;
match outcome {
RowOutcome::Compared(agreed) => {
self.compared += 1;
battery.compared += 1;
self.failed |= !agreed;
}
RowOutcome::Unmeasured => battery.unmeasured += 1,
RowOutcome::Skipped => battery.skipped += 1,
}
}
const fn established(&self) -> bool {
self.compared > 0 && !self.failed
}
}
fn measure(env: &[(OsString, OsString)]) -> Option<Established> {
let started = Instant::now();
let Some(fixture) = Fixture::create() else {
tracing::warn!("grep engine locale parity: fixture unavailable; fail-closed");
return None;
};
let ran = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_battery(&fixture, env)));
let Ok(battery) = ran else {
tracing::warn!("grep engine locale parity: battery panicked; fail-closed");
return None;
};
let verdict = battery.verdict;
tracing::info!(
established = verdict.is_some(),
plain = verdict.is_some_and(|v| v.plain),
general = verdict.is_some_and(|v| v.general),
glob = verdict.is_some_and(|v| v.glob),
rows = battery.rows,
compared = battery.compared,
skipped = battery.skipped,
unmeasured = battery.unmeasured,
real_search = ?battery.real_search,
elapsed_ms = started.elapsed().as_millis(),
"grep engine locale parity measured"
);
verdict
}
fn run_battery(fixture: &Fixture, env: &[(OsString, OsString)]) -> Battery {
let mut battery = Battery {
real_search: real_search_exists(fixture, env),
..Default::default()
};
match battery.real_search {
Probe::Absent => {
battery.verdict = Some(Established::all());
return battery;
}
Probe::Unknown => return battery,
Probe::Present => {}
}
let mut plain = Group::default();
for row in plain_rows(fixture) {
if crate::shutdown::aborting() {
return battery;
}
plain.record(&mut battery, compare_grep_row(fixture, env, &row));
}
let mut general = Group::default();
for row in general_rows(fixture) {
if crate::shutdown::aborting() {
return battery;
}
general.record(&mut battery, compare_grep_row(fixture, env, &row));
}
let mut glob = Group::default();
for pattern in GLOB_PATTERNS {
if crate::shutdown::aborting() {
return battery;
}
glob.record(&mut battery, compare_glob_row(fixture, env, pattern));
}
if crate::shutdown::aborting() {
return battery;
}
glob.record(
&mut battery,
compare_grep_row(fixture, env, &filter_row(fixture)),
);
battery.fold(&plain, &general, &glob);
battery
}
struct Row {
argv: Vec<String>,
sorted: bool,
}
impl Row {
fn new(argv: &[&str], sorted: bool) -> Self {
Row {
argv: argv.iter().map(|word| (*word).to_string()).collect(),
sorted,
}
}
}
fn plain_rows(fixture: &Fixture) -> Vec<Row> {
let a = fixture.abs("a.txt");
let b = fixture.abs("b.txt");
let uni = fixture.abs("uni.txt");
let bad = fixture.abs("bad.txt");
let bin = fixture.abs("bin.dat");
let dir = fixture.abs("dir");
vec![
Row::new(&["grep", "needle", a.as_str()], false),
Row::new(&["grep", "needle", b.as_str()], false),
Row::new(&["grep", "needle", uni.as_str()], false),
Row::new(&["grep", "needle", bad.as_str()], false),
Row::new(&["grep", "needle", bin.as_str()], false),
Row::new(&["grep", "-n", "needle", a.as_str()], false),
Row::new(&["grep", "-c", "needle", a.as_str()], false),
Row::new(&["grep", "-l", "needle", a.as_str(), b.as_str()], false),
Row::new(&["grep", "-v", "needle", a.as_str()], false),
Row::new(&["grep", "-x", "needle", a.as_str()], false),
Row::new(&["grep", "-o", "needle", a.as_str()], false),
Row::new(&["grep", "-m1", "needle", a.as_str(), b.as_str()], false),
Row::new(&["grep", "-h", "needle", a.as_str(), b.as_str()], false),
Row::new(&["grep", "-H", "needle", a.as_str()], false),
Row::new(&["grep", "-F", "a.b", a.as_str(), b.as_str()], false),
Row::new(&["grep", "-e", "needle", "-e", "alpha", a.as_str()], false),
Row::new(&["grep", "needle", a.as_str(), b.as_str()], false),
Row::new(&["grep", "-r", "needle", dir.as_str()], true),
]
}
fn general_rows(fixture: &Fixture) -> Vec<Row> {
let a = fixture.abs("a.txt");
let uni = fixture.abs("uni.txt");
let bad = fixture.abs("bad.txt");
let bin = fixture.abs("bin.dat");
vec![
Row::new(&["grep", "-i", "k", uni.as_str()], false),
Row::new(&["grep", "-i", "i", uni.as_str()], false),
Row::new(&["grep", "-i", "s", uni.as_str()], false),
Row::new(&["grep", "-i", "ß", uni.as_str()], false),
Row::new(&["grep", "-i", "µ", uni.as_str()], false),
Row::new(&["grep", "-i", "σ", uni.as_str()], false),
Row::new(&["grep", "[[:alpha:]]", uni.as_str()], false),
Row::new(&["grep", "[[:lower:]]", uni.as_str()], false),
Row::new(&["grep", "[[:upper:]]", uni.as_str()], false),
Row::new(&["grep", "[[:digit:]]", uni.as_str()], false),
Row::new(&["grep", "a.c", uni.as_str()], false),
Row::new(&["grep", "[à]", uni.as_str()], false),
Row::new(&["grep", "-w", "needle", a.as_str()], false),
Row::new(&["grep", "-i", "needle", bad.as_str()], false),
Row::new(&["grep", "-i", "needle", bin.as_str()], false),
]
}
const GLOB_PATTERNS: &[&str] = &["*.txt", "?.txt", "[à]*.txt", "[a-z]*.txt", "[!a]*.txt"];
fn filter_row(fixture: &Fixture) -> Row {
Row::new(
&[
"grep",
"-rn",
"--include=*.txt",
"needle",
fixture.abs("dir").as_str(),
],
true,
)
}
fn compare_grep_row(fixture: &Fixture, env: &[(OsString, OsString)], row: &Row) -> RowOutcome {
let Some(spec) = spec_for(&segment_text(&row.argv), fixture, ShellPlatform::Unix) else {
return RowOutcome::Skipped;
};
let Some((engine_out, engine_code)) = engine_run(&spec) else {
return RowOutcome::Skipped;
};
let Child::Ran(real_out, real_code) = real_search(&row.argv, env, &fixture.root) else {
return RowOutcome::Unmeasured;
};
RowOutcome::Compared(equivalent(
&engine_out,
engine_code,
&real_out,
real_code,
row.sorted,
))
}
fn spec_for(segment: &str, fixture: &Fixture, platform: ShellPlatform) -> Option<EngineSpec> {
let _measuring = Measuring::enter();
serve_one_grep(
segment,
"grep",
&fixture.root,
&fixture.home,
platform,
true,
PipelineCtx {
piped: false,
stdin_fed: false,
marker_ok: true,
env_changed: false,
},
)
.ok()
.map(|(spec, _redirects)| spec)
}
fn compare_glob_row(fixture: &Fixture, env: &[(OsString, OsString)], pattern: &str) -> RowOutcome {
let Some(engine) = expand_glob(pattern, &fixture.names, ShellPlatform::Unix).ok() else {
return RowOutcome::Skipped;
};
let Some(shell) = shell_glob(pattern, fixture, env) else {
return RowOutcome::Unmeasured;
};
RowOutcome::Compared(engine == shell)
}
fn shell_glob(
pattern: &str,
fixture: &Fixture,
env: &[(OsString, OsString)],
) -> Option<Vec<String>> {
let script = format!("printf \"%s\\n\" {pattern}");
let mut cmd = agent_command("sh", env, &fixture.names);
cmd.arg("-c").arg(&script);
let Child::Ran(stdout, _code) = bounded_output(&mut cmd) else {
return None;
};
Some(
String::from_utf8_lossy(&stdout)
.lines()
.map(str::to_string)
.collect(),
)
}
fn real_search(argv: &[String], env: &[(OsString, OsString)], cwd: &Path) -> Child {
let Some((verb, operands)) = argv.split_first() else {
return Child::Unusable;
};
let mut cmd = agent_command(verb, env, cwd);
cmd.args(operands);
bounded_output(&mut cmd)
}
fn agent_command(program: &str, env: &[(OsString, OsString)], cwd: &Path) -> Command {
let mut cmd = crate::tools::shell::program_command(program, env, Some(cwd));
cmd.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
cmd
}
enum Child {
Ran(Vec<u8>, i32),
Missing,
Unusable,
}
fn bounded_output(cmd: &mut Command) -> Child {
let Ok(sink) = tempfile::tempfile_in(crate::temp::shell_tmpdir()) else {
return Child::Unusable;
};
let Ok(mut captured) = sink.try_clone() else {
return Child::Unusable;
};
let mut child = match cmd.stdout(std::process::Stdio::from(sink)).spawn() {
Ok(child) => child,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Child::Missing,
Err(_) => return Child::Unusable,
};
let deadline = std::time::Instant::now() + BATTERY_SPAWN_TIMEOUT;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Err(_) => return Child::Unusable,
Ok(None) if std::time::Instant::now() >= deadline => {
reap(&mut child);
return Child::Unusable;
}
Ok(None)
if captured
.metadata()
.is_ok_and(|file| file.len() > BATTERY_STDOUT_CAP) =>
{
reap(&mut child);
return Child::Unusable;
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(5)),
}
};
if captured.seek(std::io::SeekFrom::Start(0)).is_err() {
return Child::Unusable;
}
let mut out = Vec::new();
if captured
.take(BATTERY_STDOUT_CAP)
.read_to_end(&mut out)
.is_err()
{
return Child::Unusable;
}
Child::Ran(out, status.code().unwrap_or(-1))
}
fn reap(child: &mut std::process::Child) {
let _ = child.kill();
let deadline = std::time::Instant::now() + BATTERY_SPAWN_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(_)) | Err(_) => return,
Ok(None) if std::time::Instant::now() >= deadline => return,
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(5)),
}
}
}
fn real_search_exists(fixture: &Fixture, env: &[(OsString, OsString)]) -> Probe {
let argv = [
"grep".to_string(),
"needle".to_string(),
fixture.abs("a.txt"),
];
match real_search(&argv, env, &fixture.root) {
Child::Ran(..) => Probe::Present,
Child::Missing => Probe::Absent,
Child::Unusable => Probe::Unknown,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Probe {
Present,
Absent,
#[default]
Unknown,
}
fn engine_run(spec: &EngineSpec) -> Option<(Vec<u8>, i32)> {
let matcher = build_matcher(&spec.patterns, spec.mode, &spec.flags).ok()?;
let mut out = Output::new(OutputSink::Buffer(Vec::new()), StdoutDest::of(spec, true));
let consumed = std::cell::Cell::new(false);
let code = serve_into(spec, &matcher, &mut out, io::empty(), &consumed);
let (bytes, _err) = out.take_stdio();
Some((bytes, code))
}
fn equivalent(engine: &[u8], engine_code: i32, real: &[u8], real_code: i32, sorted: bool) -> bool {
if engine_code != real_code {
return false;
}
if sorted {
sorted_lines(engine) == sorted_lines(real)
} else {
engine == real
}
}
fn sorted_lines(bytes: &[u8]) -> Vec<&[u8]> {
let mut records: Vec<&[u8]> = bytes
.split(|&b| b == b'\n')
.filter(|r| !r.is_empty())
.collect();
records.sort_unstable();
records
}
fn segment_text(argv: &[String]) -> String {
argv.iter()
.map(|word| shell_quote(word))
.collect::<Vec<_>>()
.join(" ")
}
struct Fixture {
_tmp: tempfile::TempDir,
root: PathBuf,
home: PathBuf,
names: PathBuf,
}
impl Fixture {
fn create() -> Option<Self> {
let tmp = tempfile::tempdir_in(crate::temp::shell_tmpdir()).ok()?;
let root = tmp.path().to_path_buf();
std::fs::create_dir_all(root.join("dir/sub")).ok()?;
std::fs::create_dir_all(root.join("names")).ok()?;
std::fs::create_dir_all(root.join("home")).ok()?;
let files: &[(&str, &[u8])] = &[
("a.txt", b"alpha\nbeta\nneedle\n"),
("b.txt", b"needle\nother\n"),
(
"uni.txt",
"café\nCAFÉ\nk\nK\n\u{212a}\nstraßen\nSTRASSE\nistanbul\n\u{0130}stanbul\n"
.as_bytes(),
),
("bad.txt", b"bad\x80\xff needle\ntail\n"),
("bin.dat", b"\x00\x01 needle\n"),
("dir/two.txt", b"needle\n"),
("dir/sub/one.txt", b"other\nneedle\n"),
("names/a.txt", b"x\n"),
("names/B.txt", b"x\n"),
("names/\u{00c3}.txt", b"x\n"),
("names/\u{00e0}.txt", b"x\n"),
("names/\u{00e9}.txt", b"x\n"),
];
for (name, bytes) in files {
std::fs::write(root.join(name), bytes).ok()?;
}
Some(Fixture {
names: root.join("names"),
home: root.join("home"),
root,
_tmp: tmp,
})
}
fn abs(&self, rel: &str) -> String {
self.root.join(rel).to_string_lossy().into_owned()
}
}
#[cfg(test)]
mod tests {
use super::super::{Fallback, PipelineCtx, grep_tokenize, parse_grep_words, serve_one_grep};
use super::*;
fn parsed(segment: &str) -> ParsedGrep {
let mut words = grep_tokenize(segment, ShellPlatform::Unix).expect("tokenize");
words.remove(0);
parse_grep_words(&words, "grep", ShellPlatform::Unix).expect("parse")
}
#[test]
fn a_run_with_a_row_it_could_not_compare_folds_no_verdict() {
let mut battery = Battery::default();
let mut plain = Group::default();
plain.record(&mut battery, RowOutcome::Compared(true));
plain.record(&mut battery, RowOutcome::Skipped);
assert_eq!(
(
battery.rows,
battery.compared,
battery.skipped,
battery.unmeasured
),
(2, 1, 1, 0),
"each outcome is counted where it belongs"
);
battery.fold(&plain, &Group::default(), &Group::default());
assert!(
battery.verdict.is_some_and(|v| v.plain),
"a run that compared every row it had to folds its verdict"
);
let mut battery = Battery::default();
let mut plain = Group::default();
plain.record(&mut battery, RowOutcome::Compared(true));
plain.record(&mut battery, RowOutcome::Unmeasured);
assert_eq!(battery.unmeasured, 1, "an unmeasurable row is counted");
battery.fold(&plain, &Group::default(), &Group::default());
assert!(
battery.verdict.is_none(),
"one unmeasurable row is not a verdict for the environment"
);
}
#[cfg(unix)]
#[serial_test::serial(shell_env)]
#[test]
fn a_verdict_stands_only_for_the_environment_it_was_measured_for() {
let in_effect = || EnvKey::new(&crate::tools::shell::agent_env_pairs());
let other = [(OsString::from("ONE"), OsString::from("two"))];
let gate = || ESTABLISHED.load().as_deref().copied().unwrap_or_default();
assert!(record(in_effect(), Some(Established::all())));
assert_eq!(gate(), Established::all());
assert!(
refresh_if_environment_changed(&crate::tools::shell::agent_env_pairs()),
"an environment measured with a verdict is a no-op"
);
assert!(!record(in_effect(), None));
assert_eq!(gate(), Established::default());
assert!(LAST_MEASURED.lock().unwrap_poison().is_none());
assert!(record(EnvKey::new(&other), Some(Established::all())));
assert_eq!(gate(), Established::all());
invalidate_verdict_for(&crate::tools::shell::agent_env_pairs());
assert_eq!(gate(), Established::default());
assert!(LAST_MEASURED.lock().unwrap_poison().is_none());
assert!(record(EnvKey::new(&other), Some(Established::all())));
invalidate_verdict_for(&other);
assert_eq!(gate(), Established::all());
assert!(LAST_MEASURED.lock().unwrap_poison().as_ref() == Some(&EnvKey::new(&other)));
ESTABLISHED.store(None);
*LAST_MEASURED.lock().unwrap_poison() = None;
}
#[test]
fn needs_follows_the_member_shape() {
let rows: &[(&str, &[Need])] = &[
("grep needle a.txt", &[Need::Plain]),
("grep -n needle a.txt", &[Need::Plain]),
("grep -F needle a.txt", &[Need::Plain]),
("grep -x needle a.txt", &[Need::Plain]),
("grep -i needle a.txt", &[Need::General]),
("grep -w needle a.txt", &[Need::General]),
("grep 'a.c' a.txt", &[Need::General]),
("grep -F 'a.b' a.txt", &[Need::Plain]),
(r"grep 'a\|b' a.txt", &[Need::General]),
("grep needle *.txt", &[Need::Plain, Need::Glob]),
(
"grep needle --include=*.txt a.txt",
&[Need::Plain, Need::Glob],
),
(
"grep needle --exclude-dir=sub a.txt",
&[Need::Plain, Need::Glob],
),
("grep -i needle '*.txt'", &[Need::General]),
];
for (segment, expected) in rows {
assert_eq!(
needs(&parsed(segment), ShellPlatform::Unix),
*expected,
"{segment}"
);
}
}
#[test]
fn the_gate_demotes_what_was_not_established() {
publish_for_test((false, false, false));
assert_eq!(
serve_allowed(&[Need::Plain], ShellPlatform::Unix),
Err("literal matching")
);
assert_eq!(
serve_allowed(&[Need::General], ShellPlatform::Unix),
Err("pattern matching")
);
assert_eq!(
serve_allowed(&[Need::Glob], ShellPlatform::Unix),
Err("glob expansion")
);
publish_for_test((true, false, true));
assert_eq!(serve_allowed(&[Need::Plain], ShellPlatform::Unix), Ok(()));
assert_eq!(serve_allowed(&[Need::Glob], ShellPlatform::Unix), Ok(()));
assert_eq!(
serve_allowed(&[Need::General], ShellPlatform::Unix),
Err("pattern matching")
);
publish_for_test((true, true, true));
assert_eq!(
serve_allowed(&[Need::Plain, Need::Glob], ShellPlatform::Unix),
Ok(())
);
}
#[cfg(unix)]
#[test]
fn a_host_without_a_real_search_establishes_every_capability() {
let fixture = Fixture::create().expect("fixture");
let empty = tempfile::tempdir().expect("empty dir");
let env = vec![(
OsString::from("PATH"),
empty.path().as_os_str().to_os_string(),
)];
let battery = run_battery(&fixture, &env);
assert_eq!(battery.real_search, Probe::Absent, "no search in that PATH");
assert_eq!(
battery.verdict,
Some(Established::all()),
"the fast path is the only search here"
);
assert_eq!(battery.rows, 0, "nothing was compared");
}
#[test]
fn a_windows_member_is_served_however_the_verdict_stands() {
publish_for_test((false, false, false));
let fixture = Fixture::create().expect("fixture");
let segment = "grep -n needle a.txt";
let serve = |platform| {
serve_one_grep(
segment,
"grep",
&fixture.root,
&fixture.home,
platform,
true,
PipelineCtx {
piped: false,
stdin_fed: false,
marker_ok: true,
env_changed: false,
},
)
};
assert!(
serve(ShellPlatform::Windows).is_ok(),
"a Windows serve never consults the verdict"
);
assert!(
matches!(
serve(ShellPlatform::Unix),
Err(Fallback::NotEstablished("literal matching"))
),
"the same member is demoted on unix with nothing established"
);
}
#[cfg(unix)]
#[test]
#[ignore = "manual evidence run: prints the per-locale verdicts from the host's real search"]
fn battery_reports_capabilities_per_locale() {
let locales = [
("inherited", crate::tools::shell::agent_env_pairs()),
("C", locale_env("C")),
("en_US.UTF-8", locale_env("en_US.UTF-8")),
];
for (label, env) in locales {
let fixture = Fixture::create().expect("fixture");
let battery = run_battery(&fixture, &env);
let verdict = battery.verdict.unwrap_or_default();
println!(
"grep parity [{label}]: established={} plain={} general={} glob={} rows={} compared={} skipped={} unmeasured={} real_search={:?}",
battery.verdict.is_some(),
verdict.plain,
verdict.general,
verdict.glob,
battery.rows,
battery.compared,
battery.skipped,
battery.unmeasured,
battery.real_search,
);
match battery.real_search {
Probe::Present => {}
Probe::Absent => {
println!(
" no real search here to compare against: the fast path is the only search"
);
continue;
}
Probe::Unknown => {
println!(
" the probe could not be run: nothing was established and the fast path is closed"
);
continue;
}
}
let (served, real): (Vec<Need>, Vec<Need>) = [Need::Plain, Need::General, Need::Glob]
.into_iter()
.partition(|need| verdict.covers(*need));
println!(
" served by the fast path: {} | run by the real search: {}",
capability_names(&served),
capability_names(&real),
);
assert!(battery.rows > 0, "{label}: the battery ran no row");
}
}
#[cfg(unix)]
fn capability_names(needs: &[Need]) -> String {
if needs.is_empty() {
return "none".to_string();
}
needs
.iter()
.map(|need| need.capability())
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(unix)]
fn locale_env(locale: &str) -> Vec<(OsString, OsString)> {
let path = std::env::var_os("PATH").unwrap_or_default();
vec![
(OsString::from("LC_ALL"), OsString::from(locale)),
(OsString::from("PATH"), path),
]
}
}