rusty_fork/
cmdline.rs

1//-
2// Copyright 2018, 2020 Jason Lingle
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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
12use std::env;
13
14use 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)]
19enum 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.
35static 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
60fn look_up_flag_from_table(flag: &str) -> Option<FlagType> {
61    KNOWN_FLAGS.iter().cloned().filter(|&(name, _)| name == flag)
62        .map(|(_, typ)| typ).next()
63}
64
65pub(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
72fn look_up_flag_from_env(flag: &str) -> Option<FlagType> {
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
85fn look_up_flag(flag: &str) -> Option<FlagType> {
86    look_up_flag_from_table(flag).or_else(|| look_up_flag_from_env(flag))
87}
88
89fn 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.
106pub(crate) fn strip_cmdline<A : Iterator<Item = String>>
107    (args: A) -> Result<Vec<String>>
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.
195pub(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)]
210mod test {
211    use super::*;
212
213    fn strip(cmdline: &str) -> Result<String> {
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}