use std::collections::VecDeque;
use std::ffi::OsStr;
use std::io::{self, Cursor};
use std::path::Path;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, ReadBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExitStatus {
code: Option<i32>,
}
impl ExitStatus {
#[must_use]
pub const fn from_success() -> Self {
Self { code: Some(0) }
}
#[must_use]
pub const fn from_code(code: i32) -> Self {
Self { code: Some(code) }
}
#[must_use]
pub const fn from_signal() -> Self {
Self { code: None }
}
#[must_use]
pub const fn success(&self) -> bool {
matches!(self.code, Some(0))
}
#[must_use]
pub const fn code(&self) -> Option<i32> {
self.code
}
}
impl Default for ExitStatus {
fn default() -> Self {
Self::from_success()
}
}
#[derive(Debug, Clone, Default)]
pub struct Output {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Stdio {
#[default]
Inherit,
Piped,
Null,
}
impl From<std::process::Stdio> for Stdio {
fn from(stdio: std::process::Stdio) -> Self {
let _ = stdio;
Self::Inherit
}
}
#[derive(Debug, Clone)]
pub struct MockResponse {
pub program: Option<String>,
pub args: Option<Vec<String>>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: i32,
#[cfg(feature = "time")]
pub delay: Option<std::time::Duration>,
pub fail_to_spawn: bool,
pub spawn_error: Option<String>,
}
impl Default for MockResponse {
fn default() -> Self {
Self::success()
}
}
impl MockResponse {
#[must_use]
pub const fn success() -> Self {
Self {
program: None,
args: None,
stdout: Vec::new(),
stderr: Vec::new(),
exit_code: 0,
#[cfg(feature = "time")]
delay: None,
fail_to_spawn: false,
spawn_error: None,
}
}
#[must_use]
pub const fn failure(exit_code: i32) -> Self {
Self {
program: None,
args: None,
stdout: Vec::new(),
stderr: Vec::new(),
exit_code,
#[cfg(feature = "time")]
delay: None,
fail_to_spawn: false,
spawn_error: None,
}
}
#[must_use]
pub fn for_program(mut self, program: impl Into<String>) -> Self {
self.program = Some(program.into());
self
}
#[must_use]
pub fn for_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args = Some(args.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn with_stdout(mut self, stdout: impl Into<Vec<u8>>) -> Self {
self.stdout = stdout.into();
self
}
#[must_use]
pub fn with_stderr(mut self, stderr: impl Into<Vec<u8>>) -> Self {
self.stderr = stderr.into();
self
}
#[must_use]
pub const fn with_exit_code(mut self, code: i32) -> Self {
self.exit_code = code;
self
}
#[cfg(feature = "time")]
#[must_use]
pub const fn with_delay(mut self, delay: std::time::Duration) -> Self {
self.delay = Some(delay);
self
}
#[must_use]
pub fn fail_spawn(mut self, message: impl Into<String>) -> Self {
self.fail_to_spawn = true;
self.spawn_error = Some(message.into());
self
}
fn matches(&self, program: &str, args: &[String]) -> bool {
if let Some(ref expected_program) = self.program
&& expected_program != program
{
return false;
}
if let Some(ref expected_args) = self.args
&& expected_args != args
{
return false;
}
true
}
}
#[derive(Debug, Default, Clone)]
pub struct ProcessRegistry {
responses: Arc<Mutex<VecDeque<MockResponse>>>,
default_response: Arc<Mutex<Option<MockResponse>>>,
}
impl ProcessRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, response: MockResponse) {
self.responses.lock().unwrap().push_back(response);
}
pub fn register_all<I>(&self, responses: I)
where
I: IntoIterator<Item = MockResponse>,
{
let mut queue = self.responses.lock().unwrap();
for response in responses {
queue.push_back(response);
}
}
pub fn set_default(&self, response: MockResponse) {
*self.default_response.lock().unwrap() = Some(response);
}
#[must_use]
pub fn remaining(&self) -> usize {
self.responses.lock().unwrap().len()
}
pub fn clear(&self) {
self.responses.lock().unwrap().clear();
*self.default_response.lock().unwrap() = None;
}
fn take_response(&self, program: &str, args: &[String]) -> Option<MockResponse> {
let mut responses = self.responses.lock().unwrap();
if let Some(idx) = responses.iter().position(|r| r.matches(program, args)) {
return responses.remove(idx);
}
drop(responses);
self.default_response.lock().unwrap().clone()
}
}
thread_local! {
static PROCESS_REGISTRY: std::cell::RefCell<Option<ProcessRegistry>> =
const { std::cell::RefCell::new(None) };
}
pub fn set_registry(registry: ProcessRegistry) {
PROCESS_REGISTRY.with(|r| {
*r.borrow_mut() = Some(registry);
});
}
pub fn clear_registry() {
PROCESS_REGISTRY.with(|r| {
*r.borrow_mut() = None;
});
}
#[must_use]
pub fn get_registry() -> Option<ProcessRegistry> {
PROCESS_REGISTRY.with(|r| r.borrow().clone())
}
#[derive(Debug)]
pub struct Command {
program: String,
args: Vec<String>,
current_dir: Option<std::path::PathBuf>,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
}
impl Command {
#[must_use]
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
Self {
program: program.as_ref().to_string_lossy().to_string(),
args: Vec::new(),
current_dir: None,
stdin: Stdio::default(),
stdout: Stdio::default(),
stderr: Stdio::default(),
}
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.args.push(arg.as_ref().to_string_lossy().to_string());
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
for arg in args {
self.arg(arg);
}
self
}
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
self.current_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn stdin<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
self.stdin = cfg.into();
self
}
pub fn stdout<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
self.stdout = cfg.into();
self
}
pub fn stderr<T: Into<Stdio>>(&mut self, cfg: T) -> &mut Self {
self.stderr = cfg.into();
self
}
pub async fn output(&mut self) -> io::Result<Output> {
let response = get_registry()
.and_then(|r| r.take_response(&self.program, &self.args))
.unwrap_or_default();
if response.fail_to_spawn {
return Err(io::Error::new(
io::ErrorKind::NotFound,
response
.spawn_error
.unwrap_or_else(|| "command not found".to_string()),
));
}
#[cfg(feature = "time")]
if let Some(delay) = response.delay {
crate::time::sleep(delay).await;
}
Ok(Output {
status: ExitStatus::from_code(response.exit_code),
stdout: response.stdout,
stderr: response.stderr,
})
}
pub fn spawn(&mut self) -> io::Result<Child> {
let response = get_registry()
.and_then(|r| r.take_response(&self.program, &self.args))
.unwrap_or_default();
if response.fail_to_spawn {
return Err(io::Error::new(
io::ErrorKind::NotFound,
response
.spawn_error
.unwrap_or_else(|| "command not found".to_string()),
));
}
let stdout = if matches!(self.stdout, Stdio::Piped) {
Some(ChildStdout::new(response.stdout.clone()))
} else {
None
};
let stderr = if matches!(self.stderr, Stdio::Piped) {
Some(ChildStderr::new(response.stderr.clone()))
} else {
None
};
Ok(Child {
response,
stdout,
stderr,
})
}
}
#[derive(Debug)]
pub struct Child {
response: MockResponse,
pub stdout: Option<ChildStdout>,
pub stderr: Option<ChildStderr>,
}
impl Child {
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
#[cfg(feature = "time")]
if let Some(delay) = self.response.delay {
crate::time::sleep(delay).await;
}
Ok(ExitStatus::from_code(self.response.exit_code))
}
pub async fn wait_with_output(self) -> io::Result<Output> {
#[cfg(feature = "time")]
if let Some(delay) = self.response.delay {
crate::time::sleep(delay).await;
}
Ok(Output {
status: ExitStatus::from_code(self.response.exit_code),
stdout: self.response.stdout.clone(),
stderr: self.response.stderr.clone(),
})
}
#[allow(clippy::unused_async)] pub async fn kill(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct ChildStdin;
#[derive(Debug)]
pub struct ChildStdout {
data: Cursor<Vec<u8>>,
}
impl ChildStdout {
pub(crate) const fn new(data: Vec<u8>) -> Self {
Self {
data: Cursor::new(data),
}
}
}
impl AsyncRead for ChildStdout {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let data = self.data.get_ref();
#[allow(clippy::cast_possible_truncation)]
let pos = self.data.position() as usize;
let remaining = &data[pos..];
let to_read = std::cmp::min(remaining.len(), buf.remaining());
buf.put_slice(&remaining[..to_read]);
self.data.set_position((pos + to_read) as u64);
Poll::Ready(Ok(()))
}
}
#[derive(Debug)]
pub struct ChildStderr {
data: Cursor<Vec<u8>>,
}
impl ChildStderr {
pub(crate) const fn new(data: Vec<u8>) -> Self {
Self {
data: Cursor::new(data),
}
}
}
impl AsyncRead for ChildStderr {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let data = self.data.get_ref();
#[allow(clippy::cast_possible_truncation)]
let pos = self.data.position() as usize;
let remaining = &data[pos..];
let to_read = std::cmp::min(remaining.len(), buf.remaining());
buf.put_slice(&remaining[..to_read]);
self.data.set_position((pos + to_read) as u64);
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn exit_status_success() {
let status = ExitStatus::from_success();
assert!(status.success());
assert_eq!(status.code(), Some(0));
}
#[test_log::test]
fn exit_status_failure() {
let status = ExitStatus::from_code(1);
assert!(!status.success());
assert_eq!(status.code(), Some(1));
}
#[test_log::test]
fn exit_status_signal() {
let status = ExitStatus::from_signal();
assert!(!status.success());
assert_eq!(status.code(), None);
}
#[test_log::test]
fn mock_response_builder() {
let response = MockResponse::success()
.for_program("test")
.with_stdout(b"hello".to_vec())
.with_stderr(b"error".to_vec())
.with_exit_code(42);
assert_eq!(response.program, Some("test".to_string()));
assert_eq!(response.stdout, b"hello");
assert_eq!(response.stderr, b"error");
assert_eq!(response.exit_code, 42);
}
#[test_log::test]
fn mock_response_matches() {
let response = MockResponse::success().for_program("cargo");
assert!(response.matches("cargo", &[]));
assert!(!response.matches("rustc", &[]));
let response_any = MockResponse::success();
assert!(response_any.matches("anything", &[]));
}
#[test_log::test]
fn registry_fifo_order() {
let registry = ProcessRegistry::new();
registry.register(MockResponse::success().for_program("first"));
registry.register(MockResponse::success().for_program("second"));
let first = registry.take_response("first", &[]);
assert!(first.is_some());
assert_eq!(first.unwrap().program, Some("first".to_string()));
let second = registry.take_response("second", &[]);
assert!(second.is_some());
assert_eq!(second.unwrap().program, Some("second".to_string()));
assert!(registry.take_response("third", &[]).is_none());
}
#[test_log::test]
fn registry_default_response() {
let registry = ProcessRegistry::new();
registry.set_default(MockResponse::failure(99));
let response = registry.take_response("unknown", &[]);
assert!(response.is_some());
assert_eq!(response.unwrap().exit_code, 99);
}
}