rusty-fork-0.3.0/.cargo_vcs_info.json0000644000000001121366334436700132700ustar00{ "git": { "sha1": "5bf263419092c3f99556e556a51da8620551ef9e" } } rusty-fork-0.3.0/.gitignore010064400017500001750000000001051324517021500140440ustar0000000000000000*~ target Cargo.lock *.core # VSCode: .vscode/ persistence-test.txt rusty-fork-0.3.0/.travis.yml010064400017500001750000000001371366334433600142050ustar0000000000000000language: rust sudo: false dist: trusty rust: - 1.32.0 - stable - beta - nightly cache: cargo rusty-fork-0.3.0/CHANGELOG.md010064400017500001750000000017231366334433600137070ustar0000000000000000## 0.3.0 ### Breaking Changes - The minimum required Rust version is now 1.32.0. ### Improvements - `rusty_fork_test!` can now be `use`d in Rust 2018 code. - The following flags to the test process are now understood: `--ensure-time`, `--exclude-should-panic`, `--force-run-in-process`, `--include-ignored`, `--report-time`, `--show-output`. ## 0.2.2 ### Minor changes - `wait_timeout` has been bumped to `0.2.0`. ## 0.2.1 ### Bug Fixes - Dependency on `wait_timeout` crate now requires `0.1.4` rather than `0.1` since the build doesn't work with older versions. ## 0.2.0 ### Breaking changes - APIs which used to provide a `std::process::Child` now instead provide a `rusty_fork::ChildWrapper`. ### Bug fixes - Fix that using the "timeout" feature, or otherwise using `wait_timeout` on the child process, could cause an unrelated process to get killed if the child exits within the timeout. ## 0.1.1 ### Minor changes - `tempfile` updated to 3.0. rusty-fork-0.3.0/Cargo.toml0000644000000024521366334436700112770ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "rusty-fork" version = "0.3.0" authors = ["Jason Lingle"] exclude = ["/gen-readme.sh", "/readme-*.md"] description = "Cross-platform library for running Rust tests in sub-processes using a\nfork-like interface.\n" documentation = "https://docs.rs/rusty-fork" readme = "README.md" keywords = ["testing", "process", "fork"] categories = ["development-tools::testing"] license = "MIT/Apache-2.0" repository = "https://github.com/altsysrq/rusty-fork" [dependencies.fnv] version = "1.0" [dependencies.quick-error] version = "1.2" [dependencies.tempfile] version = "3.0" [dependencies.wait-timeout] version = "0.2" optional = true [dev-dependencies] [features] default = ["timeout"] timeout = ["wait-timeout"] [badges.travis-ci] repository = "AltSysrq/rusty-fork" rusty-fork-0.3.0/Cargo.toml.orig010064400017500001750000000013541366334433600147650ustar0000000000000000[package] name = "rusty-fork" version = "0.3.0" authors = ["Jason Lingle"] license = "MIT/Apache-2.0" readme = "README.md" repository = "https://github.com/altsysrq/rusty-fork" documentation = "https://docs.rs/rusty-fork" keywords = ["testing", "process", "fork"] categories = ["development-tools::testing"] exclude = ["/gen-readme.sh", "/readme-*.md"] edition = "2018" description = """ Cross-platform library for running Rust tests in sub-processes using a fork-like interface. """ [badges] travis-ci = { repository = "AltSysrq/rusty-fork" } [dependencies] fnv = "1.0" quick-error = "1.2" tempfile = "3.0" wait-timeout = { version = "0.2", optional = true } [dev-dependencies] [features] default = [ "timeout" ] timeout = [ "wait-timeout" ] rusty-fork-0.3.0/LICENSE-APACHE010064400017500001750000000251371324462772400140270ustar0000000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. rusty-fork-0.3.0/LICENSE-MIT010064400017500001750000000020441324462772400135270ustar0000000000000000Copyright (c) 2016 FullContact, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rusty-fork-0.3.0/README.md010064400017500001750000000074701366334433600133620ustar0000000000000000# rusty-fork [![Build Status](https://travis-ci.org/AltSysrq/rusty-fork.svg?branch=master)](https://travis-ci.org/AltSysrq/rusty-fork) [![](http://meritbadge.herokuapp.com/rusty-fork)](https://crates.io/crates/rusty-fork) Rusty-fork provides a way to "fork" unit tests into separate processes. There are a number of reasons to want to run some tests in isolated processes: - When tests share a process, if any test causes the process to abort, segfault, overflow the stack, etc., the entire test runner process dies. If the test is in a subprocess, only the subprocess dies and the test runner simply fails the test. - Isolating a test to a subprocess makes it possible to add a timeout to the test and forcibly terminate it and produce a normal test failure. - Tests which need to interact with some inherently global property, such as the current working directory, can do so without interfering with other tests. This crate itself provides two things: - The [`rusty_fork_test!`](macro.rusty_fork_test.html) macro, which is a simple way to wrap standard Rust tests to be run in subprocesses with optional timeouts. - The [`fork`](fn.fork.html) function which can be used as a building block to make other types of process isolation strategies. ## Quick Start If you just want to run normal Rust tests in isolated processes, getting started is pretty quick. In `Cargo.toml`, add ```toml [dev-dependencies] rusty-fork = "0.3.0" ``` Then, you can simply wrap any test(s) to be isolated with the [`rusty_fork_test!`](macro.rusty_fork_test.html) macro. ```rust use rusty_fork::rusty_fork_test; rusty_fork_test! { #[test] fn my_test() { assert_eq!(2, 1 + 1); } // more tests... } ``` For more advanced usage, have a look at the [`fork`](fn.fork.html) function. ## How rusty-fork works Unix-style process forking isn't really viable within the standard Rust test environment for a number of reasons. - While true process forking can be done on Windows, it's neither fast nor reliable. - The Rust test environment is multi-threaded, so attempting to do anything non-trivial after a process fork would result in undefined behaviour. Rusty-fork instead works by _spawning_ a fresh instance of the current process, after adjusting the command-line to ensure that only the desired test is entered. Some additional coordination establishes the parent/child branches and (not quite seamlessly) integrates the child's output with the test output capture system. Coordination between the processes is performed via environment variables, since there is otherwise no way to pass parameters to a test. Since it needs to spawn new copies of the test runner executable, rusty-fork does need to know about the meaning of every flag passed by the user. If any unknown flags are encountered, forking will fail. Please do not hesitate to file [issues](https://github.com/AltSysrq/rusty-fork/issues) if rusty-fork fails to recognise any valid flags passed to the test runner. It is possible to inform rusty-fork of new flags without patching by setting environment variables. For example, if a new `--frob-widgets` flag were added to the test runner, you could set `RUSTY_FORK_FLAG_FROB_WIDGETS` to one of the following: - `pass` — Pass the flag (just the flag) to the child process - `pass-arg` — Pass the flag and its following argument to the child process - `drop` — Don't pass the flag to the child process - `drop-arg` — Don't pass the flag to the child process, and ignore whatever argument follows. In general, arguments that affect which tests are run should be dropped, and others should be passed. ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. rusty-fork-0.3.0/src/child_wrapper.rs010064400017500001750000000221301345326656500160550ustar0000000000000000//- // Copyright 2018 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::fmt; use std::io; use std::process::{Child, Output}; #[cfg(feature = "timeout")] use std::time::Duration; #[cfg(feature = "timeout")] use wait_timeout::ChildExt; /// Wraps `std::process::ExitStatus`. Historically, this was due to the /// `wait_timeout` crate having its own `ExitStatus` type. /// /// Method documentation is copied from the [Rust std /// docs](https://doc.rust-lang.org/stable/std/process/struct.ExitStatus.html) /// and the [`wait_timeout` /// docs](https://docs.rs/wait-timeout/0.1.5/wait_timeout/struct.ExitStatus.html). #[derive(Clone, Copy)] pub struct ExitStatusWrapper(ExitStatusEnum); #[derive(Debug, Clone, Copy)] enum ExitStatusEnum { Std(::std::process::ExitStatus), } impl ExitStatusWrapper { fn std(es: ::std::process::ExitStatus) -> Self { ExitStatusWrapper(ExitStatusEnum::Std(es)) } /// Was termination successful? Signal termination is not considered a /// success, and success is defined as a zero exit status. pub fn success(&self) -> bool { match self.0 { ExitStatusEnum::Std(es) => es.success(), } } /// Returns the exit code of the process, if any. /// /// On Unix, this will return `None` if the process was terminated by a /// signal; `std::os::unix` provides an extension trait for extracting the /// signal and other details from the `ExitStatus`. pub fn code(&self) -> Option { match self.0 { ExitStatusEnum::Std(es) => es.code(), } } /// Returns the Unix signal which terminated this process. /// /// Note that on Windows this will always return None and on Unix this will /// return None if the process successfully exited otherwise. /// /// For simplicity and to match `wait_timeout`, this method is always /// present even on systems that do not support it. #[cfg(not(target_os = "windows"))] pub fn unix_signal(&self) -> Option { use std::os::unix::process::ExitStatusExt; match self.0 { ExitStatusEnum::Std(es) => es.signal(), } } /// Returns the Unix signal which terminated this process. /// /// Note that on Windows this will always return None and on Unix this will /// return None if the process successfully exited otherwise. /// /// For simplicity and to match `wait_timeout`, this method is always /// present even on systems that do not support it. #[cfg(target_os = "windows")] pub fn unix_signal(&self) -> Option { None } } impl fmt::Debug for ExitStatusWrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { ExitStatusEnum::Std(ref es) => fmt::Debug::fmt(es, f), } } } impl fmt::Display for ExitStatusWrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { ExitStatusEnum::Std(ref es) => fmt::Display::fmt(es, f), } } } /// Wraps a `std::process::Child` to coordinate state between `std` and /// `wait_timeout`. /// /// This is necessary because the completion of a call to /// `wait_timeout::ChildExt::wait_timeout` leaves the `Child` in an /// inconsistent state, as it does not know the child has exited, and on Unix /// may end up referencing another process. /// /// Documentation for this struct's methods is largely copied from the [Rust /// std docs](https://doc.rust-lang.org/stable/std/process/struct.Child.html). #[derive(Debug)] pub struct ChildWrapper { child: Child, exit_status: Option, } impl ChildWrapper { pub(crate) fn new(child: Child) -> Self { ChildWrapper { child, exit_status: None } } /// Return a reference to the inner `std::process::Child`. /// /// Use care on the returned object, as it does not necessarily reference /// the correct process unless you know the child process has not exited /// and no wait calls have succeeded. pub fn inner(&self) -> &Child { &self.child } /// Return a mutable reference to the inner `std::process::Child`. /// /// Use care on the returned object, as it does not necessarily reference /// the correct process unless you know the child process has not exited /// and no wait calls have succeeded. pub fn inner_mut(&mut self) -> &mut Child { &mut self.child } /// Forces the child to exit. This is equivalent to sending a SIGKILL on /// unix platforms. /// /// If the process has already been reaped by this handle, returns a /// `NotFound` error. pub fn kill(&mut self) -> io::Result<()> { if self.exit_status.is_none() { self.child.kill() } else { Err(io::Error::new(io::ErrorKind::NotFound, "Process already reaped")) } } /// Returns the OS-assigned processor identifier associated with this child. /// /// This succeeds even if the child has already been reaped. In this case, /// the process id may reference no process at all or even an unrelated /// process. pub fn id(&self) -> u32 { self.child.id() } /// Waits for the child to exit completely, returning the status that it /// exited with. This function will continue to have the same return value /// after it has been called at least once. /// /// The stdin handle to the child process, if any, will be closed before /// waiting. This helps avoid deadlock: it ensures that the child does not /// block waiting for input from the parent, while the parent waits for the /// child to exit. /// /// If the child process has already been reaped, returns its exit status /// without blocking. pub fn wait(&mut self) -> io::Result { if let Some(status) = self.exit_status { Ok(status) } else { let status = ExitStatusWrapper::std(self.child.wait()?); self.exit_status = Some(status); Ok(status) } } /// Attempts to collect the exit status of the child if it has already exited. /// /// This function will not block the calling thread and will only /// advisorily check to see if the child process has exited or not. If the /// child has exited then on Unix the process id is reaped. This function /// is guaranteed to repeatedly return a successful exit status so long as /// the child has already exited. /// /// If the child has exited, then `Ok(Some(status))` is returned. If the /// exit status is not available at this time then `Ok(None)` is returned. /// If an error occurs, then that error is returned. pub fn try_wait(&mut self) -> io::Result> { if let Some(status) = self.exit_status { Ok(Some(status)) } else { let status = self.child.try_wait()?.map(ExitStatusWrapper::std); self.exit_status = status; Ok(status) } } /// Simultaneously waits for the child to exit and collect all remaining /// output on the stdout/stderr handles, returning an `Output` instance. /// /// The stdin handle to the child process, if any, will be closed before /// waiting. This helps avoid deadlock: it ensures that the child does not /// block waiting for input from the parent, while the parent waits for the /// child to exit. /// /// By default, stdin, stdout and stderr are inherited from the parent. (In /// the context of `rusty_fork`, they are by default redirected to a file.) /// In order to capture the output into this `Result` it is /// necessary to create new pipes between parent and child. Use /// `stdout(Stdio::piped())` or `stderr(Stdio::piped())`, respectively. /// /// If the process has already been reaped, returns a `NotFound` error. pub fn wait_with_output(self) -> io::Result { if self.exit_status.is_some() { return Err(io::Error::new( io::ErrorKind::NotFound, "Process already reaped")); } self.child.wait_with_output() } /// Wait for the child to exit, but only up to the given maximum duration. /// /// If the process has already been reaped, returns its exit status /// immediately. Otherwise, if the process terminates within the duration, /// returns `Ok(Sone(..))`, or `Ok(None)` otherwise. /// /// This is only present if the "timeout" feature is enabled. #[cfg(feature = "timeout")] pub fn wait_timeout(&mut self, dur: Duration) -> io::Result> { if let Some(status) = self.exit_status { Ok(Some(status)) } else { let status = self.child.wait_timeout(dur)?.map(ExitStatusWrapper::std); self.exit_status = status; Ok(status) } } } rusty-fork-0.3.0/src/cmdline.rs010064400017500001750000000235151366334433600146510ustar0000000000000000//- // Copyright 2018, 2020 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Internal module which parses and modifies the rust test command-line. use std::env; use crate::error::*; /// How a hyphen-prefixed argument passed to the parent process should be /// handled when constructing the command-line for the child process. #[derive(Clone, Copy, Debug, PartialEq)] enum FlagType { /// Pass the flag through unchanged. The boolean indicates whether the flag /// is followed by an argument. Pass(bool), /// Drop the flag entirely. The boolean indicates whether the flag is /// followed by an argument. Drop(bool), /// Indicates a known flag that should never be encountered. The string is /// a human-readable error message. Error(&'static str), } /// Table of all flags in the 2020-05-26 nightly build. /// /// A number of these that affect output are dropped because we append our own /// options. static KNOWN_FLAGS: &[(&str, FlagType)] = &[ ("--bench", FlagType::Pass(false)), ("--color", FlagType::Pass(true)), ("--ensure-time", FlagType::Drop(false)), ("--exact", FlagType::Drop(false)), ("--exclude-should-panic", FlagType::Pass(false)), ("--force-run-in-process", FlagType::Pass(false)), ("--format", FlagType::Drop(true)), ("--help", FlagType::Error("Tests run but --help passed to process?")), ("--ignored", FlagType::Pass(false)), ("--include-ignored", FlagType::Pass(false)), ("--list", FlagType::Error("Tests run but --list passed to process?")), ("--logfile", FlagType::Drop(true)), ("--nocapture", FlagType::Drop(true)), ("--quiet", FlagType::Drop(false)), ("--report-time", FlagType::Drop(true)), ("--show-output", FlagType::Pass(false)), ("--skip", FlagType::Drop(true)), ("--test", FlagType::Pass(false)), ("--test-threads", FlagType::Drop(true)), ("-Z", FlagType::Pass(true)), ("-h", FlagType::Error("Tests run but -h passed to process?")), ("-q", FlagType::Drop(false)), ]; fn look_up_flag_from_table(flag: &str) -> Option { KNOWN_FLAGS.iter().cloned().filter(|&(name, _)| name == flag) .map(|(_, typ)| typ).next() } pub(crate) fn env_var_for_flag(flag: &str) -> String { let mut var = "RUSTY_FORK_FLAG_".to_owned(); var.push_str( &flag.trim_start_matches('-').to_uppercase().replace('-', "_")); var } fn look_up_flag_from_env(flag: &str) -> Option { env::var(&env_var_for_flag(flag)).ok().map( |value| match &*value { "pass" => FlagType::Pass(false), "pass-arg" => FlagType::Pass(true), "drop" => FlagType::Drop(false), "drop-arg" => FlagType::Drop(true), _ => FlagType::Error("incorrect flag type in environment; \ must be one of `pass`, `pass-arg`, \ `drop`, `drop-arg`"), }) } fn look_up_flag(flag: &str) -> Option { look_up_flag_from_table(flag).or_else(|| look_up_flag_from_env(flag)) } fn look_up_flag_or_err(flag: &str) -> Result<(bool, bool)> { match look_up_flag(flag) { None => Err(Error::UnknownFlag(flag.to_owned())), Some(FlagType::Error(message)) => Err(Error::DisallowedFlag(flag.to_owned(), message.to_owned())), Some(FlagType::Pass(has_arg)) => Ok((true, has_arg)), Some(FlagType::Drop(has_arg)) => Ok((false, has_arg)), } } /// Parse the full command line as would be given to the Rust test harness, and /// strip out any flags that should be dropped as well as all filters. The /// resulting argument list is also guaranteed to not have "--", so that new /// flags can be appended. /// /// The zeroth argument (the command name) is also dropped. pub(crate) fn strip_cmdline> (args: A) -> Result> { #[derive(Clone, Copy)] enum State { Ground, PassingArg, DroppingArg, } // Start in DroppingArg since we need to drop the exec name. let mut state = State::DroppingArg; let mut ret = Vec::new(); for arg in args { match state { State::DroppingArg => { state = State::Ground; }, State::PassingArg => { ret.push(arg); state = State::Ground; }, State::Ground => { if &arg == "--" { // Everything after this point is a filter break; } else if &arg == "-" { // "-" by itself is interpreted as a filter continue; } else if arg.starts_with("--") { let (pass, has_arg) = look_up_flag_or_err( arg.split('=').next().expect("split returned empty"))?; // If there's an = sign, the physical argument also // contains the associated value, so don't pay attention to // has_arg. let has_arg = has_arg && !arg.contains('='); if pass { ret.push(arg); if has_arg { state = State::PassingArg; } } else if has_arg { state = State::DroppingArg; } } else if arg.starts_with("-") { let mut chars = arg.chars(); let mut to_pass = "-".to_owned(); chars.next(); // skip initial '-' while let Some(flag_ch) = chars.next() { let flag = format!("-{}", flag_ch); let (pass, has_arg) = look_up_flag_or_err(&flag)?; if pass { to_pass.push(flag_ch); if has_arg { if chars.clone().next().is_some() { // Arg is attached to this one to_pass.extend(chars); } else { // Arg is separate state = State::PassingArg; } break; } } else if has_arg { if chars.clone().next().is_none() { // Arg is separate state = State::DroppingArg; } break; } } if "-" != &to_pass { ret.push(to_pass); } } else { // It's a filter, drop } }, } } Ok(ret) } /// Extra arguments to add after the stripped command line when running a /// single test. pub(crate) static RUN_TEST_ARGS: &[&str] = &[ // --quiet because the test runner output is redundant "--quiet", // Single threaded because we get parallelism from the parent process "--test-threads", "1", // Disable capture since we want the output to be captured by the *parent* // process. "--nocapture", // Match our test filter exactly so we run exactly one test "--exact", // Ensure everything else is interpreted as filters "--", ]; #[cfg(test)] mod test { use super::*; fn strip(cmdline: &str) -> Result { strip_cmdline(cmdline.split_whitespace().map(|s| s.to_owned())) .map(|strs| strs.join(" ")) } #[test] fn test_strip() { assert_eq!("", &strip("test").unwrap()); assert_eq!("--ignored", &strip("test --ignored").unwrap()); assert_eq!("", &strip("test --quiet").unwrap()); assert_eq!("", &strip("test -q").unwrap()); assert_eq!("", &strip("test -qq").unwrap()); assert_eq!("", &strip("test --test-threads 42").unwrap()); assert_eq!("-Z unstable-options", &strip("test -Z unstable-options").unwrap()); assert_eq!("-Zunstable-options", &strip("test -Zunstable-options").unwrap()); assert_eq!("-Zunstable-options", &strip("test -qZunstable-options").unwrap()); assert_eq!("--color auto", &strip("test --color auto").unwrap()); assert_eq!("--color=auto", &strip("test --color=auto").unwrap()); assert_eq!("", &strip("test filter filter2").unwrap()); assert_eq!("", &strip("test -- --color=auto").unwrap()); match strip("test --plugh").unwrap_err() { Error::UnknownFlag(ref flag) => assert_eq!("--plugh", flag), e => panic!("Unexpected error: {}", e), } match strip("test --help").unwrap_err() { Error::DisallowedFlag(ref flag, _) => assert_eq!("--help", flag), e => panic!("Unexpected error: {}", e), } } // Subprocess so we can change the environment without affecting other // tests rusty_fork_test! { #[test] fn define_args_via_env() { env::set_var("RUSTY_FORK_FLAG_X", "pass"); env::set_var("RUSTY_FORK_FLAG_FOO", "pass-arg"); env::set_var("RUSTY_FORK_FLAG_BAR", "drop"); env::set_var("RUSTY_FORK_FLAG_BAZ", "drop-arg"); assert_eq!("-X", &strip("test -X foo").unwrap()); assert_eq!("--foo bar", &strip("test --foo bar").unwrap()); assert_eq!("", &strip("test --bar").unwrap()); assert_eq!("", &strip("test --baz --notaflag").unwrap()); } } } rusty-fork-0.3.0/src/error.rs010064400017500001750000000051411366334433600143620ustar0000000000000000//- // Copyright 2018 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::io; use crate::cmdline; quick_error! { /// Enum for errors produced by the rusty-fork crate. #[derive(Debug)] pub enum Error { /// An unknown flag was encountered when examining the current /// process's argument list. /// /// The string is the flag that was encountered. UnknownFlag(flag: String) { display("The flag '{:?}' was passed to the Rust test \ process, but rusty-fork does not know how to \ handle it.\n\ If you are using the standard Rust \ test harness and have the latest version of the \ rusty-fork crate, please report a bug to\n\ \thttps://github.com/AltSysrq/rusty-fork/issues\n\ In the mean time, you can tell rusty-fork how to \ handle this flag by setting the environment variable \ `{}` to one of the following values:\n\ \tpass - Pass the flag (alone) to the child process\n\ \tpass-arg - Pass the flag and its following argument \ to the child process.\n\ \tdrop - Don't pass the flag to the child process.\n\ \tdrop-arg - Don't pass the flag or its following \ argument to the child process.", flag, cmdline::env_var_for_flag(&flag)) } /// A flag was encountered when examining the current process's /// argument list which is known but cannot be handled in any sensible /// way. /// /// The strings are the flag encountered and a human-readable message /// about why the flag could not be handled. DisallowedFlag(flag: String, message: String) { display("The flag '{:?}' was passed to the Rust test \ process, but rusty-fork cannot handle it; \ reason: {}", flag, message) } /// Spawning a subprocess failed. SpawnError(err: io::Error) { from() cause(err) display("Spawn failed: {}", err) } } } /// General `Result` type for rusty-fork. pub type Result = ::std::result::Result; rusty-fork-0.3.0/src/file-preamble010064400017500001750000000005051324463030600152770ustar0000000000000000//- // Copyright 2018 // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. rusty-fork-0.3.0/src/fork.rs010064400017500001750000000267701366334433600142050ustar0000000000000000//- // Copyright 2018 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::fs; use std::env; use std::hash::{Hash, Hasher}; use std::io::{self, BufRead, Seek}; use std::panic; use std::process; use fnv; use tempfile; use crate::cmdline; use crate::error::*; use crate::child_wrapper::ChildWrapper; const OCCURS_ENV: &str = "RUSTY_FORK_OCCURS"; const OCCURS_TERM_LENGTH: usize = 17; /* ':' plus 16 hexits */ /// Simulate a process fork. /// /// The function documentation here only lists information unique to calling it /// directly; please see the crate documentation for more details on how the /// forking process works. /// /// Since this is not a true process fork, the calling code must be structured /// to ensure that the child process, upon starting from the same entry point, /// also reaches this same `fork()` call. Recursive forks are supported; the /// child branch is taken from all child processes of the fork even if it is /// not directly the child of a particular branch. However, encountering the /// same fork point more than once in a single execution sequence of a child /// process is not (e.g., putting this call in a recursive function) and /// results in unspecified behaviour. /// /// The child's output is buffered into an anonymous temporary file. Before /// this call returns, this output is copied to the parent's standard output /// (passing through the redirect mechanism Rust test uses). /// /// `test_name` must exactly match the full path of the test function being /// run. /// /// `fork_id` is a unique identifier identifying this particular fork location. /// This *must* be stable across processes of the same executable; pointers are /// not suitable stable, and string constants may not be suitably unique. The /// [`rusty_fork_id!()`](macro.rusty_fork_id.html) macro is the recommended way /// to supply this parameter. /// /// If this is the parent process, `in_parent` is invoked, and the return value /// becomes the return value from this function. The callback is passed a /// handle to the file which receives the child's output. If is the callee's /// responsibility to wait for the child to exit. If this is the child process, /// `in_child` is invoked, and when the callback returns, the child process /// exits. /// /// If `in_parent` returns or panics before the child process has terminated, /// the child process is killed. /// /// If `in_child` panics, the child process exits with a failure code /// immediately rather than let the panic propagate out of the `fork()` call. /// /// `process_modifier` is invoked on the `std::process::Command` immediately /// before spawning the new process. The callee may modify the process /// parameters if desired, but should not do anything that would modify or /// remove any environment variables beginning with `RUSTY_FORK_`. /// /// ## Panics /// /// Panics if the environment indicates that there are already at least 16 /// levels of fork nesting. /// /// Panics if `std::env::current_exe()` fails determine the path to the current /// executable. /// /// Panics if any argument to the current process is not valid UTF-8. pub fn fork( test_name: &str, fork_id: ID, process_modifier: MODIFIER, in_parent: PARENT, in_child: CHILD) -> Result where ID : Hash, MODIFIER : FnOnce (&mut process::Command), PARENT : FnOnce (&mut ChildWrapper, &mut fs::File) -> R, CHILD : FnOnce () { let fork_id = id_str(fork_id); // Erase the generics so we don't instantiate the actual implementation for // every single test let mut return_value = None; let mut process_modifier = Some(process_modifier); let mut in_parent = Some(in_parent); let mut in_child = Some(in_child); fork_impl(test_name, fork_id, &mut |cmd| process_modifier.take().unwrap()(cmd), &mut |child, file| return_value = Some( in_parent.take().unwrap()(child, file)), &mut || in_child.take().unwrap()()) .map(|_| return_value.unwrap()) } fn fork_impl(test_name: &str, fork_id: String, process_modifier: &mut dyn FnMut (&mut process::Command), in_parent: &mut dyn FnMut (&mut ChildWrapper, &mut fs::File), in_child: &mut dyn FnMut ()) -> Result<()> { let mut occurs = env::var(OCCURS_ENV).unwrap_or_else(|_| String::new()); if occurs.contains(&fork_id) { match panic::catch_unwind(panic::AssertUnwindSafe(in_child)) { Ok(_) => process::exit(0), // Assume that the default panic handler already printed something // // We don't use process::abort() since it produces core dumps on // some systems and isn't something more special than a normal // panic. Err(_) => process::exit(70 /* EX_SOFTWARE */), } } else { // Prevent misconfiguration creating a fork bomb if occurs.len() > 16 * OCCURS_TERM_LENGTH { panic!("rusty-fork: Not forking due to >=16 levels of recursion"); } let file = tempfile::tempfile()?; struct KillOnDrop(ChildWrapper, fs::File); impl Drop for KillOnDrop { fn drop(&mut self) { // Kill the child if it hasn't exited yet let _ = self.0.kill(); // Copy the child's output to our own // Awkwardly, `print!()` and `println!()` are our only gateway // to putting things in the captured output. Generally test // output really is text, so work on that assumption and read // line-by-line, converting lossily into UTF-8 so we can // println!() it. let _ = self.1.seek(io::SeekFrom::Start(0)); let mut buf = Vec::new(); let mut br = io::BufReader::new(&mut self.1); loop { // We can't use read_line() or lines() since they break if // there's any non-UTF-8 output at all. \n occurs at the // end of the line endings on all major platforms, so we // can just use that as a delimiter. if br.read_until(b'\n', &mut buf).is_err() { break; } if buf.is_empty() { break; } // not println!() because we already have a line ending // from above. print!("{}", String::from_utf8_lossy(&buf)); buf.clear(); } } } occurs.push_str(&fork_id); let mut command = process::Command::new( env::current_exe() .expect("current_exe() failed, cannot fork")); command .args(cmdline::strip_cmdline(env::args())?) .args(cmdline::RUN_TEST_ARGS) .arg(test_name) .env(OCCURS_ENV, &occurs) .stdin(process::Stdio::null()) .stdout(file.try_clone()?) .stderr(file.try_clone()?); process_modifier(&mut command); let mut child = command.spawn().map(ChildWrapper::new) .map(|p| KillOnDrop(p, file))?; let ret = in_parent(&mut child.0, &mut child.1); Ok(ret) } } fn id_str(id: ID) -> String { let mut hasher = fnv::FnvHasher::default(); id.hash(&mut hasher); return format!(":{:016X}", hasher.finish()); } #[cfg(test)] mod test { use std::io::Read; use std::thread; use super::*; fn sleep(ms: u64) { thread::sleep(::std::time::Duration::from_millis(ms)); } fn capturing_output(cmd: &mut process::Command) { // Only actually capture stdout since we can't use // wait_with_output() since it for some reason consumes the `Child`. cmd.stdout(process::Stdio::piped()) .stderr(process::Stdio::inherit()); } fn inherit_output(cmd: &mut process::Command) { cmd.stdout(process::Stdio::inherit()) .stderr(process::Stdio::inherit()); } fn wait_for_child_output(child: &mut ChildWrapper, _file: &mut fs::File) -> String { let mut output = String::new(); child.inner_mut().stdout.as_mut().unwrap() .read_to_string(&mut output).unwrap(); assert!(child.wait().unwrap().success()); output } fn wait_for_child(child: &mut ChildWrapper, _file: &mut fs::File) { assert!(child.wait().unwrap().success()); } #[test] fn fork_basically_works() { let status = fork("fork::test::fork_basically_works", rusty_fork_id!(), |_| (), |child, _| child.wait().unwrap(), || println!("hello from child")).unwrap(); assert!(status.success()); } #[test] fn child_output_captured_and_repeated() { let output = fork( "fork::test::child_output_captured_and_repeated", rusty_fork_id!(), capturing_output, wait_for_child_output, || fork( "fork::test::child_output_captured_and_repeated", rusty_fork_id!(), |_| (), wait_for_child, || println!("hello from child")).unwrap()) .unwrap(); assert!(output.contains("hello from child")); } #[test] fn child_killed_if_parent_exits_first() { let output = fork( "fork::test::child_killed_if_parent_exits_first", rusty_fork_id!(), capturing_output, wait_for_child_output, || fork( "fork::test::child_killed_if_parent_exits_first", rusty_fork_id!(), inherit_output, |_, _| (), || { sleep(1_000); println!("hello from child"); }).unwrap()).unwrap(); sleep(2_000); assert!(!output.contains("hello from child"), "Had unexpected output:\n{}", output); } #[test] fn child_killed_if_parent_panics_first() { let output = fork( "fork::test::child_killed_if_parent_panics_first", rusty_fork_id!(), capturing_output, wait_for_child_output, || { assert!( panic::catch_unwind(panic::AssertUnwindSafe(|| fork( "fork::test::child_killed_if_parent_panics_first", rusty_fork_id!(), inherit_output, |_, _| panic!("testing a panic, nothing to see here"), || { sleep(1_000); println!("hello from child"); }).unwrap())).is_err()); }).unwrap(); sleep(2_000); assert!(!output.contains("hello from child"), "Had unexpected output:\n{}", output); } #[test] fn child_aborted_if_panics() { let status = fork( "fork::test::child_aborted_if_panics", rusty_fork_id!(), |_| (), |child, _| child.wait().unwrap(), || panic!("testing a panic, nothing to see here")).unwrap(); assert_eq!(70, status.code().unwrap()); } } rusty-fork-0.3.0/src/fork_test.rs010064400017500001750000000135001366334433600152270ustar0000000000000000//- // Copyright 2018, 2020 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Support code for the `rusty_fork_test!` macro and similar. //! //! Some functionality in this module is useful to other implementors and //! unlikely to change. This subset is documented and considered stable. use std::process::Command; use crate::child_wrapper::ChildWrapper; /// Run Rust tests in subprocesses. /// /// The basic usage is to simply put this macro around your `#[test]` /// functions. /// /// ``` /// use rusty_fork::rusty_fork_test; /// /// rusty_fork_test! { /// # /* /// #[test] /// # */ /// fn my_test() { /// assert_eq!(2, 1 + 1); /// } /// /// // more tests... /// } /// # /// # fn main() { my_test(); } /// ``` /// /// Each test will be run in its own process. If the subprocess exits /// unsuccessfully for any reason, including due to signals, the test fails. /// /// It is also possible to specify a timeout which is applied to all tests in /// the block, like so: /// /// ``` /// use rusty_fork::rusty_fork_test; /// /// rusty_fork_test! { /// #![rusty_fork(timeout_ms = 1000)] /// # /* /// #[test] /// # */ /// fn my_test() { /// do_some_expensive_computation(); /// } /// /// // more tests... /// } /// # fn do_some_expensive_computation() { } /// # fn main() { my_test(); } /// ``` /// /// If any individual test takes more than the given timeout, the child is /// terminated and the test panics. /// /// Using the timeout feature requires the `timeout` feature for this crate to /// be enabled (which it is by default). #[macro_export] macro_rules! rusty_fork_test { (#![rusty_fork(timeout_ms = $timeout:expr)] $( $(#[$meta:meta])* fn $test_name:ident() $body:block )*) => { $( $(#[$meta])* fn $test_name() { // Eagerly convert everything to function pointers so that all // tests use the same instantiation of `fork`. fn body_fn() $body let body: fn () = body_fn; fn supervise_fn(child: &mut $crate::ChildWrapper, _file: &mut ::std::fs::File) { $crate::fork_test::supervise_child(child, $timeout) } let supervise: fn (&mut $crate::ChildWrapper, &mut ::std::fs::File) = supervise_fn; $crate::fork( $crate::rusty_fork_test_name!($test_name), $crate::rusty_fork_id!(), $crate::fork_test::no_configure_child, supervise, body).expect("forking test failed") } )* }; ($( $(#[$meta:meta])* fn $test_name:ident() $body:block )*) => { rusty_fork_test! { #![rusty_fork(timeout_ms = 0)] $($(#[$meta])* fn $test_name() $body)* } }; } /// Given the unqualified name of a `#[test]` function, produce a /// `&'static str` corresponding to the name of the test as filtered by the /// standard test harness. /// /// This is internally used by `rusty_fork_test!` but is made available since /// other test wrapping implementations will likely need it too. /// /// This does not currently produce a constant expression. #[macro_export] macro_rules! rusty_fork_test_name { ($function_name:ident) => { $crate::fork_test::fix_module_path( concat!(module_path!(), "::", stringify!($function_name))) } } #[allow(missing_docs)] #[doc(hidden)] pub fn supervise_child(child: &mut ChildWrapper, timeout_ms: u64) { if timeout_ms > 0 { wait_timeout(child, timeout_ms) } else { let status = child.wait().expect("failed to wait for child"); assert!(status.success(), "child exited unsuccessfully with {}", status); } } #[allow(missing_docs)] #[doc(hidden)] pub fn no_configure_child(_child: &mut Command) { } /// Transform a string representing a qualified path as generated via /// `module_path!()` into a qualified path as expected by the standard Rust /// test harness. pub fn fix_module_path(path: &str) -> &str { path.find("::").map(|ix| &path[ix+2..]).unwrap_or(path) } #[cfg(feature = "timeout")] fn wait_timeout(child: &mut ChildWrapper, timeout_ms: u64) { use std::time::Duration; let timeout = Duration::from_millis(timeout_ms); let status = child.wait_timeout(timeout).expect("failed to wait for child"); if let Some(status) = status { assert!(status.success(), "child exited unsuccessfully with {}", status); } else { panic!("child process exceeded {} ms timeout", timeout_ms); } } #[cfg(not(feature = "timeout"))] fn wait_timeout(_: &mut ChildWrapper, _: u64) { panic!("Using the timeout feature of rusty_fork_test! requires \ enabling the `timeout` feature on the rusty-fork crate."); } #[cfg(test)] mod test { rusty_fork_test! { #[test] fn trivial() { } #[test] #[should_panic] fn panicking_child() { panic!("just testing a panic, nothing to see here"); } #[test] #[should_panic] fn aborting_child() { ::std::process::abort(); } } rusty_fork_test! { #![rusty_fork(timeout_ms = 1000)] #[test] #[cfg(feature = "timeout")] fn timeout_passes() { } #[test] #[should_panic] #[cfg(feature = "timeout")] fn timeout_fails() { println!("hello from child"); ::std::thread::sleep( ::std::time::Duration::from_millis(10000)); println!("goodbye from child"); } } } rusty-fork-0.3.0/src/lib.rs010064400017500001750000000107731366334433600140060ustar0000000000000000//- // Copyright 2018 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. #![deny(missing_docs, unsafe_code)] //! Rusty-fork provides a way to "fork" unit tests into separate processes. //! //! There are a number of reasons to want to run some tests in isolated //! processes: //! //! - When tests share a process, if any test causes the process to abort, //! segfault, overflow the stack, etc., the entire test runner process dies. If //! the test is in a subprocess, only the subprocess dies and the test runner //! simply fails the test. //! //! - Isolating a test to a subprocess makes it possible to add a timeout to //! the test and forcibly terminate it and produce a normal test failure. //! //! - Tests which need to interact with some inherently global property, such //! as the current working directory, can do so without interfering with other //! tests. //! //! This crate itself provides two things: //! //! - The [`rusty_fork_test!`](macro.rusty_fork_test.html) macro, which is a //! simple way to wrap standard Rust tests to be run in subprocesses with //! optional timeouts. //! //! - The [`fork`](fn.fork.html) function which can be used as a building block //! to make other types of process isolation strategies. //! //! ## Quick Start //! //! If you just want to run normal Rust tests in isolated processes, getting //! started is pretty quick. //! //! In `Cargo.toml`, add //! //! ```toml //! [dev-dependencies] //! rusty-fork = "0.3.0" //! ``` //! //! Then, you can simply wrap any test(s) to be isolated with the //! [`rusty_fork_test!`](macro.rusty_fork_test.html) macro. //! //! ```rust //! use rusty_fork::rusty_fork_test; //! //! rusty_fork_test! { //! # /* NOREADME //! #[test] //! # NOREADME */ //! fn my_test() { //! assert_eq!(2, 1 + 1); //! } //! //! // more tests... //! } //! # // NOREADME //! # fn main() { my_test(); } // NOREADME //! ``` //! //! For more advanced usage, have a look at the [`fork`](fn.fork.html) //! function. //! //! ## How rusty-fork works //! //! Unix-style process forking isn't really viable within the standard Rust //! test environment for a number of reasons. //! //! - While true process forking can be done on Windows, it's neither fast nor //! reliable. //! //! - The Rust test environment is multi-threaded, so attempting to do anything //! non-trivial after a process fork would result in undefined behaviour. //! //! Rusty-fork instead works by _spawning_ a fresh instance of the current //! process, after adjusting the command-line to ensure that only the desired //! test is entered. Some additional coordination establishes the parent/child //! branches and (not quite seamlessly) integrates the child's output with the //! test output capture system. //! //! Coordination between the processes is performed via environment variables, //! since there is otherwise no way to pass parameters to a test. //! //! Since it needs to spawn new copies of the test runner executable, //! rusty-fork does need to know about the meaning of every flag passed by the //! user. If any unknown flags are encountered, forking will fail. Please do //! not hesitate to file //! [issues](https://github.com/AltSysrq/rusty-fork/issues) if rusty-fork fails //! to recognise any valid flags passed to the test runner. //! //! It is possible to inform rusty-fork of new flags without patching by //! setting environment variables. For example, if a new `--frob-widgets` flag //! were added to the test runner, you could set `RUSTY_FORK_FLAG_FROB_WIDGETS` //! to one of the following: //! //! - `pass` — Pass the flag (just the flag) to the child process //! - `pass-arg` — Pass the flag and its following argument to the child process //! - `drop` — Don't pass the flag to the child process //! - `drop-arg` — Don't pass the flag to the child process, and ignore whatever //! argument follows. //! //! In general, arguments that affect which tests are run should be dropped, //! and others should be passed. //! //! #[macro_use] extern crate quick_error; #[macro_use] mod sugar; #[macro_use] pub mod fork_test; mod error; mod cmdline; mod fork; mod child_wrapper; pub use crate::sugar::RustyForkId; pub use crate::error::{Error, Result}; pub use crate::fork::fork; pub use crate::child_wrapper::{ChildWrapper, ExitStatusWrapper}; rusty-fork-0.3.0/src/sugar.rs010064400017500001750000000024241324527011400143370ustar0000000000000000//- // Copyright 2018 Jason Lingle // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. /// Produce a hashable identifier unique to the particular macro invocation /// which is stable across processes of the same executable. /// /// This is usually the best thing to pass for the `fork_id` argument of /// [`fork`](fn.fork.html). /// /// The type of the expression this macro expands to is /// [`RustyForkId`](struct.RustyForkId.html). #[macro_export] macro_rules! rusty_fork_id { () => { { struct _RustyForkId; $crate::RustyForkId::of(::std::any::TypeId::of::<_RustyForkId>()) } } } /// The type of the value produced by /// [`rusty_fork_id!`](macro.rusty_fork_id.html). #[derive(Clone, Hash, PartialEq, Debug)] pub struct RustyForkId(::std::any::TypeId); impl RustyForkId { #[allow(missing_docs)] #[doc(hidden)] pub fn of(id: ::std::any::TypeId) -> Self { RustyForkId(id) } } #[cfg(test)] mod test { #[test] fn ids_are_actually_distinct() { assert_ne!(rusty_fork_id!(), rusty_fork_id!()); } }