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