//! Tests for config settings. use std::borrow::Borrow; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io; use std::os; use std::path::Path; use cargo::core::{enable_nightly_features, Shell}; use cargo::util::config::{self, Config, SslVersionConfig}; use cargo::util::toml::{self, VecStringOrBool as VSOB}; use cargo::CargoResult; use cargo_test_support::{paths, project, t}; use serde::Deserialize; /// Helper for constructing a `Config` object. pub struct ConfigBuilder { env: HashMap, unstable: Vec, } impl ConfigBuilder { pub fn new() -> ConfigBuilder { ConfigBuilder { env: HashMap::new(), unstable: Vec::new(), } } /// Passes a `-Z` flag. pub fn unstable_flag(&mut self, s: impl Into) -> &mut Self { self.unstable.push(s.into()); self } /// Sets an environment variable. pub fn env(&mut self, key: impl Into, val: impl Into) -> &mut Self { self.env.insert(key.into(), val.into()); self } /// Creates the `Config`. pub fn build(&self) -> Config { self.build_err().unwrap() } /// Creates the `Config`, returning a Result. pub fn build_err(&self) -> CargoResult { if !self.unstable.is_empty() { // This is unfortunately global. Some day that should be fixed. enable_nightly_features(); } let output = Box::new(fs::File::create(paths::root().join("shell.out")).unwrap()); let shell = Shell::from_write(output); let cwd = paths::root(); let homedir = paths::home(); let mut config = Config::new(shell, cwd, homedir); config.set_env(self.env.clone()); config.configure(0, None, None, false, false, false, &None, &self.unstable)?; Ok(config) } } fn new_config() -> Config { ConfigBuilder::new().build() } fn lines_match(a: &str, b: &str) -> bool { // Perform a small amount of normalization for filesystem paths before we // send this to the `lines_match` function. cargo_test_support::lines_match(&a.replace("\\", "/"), &b.replace("\\", "/")) } #[cargo_test] fn read_env_vars_for_config() { let p = project() .file( "Cargo.toml", r#" [package] name = "foo" authors = [] version = "0.0.0" build = "build.rs" "#, ) .file("src/lib.rs", "") .file( "build.rs", r#" use std::env; fn main() { assert_eq!(env::var("NUM_JOBS").unwrap(), "100"); } "#, ) .build(); p.cargo("build").env("CARGO_BUILD_JOBS", "100").run(); } pub fn write_config(config: &str) { let path = paths::root().join(".cargo/config"); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, config).unwrap(); } fn write_config_toml(config: &str) { let path = paths::root().join(".cargo/config.toml"); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, config).unwrap(); } // Several test fail on windows if the user does not have permission to // create symlinks (the `SeCreateSymbolicLinkPrivilege`). Instead of // disabling these test on Windows, use this function to test whether we // have permission, and return otherwise. This way, we still don't run these // tests most of the time, but at least we do if the user has the right // permissions. // This function is derived from libstd fs tests. pub fn got_symlink_permission() -> bool { if cfg!(unix) { return true; } let link = paths::root().join("some_hopefully_unique_link_name"); let target = paths::root().join("nonexisting_target"); match symlink_file(&target, &link) { Ok(_) => true, // ERROR_PRIVILEGE_NOT_HELD = 1314 Err(ref err) if err.raw_os_error() == Some(1314) => false, Err(_) => true, } } #[cfg(unix)] fn symlink_file(target: &Path, link: &Path) -> io::Result<()> { os::unix::fs::symlink(target, link) } #[cfg(windows)] fn symlink_file(target: &Path, link: &Path) -> io::Result<()> { os::windows::fs::symlink_file(target, link) } fn symlink_config_to_config_toml() { let toml_path = paths::root().join(".cargo/config.toml"); let symlink_path = paths::root().join(".cargo/config"); t!(symlink_file(&toml_path, &symlink_path)); } fn assert_error>(error: E, msgs: &str) { let causes = error .borrow() .iter_chain() .map(|e| e.to_string()) .collect::>() .join("\n"); if !lines_match(msgs, &causes) { panic!( "Did not find expected:\n{}\nActual error:\n{}\n", msgs, causes ); } } #[cargo_test] fn get_config() { write_config( "\ [S] f1 = 123 ", ); let config = new_config(); #[derive(Debug, Deserialize, Eq, PartialEq)] struct S { f1: Option, } let s: S = config.get("S").unwrap(); assert_eq!(s, S { f1: Some(123) }); let config = ConfigBuilder::new().env("CARGO_S_F1", "456").build(); let s: S = config.get("S").unwrap(); assert_eq!(s, S { f1: Some(456) }); } #[cargo_test] fn config_works_with_extension() { write_config_toml( "\ [foo] f1 = 1 ", ); let config = new_config(); assert_eq!(config.get::>("foo.f1").unwrap(), Some(1)); } #[cargo_test] fn config_ambiguous_filename_symlink_doesnt_warn() { // Windows requires special permissions to create symlinks. // If we don't have permission, just skip this test. if !got_symlink_permission() { return; }; write_config_toml( "\ [foo] f1 = 1 ", ); symlink_config_to_config_toml(); let config = new_config(); assert_eq!(config.get::>("foo.f1").unwrap(), Some(1)); // It should NOT have warned for the symlink. drop(config); // Paranoid about flushing the file. let path = paths::root().join("shell.out"); let output = fs::read_to_string(path).unwrap(); let unexpected = "\ warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config` "; if lines_match(unexpected, &output) { panic!( "Found unexpected:\n{}\nActual error:\n{}\n", unexpected, output ); } } #[cargo_test] fn config_ambiguous_filename() { write_config( "\ [foo] f1 = 1 ", ); write_config_toml( "\ [foo] f1 = 2 ", ); let config = new_config(); // It should use the value from the one without the extension for // backwards compatibility. assert_eq!(config.get::>("foo.f1").unwrap(), Some(1)); // But it also should have warned. drop(config); // Paranoid about flushing the file. let path = paths::root().join("shell.out"); let output = fs::read_to_string(path).unwrap(); let expected = "\ warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config` "; if !lines_match(expected, &output) { panic!( "Did not find expected:\n{}\nActual error:\n{}\n", expected, output ); } } #[cargo_test] fn config_unused_fields() { write_config( "\ [S] unused = 456 ", ); let config = ConfigBuilder::new() .env("CARGO_S_UNUSED2", "1") .env("CARGO_S2_UNUSED", "2") .build(); #[derive(Debug, Deserialize, Eq, PartialEq)] struct S { f1: Option, } // This prints a warning (verified below). let s: S = config.get("S").unwrap(); assert_eq!(s, S { f1: None }); // This does not print anything, we cannot easily/reliably warn for // environment variables. let s: S = config.get("S2").unwrap(); assert_eq!(s, S { f1: None }); // Verify the warnings. drop(config); // Paranoid about flushing the file. let path = paths::root().join("shell.out"); let output = fs::read_to_string(path).unwrap(); let expected = "\ warning: unused key `S.unused` in config `[..]/.cargo/config` "; if !lines_match(expected, &output) { panic!( "Did not find expected:\n{}\nActual error:\n{}\n", expected, output ); } } #[cargo_test] fn config_load_toml_profile() { write_config( "\ [profile.dev] opt-level = 's' lto = true codegen-units=4 debug = true debug-assertions = true rpath = true panic = 'abort' overflow-checks = true incremental = true [profile.dev.build-override] opt-level = 1 [profile.dev.package.bar] codegen-units = 9 [profile.no-lto] inherits = 'dev' dir-name = 'without-lto' lto = false ", ); let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_PROFILE_DEV_CODEGEN_UNITS", "5") .env("CARGO_PROFILE_DEV_BUILD_OVERRIDE_CODEGEN_UNITS", "11") .env("CARGO_PROFILE_DEV_PACKAGE_env_CODEGEN_UNITS", "13") .env("CARGO_PROFILE_DEV_PACKAGE_bar_OPT_LEVEL", "2") .build(); // TODO: don't use actual `tomlprofile`. let p: toml::TomlProfile = config.get("profile.dev").unwrap(); let mut packages = BTreeMap::new(); let key = toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("bar").unwrap()); let o_profile = toml::TomlProfile { opt_level: Some(toml::TomlOptLevel("2".to_string())), codegen_units: Some(9), ..Default::default() }; packages.insert(key, o_profile); let key = toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("env").unwrap()); let o_profile = toml::TomlProfile { codegen_units: Some(13), ..Default::default() }; packages.insert(key, o_profile); assert_eq!( p, toml::TomlProfile { opt_level: Some(toml::TomlOptLevel("s".to_string())), lto: Some(toml::StringOrBool::Bool(true)), codegen_units: Some(5), debug: Some(toml::U32OrBool::Bool(true)), debug_assertions: Some(true), rpath: Some(true), panic: Some("abort".to_string()), overflow_checks: Some(true), incremental: Some(true), package: Some(packages), build_override: Some(Box::new(toml::TomlProfile { opt_level: Some(toml::TomlOptLevel("1".to_string())), codegen_units: Some(11), ..Default::default() })), ..Default::default() } ); let p: toml::TomlProfile = config.get("profile.no-lto").unwrap(); assert_eq!( p, toml::TomlProfile { lto: Some(toml::StringOrBool::Bool(false)), dir_name: Some("without-lto".to_string()), inherits: Some("dev".to_string()), ..Default::default() } ); } #[cargo_test] fn config_deserialize_any() { // Some tests to exercise deserialize_any for deserializers that need to // be told the format. write_config( "\ a = true b = ['b'] c = ['c'] ", ); let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_ENVB", "false") .env("CARGO_C", "['d']") .env("CARGO_ENVL", "['a', 'b']") .build(); let a = config.get::("a").unwrap(); match a { VSOB::VecString(_) => panic!("expected bool"), VSOB::Bool(b) => assert_eq!(b, true), } let b = config.get::("b").unwrap(); match b { VSOB::VecString(l) => assert_eq!(l, vec!["b".to_string()]), VSOB::Bool(_) => panic!("expected list"), } let c = config.get::("c").unwrap(); match c { VSOB::VecString(l) => assert_eq!(l, vec!["c".to_string(), "d".to_string()]), VSOB::Bool(_) => panic!("expected list"), } let envb = config.get::("envb").unwrap(); match envb { VSOB::VecString(_) => panic!("expected bool"), VSOB::Bool(b) => assert_eq!(b, false), } let envl = config.get::("envl").unwrap(); match envl { VSOB::VecString(l) => assert_eq!(l, vec!["a".to_string(), "b".to_string()]), VSOB::Bool(_) => panic!("expected list"), } } #[cargo_test] fn config_toml_errors() { write_config( "\ [profile.dev] opt-level = 'foo' ", ); let config = new_config(); assert_error( config.get::("profile.dev").unwrap_err(), "error in [..]/.cargo/config: \ could not load config key `profile.dev.opt-level`: \ must be an integer, `z`, or `s`, but found: foo", ); let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_OPT_LEVEL", "asdf") .build(); assert_error( config.get::("profile.dev").unwrap_err(), "error in environment variable `CARGO_PROFILE_DEV_OPT_LEVEL`: \ could not load config key `profile.dev.opt-level`: \ must be an integer, `z`, or `s`, but found: asdf", ); } #[cargo_test] fn load_nested() { write_config( "\ [nest.foo] f1 = 1 f2 = 2 [nest.bar] asdf = 3 ", ); let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_NEST_foo_f2", "3") .env("CARGO_NESTE_foo_f1", "1") .env("CARGO_NESTE_foo_f2", "3") .env("CARGO_NESTE_bar_asdf", "3") .build(); type Nested = HashMap>; let n: Nested = config.get("nest").unwrap(); let mut expected = HashMap::new(); let mut foo = HashMap::new(); foo.insert("f1".to_string(), 1); foo.insert("f2".to_string(), 3); expected.insert("foo".to_string(), foo); let mut bar = HashMap::new(); bar.insert("asdf".to_string(), 3); expected.insert("bar".to_string(), bar); assert_eq!(n, expected); let n: Nested = config.get("neste").unwrap(); assert_eq!(n, expected); } #[cargo_test] fn get_errors() { write_config( "\ [S] f1 = 123 f2 = 'asdf' big = 123456789 ", ); let config = ConfigBuilder::new() .env("CARGO_E_S", "asdf") .env("CARGO_E_BIG", "123456789") .build(); assert_error( config.get::("foo").unwrap_err(), "missing config key `foo`", ); assert_error( config.get::("foo.bar").unwrap_err(), "missing config key `foo.bar`", ); assert_error( config.get::("S.f2").unwrap_err(), "error in [..]/.cargo/config: `S.f2` expected an integer, but found a string", ); assert_error( config.get::("S.big").unwrap_err(), "error in [..].cargo/config: could not load config key `S.big`: \ invalid value: integer `123456789`, expected u8", ); // Environment variable type errors. assert_error( config.get::("e.s").unwrap_err(), "error in environment variable `CARGO_E_S`: invalid digit found in string", ); assert_error( config.get::("e.big").unwrap_err(), "error in environment variable `CARGO_E_BIG`: \ could not load config key `e.big`: \ invalid value: integer `123456789`, expected i8", ); #[derive(Debug, Deserialize)] struct S { f1: i64, f2: String, f3: i64, big: i64, } assert_error( config.get::("S").unwrap_err(), "missing config key `S.f3`", ); } #[cargo_test] fn config_get_option() { write_config( "\ [foo] f1 = 1 ", ); let config = ConfigBuilder::new().env("CARGO_BAR_ASDF", "3").build(); assert_eq!(config.get::>("a").unwrap(), None); assert_eq!(config.get::>("a.b").unwrap(), None); assert_eq!(config.get::>("foo.f1").unwrap(), Some(1)); assert_eq!(config.get::>("bar.asdf").unwrap(), Some(3)); assert_eq!(config.get::>("bar.zzzz").unwrap(), None); } #[cargo_test] fn config_bad_toml() { write_config("asdf"); let config = new_config(); assert_error( config.get::("foo").unwrap_err(), "\ could not load Cargo configuration Caused by: could not parse TOML configuration in `[..]/.cargo/config` Caused by: could not parse input as TOML Caused by: expected an equals, found eof at line 1 column 5", ); } #[cargo_test] fn config_get_list() { write_config( "\ l1 = [] l2 = ['one', 'two'] l3 = 123 l4 = ['one', 'two'] [nested] l = ['x'] [nested2] l = ['y'] [nested-empty] ", ); type L = Vec; let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_L4", "['three', 'four']") .env("CARGO_L5", "['a']") .env("CARGO_ENV_EMPTY", "[]") .env("CARGO_ENV_BLANK", "") .env("CARGO_ENV_NUM", "1") .env("CARGO_ENV_NUM_LIST", "[1]") .env("CARGO_ENV_TEXT", "asdf") .env("CARGO_LEPAIR", "['a', 'b']") .env("CARGO_NESTED2_L", "['z']") .env("CARGO_NESTEDE_L", "['env']") .env("CARGO_BAD_ENV", "[zzz]") .build(); assert_eq!(config.get::("unset").unwrap(), vec![] as Vec); assert_eq!(config.get::("l1").unwrap(), vec![] as Vec); assert_eq!(config.get::("l2").unwrap(), vec!["one", "two"]); assert_error( config.get::("l3").unwrap_err(), "\ invalid configuration for key `l3` expected a list, but found a integer for `l3` in [..]/.cargo/config", ); assert_eq!( config.get::("l4").unwrap(), vec!["one", "two", "three", "four"] ); assert_eq!(config.get::("l5").unwrap(), vec!["a"]); assert_eq!(config.get::("env-empty").unwrap(), vec![] as Vec); assert_error( config.get::("env-blank").unwrap_err(), "error in environment variable `CARGO_ENV_BLANK`: \ should have TOML list syntax, found ``", ); assert_error( config.get::("env-num").unwrap_err(), "error in environment variable `CARGO_ENV_NUM`: \ should have TOML list syntax, found `1`", ); assert_error( config.get::("env-num-list").unwrap_err(), "error in environment variable `CARGO_ENV_NUM_LIST`: \ expected string, found integer", ); assert_error( config.get::("env-text").unwrap_err(), "error in environment variable `CARGO_ENV_TEXT`: \ should have TOML list syntax, found `asdf`", ); // "invalid number" here isn't the best error, but I think it's just toml.rs. assert_error( config.get::("bad-env").unwrap_err(), "error in environment variable `CARGO_BAD_ENV`: \ could not parse TOML list: invalid number at line 1 column 8", ); // Try some other sequence-like types. assert_eq!( config .get::<(String, String, String, String)>("l4") .unwrap(), ( "one".to_string(), "two".to_string(), "three".to_string(), "four".to_string() ) ); assert_eq!(config.get::<(String,)>("l5").unwrap(), ("a".to_string(),)); // Tuple struct #[derive(Debug, Deserialize, Eq, PartialEq)] struct TupS(String, String); assert_eq!( config.get::("lepair").unwrap(), TupS("a".to_string(), "b".to_string()) ); // Nested with an option. #[derive(Debug, Deserialize, Eq, PartialEq)] struct S { l: Option>, } assert_eq!(config.get::("nested-empty").unwrap(), S { l: None }); assert_eq!( config.get::("nested").unwrap(), S { l: Some(vec!["x".to_string()]), } ); assert_eq!( config.get::("nested2").unwrap(), S { l: Some(vec!["y".to_string(), "z".to_string()]), } ); assert_eq!( config.get::("nestede").unwrap(), S { l: Some(vec!["env".to_string()]), } ); } #[cargo_test] fn config_get_other_types() { write_config( "\ ns = 123 ns2 = 456 ", ); let config = ConfigBuilder::new() .env("CARGO_NSE", "987") .env("CARGO_NS2", "654") .build(); #[derive(Debug, Deserialize, Eq, PartialEq)] #[serde(transparent)] struct NewS(i32); assert_eq!(config.get::("ns").unwrap(), NewS(123)); assert_eq!(config.get::("ns2").unwrap(), NewS(654)); assert_eq!(config.get::("nse").unwrap(), NewS(987)); assert_error( config.get::("unset").unwrap_err(), "missing config key `unset`", ); } #[cargo_test] fn config_relative_path() { write_config(&format!( "\ p1 = 'foo/bar' p2 = '../abc' p3 = 'b/c' abs = '{}' ", paths::home().display(), )); let config = ConfigBuilder::new() .env("CARGO_EPATH", "a/b") .env("CARGO_P3", "d/e") .build(); assert_eq!( config .get::("p1") .unwrap() .resolve_path(&config), paths::root().join("foo/bar") ); assert_eq!( config .get::("p2") .unwrap() .resolve_path(&config), paths::root().join("../abc") ); assert_eq!( config .get::("p3") .unwrap() .resolve_path(&config), paths::root().join("d/e") ); assert_eq!( config .get::("abs") .unwrap() .resolve_path(&config), paths::home() ); assert_eq!( config .get::("epath") .unwrap() .resolve_path(&config), paths::root().join("a/b") ); } #[cargo_test] fn config_get_integers() { write_config( "\ npos = 123456789 nneg = -123456789 i64max = 9223372036854775807 ", ); let config = ConfigBuilder::new() .env("CARGO_EPOS", "123456789") .env("CARGO_ENEG", "-1") .env("CARGO_EI64MAX", "9223372036854775807") .build(); assert_eq!( config.get::("i64max").unwrap(), 9_223_372_036_854_775_807 ); assert_eq!( config.get::("i64max").unwrap(), 9_223_372_036_854_775_807 ); assert_eq!( config.get::("ei64max").unwrap(), 9_223_372_036_854_775_807 ); assert_eq!( config.get::("ei64max").unwrap(), 9_223_372_036_854_775_807 ); assert_error( config.get::("nneg").unwrap_err(), "error in [..].cargo/config: \ could not load config key `nneg`: \ invalid value: integer `-123456789`, expected u32", ); assert_error( config.get::("eneg").unwrap_err(), "error in environment variable `CARGO_ENEG`: \ could not load config key `eneg`: \ invalid value: integer `-1`, expected u32", ); assert_error( config.get::("npos").unwrap_err(), "error in [..].cargo/config: \ could not load config key `npos`: \ invalid value: integer `123456789`, expected i8", ); assert_error( config.get::("epos").unwrap_err(), "error in environment variable `CARGO_EPOS`: \ could not load config key `epos`: \ invalid value: integer `123456789`, expected i8", ); } #[cargo_test] fn config_get_ssl_version_missing() { write_config( "\ [http] hello = 'world' ", ); let config = new_config(); assert!(config .get::>("http.ssl-version") .unwrap() .is_none()); } #[cargo_test] fn config_get_ssl_version_single() { write_config( "\ [http] ssl-version = 'tlsv1.2' ", ); let config = new_config(); let a = config .get::>("http.ssl-version") .unwrap() .unwrap(); match a { SslVersionConfig::Single(v) => assert_eq!(&v, "tlsv1.2"), SslVersionConfig::Range(_) => panic!("Did not expect ssl version min/max."), }; } #[cargo_test] fn config_get_ssl_version_min_max() { write_config( "\ [http] ssl-version.min = 'tlsv1.2' ssl-version.max = 'tlsv1.3' ", ); let config = new_config(); let a = config .get::>("http.ssl-version") .unwrap() .unwrap(); match a { SslVersionConfig::Single(_) => panic!("Did not expect exact ssl version."), SslVersionConfig::Range(range) => { assert_eq!(range.min, Some(String::from("tlsv1.2"))); assert_eq!(range.max, Some(String::from("tlsv1.3"))); } }; } #[cargo_test] fn config_get_ssl_version_both_forms_configured() { // this is not allowed write_config( "\ [http] ssl-version = 'tlsv1.1' ssl-version.min = 'tlsv1.2' ssl-version.max = 'tlsv1.3' ", ); let config = new_config(); assert_error( config .get::("http.ssl-version") .unwrap_err(), "\ could not load Cargo configuration Caused by: could not parse TOML configuration in `[..]/.cargo/config` Caused by: could not parse input as TOML Caused by: dotted key attempted to extend non-table type at line 2 column 15", ); assert!(config .get::>("http.ssl-version") .unwrap() .is_none()); }