1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! Utilities for running and testing shell commands.

use std::cell::{Ref, RefCell};
use std::ffi::{OsStr, OsString};
use std::marker::PhantomData;
use std::path::Path;
use std::process;
use std::rc::Rc;

use crate::errors::*;

/// A factory that produces objects conforming to our `Command` wrapper
/// trait.  During tests, we'll use this to mock out the underlying system
/// and record all commands executed.
pub trait CommandRunner {
    /// The type of the commands we build.  Must implement our custom
    /// `Command` trait.
    type Command: Command;

    /// Build a new command.
    fn build<S: AsRef<OsStr>>(&self, program: S) -> Self::Command;
}

/// A stripped down interface based on `std::process::Command`.  We use
/// this so we can mock out shell commands during tests.
pub trait Command {
    /// Add an arugment to our command.
    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self;

    /// Add several arguments to our command.
    fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
        for arg in args {
            self.arg(arg);
        }
        self
    }

    /// Set an environment variable for the process we're about to run.
    fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>;

    /// Set the current working directory for the child process we'll
    /// create.
    fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self;

    /// Run our command.
    fn status(&mut self) -> Result<process::ExitStatus>;

    /// Run our command as per `status`, returning an error if the command
    /// fails.
    fn exec(&mut self) -> Result<()> {
        let status = self.status()?;
        if status.success() {
            Ok(())
        } else {
            Err(self.command_failed_error().into())
        }
    }

    /// Make an error representing a failure of this command.
    fn command_failed_error(&self) -> ErrorKind;
}

/// Support for running operating system commands.
#[derive(Debug, Default)]
#[allow(missing_copy_implementations)]
pub struct OsCommandRunner {
    /// PRIVATE: This field is a stand-in for future options.
    /// See http://stackoverflow.com/q/39277157/12089
    #[doc(hidden)]
    pub _nonexhaustive: PhantomData<()>,
}

impl OsCommandRunner {
    /// Create a new OsCommandRunner.
    pub fn new() -> OsCommandRunner {
        OsCommandRunner {
            _nonexhaustive: PhantomData,
        }
    }
}

impl CommandRunner for OsCommandRunner {
    type Command = OsCommand;

    fn build<S: AsRef<OsStr>>(&self, program: S) -> Self::Command {
        let program = program.as_ref();
        OsCommand {
            command: process::Command::new(program),
            arg_log: vec![program.to_owned()],
        }
    }
}

/// A wrapper around `std::process:Command` which logs the commands run.
#[derive(Debug)]
pub struct OsCommand {
    /// The actual command we're going to run.
    command: process::Command,
    /// A collection of all the arguments to the command we're going to
    /// run, for logging purposes.
    arg_log: Vec<OsString>,
}

impl Command for OsCommand {
    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
        let arg = arg.as_ref();
        self.arg_log.push(arg.to_owned());
        self.command.arg(arg);
        self
    }

    fn args<S: AsRef<OsStr>>(&mut self, args: &[S]) -> &mut Self {
        let args: Vec<_> = args.iter().map(|a| a.as_ref().to_owned()).collect();
        self.arg_log.extend_from_slice(&args);
        self.command.args(&args);
        self
    }

    fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.command.env(key, val);
        self
    }

    fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
        self.command.current_dir(dir);
        self
    }

    fn status(&mut self) -> Result<process::ExitStatus> {
        debug!("Running {:?}", &self.arg_log);
        self.command
            .status()
            .chain_err(|| self.command_failed_error())
    }

    fn command_failed_error(&self) -> ErrorKind {
        ErrorKind::CommandFailed(self.arg_log.clone())
    }
}

#[test]
fn os_command_runner_runs_commands() {
    let runner = OsCommandRunner::new();
    assert!(runner.build("true").status().unwrap().success());
    assert!(!runner.build("false").status().unwrap().success());
}

/// Support for running commands in test mode.
#[derive(Debug)]
pub struct TestCommandRunner {
    /// The commands that have been executed.  Because we want to avoid
    /// borrow checker hell, we use `Rc<RefCell<_>>` to implement a shared,
    /// mutable value.
    cmds: Rc<RefCell<Vec<Vec<OsString>>>>,
}

impl TestCommandRunner {
    /// Create a new `TestCommandRunner`.
    pub fn new() -> TestCommandRunner {
        TestCommandRunner {
            cmds: Rc::new(RefCell::new(vec![])),
        }
    }

    /// Access the list of commands run.
    pub fn cmds(&self) -> Ref<'_, Vec<Vec<OsString>>> {
        self.cmds.borrow()
    }
}

// This mostly exists to shut Clippy up, because Clippy doesn't like
// zero-argument `new` without a `default` implementation as well.
impl Default for TestCommandRunner {
    fn default() -> TestCommandRunner {
        TestCommandRunner::new()
    }
}

impl CommandRunner for TestCommandRunner {
    type Command = TestCommand;

    fn build<S: AsRef<OsStr>>(&self, program: S) -> Self::Command {
        TestCommand {
            cmd: vec![program.as_ref().to_owned()],
            cmds: self.cmds.clone(),
        }
    }
}

/// A fake command that gets logged to a `TestCommandRunner` instead of
/// actually getting run.
#[derive(Debug)]
pub struct TestCommand {
    /// The command we're building.
    cmd: Vec<OsString>,
    /// The list of commands we share with our `TestCommandRunner`, into which
    /// we'll insert `self.cmd` just before running.
    cmds: Rc<RefCell<Vec<Vec<OsString>>>>,
}

impl TestCommand {
    /// Record the execution of this command.
    fn record_execution(&self) {
        self.cmds.borrow_mut().push(self.cmd.clone());
    }
}

impl Command for TestCommand {
    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
        self.cmd.push(arg.as_ref().to_owned());
        self
    }

    fn env<K, V>(&mut self, _key: K, _val: V) -> &mut Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        // Just ignore this in test mode.
        self
    }

    fn current_dir<P: AsRef<Path>>(&mut self, _dir: P) -> &mut Self {
        // Just ignore this in test mode.
        self
    }

    fn status(&mut self) -> Result<process::ExitStatus> {
        self.record_execution();

        // There's no portable way to build an `ExitStatus` in portable
        // Rust without actually running a command, so just choose an
        // inoffensive one with the result we want.
        process::Command::new("true")
            .status()
            .chain_err(|| self.command_failed_error())
    }

    fn command_failed_error(&self) -> ErrorKind {
        ErrorKind::CommandFailed(self.cmd.clone())
    }
}

/// A macro for comparing the commands that were run with the commands we
/// hoped were run.  This is a bit trickier than you'd expect, because we
/// need to handle two complications:
///
/// 1. `TestCommandRunner::cmds` returns a `Ref<_>` type, which we
///    need to explicitly `deref()` before trying to compare, so that
///    we get a real `&`-style reference.
/// 2. We want to allow this macro to be passed a mix of `&'static str`
///    and `Path` objects so that it's easier to use, and internally
///    convert everything to an `OsString`.  So we need an internal `coerce`
///    helper that converts from `AsRef<OsStr>` (a very general trait
///    interface) to an actual `OsString`.
#[allow(unused_macros)]
macro_rules! assert_ran {
    ($runner:expr, { $( [ $($arg:expr),+ $(,)? ] ),* }) => {
        use std::ops::Deref;
        fn coerce<S: AsRef<::std::ffi::OsStr>>(s: S) ->
            ::std::ffi::OsString
        {
            s.as_ref().to_owned()
        }
        let expected = vec!( $( vec!( $( coerce($arg) ),+ ) ),* );
        assert_eq!($runner.cmds().deref(), &expected);
    };
}

#[test]
pub fn test_command_runner_logs_commands() {
    let runner = TestCommandRunner::new();

    let exit_code = runner
        .build("git")
        .args(&["clone", "https://github.com/torvalds/linux"])
        .status()
        .unwrap();
    assert!(exit_code.success());

    runner.build("echo").arg("a").arg("b").status().unwrap();

    assert_ran!(runner, {
        ["git", "clone", "https://github.com/torvalds/linux"],
        ["echo", "a", "b"]
    });
}