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
//! Test binary generation for integration tests under Cargo.
//!
//! If you have integration tests for things that involve subprocess management,
//! inter-process communication, or platform tools, you might need to write some
//! mock binaries of your own to test against. And if you're already using Cargo
//! to build and test, it would be nice to be able to write those test binaries
//! in Rust, in the crate you're testing, as crate binaries themselves.
//!
//! This crate provides a simple interface for invoking Cargo to build test
//! binaries in your own crate, defined in your `Cargo.toml`. Call
//! [`build_mock_binary("name_of_binary")`](build_mock_binary) where
//! `"name_of_binary"` is the binary name you'd pass to Cargo eg. `cargo build
//! --bin name_of_binary`. If you need to change profiles or features, there is
//! [`build_mock_binary_with_opts()`].
//!
//! Here's an example of how you might use this in a test, with a binary named
//! `test_it_builds`
//!
//! ```rust
//! # use test_binary::build_mock_binary;
//! let test_bin_path = build_mock_binary("test_it_builds").expect("Error building test binary");
//! let mut test_bin_subproc = std::process::Command::new(test_bin_path)
//!     .spawn()
//!     .expect("Error running test binary");
//!
//! // Test behaviour of your program against the mock binary eg. send it
//! // something on stdin and assert what it prints on stdout, do some IPC,
//! // check for side effects.
//!
//! assert!(test_bin_subproc
//!     .wait()
//!     .expect("Error waiting for test binary")
//!     .success());
//! ```
//!
//! The result returned by these functions contains the path of the built binary
//! as a [`std::ffi::OsString`], which can be passed to
//! [`std::process::Command`] or other crates that deal with subprocesses. The
//! path is not resolved to an absolute path, although it might be one anyway.
//! Since it is the path provided by Cargo after being invoked in the current
//! process' working directory, it will be valid as long as you do not change
//! the working directory between obtaining it and using it.

#![warn(missing_docs, missing_debug_implementations)]
#![cfg_attr(docsrs, feature(doc_cfg))]

use cargo_metadata::Message;
use std::ffi::OsString;
use std::io::{BufReader, Read};
use std::process::{Command, Stdio};

// For the build_mock_binary_once macro.
pub use once_cell;
pub use paste;

/// Builds the binary crate we've prepared solely for the purposes of testing
/// this library. This goes through cargo, so it should function identically to
/// `cargo build --bin testbin`.
pub fn build_mock_binary(name: &str) -> Result<OsString, TestBinaryError> {
    build_mock_binary_with_opts(name, None, [])
}

/// Same as [`build_mock_binary()`] but accepts additional arguments to specify
/// the build profile and features. To leave the profile as the default pass
/// `None`.
pub fn build_mock_binary_with_opts<'a, T>(
    name: &str,
    profile: Option<&str>,
    features: T,
) -> Result<OsString, TestBinaryError>
where
    T: IntoIterator<Item = &'a str>,
{
    let cargo_path = env!("CARGO");

    let mut cargo_args = vec!["build", "--message-format=json", "-q", "--bin", name];

    if let Some(prof) = profile {
        cargo_args.push("--profile");
        cargo_args.push(prof);
    }

    for feature in features {
        cargo_args.push("--features");
        cargo_args.push(feature);
    }

    let mut cargo_command = Command::new(cargo_path)
        .args(cargo_args)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    let reader = BufReader::new(
        // The child process' stdout being None is legitimately a programming
        // error, since we created it ourselves two lines ago.
        cargo_command
            .stdout
            .take()
            .expect("cargo subprocess output has already been claimed"),
    );
    let mut messages = Message::parse_stream(reader);
    let binary_path = messages
        .find_map(|m| match m {
            Ok(Message::CompilerArtifact(artf)) if (artf.target.name == name) => Some(artf),
            _ => None,
        })
        .and_then(|a| a.executable)
        .ok_or_else(|| TestBinaryError::BinaryNotFound(name.to_owned()));

    // See above re. stderr being None.
    let mut error_reader = BufReader::new(
        cargo_command
            .stderr
            .take()
            .expect("cargo subprocess error output has already been claimed"),
    );

    let mut error_msg = String::new();

    error_reader.read_to_string(&mut error_msg)?;

    if cargo_command.wait()?.success() {
        Ok(binary_path?.into())
    } else {
        Err(TestBinaryError::CargoFailure(error_msg))
    }
}

/// Error type for build result.
#[derive(thiserror::Error, Debug)]
pub enum TestBinaryError {
    /// An error running cargo itself.
    #[error("error running cargo")]
    CargoRunError(#[from] std::io::Error),
    /// Cargo ran but did not succeed.
    #[error("cargo failed (message is stderr only)")]
    CargoFailure(String),
    /// Cargo ran and seemed to succeed but the requested binary did not appear
    /// in its build output.
    #[error(r#"could not find binary "{0}" in cargo output"#)]
    BinaryNotFound(String),
}

/// Generate a singleton function to save invoking cargo multiple times for the
/// same binary. This is useful when you have many integration tests that use
/// the one test binary, and don't want to invoke Cargo over and over for each
/// one.
///
/// Calling `build_mock_binary_once(binary_name)` (no quotes) will generate a
/// function `binary_name_path()` that returns the path of the built test binary
/// as an `OsString`, just like `build_mock_binary("binary_name")` would. Unlike
/// `build_mock_binary("binary_name")`, the generated function will only build
/// the binary once, and only on the first call. Subsequent calls will use a
/// cached path and assume the initial build is still valid. The generated
/// function unwraps the result internally and will panic on build errors.
///
/// For example, if you use `build_mock_binary_once(test_it_builds)` in
/// `tests/common/mod.rs`, that module will then contain a function
/// `test_it_builds_path() -> std::ffi::OsString`. Multiple integration tests
/// can then use `common::test_it_builds_path()` to obtain the path. Cargo will
/// only be run once for this binary, even if the integration
/// tests that use it are being run in multiple threads.
///
/// See this module's own integration tests for an example. If you need to use
/// the extra arguments from [`build_mock_binary_with_opts()`] you will need to
/// implement it manually.
#[macro_export]
macro_rules! build_mock_binary_once {
    ($name:ident) => {
        $crate::paste::paste! {
            pub fn [<$name _path>]() -> std::ffi::OsString {
                use $crate::once_cell::sync::Lazy;
                use std::ffi::OsString;

                static [<$name _path_lazy>]: Lazy<OsString> =
                    Lazy::new(|| $crate::build_mock_binary(stringify!($name)).unwrap());
                [<$name _path_lazy>].clone()
            }
        }
    };
}