use super::super::super::{DiagMode, StartupWriter, cli};
use super::super::{ConfigurationLoadContext, resolve_configuration};
use super::{ConfigurationLoadScenario, configuration_clock};
use crate::test_tracing_capture::with_test_subscriber;
use anyhow::{Result, ensure};
use clap::CommandFactory;
use std::cell::Cell;
use std::ffi::OsString;
use std::time::Duration;
use tempfile::tempdir;
use tracing_subscriber::filter::LevelFilter;
pub(super) struct EmptyConfigEnv;
impl cli::ConfigEnvProvider for EmptyConfigEnv {
fn get(&self, _key: &str) -> Option<OsString> {
None
}
fn entries(&self) -> Vec<(OsString, OsString)> {
Vec::new()
}
}
struct RecordingConfigEnv {
json_reads: Cell<usize>,
entries_reads: Cell<usize>,
}
impl RecordingConfigEnv {
const fn new() -> Self {
Self {
json_reads: Cell::new(0),
entries_reads: Cell::new(0),
}
}
}
impl cli::ConfigEnvProvider for RecordingConfigEnv {
fn get(&self, key: &str) -> Option<OsString> {
if key == "NETSUKE_JSON" {
self.json_reads.set(self.json_reads.get() + 1);
Some(OsString::from("true"))
} else {
None
}
}
fn entries(&self) -> Vec<(OsString, OsString)> {
self.entries_reads.set(self.entries_reads.get() + 1);
vec![(OsString::from("NETSUKE_JOBS"), OsString::from("7"))]
}
}
#[test]
fn configuration_context_uses_its_injected_environment_for_both_phases() -> Result<()> {
let parsed_cli = cli::Cli::default();
let matches = cli::Cli::command().get_matches_from(["netsuke"]);
let startup_writer = StartupWriter::buffering();
let config_env = RecordingConfigEnv::new();
let context = ConfigurationLoadContext {
parsed_cli: &parsed_cli,
matches: &matches,
startup_mode: DiagMode::Human,
startup_writer: &startup_writer,
config_env: &config_env,
};
let clock = configuration_clock(
ConfigurationLoadScenario::SuccessfulMerge,
Duration::from_millis(1),
)?;
let (merged, fields) = with_test_subscriber(LevelFilter::TRACE, |captured| {
resolve_configuration(&context, &clock)
.map(|merged| (merged, captured.discovery_span_fields()))
.map_err(|code| anyhow::anyhow!("configuration should succeed, got {code:?}"))
})?;
ensure!(
config_env.json_reads.get() == 1,
"early JSON resolution should read NETSUKE_JSON through the context provider"
);
ensure!(
config_env.entries_reads.get() == 1,
"cached merge should read configuration entries through the context provider"
);
ensure!(
merged.jobs == Some(7),
"cached merge should apply the injected NETSUKE_JOBS value"
);
ensure!(
fields.contains(&"outcome=\"success\"".to_owned()),
"startup must record a successful discovery outcome: {fields:?}"
);
ensure!(
!fields
.iter()
.any(|field| field.starts_with("error_category=")),
"successful discovery must not record an error category: {fields:?}"
);
Ok(())
}
#[test]
fn startup_resolution_records_the_retained_discovery_span() -> Result<()> {
let temp = tempdir()?;
let parsed_cli = cli::Cli {
config: Some(temp.path().join("missing.toml")),
..cli::Cli::default()
};
let matches = cli::Cli::command().get_matches_from(["netsuke"]);
let startup_writer = StartupWriter::buffering();
let context = ConfigurationLoadContext {
parsed_cli: &parsed_cli,
matches: &matches,
startup_mode: DiagMode::Human,
startup_writer: &startup_writer,
config_env: &EmptyConfigEnv,
};
let clock = configuration_clock(
ConfigurationLoadScenario::JsonResolutionFailure,
Duration::from_millis(1),
)?;
let (result, fields) = with_test_subscriber(LevelFilter::TRACE, |captured| {
let result = resolve_configuration(&context, &clock);
(result, captured.discovery_span_fields())
});
ensure!(
result.is_err(),
"the missing explicit configuration must fail"
);
ensure!(
fields.contains(&"outcome=\"error\"".to_owned()),
"startup must record an error discovery outcome: {fields:?}"
);
ensure!(
fields.contains(&"error_category=\"file\"".to_owned()),
"startup must record the bounded file error category: {fields:?}"
);
Ok(())
}