mockable/
cmd.rs

1use std::{collections::HashMap, io::Result, path::PathBuf, process::Output};
2
3use async_trait::async_trait;
4// Command
5
6/// A command.
7///
8/// **This is supported on `feature=cmd` only.**
9#[derive(Debug, Clone, Eq, PartialEq)]
10pub struct Command {
11    /// The arguments to pass to the command.
12    pub args: Vec<String>,
13    /// The current working directory to run the command in.
14    pub cwd: Option<PathBuf>,
15    /// The environment variables to set for the command.
16    pub env: Option<HashMap<String, String>>,
17    /// The group to run the command as.
18    #[cfg(unix)]
19    pub gid: Option<u32>,
20    /// The program to run.
21    pub program: String,
22    /// The user to run the command as.
23    #[cfg(unix)]
24    pub uid: Option<u32>,
25}
26
27impl Command {
28    /// Creates a new command.
29    pub fn new<S: Into<String>>(program: S) -> Self {
30        Self {
31            args: vec![],
32            cwd: None,
33            env: None,
34            #[cfg(unix)]
35            gid: None,
36            program: program.into(),
37            #[cfg(unix)]
38            uid: None,
39        }
40    }
41
42    /// Add argument.
43    pub fn with_arg<S: Into<String>>(mut self, arg: S) -> Self {
44        self.args.push(arg.into());
45        self
46    }
47
48    /// Set arguments.
49    pub fn with_args(mut self, args: Vec<String>) -> Self {
50        self.args = args;
51        self
52    }
53
54    /// Set current working directory.
55    pub fn with_cwd<P: Into<PathBuf>>(mut self, cwd: P) -> Self {
56        self.cwd = Some(cwd.into());
57        self
58    }
59
60    /// Set environment variable.
61    pub fn with_env<S: Into<String>>(mut self, key: S, val: S) -> Self {
62        match self.env {
63            Some(ref mut env) => {
64                env.insert(key.into(), val.into());
65            }
66            None => {
67                self.env = Some(HashMap::from_iter([(key.into(), val.into())]));
68            }
69        }
70        self
71    }
72
73    /// Set all environment variables.
74    pub fn with_envs(mut self, env: HashMap<String, String>) -> Self {
75        self.env = Some(env);
76        self
77    }
78
79    /// Set GID.
80    #[cfg(unix)]
81    pub fn with_gid(mut self, gid: u32) -> Self {
82        self.gid = Some(gid);
83        self
84    }
85
86    /// Set UID.
87    #[cfg(unix)]
88    pub fn with_uid(mut self, uid: u32) -> Self {
89        self.uid = Some(uid);
90        self
91    }
92}
93
94// CommandOutput
95
96/// The output of a command.
97///
98/// **This is supported on `feature=cmd` only.**
99#[derive(Debug, Clone, Eq, PartialEq)]
100pub struct CommandOutput {
101    /// The exit code of the command.
102    pub code: Option<i32>,
103    /// The standard error output of the command.
104    pub stderr: Vec<u8>,
105    /// The standard output of the command.
106    pub stdout: Vec<u8>,
107}
108
109impl From<Output> for CommandOutput {
110    fn from(output: Output) -> Self {
111        Self {
112            code: output.status.code(),
113            stderr: output.stderr,
114            stdout: output.stdout,
115        }
116    }
117}
118
119// CommandRunner
120
121/// A trait for running commands.
122///
123/// **This is supported on `feature=cmd` only.**
124///
125/// [Example](https://github.com/leroyguillaume/mockable/tree/main/examples/cmd.rs).
126#[async_trait]
127pub trait CommandRunner: Send + Sync {
128    /// Runs the given command.
129    async fn run(&self, cmd: &Command) -> Result<CommandOutput>;
130}
131
132// DefaultCommandRunner
133
134/// Default implementation of [`CommandRunner`](trait.CommandRunner.html).
135///
136/// **This is supported on `feature=cmd` only.**
137///
138/// [Example](https://github.com/leroyguillaume/mockable/tree/main/examples/cmd.rs).
139pub struct DefaultCommandRunner;
140
141#[async_trait]
142impl CommandRunner for DefaultCommandRunner {
143    async fn run(&self, cmd: &Command) -> Result<CommandOutput> {
144        let mut builder = tokio::process::Command::new(&cmd.program);
145        builder.args(&cmd.args);
146        if let Some(cwd) = &cmd.cwd {
147            builder.current_dir(cwd);
148        }
149        if let Some(env) = &cmd.env {
150            builder.envs(env);
151        }
152        if cfg!(unix) {
153            if let Some(gid) = cmd.gid {
154                builder.gid(gid);
155            }
156            if let Some(uid) = cmd.uid {
157                builder.uid(uid);
158            }
159        }
160        let output = builder.output().await?;
161        Ok(output.into())
162    }
163}
164
165// MockCommandRunner
166
167#[cfg(feature = "mock")]
168mockall::mock! {
169    /// `mockall` implementation of [`CommandRunner`](trait.CommandRunner.html).
170    ///
171    /// **This is supported on `feature=cmd,mock` only.**
172    ///
173    /// [Example](https://github.com/leroyguillaume/mockable/tree/main/examples/cmd.rs).
174    pub CommandRunner {}
175
176    #[async_trait]
177    impl CommandRunner for CommandRunner {
178        async fn run(&self, cmd: &Command) -> Result<CommandOutput>;
179    }
180}