//- // 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()); } } }