subprocess_test/lib.rs
1//! This crate exposes single utility macro `subprocess_test`
2//!
3//! Macro generates test function code in such a way that first test code block
4//! is executed in separate subprocess by re-invoking current test executable.
5//! Its output is captured, filtered a bit and then fed to verification function.
6//! Test decides whether it's in normal or subprocess mode through marker environment variable
7//!
8//! Used when one needs to either run some test in isolation or validate test output
9//! regardless of its proper completion, i.e. even if it aborts
10//!
11//! # Small examples
12//!
13//! ```rust
14//! subprocess_test::subprocess_test! {
15//! #[test]
16//! fn just_success() {
17//! let value = 1;
18//! assert_eq!(value + 1, 2);
19//! }
20//!
21//! /// Test's doc comments are supported just fine
22//! #[test]
23//! fn one_plus_one() {
24//! println!("{}", 1 + 1);
25//! }
26//! verify |success, output| {
27//! assert!(success);
28//! assert_eq!(output, "2\n");
29//! }
30//!
31//! #[test]
32//! fn test_with_result() -> Result<(), ()> {
33//! print!("{}", 1 + 1);
34//! Ok(())
35//! }
36//! verify |success, output| {
37//! if success && output == "2" {
38//! Ok(())
39//! } else {
40//! Err(())
41//! }
42//! }
43//! }
44//! ```
45//!
46//! # Usage
47//!
48//! ```rust
49//! // Single macro invocation can include multiple test function definitions,
50//! // but not other functions or lang items
51//! subprocess_test::subprocess_test! {
52//! /// You can specify doc comments for your subprocess test,
53//! /// but only before `#[test]` attribute
54//! // Mandatory test marker attribute; parens are needed
55//! // only if any attribute parameters are specified.
56//! //
57//! // Please also note that this attribute must be first,
58//! // and its optional parameters must maintain order.
59//! // This is due to limitations of Rust's macro-by-example.
60//! #[test(
61//! // Optionally specify name of environment variable used to mark subprocess mode.
62//! // Default name is "__TEST_RUN_SUBPROCESS__", so in very improbable case case
63//! // you're getting name collision here, you can change it.
64//! env_var_name = "RUN_SUBPROCESS_ENV_VAR",
65//! // While subprocess is executed using `cargo test -q -- --nocapture`,
66//! // there's still some output from test harness.
67//! // To filter it out, test prints two boundary lines, in the beginning
68//! // and in the end of test's output, regardless if it succeeds or panics.
69//! // The default boundary line is "========================================",
70//! // so in rare case you expect conflict with actual test output, you can use
71//! // this parameter to set custom output boundary.
72//! output_boundary = "<><><><><><><><>",
73//! )]
74//! // Any other attributes are allowed, yet are optional
75//! #[ignore]
76//! // Test can have any valid name, same as normal test function
77//! fn dummy() {
78//! // This block is intended to generate test output,
79//! // although it can be used as normal test body
80//! println!("Foo");
81//! eprintln!("Bar");
82//! }
83//! // `verify` block is optional;
84//! // if absent, it's substituted with block which just asserts that subprocess succeeded
85//! // and prints test output in case of failure
86//! //
87//! // Parameters can be any names. Their meanings:
88//! // * `success` - boolean which is `true` if subprocess succeeded
89//! // * `output` - subprocess output collected into string, both `stdout` and `stderr`
90//! verify |success, output| {
91//! // This block is run as normal part of test and in general must succeed
92//! assert!(success);
93//! assert_eq!(output, "Foo\nBar\n");
94//! }
95//!
96//! #[test]
97//! // Test writer can use explicit `Result` type, like with normal test functions.
98//! // In this case, `verify` block is mandatory, and both main test block and `verify`
99//! // block must return same result type
100//! fn test_returns_result() -> Result<(), String> {
101//! Ok(())
102//! }
103//! verify |success, output| {
104//! if success && output.is_empty() {
105//! Ok(())
106//! } else {
107//! Err("Oopsie, test failed!")
108//! }
109//! }
110//! }
111//! ```
112//!
113//! # Limitations
114//!
115//! Macro doesn't work well with `#[should_panic]` attribute because there's only one test function
116//! which runs in two modes. If subprocess test panics as expected, subprocess succeeds, and
117//! `verify` block must panic too. Just use `verify` block and do any checks you need there.
118//!
119//! Another minor limitation, as described in [#Usage] section, is that first goes doc comment,
120//! then mandatory `#[test]` attribute with extensions, then any other attributes,
121//! then function body and `verify` block.
122//!
123//! If test writer uses explicit result type and forgets to write `verify` block, he'll get error
124//! like "expected return value `Result<_, _>`, got `()" instead of possibly more comprehensive
125//! "missing `verify` block". Again, this is due to limitations of macro-by-example
126use std::borrow::Cow;
127use std::env::{args_os, var_os};
128use std::fs::File;
129use std::io::{Read, Seek, SeekFrom};
130use std::process::{Command, Stdio};
131
132use defer::defer;
133use tempfile::tempfile;
134/// Implementation of `subprocess_test` macro. See crate-level documentation for details and usage examples
135#[macro_export]
136macro_rules! subprocess_test {
137 (
138 $(
139 $(#[doc = $doc_lit:literal])*
140 #[test $((
141 $(env_var_name = $subp_var_name:literal $(,)?)?
142 $(output_boundary = $subp_output_boundary:literal $(,)?)?
143 ))?]
144 $(#[$attrs:meta])*
145 fn $test_name:ident () $(-> $test_result:ty)? $test_block:block
146 $(verify |$success_param:ident, $stdout_param:ident| $verify_block:block)?
147 )*
148 ) => {
149 $(
150 $(#[doc = $doc_lit])*
151 #[test]
152 $(#[$attrs])*
153 fn $test_name() $(-> $test_result)? {
154 $crate::run_subprocess_test(
155 concat!(module_path!(), "::", stringify!($test_name)),
156 $crate::subprocess_test! {
157 @tokens_or_default { $($(Some($subp_var_name))?)? }
158 or { None }
159 },
160 $crate::subprocess_test! {
161 @tokens_or_default { $($(Some($subp_output_boundary))?)? }
162 or { None }
163 },
164 || $test_block,
165 $crate::subprocess_test! {
166 @tokens_or_default {
167 $(|$success_param, $stdout_param| $verify_block)?
168 } or {
169 // NB: we inject closure here, to make panic report its location
170 // at macro expansion
171 |success, output| {
172 if !success {
173 eprintln!("{output}");
174 // In case panic location will point to whole macro start,
175 // you'll get at least test name
176 panic!("Test {} subprocess failed", stringify!($test_name));
177 }
178 }
179 }
180 },
181 )
182 }
183 )*
184 };
185 (
186 @tokens_or_default { $($tokens:tt)+ } or { $($_:tt)* }
187 ) => {
188 $($tokens)+
189 };
190 (
191 @tokens_or_default { } or { $($tokens:tt)* }
192 ) => {
193 $($tokens)*
194 };
195}
196
197#[doc(hidden)]
198pub fn run_subprocess_test<R>(
199 full_test_name: &str,
200 var_name: Option<&str>,
201 boundary: Option<&str>,
202 test_fn: impl FnOnce() -> R,
203 verify_fn: impl FnOnce(bool, String) -> R,
204) -> R {
205 const DEFAULT_SUBPROCESS_ENV_VAR_NAME: &str = "__TEST_RUN_SUBPROCESS__";
206 const DEFAULT_OUTPUT_BOUNDARY: &str = "\n========================================\n";
207
208 let full_test_name = &full_test_name[full_test_name
209 .find("::")
210 .expect("Full test path is expected to include crate name")
211 + 2..];
212 let var_name = var_name.unwrap_or(DEFAULT_SUBPROCESS_ENV_VAR_NAME);
213 let boundary: Cow<'static, str> = if let Some(boundary) = boundary {
214 format!("\n{boundary}\n").into()
215 } else {
216 DEFAULT_OUTPUT_BOUNDARY.into()
217 };
218 // If test phase is requested, execute it and bail immediately
219 if var_os(var_name).is_some() {
220 print!("{boundary}");
221 // We expect that in case of panic we'll get test harness footer,
222 // but in case of abort we won't get it, so finisher won't be needed
223 defer! { print!("{boundary}") };
224 return test_fn();
225 }
226 // Otherwise, perform main runner phase.
227 // Just run same executable but with different options
228 let (tmpfile, stdout, stderr) = tmpfile_buffer();
229 let exe_path = args_os().next().expect("Test executable path not found");
230
231 let success = Command::new(exe_path)
232 .args([
233 "--include-ignored",
234 "--nocapture",
235 "--quiet",
236 "--exact",
237 "--test",
238 ])
239 .arg(full_test_name)
240 .env(var_name, "")
241 .stdin(Stdio::null())
242 .stdout(stdout)
243 .stderr(stderr)
244 .status()
245 .expect("Failed to execute test as subprocess")
246 .success();
247
248 let mut output = read_file(tmpfile);
249 let boundary_at = output
250 .find(&*boundary)
251 .expect("Subprocess output should always include at least one boundary");
252
253 output.replace_range(..(boundary_at + boundary.len()), "");
254
255 if let Some(boundary_at) = output.find(&*boundary) {
256 output.truncate(boundary_at);
257 }
258
259 verify_fn(success, output)
260}
261
262fn tmpfile_buffer() -> (File, File, File) {
263 let file = tempfile().expect("Failed to create temporary file for subprocess output");
264 let stdout = file
265 .try_clone()
266 .expect("Failed to clone tmpfile descriptor");
267 let stderr = file
268 .try_clone()
269 .expect("Failed to clone tmpfile descriptor");
270
271 (file, stdout, stderr)
272}
273
274fn read_file(mut file: File) -> String {
275 file.seek(SeekFrom::Start(0))
276 .expect("Rewind to start failed");
277
278 let mut buffer = String::new();
279 file.read_to_string(&mut buffer)
280 .expect("Failed to read file into buffer");
281
282 buffer
283}
284
285subprocess_test! {
286 #[test]
287 fn name_collision() {
288 println!("One");
289 }
290 verify |success, output| {
291 assert!(success);
292 assert_eq!(output, "One\n");
293 }
294
295 #[test]
296 fn simple_success() {
297 let value = 1;
298 assert_eq!(value + 1, 2);
299 }
300
301 #[test]
302 fn simple_verify() {
303 println!("Simple verify test");
304 }
305 verify |success, output| {
306 assert!(success);
307 assert_eq!(output, "Simple verify test\n");
308 }
309
310 #[test]
311 fn simple_failure() {
312 panic!("Oopsie!");
313 }
314 verify |success, output| {
315 assert!(!success);
316 // Note that panic output contains stacktrace and other stuff
317 assert!(output.contains("Oopsie!\n"));
318 }
319
320 #[test(
321 env_var_name = "__CUSTOM_SUBPROCESS_VAR__"
322 )]
323 fn custom_var() {
324 assert!(var_os("__CUSTOM_SUBPROCESS_VAR__").is_some());
325 }
326
327 #[test(
328 output_boundary = "!!!!!!!!!!!!!!!!"
329 )]
330 fn custom_boundary() {
331 println!("One");
332 println!("Two");
333 println!("\n!!!!!!!!!!!!!!!!\n");
334 println!("Three");
335 }
336 verify |success, output| {
337 assert!(success);
338 assert_eq!(output, "One\nTwo\n");
339 }
340
341 #[test]
342 #[should_panic]
343 fn should_panic_test() {
344 panic!("Oopsie!");
345 }
346 verify |success, _output| {
347 assert!(!success, "Correct result should cause panic");
348 }
349
350 #[test]
351 fn test_aborts() {
352 println!("Banana");
353 eprintln!("Mango");
354 std::process::abort();
355 }
356 verify |success, output| {
357 assert!(!success);
358 assert_eq!(output, "Banana\nMango\n");
359 }
360
361 /// Checks that positive test with result works as intended
362 #[test]
363 fn positive_test_result() -> Result<(), ()> {
364 print!("Result succeeds");
365 Ok(())
366 }
367 verify |success, output| {
368 if success && output == "Result succeeds" {
369 Ok(())
370 } else {
371 Err(())
372 }
373 }
374
375 /// Checks that negative test with result works as intended
376 #[test]
377 fn negative_test_result() -> Result<(), ()> {
378 print!("Result fails");
379 Err(())
380 }
381 verify |success, output| {
382 if !success && output == "Result fails" {
383 Ok(())
384 } else {
385 Err(())
386 }
387 }
388
389 #[test]
390 fn panicking_test_result() -> Result<(), ()> {
391 panic!("Result panics")
392 }
393 verify |success, output| {
394 if !success && output.contains("Result panics") {
395 Ok(())
396 } else {
397 Err(())
398 }
399 }
400}
401
402#[cfg(test)]
403mod submodule_tests {
404 use std::sync::atomic::{AtomicUsize, Ordering};
405 // Used to check that only single test is run per subprocess
406 static COMMON_PREFIX_COUNTER: AtomicUsize = AtomicUsize::new(0);
407
408 subprocess_test! {
409 #[test]
410 fn submodule_test() {
411 let value = 1;
412 assert_eq!(value + 1, 2);
413 }
414
415 #[test]
416 fn common_prefix() {
417 print!("One");
418 COMMON_PREFIX_COUNTER.fetch_add(1, Ordering::Relaxed);
419 assert_eq!(COMMON_PREFIX_COUNTER.load(Ordering::Relaxed), 1);
420 }
421 verify |success, output| {
422 assert!(success);
423 assert_eq!(output, "One");
424 }
425
426 #[test]
427 fn common_prefix_2() {
428 print!("Two");
429 COMMON_PREFIX_COUNTER.fetch_add(1, Ordering::Relaxed);
430 assert_eq!(COMMON_PREFIX_COUNTER.load(Ordering::Relaxed), 1);
431 }
432 verify |success, output| {
433 assert!(success);
434 assert_eq!(output, "Two");
435 }
436 }
437
438 mod common_prefix {
439 subprocess_test! {
440 #[test]
441 fn inner() {
442 print!("Three");
443 super::COMMON_PREFIX_COUNTER.fetch_add(1, super::Ordering::Relaxed);
444 assert_eq!(super::COMMON_PREFIX_COUNTER.load(super::Ordering::Relaxed), 1);
445 }
446 verify |success, output| {
447 assert!(success);
448 assert_eq!(output, "Three");
449 }
450 }
451 }
452}