use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use running_process::daemon::client::{DaemonClient, SpawnCommandRequest};
use super::{scaled, start_server_with_tempdb};
fn env_dump_path() -> PathBuf {
let output = Command::new(env!("CARGO"))
.args([
"build",
"-p",
"testbins",
"--bin",
"testbin-env-dump",
"--message-format=json",
])
.stderr(std::process::Stdio::inherit())
.output()
.expect("cargo build");
assert!(output.status.success(), "cargo build failed");
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.contains("\"compiler-artifact\"") || !line.contains("testbin-env-dump") {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v["target"]["kind"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "bin"))
{
if let Some(exe) = v["executable"].as_str() {
let p = PathBuf::from(exe);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !p.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(p.exists(), "env-dump bin not found at {p:?}");
return p;
}
}
}
}
panic!("env-dump executable not in cargo output");
}
fn read_env_file(path: &Path) -> HashMap<String, String> {
let deadline = std::time::Instant::now() + scaled(std::time::Duration::from_secs(5));
while !path.exists() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(50));
}
let contents = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("read env dump file {path:?}: {e}"));
let mut map = HashMap::new();
for line in contents.lines() {
if let Some((k, v)) = line.split_once('=') {
map.insert(k.to_string(), v.to_string());
}
}
map
}
fn shell_quote_path(path: &Path) -> String {
format!("\"{}\"", path.display())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn default_inherits_daemon_env_and_layers_caller_env() {
let scope = format!("envrep-inherit-{}", line!());
let (server_handle, socket, _tmp_dir) = start_server_with_tempdb(&scope);
tokio::time::sleep(scaled(std::time::Duration::from_millis(300))).await;
let dump_bin = env_dump_path();
let workdir = tempfile::tempdir().expect("tempdir");
let out = workdir.path().join("env.dump");
let command = format!("{} {}", shell_quote_path(&dump_bin), shell_quote_path(&out));
let socket_for_client = socket.clone();
let out_for_client = out.clone();
let task = tokio::task::spawn_blocking(move || {
let mut client = DaemonClient::connect_to(&socket_for_client).expect("connect");
let req = SpawnCommandRequest::shell(command).with_env("RP_TEST_LAYERED", "from-caller");
let _ = client.spawn_command(&req).expect("spawn_command");
let env_map = read_env_file(&out_for_client);
assert_eq!(
env_map.get("RP_TEST_LAYERED").map(String::as_str),
Some("from-caller"),
"caller-supplied env var must reach the subprocess"
);
let has_path_like = env_map.contains_key("PATH") || env_map.contains_key("Path");
assert!(
has_path_like,
"subprocess should inherit a PATH/Path var via the daemon, env was: {env_map:?}"
);
let _ = client.shutdown(true, 5.0);
})
.await;
task.expect("client task");
let _ = tokio::time::timeout(scaled(std::time::Duration::from_secs(5)), server_handle).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn replace_mode_subprocess_sees_only_caller_env() {
let scope = format!("envrep-replace-{}", line!());
let (server_handle, socket, _tmp_dir) = start_server_with_tempdb(&scope);
tokio::time::sleep(scaled(std::time::Duration::from_millis(300))).await;
let dump_bin = env_dump_path();
let workdir = tempfile::tempdir().expect("tempdir");
let out = workdir.path().join("env.dump");
let command = format!("{} {}", shell_quote_path(&dump_bin), shell_quote_path(&out));
let mut caller_env: Vec<(String, String)> =
vec![("RP_TEST_REPLACED".to_string(), "from-replace".to_string())];
if cfg!(windows) {
if let Ok(root) = std::env::var("SystemRoot") {
caller_env.push(("SystemRoot".to_string(), root));
}
if let Ok(path) = std::env::var("PATH") {
caller_env.push(("PATH".to_string(), path));
}
} else {
if let Ok(path) = std::env::var("PATH") {
caller_env.push(("PATH".to_string(), path));
}
}
let socket_for_client = socket.clone();
let out_for_client = out.clone();
let task = tokio::task::spawn_blocking(move || {
let mut client = DaemonClient::connect_to(&socket_for_client).expect("connect");
let req = SpawnCommandRequest::shell(command).with_env_replace(caller_env.clone());
assert!(req.clear_inherited_env, "builder must set the clear flag");
let _ = client.spawn_command(&req).expect("spawn_command");
let env_map = read_env_file(&out_for_client);
assert_eq!(
env_map.get("RP_TEST_REPLACED").map(String::as_str),
Some("from-replace"),
"caller env must reach the subprocess in replace mode"
);
if cfg!(windows) {
assert!(
env_map.contains_key("SystemRoot"),
"caller-provided SystemRoot must reach subprocess (got {env_map:?})"
);
}
if cfg!(unix) {
assert!(
!env_map.contains_key("HOME"),
"replace mode leaked HOME from daemon env: {env_map:?}"
);
} else {
assert!(
!env_map.contains_key("USERPROFILE"),
"replace mode leaked USERPROFILE from daemon env: {env_map:?}"
);
}
let _ = client.shutdown(true, 5.0);
})
.await;
task.expect("client task");
let _ = tokio::time::timeout(scaled(std::time::Duration::from_secs(5)), server_handle).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn layer_mode_caller_env_wins_ties_against_inherited() {
let scope = format!("envrep-layer-tie-{}", line!());
let (server_handle, socket, _tmp_dir) = start_server_with_tempdb(&scope);
tokio::time::sleep(scaled(std::time::Duration::from_millis(300))).await;
let dump_bin = env_dump_path();
let workdir = tempfile::tempdir().expect("tempdir");
let out = workdir.path().join("env.dump");
let command = format!("{} {}", shell_quote_path(&dump_bin), shell_quote_path(&out));
let caller_path_override = if cfg!(windows) {
"C:\\caller-supplied-path-override"
} else {
"/caller-supplied-path-override"
};
let socket_for_client = socket.clone();
let out_for_client = out.clone();
let path_override = caller_path_override.to_string();
let task = tokio::task::spawn_blocking(move || {
let mut client = DaemonClient::connect_to(&socket_for_client).expect("connect");
let path_key = if cfg!(windows) { "Path" } else { "PATH" };
let req = SpawnCommandRequest::shell(command).with_env(path_key, path_override.clone());
let _ = client.spawn_command(&req).expect("spawn_command");
let env_map = read_env_file(&out_for_client);
let observed = env_map
.get(path_key)
.or_else(|| env_map.get("PATH"))
.or_else(|| env_map.get("Path"))
.cloned()
.unwrap_or_default();
assert_eq!(
observed, path_override,
"caller's env override must beat the inherited value"
);
let _ = client.shutdown(true, 5.0);
})
.await;
task.expect("client task");
let _ = tokio::time::timeout(scaled(std::time::Duration::from_secs(5)), server_handle).await;
}
#[cfg(windows)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn windows_case_insensitive_override_beats_inherited_path() {
let scope = format!("envrep-winci-{}", line!());
let (server_handle, socket, _tmp_dir) = start_server_with_tempdb(&scope);
tokio::time::sleep(scaled(std::time::Duration::from_millis(300))).await;
let dump_bin = env_dump_path();
let workdir = tempfile::tempdir().expect("tempdir");
let out = workdir.path().join("env.dump");
let command = format!("{} {}", shell_quote_path(&dump_bin), shell_quote_path(&out));
let inherited_marker = "C:\\should-not-win-marker";
let override_marker = "C:\\override-marker";
let socket_for_client = socket.clone();
let out_for_client = out.clone();
let task = tokio::task::spawn_blocking(move || {
let mut client = DaemonClient::connect_to(&socket_for_client).expect("connect");
let req = SpawnCommandRequest::shell(command).with_envs([
("PATH".to_string(), inherited_marker.to_string()),
("Path".to_string(), override_marker.to_string()),
(
"SystemRoot".to_string(),
std::env::var("SystemRoot").unwrap_or_default(),
),
]);
let _ = client.spawn_command(&req).expect("spawn_command");
let env_map = read_env_file(&out_for_client);
let observed = env_map
.get("Path")
.or_else(|| env_map.get("PATH"))
.cloned()
.unwrap_or_default();
assert_eq!(
observed, override_marker,
"caller's last-listed override must beat the earlier case variant; got env: {env_map:?}"
);
let _ = client.shutdown(true, 5.0);
})
.await;
task.expect("client task");
let _ = tokio::time::timeout(scaled(std::time::Duration::from_secs(5)), server_handle).await;
}