use std::{
collections::{HashMap, HashSet},
io::{BufReader, Read as _},
path::{Path, PathBuf},
process, thread,
time::Duration,
};
use crate::error::Result;
use wait_timeout::ChildExt;
pub trait Environment: Send + Sync {
fn var(&self, key: &str) -> Option<String>;
fn current_dir(&self) -> Result<PathBuf>;
fn path_exists(&self, path: &Path) -> bool;
fn home_dir(&self) -> Option<PathBuf>;
fn run_command(&self, cmd: &str, args: &[&str], timeout_ms: u64) -> Option<String>;
fn read_file(&self, path: &Path) -> Result<String>;
fn find_file_upward(&self, start: &Path, filename: &str) -> Option<PathBuf>;
}
pub struct RealEnvironment;
impl Environment for RealEnvironment {
fn var(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn current_dir(&self) -> Result<PathBuf> {
std::env::current_dir().map_err(Into::into)
}
fn path_exists(&self, path: &Path) -> bool {
path.exists()
}
fn home_dir(&self) -> Option<PathBuf> {
dirs::home_dir()
}
fn run_command(&self, cmd: &str, args: &[&str], timeout_ms: u64) -> Option<String> {
let mut child = process::Command::new(cmd)
.args(args)
.stdin(process::Stdio::null())
.stdout(process::Stdio::piped())
.stderr(process::Stdio::null())
.spawn()
.ok()?;
let stdout = child.stdout.take()?;
let reader_handle = thread::spawn(move || {
let mut output = String::new();
let mut reader = BufReader::new(stdout);
let _ = reader.read_to_string(&mut output);
output
});
let timeout = Duration::from_millis(timeout_ms);
match child.wait_timeout(timeout) {
Ok(Some(status)) if status.success() => {
reader_handle.join().ok().map(|o| o.trim().to_string())
}
Ok(Some(_)) => {
None
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
let _ = reader_handle.join();
None
}
Err(_) => None,
}
}
fn read_file(&self, path: &Path) -> Result<String> {
Ok(std::fs::read_to_string(path)?)
}
fn find_file_upward(&self, start: &Path, filename: &str) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
let candidate = current.join(filename);
if candidate.is_file() {
return Some(candidate);
}
if !current.pop() {
return None;
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MockEnvironment {
pub env_vars: HashMap<String, String>,
pub cwd: PathBuf,
pub existing_paths: HashSet<PathBuf>,
pub home: Option<PathBuf>,
pub command_outputs: HashMap<String, String>,
pub files: HashMap<PathBuf, String>,
}
impl Environment for MockEnvironment {
fn var(&self, key: &str) -> Option<String> {
self.env_vars.get(key).cloned()
}
fn current_dir(&self) -> Result<PathBuf> {
Ok(self.cwd.clone())
}
fn path_exists(&self, path: &Path) -> bool {
self.existing_paths.contains(path) || self.files.contains_key(path)
}
fn home_dir(&self) -> Option<PathBuf> {
self.home.clone()
}
fn run_command(&self, cmd: &str, args: &[&str], _timeout_ms: u64) -> Option<String> {
let key = format!("{} {}", cmd, args.join(" "));
self.command_outputs.get(&key).cloned()
}
fn read_file(&self, path: &Path) -> Result<String> {
self.files.get(path).cloned().ok_or_else(|| {
crate::error::Error::Other(format!("mock file not found: {}", path.display()))
})
}
fn find_file_upward(&self, start: &Path, filename: &str) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
let candidate = current.join(filename);
if self.files.contains_key(&candidate) {
return Some(candidate);
}
if !current.pop() {
return None;
}
}
}
}