Skip to content

Commit c5c50e2

Browse files
authored
Merge pull request #82 from delan/fix-ignore
Fix handling of `ignore` in wait() and wait_with_all()
2 parents 2eb19ae + 7a8dbd8 commit c5c50e2

File tree

2 files changed

+140
-25
lines changed

2 files changed

+140
-25
lines changed

src/child.rs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ impl CmdChildren {
3131

3232
/// Waits for the children processes to exit completely, returning the status that they exited with.
3333
pub fn wait(&mut self) -> CmdResult {
34-
// wait for the last child result
35-
let handle = self.children.pop().unwrap();
36-
if let Err(e) = handle.wait(true) {
37-
let _ = Self::wait_children(&mut self.children);
38-
return Err(e);
39-
}
40-
Self::wait_children(&mut self.children)
34+
let last_child = self.children.pop().unwrap();
35+
let last_child_res = last_child.wait(true);
36+
let other_children_res = Self::wait_children(&mut self.children);
37+
38+
self.ignore_error
39+
.then_some(Ok(()))
40+
.unwrap_or(last_child_res.and(other_children_res))
4141
}
4242

4343
fn wait_children(children: &mut Vec<CmdChild>) -> CmdResult {
@@ -149,21 +149,23 @@ impl FunChildren {
149149
}
150150

151151
fn inner_wait_with_all(&mut self, capture_stderr: bool) -> (CmdResult, String, String) {
152-
// wait for the last child result
153-
let last_handle = self.children.pop().unwrap();
154-
let mut stdout_buf = Vec::new();
152+
let mut stdout = Vec::new();
155153
let mut stderr = String::new();
156-
let last_res = last_handle.wait_with_all(capture_stderr, &mut stdout_buf, &mut stderr);
157-
let res = CmdChildren::wait_children(&mut self.children);
158-
let mut stdout: String = String::from_utf8_lossy(&stdout_buf).into();
154+
155+
let last_child = self.children.pop().unwrap();
156+
let last_child_res = last_child.wait_with_all(capture_stderr, &mut stdout, &mut stderr);
157+
let other_children_res = CmdChildren::wait_children(&mut self.children);
158+
let cmd_result = self
159+
.ignore_error
160+
.then_some(Ok(()))
161+
.unwrap_or(last_child_res.and(other_children_res));
162+
163+
let mut stdout: String = String::from_utf8_lossy(&stdout).into();
159164
if stdout.ends_with('\n') {
160165
stdout.pop();
161166
}
162-
if res.is_err() && !self.ignore_error && process::pipefail_enabled() {
163-
(res, stdout, stderr)
164-
} else {
165-
(last_res, stdout, stderr)
166-
}
167+
168+
(cmd_result, stdout, stderr)
167169
}
168170
}
169171

tests/test_macros.rs

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,126 @@ fn test_pipe() {
151151
let wc_cmd = "wc";
152152
assert!(run_cmd!(ls | $wc_cmd).is_ok());
153153

154-
// test pipefail
155-
assert!(run_cmd!(false | true).is_err());
156-
assert!(run_fun!(false | true).is_err());
157-
assert!(run_fun!(ignore false | true).is_ok());
158-
set_pipefail(false);
159-
assert!(run_fun!(false | true).is_ok());
160-
set_pipefail(true);
154+
// test `ignore` command and pipefail mode
155+
// FIXME: make set_pipefail() thread safe, then move this to a separate test_ignore_and_pipefail()
156+
struct TestCase {
157+
/// Run the test case, returning whether the result `.is_ok()`.
158+
code: fn() -> bool,
159+
/// Stringified version of `code`, for identifying assertion failures.
160+
code_str: &'static str,
161+
/// Do we expect `.is_ok()` when pipefail is on?
162+
expected_ok_pipefail_on: bool,
163+
/// Do we expect `.is_ok()` when pipefail is off?
164+
expected_ok_pipefail_off: bool,
165+
}
166+
/// Make a function for [TestCase::code].
167+
///
168+
/// Usage: `code!((macro!(command)).extra)`
169+
/// - `(macro!(command)).extra` is an expression of type CmdResult
170+
macro_rules! code {
171+
(($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => {
172+
|| $macro$bang($($command)+)$($after)*.is_ok()
173+
};
174+
}
175+
/// Make a string for [TestCase::code_str].
176+
///
177+
/// Usage: `code_str!((macro!(command)).extra)`
178+
/// - `(macro!(command)).extra` is an expression of type CmdResult
179+
macro_rules! code_str {
180+
(($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => {
181+
stringify!($macro$bang($($command)+)$($after)*.is_ok())
182+
};
183+
}
184+
/// Make a [TestCase].
185+
/// Usage: `test_case!(true/false, true/false, (macro!(command)).extra)`
186+
/// - the first `true/false` is TestCase::expected_ok_pipefail_on
187+
/// - the second `true/false` is TestCase::expected_ok_pipefail_off
188+
/// - `(macro!(command)).extra` is an expression of type CmdResult
189+
macro_rules! test_case {
190+
($expected_ok_pipefail_on:expr, $expected_ok_pipefail_off:expr, ($macro:tt $bang:tt ($($command:tt)+)) $($after:tt)*) => {
191+
TestCase {
192+
code: code!(($macro $bang ($($command)+)) $($after)*),
193+
code_str: code_str!(($macro $bang ($($command)+)) $($after)*),
194+
expected_ok_pipefail_on: $expected_ok_pipefail_on,
195+
expected_ok_pipefail_off: $expected_ok_pipefail_off,
196+
}
197+
};
198+
}
199+
/// Generate test cases for the given entry point.
200+
/// For each test case, every entry point should yield the same results.
201+
macro_rules! test_cases_for_entry_point {
202+
(($macro:tt $bang:tt (...)) $($after:tt)*) => {
203+
&[
204+
// Use result of last command in pipeline, if all others exit successfully.
205+
test_case!(true, true, ($macro $bang (true)) $($after)*),
206+
test_case!(false, false, ($macro $bang (false)) $($after)*),
207+
test_case!(true, true, ($macro $bang (true | true)) $($after)*),
208+
test_case!(false, false, ($macro $bang (true | false)) $($after)*),
209+
// Use failure of other commands, if pipefail is on.
210+
test_case!(false, true, ($macro $bang (false | true)) $($after)*),
211+
// Use failure of last command in pipeline.
212+
test_case!(false, false, ($macro $bang (false | false)) $($after)*),
213+
// Ignore all failures, when using `ignore` command.
214+
test_case!(true, true, ($macro $bang (ignore true)) $($after)*),
215+
test_case!(true, true, ($macro $bang (ignore false)) $($after)*),
216+
test_case!(true, true, ($macro $bang (ignore true | true)) $($after)*),
217+
test_case!(true, true, ($macro $bang (ignore true | false)) $($after)*),
218+
test_case!(true, true, ($macro $bang (ignore false | true)) $($after)*),
219+
test_case!(true, true, ($macro $bang (ignore false | false)) $($after)*),
220+
]
221+
};
222+
}
223+
224+
let test_cases: &[&[TestCase]] = &[
225+
test_cases_for_entry_point!((run_cmd!(...))),
226+
test_cases_for_entry_point!((run_fun!(...)).map(|_stdout| ())),
227+
test_cases_for_entry_point!((spawn!(...)).unwrap().wait()),
228+
test_cases_for_entry_point!((spawn_with_output!(...)).unwrap().wait_with_all().0),
229+
test_cases_for_entry_point!((spawn_with_output!(...))
230+
.unwrap()
231+
.wait_with_output()
232+
.map(|_stdout| ())),
233+
test_cases_for_entry_point!((spawn_with_output!(...))
234+
.unwrap()
235+
.wait_with_raw_output(&mut vec![])),
236+
// FIXME: wait_with_pipe() is currently busted
237+
// test_cases_for_entry_point!((spawn_with_output!(...))
238+
// .unwrap()
239+
// .wait_with_pipe(&mut |_stdout| {})),
240+
];
241+
242+
macro_rules! check_eq {
243+
($left:expr, $right:expr, $($rest:tt)+) => {{
244+
let left = $left;
245+
let right = $right;
246+
if left != right {
247+
eprintln!("assertion failed ({} != {}): {}", left, right, format!($($rest)+));
248+
false
249+
} else {
250+
true
251+
}
252+
}};
253+
}
254+
255+
let mut ok = true;
256+
for case in test_cases.iter().flat_map(|items| items.iter()) {
257+
ok &= check_eq!(
258+
(case.code)(),
259+
case.expected_ok_pipefail_on,
260+
"{} when pipefail is on",
261+
case.code_str
262+
);
263+
set_pipefail(false);
264+
ok &= check_eq!(
265+
(case.code)(),
266+
case.expected_ok_pipefail_off,
267+
"{} when pipefail is off",
268+
case.code_str
269+
);
270+
set_pipefail(true);
271+
}
272+
273+
assert!(ok);
161274
}
162275

163276
#[test]

0 commit comments

Comments
 (0)