assert_cli/
assert.rs

1use std::default;
2use std::ffi::{OsStr, OsString};
3use std::io::Write;
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
6use std::vec::Vec;
7
8use environment::Environment;
9use failure;
10use failure::Fail;
11
12use errors::*;
13use output::{Content, Output, OutputKind, OutputPredicate};
14
15/// Assertions for a specific command.
16#[derive(Debug)]
17#[must_use]
18pub struct Assert {
19    cmd: Vec<OsString>,
20    env: Environment,
21    current_dir: Option<PathBuf>,
22    expect_success: Option<bool>,
23    expect_exit_code: Option<i32>,
24    expect_output: Vec<OutputPredicate>,
25    stdin_contents: Option<Vec<u8>>,
26}
27
28impl default::Default for Assert {
29    /// Construct an assert using `cargo run --` as command.
30    ///
31    /// Defaults to asserting _successful_ execution.
32    fn default() -> Self {
33        Assert {
34            cmd: vec![
35                "cargo",
36                "run",
37                #[cfg(not(debug_assertions))]
38                "--release",
39                "--quiet",
40                "--",
41            ].into_iter()
42                .map(OsString::from)
43                .collect(),
44            env: Environment::inherit(),
45            current_dir: None,
46            expect_success: Some(true),
47            expect_exit_code: None,
48            expect_output: vec![],
49            stdin_contents: None,
50        }
51    }
52}
53
54impl Assert {
55    /// Run the crate's main binary.
56    ///
57    /// Defaults to asserting _successful_ execution.
58    pub fn main_binary() -> Self {
59        Assert::default()
60    }
61
62    /// Run a specific binary of the current crate.
63    ///
64    /// Defaults to asserting _successful_ execution.
65    pub fn cargo_binary<S: AsRef<OsStr>>(name: S) -> Self {
66        Assert {
67            cmd: vec![
68                OsStr::new("cargo"),
69                OsStr::new("run"),
70                #[cfg(not(debug_assertions))]
71                OsStr::new("--release"),
72                OsStr::new("--quiet"),
73                OsStr::new("--bin"),
74                name.as_ref(),
75                OsStr::new("--"),
76            ].into_iter()
77                .map(OsString::from)
78                .collect(),
79            ..Self::default()
80        }
81    }
82
83    /// Run a specific example of the current crate.
84    ///
85    /// Defaults to asserting _successful_ execution.
86    pub fn example<S: AsRef<OsStr>>(name: S) -> Self {
87        Assert {
88            cmd: vec![
89                OsStr::new("cargo"),
90                OsStr::new("run"),
91                #[cfg(not(debug_assertions))]
92                OsStr::new("--release"),
93                OsStr::new("--quiet"),
94                OsStr::new("--example"),
95                name.as_ref(),
96                OsStr::new("--"),
97            ].into_iter()
98                .map(OsString::from)
99                .collect(),
100            ..Self::default()
101        }
102    }
103
104    /// Run a custom command.
105    ///
106    /// Defaults to asserting _successful_ execution.
107    ///
108    /// # Examples
109    ///
110    /// ```rust
111    /// extern crate assert_cli;
112    ///
113    /// assert_cli::Assert::command(&["echo", "1337"])
114    ///     .unwrap();
115    /// ```
116    pub fn command<S: AsRef<OsStr>>(cmd: &[S]) -> Self {
117        Assert {
118            cmd: cmd.into_iter().map(OsString::from).collect(),
119            ..Self::default()
120        }
121    }
122
123    /// Add arguments to the command.
124    ///
125    /// # Examples
126    ///
127    /// ```rust
128    /// extern crate assert_cli;
129    ///
130    /// assert_cli::Assert::command(&["echo"])
131    ///     .with_args(&["42"])
132    ///     .stdout().contains("42")
133    ///     .unwrap();
134    ///
135    /// ```
136    pub fn with_args<S: AsRef<OsStr>>(mut self, args: &[S]) -> Self {
137        self.cmd.extend(args.into_iter().map(OsString::from));
138        self
139    }
140
141    /// Add stdin to the command.
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// extern crate assert_cli;
147    ///
148    /// assert_cli::Assert::command(&["cat"])
149    ///     .stdin("42")
150    ///     .stdout().contains("42")
151    ///     .unwrap();
152    /// ```
153    pub fn stdin<S: Into<Vec<u8>>>(mut self, contents: S) -> Self {
154        self.stdin_contents = Some(contents.into());
155        self
156    }
157
158    /// Sets the working directory for the command.
159    ///
160    /// # Examples
161    ///
162    /// ```rust
163    /// extern crate assert_cli;
164    ///
165    /// assert_cli::Assert::command(&["wc", "lib.rs"])
166    ///     .current_dir(std::path::Path::new("src"))
167    ///     .stdout().contains("lib.rs")
168    ///     .execute()
169    ///     .unwrap();
170    /// ```
171    pub fn current_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
172        self.current_dir = Some(dir.into());
173        self
174    }
175
176    /// Sets environments variables for the command.
177    ///
178    /// # Examples
179    ///
180    /// ```rust
181    /// extern crate assert_cli;
182    ///
183    /// assert_cli::Assert::command(&["printenv"])
184    ///     .with_env(&[("TEST_ENV", "OK")])
185    ///     .stdout().contains("TEST_ENV=OK")
186    ///     .execute()
187    ///     .unwrap();
188    ///
189    /// let env = assert_cli::Environment::empty()
190    ///     .insert("FOO", "BAR");
191    ///
192    /// assert_cli::Assert::command(&["printenv"])
193    ///     .with_env(&env)
194    ///     .stdout().is("FOO=BAR")
195    ///     .execute()
196    ///     .unwrap();
197    ///
198    /// ::std::env::set_var("BAZ", "BAR");
199    ///
200    /// assert_cli::Assert::command(&["printenv"])
201    ///     .stdout().contains("BAZ=BAR")
202    ///     .execute()
203    ///     .unwrap();
204    /// ```
205    pub fn with_env<E: Into<Environment>>(mut self, env: E) -> Self {
206        self.env = env.into();
207
208        self
209    }
210
211    /// Small helper to make chains more readable.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// extern crate assert_cli;
217    ///
218    /// assert_cli::Assert::command(&["cat", "non-existing-file"])
219    ///     .fails()
220    ///     .and()
221    ///     .stderr().contains("non-existing-file")
222    ///     .unwrap();
223    /// ```
224    pub fn and(self) -> Self {
225        self
226    }
227
228    /// Expect the command to be executed successfully.
229    ///
230    /// # Examples
231    ///
232    /// ```rust
233    /// extern crate assert_cli;
234    ///
235    /// assert_cli::Assert::command(&["echo", "42"])
236    ///     .succeeds()
237    ///     .unwrap();
238    /// ```
239    pub fn succeeds(mut self) -> Self {
240        self.expect_exit_code = None;
241        self.expect_success = Some(true);
242        self
243    }
244
245    /// Expect the command to fail.
246    ///
247    /// Note: This does not include shell failures like `command not found`. I.e. the
248    ///       command must _run_ and fail for this assertion to pass.
249    ///
250    /// # Examples
251    ///
252    /// ```rust
253    /// extern crate assert_cli;
254    ///
255    /// assert_cli::Assert::command(&["cat", "non-existing-file"])
256    ///     .fails()
257    ///     .and()
258    ///     .stderr().contains("non-existing-file")
259    ///     .unwrap();
260    /// ```
261    pub fn fails(mut self) -> Self {
262        self.expect_success = Some(false);
263        self
264    }
265
266    /// Expect the command to fail and return a specific error code.
267    ///
268    /// # Examples
269    ///
270    /// ```rust
271    /// extern crate assert_cli;
272    ///
273    /// assert_cli::Assert::command(&["cat", "non-existing-file"])
274    ///     .fails_with(1)
275    ///     .and()
276    ///     .stderr().is("cat: non-existing-file: No such file or directory")
277    ///     .unwrap();
278    /// ```
279    pub fn fails_with(mut self, expect_exit_code: i32) -> Self {
280        self.expect_success = Some(false);
281        self.expect_exit_code = Some(expect_exit_code);
282        self
283    }
284
285    /// Do not care whether the command exits successfully or if it fails.
286    ///
287    /// This function removes any assertions that were already set, including
288    /// any expected exit code that was set with [`fails_with`].
289    ///
290    /// # Examples
291    ///
292    /// ```rust
293    /// extern crate assert_cli;
294    ///
295    /// assert_cli::Assert::command(&["cat", "non-existing-file"])
296    ///     .ignore_status()
297    ///     .and()
298    ///     .stderr().is("cat: non-existing-file: No such file or directory")
299    ///     .unwrap();
300    /// ```
301    ///
302    /// [`fails_with`]: #method.fails_with
303    pub fn ignore_status(mut self) -> Self {
304        self.expect_exit_code = None;
305        self.expect_success = None;
306        self
307    }
308
309    /// Create an assertion for stdout's contents
310    ///
311    /// # Examples
312    ///
313    /// ```rust
314    /// extern crate assert_cli;
315    ///
316    /// assert_cli::Assert::command(&["echo", "42"])
317    ///     .stdout().contains("42")
318    ///     .unwrap();
319    /// ```
320    pub fn stdout(self) -> OutputAssertionBuilder {
321        OutputAssertionBuilder {
322            assertion: self,
323            kind: OutputKind::StdOut,
324        }
325    }
326
327    /// Create an assertion for stdout's contents
328    ///
329    /// # Examples
330    ///
331    /// ```rust
332    /// extern crate assert_cli;
333    ///
334    /// assert_cli::Assert::command(&["cat", "non-existing-file"])
335    ///     .fails_with(1)
336    ///     .and()
337    ///     .stderr().is("cat: non-existing-file: No such file or directory")
338    ///     .unwrap();
339    /// ```
340    pub fn stderr(self) -> OutputAssertionBuilder {
341        OutputAssertionBuilder {
342            assertion: self,
343            kind: OutputKind::StdErr,
344        }
345    }
346
347    /// Execute the command and check the assertions.
348    ///
349    /// # Examples
350    ///
351    /// ```rust
352    /// extern crate assert_cli;
353    ///
354    /// let test = assert_cli::Assert::command(&["echo", "42"])
355    ///     .stdout().contains("42")
356    ///     .execute();
357    /// assert!(test.is_ok());
358    /// ```
359    pub fn execute(self) -> Result<(), AssertionError> {
360        let bin = &self.cmd[0];
361
362        let args: Vec<_> = self.cmd.iter().skip(1).collect();
363        let mut command = Command::new(bin);
364        let command = command
365            .stdin(Stdio::piped())
366            .stdout(Stdio::piped())
367            .stderr(Stdio::piped())
368            .env_clear()
369            .envs(self.env.clone().compile())
370            .args(&args);
371
372        let command = match self.current_dir {
373            Some(ref dir) => command.current_dir(dir),
374            None => command,
375        };
376
377        let mut spawned = command
378            .spawn()
379            .chain_with(|| AssertionError::new(self.cmd.clone()))?;
380
381        if let Some(ref contents) = self.stdin_contents {
382            spawned
383                .stdin
384                .as_mut()
385                .expect("Couldn't get mut ref to command stdin")
386                .write_all(contents)
387                .chain_with(|| AssertionError::new(self.cmd.clone()))?;
388        }
389        let output = spawned
390            .wait_with_output()
391            .chain_with(|| AssertionError::new(self.cmd.clone()))?;
392
393        if let Some(expect_success) = self.expect_success {
394            let actual_success = output.status.success();
395            if expect_success != actual_success {
396                return Err(
397                    AssertionError::new(self.cmd.clone()).chain(StatusError::new(
398                        actual_success,
399                        output.stdout.clone(),
400                        output.stderr.clone(),
401                    )),
402                )?;
403            }
404        }
405
406        if self.expect_exit_code.is_some() && self.expect_exit_code != output.status.code() {
407            return Err(
408                AssertionError::new(self.cmd.clone()).chain(ExitCodeError::new(
409                    self.expect_exit_code,
410                    output.status.code(),
411                    output.stdout.clone(),
412                    output.stderr.clone(),
413                )),
414            );
415        }
416
417        self.expect_output
418            .iter()
419            .map(|a| {
420                a.verify(&output)
421                    .chain_with(|| AssertionError::new(self.cmd.clone()))
422            })
423            .collect::<Result<Vec<()>, AssertionError>>()?;
424
425        Ok(())
426    }
427
428    /// Execute the command, check the assertions, and panic when they fail.
429    ///
430    /// # Examples
431    ///
432    /// ```rust,should_panic="Assert CLI failure"
433    /// extern crate assert_cli;
434    ///
435    /// assert_cli::Assert::command(&["echo", "42"])
436    ///     .fails()
437    ///     .unwrap(); // panics
438    /// ```
439    pub fn unwrap(self) {
440        if let Err(err) = self.execute() {
441            panic!(Self::format_causes(err.causes()));
442        }
443    }
444
445    fn format_causes(mut causes: failure::Causes) -> String {
446        let mut result = causes.next().expect("an error should exist").to_string();
447        for cause in causes {
448            result.push_str(&format!("\nwith: {}", cause));
449        }
450        result
451    }
452}
453
454/// Assertions for command output.
455#[derive(Debug)]
456#[must_use]
457pub struct OutputAssertionBuilder {
458    assertion: Assert,
459    kind: OutputKind,
460}
461
462impl OutputAssertionBuilder {
463    /// Expect the command's output to **contain** `output`.
464    ///
465    /// # Examples
466    ///
467    /// ```rust
468    /// extern crate assert_cli;
469    ///
470    /// assert_cli::Assert::command(&["echo", "42"])
471    ///     .stdout().contains("42")
472    ///     .unwrap();
473    /// ```
474    pub fn contains<O: Into<Content>>(mut self, output: O) -> Assert {
475        let pred = OutputPredicate::new(self.kind, Output::contains(output));
476        self.assertion.expect_output.push(pred);
477        self.assertion
478    }
479
480    /// Expect the command to output **exactly** this `output`.
481    ///
482    /// # Examples
483    ///
484    /// ```rust
485    /// extern crate assert_cli;
486    ///
487    /// assert_cli::Assert::command(&["echo", "42"])
488    ///     .stdout().is("42")
489    ///     .unwrap();
490    /// ```
491    pub fn is<O: Into<Content>>(mut self, output: O) -> Assert {
492        let pred = OutputPredicate::new(self.kind, Output::is(output));
493        self.assertion.expect_output.push(pred);
494        self.assertion
495    }
496
497    /// Expect the command's output to not **contain** `output`.
498    ///
499    /// # Examples
500    ///
501    /// ```rust
502    /// extern crate assert_cli;
503    ///
504    /// assert_cli::Assert::command(&["echo", "42"])
505    ///     .stdout().doesnt_contain("73")
506    ///     .unwrap();
507    /// ```
508    pub fn doesnt_contain<O: Into<Content>>(mut self, output: O) -> Assert {
509        let pred = OutputPredicate::new(self.kind, Output::doesnt_contain(output));
510        self.assertion.expect_output.push(pred);
511        self.assertion
512    }
513
514    /// Expect the command to output to not be **exactly** this `output`.
515    ///
516    /// # Examples
517    ///
518    /// ```rust
519    /// extern crate assert_cli;
520    ///
521    /// assert_cli::Assert::command(&["echo", "42"])
522    ///     .stdout().isnt("73")
523    ///     .unwrap();
524    /// ```
525    pub fn isnt<O: Into<Content>>(mut self, output: O) -> Assert {
526        let pred = OutputPredicate::new(self.kind, Output::isnt(output));
527        self.assertion.expect_output.push(pred);
528        self.assertion
529    }
530
531    /// Expect the command output to satisfy the given predicate.
532    ///
533    /// # Examples
534    ///
535    /// ```rust
536    /// extern crate assert_cli;
537    ///
538    /// assert_cli::Assert::command(&["echo", "-n", "42"])
539    ///     .stdout().satisfies(|x| x.len() == 2, "bad length")
540    ///     .unwrap();
541    /// ```
542    pub fn satisfies<F, M>(mut self, pred: F, msg: M) -> Assert
543    where
544        F: 'static + Fn(&str) -> bool,
545        M: Into<String>,
546    {
547        let pred = OutputPredicate::new(self.kind, Output::satisfies(pred, msg));
548        self.assertion.expect_output.push(pred);
549        self.assertion
550    }
551}
552
553#[cfg(test)]
554mod test {
555    use super::*;
556    use std::ffi::OsString;
557
558    fn command() -> Assert {
559        Assert::command(&["printenv"])
560    }
561
562    #[test]
563    fn main_binary_default_uses_active_profile() {
564        let assert = Assert::main_binary();
565
566        let expected = if cfg!(debug_assertions) {
567            OsString::from("cargo run --quiet -- ")
568        } else {
569            OsString::from("cargo run --release --quiet -- ")
570        };
571
572        assert_eq!(
573            expected,
574            assert
575                .cmd
576                .into_iter()
577                .fold(OsString::from(""), |mut cmd, token| {
578                    cmd.push(token);
579                    cmd.push(" ");
580                    cmd
581                })
582        );
583    }
584
585    #[test]
586    fn cargo_binary_default_uses_active_profile() {
587        let assert = Assert::cargo_binary("hello");
588
589        let expected = if cfg!(debug_assertions) {
590            OsString::from("cargo run --quiet --bin hello -- ")
591        } else {
592            OsString::from("cargo run --release --quiet --bin hello -- ")
593        };
594
595        assert_eq!(
596            expected,
597            assert
598                .cmd
599                .into_iter()
600                .fold(OsString::from(""), |mut cmd, token| {
601                    cmd.push(token);
602                    cmd.push(" ");
603                    cmd
604                })
605        );
606    }
607
608    #[test]
609    fn take_ownership() {
610        let x = Environment::inherit();
611
612        command()
613            .with_env(x.clone())
614            .with_env(&x)
615            .with_env(x)
616            .unwrap();
617    }
618
619    #[test]
620    fn in_place_mod() {
621        let y = Environment::empty();
622
623        let y = y.insert("key", "value");
624
625        assert_eq!(
626            y.compile(),
627            vec![(OsString::from("key"), OsString::from("value"))]
628        );
629    }
630
631    #[test]
632    fn in_place_mod2() {
633        let x = Environment::inherit();
634
635        command()
636            .with_env(&x.insert("key", "value").insert("key", "vv"))
637            .stdout()
638            .contains("key=vv")
639            .execute()
640            .unwrap();
641        // Granted, `insert` moved `x`, so we can no longer reference it, even
642        // though only a reference was passed to `with_env`
643    }
644
645    #[test]
646    fn in_place_mod3() {
647        // In-place modification while allowing later accesses to the `Environment`
648        let y = Environment::empty();
649
650        assert_eq!(
651            y.clone().insert("key", "value").compile(),
652            vec![(OsString::from("key"), OsString::from("value"))]
653        );
654
655        command()
656            .with_env(y)
657            .stdout()
658            .doesnt_contain("key=value")
659            .execute()
660            .unwrap();
661    }
662
663    #[test]
664    fn empty_env() {
665        // In-place modification while allowing later accesses to the `Environment`
666        let y = Environment::empty();
667
668        assert!(command().with_env(y).stdout().is("").execute().is_ok());
669    }
670    #[test]
671    fn take_vec() {
672        let v = vec![("bar".to_string(), "baz".to_string())];
673
674        command()
675            .with_env(&vec![("bar", "baz")])
676            .stdout()
677            .contains("bar=baz")
678            .execute()
679            .unwrap();
680
681        command()
682            .with_env(&v)
683            .stdout()
684            .contains("bar=baz")
685            .execute()
686            .unwrap();
687
688        command()
689            .with_env(&vec![("bar", "baz")])
690            .stdout()
691            .isnt("")
692            .execute()
693            .unwrap();
694    }
695
696    #[test]
697    fn take_slice_of_strs() {
698        command()
699            .with_env(&[("bar", "BAZ")])
700            .stdout()
701            .contains("bar=BAZ")
702            .execute()
703            .unwrap();
704
705        command()
706            .with_env(&[("bar", "BAZ")][..])
707            .stdout()
708            .contains("bar=BAZ")
709            .execute()
710            .unwrap();
711
712        command()
713            .with_env([("bar", "BAZ")].as_ref())
714            .stdout()
715            .contains("bar=BAZ")
716            .execute()
717            .unwrap();
718    }
719
720    #[test]
721    fn take_slice_of_strings() {
722        // same deal as above
723
724        command()
725            .with_env(&[("bar".to_string(), "BAZ".to_string())])
726            .stdout()
727            .contains("bar=BAZ")
728            .execute()
729            .unwrap();
730
731        command()
732            .with_env(&[("bar".to_string(), "BAZ".to_string())][..])
733            .stdout()
734            .contains("bar=BAZ")
735            .execute()
736            .unwrap();
737    }
738
739    #[test]
740    fn take_slice() {
741        command()
742            .with_env(&[("hey", "ho")])
743            .stdout()
744            .contains("hey=ho")
745            .execute()
746            .unwrap();
747
748        command()
749            .with_env(&[("hey", "ho".to_string())])
750            .stdout()
751            .contains("hey=ho")
752            .execute()
753            .unwrap();
754    }
755
756    #[test]
757    fn take_string_i32() {
758        command()
759            .with_env(&[("bar", 3 as i32)])
760            .stdout()
761            .contains("bar=3")
762            .execute()
763            .unwrap();
764    }
765}