use super::super::{NINJA_ENV, NINJA_PROGRAM};
use camino::Utf8PathBuf;
use mockable::Env;
use std::path::PathBuf;
use tracing::debug;
pub(super) fn resolve_ninja_program_utf8_with(env: &impl Env) -> Utf8PathBuf {
env.os_string(NINJA_ENV).map_or_else(
|| {
debug!(
ninja_program = NINJA_PROGRAM,
source = "fallback",
"Resolved Ninja executable from default program",
);
Utf8PathBuf::from(NINJA_PROGRAM)
},
|value| {
let path = PathBuf::from(value);
if path.as_os_str().is_empty() {
debug!(
ninja_program = NINJA_PROGRAM,
source = "fallback",
"Ignoring empty Ninja executable override",
);
Utf8PathBuf::from(NINJA_PROGRAM)
} else {
match Utf8PathBuf::from_path_buf(path) {
Ok(program) => {
debug!(
ninja_program = %program,
source = NINJA_ENV,
"Resolved Ninja executable from environment override",
);
program
}
Err(non_utf8_path) => {
debug!(
configured_ninja = %non_utf8_path.to_string_lossy(),
ninja_program = NINJA_PROGRAM,
source = "fallback",
"Ignoring non-UTF-8 Ninja executable override",
);
Utf8PathBuf::from(NINJA_PROGRAM)
}
}
}
},
)
}
#[must_use]
pub fn resolve_ninja_program() -> Utf8PathBuf {
resolve_ninja_program_utf8_with(&mockable::DefaultEnv)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_tracing_capture::with_test_subscriber;
use mockable::MockEnv;
use proptest::prelude::*;
use rstest::{fixture, rstest};
use std::ffi::OsString;
#[cfg(unix)]
use std::path::PathBuf;
use tracing_subscriber::filter::LevelFilter;
#[fixture]
fn ninja_env(#[default(None)] value: Option<OsString>) -> MockEnv {
let mut env = MockEnv::new();
env.expect_os_string()
.times(1)
.withf(|key| key == NINJA_ENV)
.return_const(value);
env
}
#[rstest]
fn resolve_ninja_program_utf8_logs_override_resolution(
#[with(Some(OsString::from("/opt/ninja")))] ninja_env: MockEnv,
) {
let (resolved, events) = with_test_subscriber(LevelFilter::DEBUG, |captured| {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
(resolved, captured.snapshot())
});
assert_eq!(resolved, Utf8PathBuf::from("/opt/ninja"));
let [event] = events.as_slice() else {
panic!("expected one Ninja-resolution event, got {events:?}");
};
assert!(
event.contains("ninja_program=/opt/ninja")
&& event.contains("source=\"NETSUKE_NINJA\"")
&& event.contains("message=Resolved Ninja executable from environment override"),
"override resolution should identify its program, source, and message: {event}"
);
}
#[rstest]
fn resolve_ninja_program_utf8_logs_fallback_resolution(ninja_env: MockEnv) {
let (resolved, events) = with_test_subscriber(LevelFilter::DEBUG, |captured| {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
(resolved, captured.snapshot())
});
assert_eq!(resolved, Utf8PathBuf::from(NINJA_PROGRAM));
let [event] = events.as_slice() else {
panic!("expected one Ninja-resolution event, got {events:?}");
};
assert!(
event.contains(&format!("ninja_program={NINJA_PROGRAM:?}"))
&& event.contains("source=\"fallback\"")
&& event.contains("message=Resolved Ninja executable from default program"),
"fallback resolution should identify its program, source, and message: {event}"
);
}
#[rstest]
fn resolve_ninja_program_utf8_prefers_env_override(
#[with(Some(OsString::from("/opt/ninja")))] ninja_env: MockEnv,
) {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
assert_eq!(resolved, Utf8PathBuf::from("/opt/ninja"));
}
#[rstest]
fn resolve_ninja_program_utf8_defaults_without_override(ninja_env: MockEnv) {
let (resolved, events) = with_test_subscriber(LevelFilter::INFO, |captured| {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
(resolved, captured.snapshot())
});
assert_eq!(resolved, Utf8PathBuf::from(NINJA_PROGRAM));
assert!(
events.is_empty(),
"resolver debug events must be hidden by an INFO filter: {events:?}"
);
}
#[rstest]
fn resolve_ninja_program_utf8_defaults_for_empty_override(
#[with(Some(OsString::new()))] ninja_env: MockEnv,
) {
let (resolved, events) = with_test_subscriber(LevelFilter::DEBUG, |captured| {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
(resolved, captured.snapshot())
});
assert_eq!(resolved, Utf8PathBuf::from(NINJA_PROGRAM));
let [event] = events.as_slice() else {
panic!("expected one Ninja-resolution event, got {events:?}");
};
assert!(
event.contains(&format!("ninja_program={NINJA_PROGRAM:?}"))
&& event.contains("source=\"fallback\"")
&& event.contains("message=Ignoring empty Ninja executable override"),
"empty override should identify its fallback resolution: {event}"
);
}
#[cfg(unix)]
#[rstest]
fn resolve_ninja_program_utf8_ignores_invalid_utf8_override(
#[with(Some(invalid_utf8_override()))] ninja_env: MockEnv,
) {
let (resolved, events) = with_test_subscriber(LevelFilter::DEBUG, |captured| {
let resolved = resolve_ninja_program_utf8_with(&ninja_env);
(resolved, captured.snapshot())
});
assert_eq!(resolved, Utf8PathBuf::from(NINJA_PROGRAM));
let [event] = events.as_slice() else {
panic!("expected one Ninja-resolution event, got {events:?}");
};
assert!(
event.contains(&format!("ninja_program={NINJA_PROGRAM:?}"))
&& event.contains("source=\"fallback\"")
&& event.contains("message=Ignoring non-UTF-8 Ninja executable override"),
"invalid override should identify its fallback resolution: {event}"
);
}
#[cfg(unix)]
fn invalid_utf8_override() -> OsString {
use std::os::unix::ffi::OsStringExt;
OsString::from_vec(vec![0xff, b'n', b'i', b'n', b'j', b'a'])
}
proptest! {
#[test]
fn resolve_ninja_program_utf8_matches_utf8_env_invariant(
override_value in prop::option::of(".*")
) {
let env_value = override_value.clone().map(OsString::from);
let expected = match override_value {
Some(value) if !value.is_empty() => Utf8PathBuf::from(value),
_ => Utf8PathBuf::from(NINJA_PROGRAM),
};
let resolved = resolve_ninja_program_utf8_with(&ninja_env(env_value));
prop_assert_eq!(resolved, expected);
}
}
#[cfg(unix)]
proptest! {
#[test]
fn resolve_ninja_program_utf8_falls_back_for_non_utf8_env_values(
bytes in prop::collection::vec(any::<u8>(), 0..32)
) {
use std::os::unix::ffi::OsStringExt;
let env_value = OsString::from_vec(bytes);
let expected = if env_value.as_os_str().is_empty() {
Utf8PathBuf::from(NINJA_PROGRAM)
} else {
Utf8PathBuf::from_path_buf(PathBuf::from(env_value.clone()))
.unwrap_or_else(|_| Utf8PathBuf::from(NINJA_PROGRAM))
};
let resolved = resolve_ninja_program_utf8_with(&ninja_env(Some(env_value)));
prop_assert_eq!(resolved, expected);
}
}
}