//! Tests for config settings. use cargo::core::{PackageIdSpec, Shell}; use cargo::util::config::{self, Config, Definition, JobsConfig, SslVersionConfig, StringList}; use cargo::CargoResult; use cargo_test_support::compare; use cargo_test_support::{panic_error, paths, project, symlink_supported, t}; use cargo_util_schemas::manifest::TomlTrimPaths; use cargo_util_schemas::manifest::TomlTrimPathsValue; use cargo_util_schemas::manifest::{self as cargo_toml, TomlDebugInfo, VecStringOrBool as VSOB}; use serde::Deserialize; use std::borrow::Borrow; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io; use std::os; use std::path::{Path, PathBuf}; /// Helper for constructing a `Config` object. pub struct ConfigBuilder { env: HashMap, unstable: Vec, config_args: Vec, cwd: Option, root: Option, enable_nightly_features: bool, } impl ConfigBuilder { pub fn new() -> ConfigBuilder { ConfigBuilder { env: HashMap::new(), unstable: Vec::new(), config_args: Vec::new(), root: None, cwd: None, enable_nightly_features: false, } } /// 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 } /// Unconditionally enable nightly features, even on stable channels. pub fn nightly_features_allowed(&mut self, allowed: bool) -> &mut Self { self.enable_nightly_features = allowed; self } /// Passes a `--config` flag. pub fn config_arg(&mut self, arg: impl Into) -> &mut Self { self.config_args.push(arg.into()); self } /// Sets the current working directory where config files will be loaded. /// /// Default is the root from [`ConfigBuilder::root`] or [`paths::root`]. pub fn cwd(&mut self, path: impl AsRef) -> &mut Self { let path = path.as_ref(); let cwd = self .root .as_ref() .map_or_else(|| paths::root().join(path), |r| r.join(path)); self.cwd = Some(cwd); self } /// Sets the test root directory. /// /// This generally should not be necessary. It is only useful if you want /// to create a `Config` from within a thread. Since Cargo's testsuite /// uses thread-local storage, this can be used to avoid accessing that /// thread-local storage. /// /// Default is [`paths::root`]. pub fn root(&mut self, path: impl Into) -> &mut Self { self.root = Some(path.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 { let root = self.root.clone().unwrap_or_else(|| paths::root()); let output = Box::new(fs::File::create(root.join("shell.out")).unwrap()); let shell = Shell::from_write(output); let cwd = self.cwd.clone().unwrap_or_else(|| root.clone()); let homedir = root.join("home").join(".cargo"); let mut config = Config::new(shell, cwd, homedir); config.nightly_features_allowed = self.enable_nightly_features || !self.unstable.is_empty(); config.set_env(self.env.clone()); config.set_search_stop_path(&root); config.configure( 0, false, None, false, false, false, &None, &self.unstable, &self.config_args, )?; Ok(config) } } fn new_config() -> Config { ConfigBuilder::new().build() } /// Read the output from Config. pub fn read_output(config: Config) -> String { drop(config); // Paranoid about flushing the file. let path = paths::root().join("shell.out"); fs::read_to_string(path).unwrap() } #[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("check").env("CARGO_BUILD_JOBS", "100").run(); } pub fn write_config_extless(config: &str) { write_config_at(paths::root().join(".cargo/config"), config); } pub fn write_config_at(path: impl AsRef, contents: &str) { let path = paths::root().join(path.as_ref()); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, contents).unwrap(); } pub fn write_config_toml(config: &str) { write_config_at(paths::root().join(".cargo/config.toml"), config); } #[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)); } #[track_caller] pub fn assert_error>(error: E, msgs: &str) { let causes = error .borrow() .chain() .enumerate() .map(|(i, e)| { if i == 0 { e.to_string() } else { format!("Caused by:\n {}", e) } }) .collect::>() .join("\n\n"); assert_match(msgs, &causes); } #[track_caller] pub fn assert_match(expected: &str, actual: &str) { if let Err(e) = compare::match_exact(expected, actual, "output", "", None) { panic_error("", e); } } #[cargo_test] fn get_config() { write_config_toml( "\ [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) }); } #[cfg(windows)] #[cargo_test] fn environment_variable_casing() { // Issue #11814: Environment variable names are case-insensitive on Windows. let config = ConfigBuilder::new() .env("Path", "abc") .env("Two-Words", "abc") .env("two_words", "def") .build(); let var = config.get_env("PATH").unwrap(); assert_eq!(var, String::from("abc")); let var = config.get_env("path").unwrap(); assert_eq!(var, String::from("abc")); let var = config.get_env("TWO-WORDS").unwrap(); assert_eq!(var, String::from("abc")); // Make sure that we can still distinguish between dashes and underscores // in variable names. let var = config.get_env("Two_Words").unwrap(); assert_eq!(var, String::from("def")); } #[cargo_test] fn config_works_without_extension() { write_config_extless( "\ [foo] f1 = 1 ", ); let config = new_config(); assert_eq!(config.get::>("foo.f1").unwrap(), Some(1)); // It should NOT have warned for the symlink. let output = read_output(config); let expected = "\ warning: `[ROOT]/.cargo/config` is deprecated in favor of `config.toml` note: If you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`"; assert_match(expected, &output); } #[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 !symlink_supported() { 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. let output = read_output(config); assert_match("", &output); } #[cargo_test] fn config_ambiguous_filename() { write_config_extless( "\ [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. let output = read_output(config); let expected = "\ warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config` "; assert_match(expected, &output); } #[cargo_test] fn config_unused_fields() { write_config_toml( "\ [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. let output = read_output(config); let expected = "\ warning: unused config key `S.unused` in `[..]/.cargo/config.toml` "; assert_match(expected, &output); } #[cargo_test] fn config_load_toml_profile() { write_config_toml( "\ [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: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); let mut packages = BTreeMap::new(); let key = cargo_toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("bar").unwrap()); let o_profile = cargo_toml::TomlProfile { opt_level: Some(cargo_toml::TomlOptLevel("2".to_string())), codegen_units: Some(9), ..Default::default() }; packages.insert(key, o_profile); let key = cargo_toml::ProfilePackageSpec::Spec(::cargo::core::PackageIdSpec::parse("env").unwrap()); let o_profile = cargo_toml::TomlProfile { codegen_units: Some(13), ..Default::default() }; packages.insert(key, o_profile); assert_eq!( p, cargo_toml::TomlProfile { opt_level: Some(cargo_toml::TomlOptLevel("s".to_string())), lto: Some(cargo_toml::StringOrBool::Bool(true)), codegen_units: Some(5), debug: Some(cargo_toml::TomlDebugInfo::Full), 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(cargo_toml::TomlProfile { opt_level: Some(cargo_toml::TomlOptLevel("1".to_string())), codegen_units: Some(11), ..Default::default() })), ..Default::default() } ); let p: cargo_toml::TomlProfile = config.get("profile.no-lto").unwrap(); assert_eq!( p, cargo_toml::TomlProfile { lto: Some(cargo_toml::StringOrBool::Bool(false)), dir_name: Some(String::from("without-lto")), inherits: Some(String::from("dev")), ..Default::default() } ); } #[cargo_test] fn profile_env_var_prefix() { // Check for a bug with collision on DEBUG vs DEBUG_ASSERTIONS. let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false") .build(); let p: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); assert_eq!(p.debug_assertions, Some(false)); assert_eq!(p.debug, None); let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_DEBUG", "1") .build(); let p: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); assert_eq!(p.debug_assertions, None); assert_eq!(p.debug, Some(cargo_toml::TomlDebugInfo::Limited)); let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_DEBUG_ASSERTIONS", "false") .env("CARGO_PROFILE_DEV_DEBUG", "1") .build(); let p: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); assert_eq!(p.debug_assertions, Some(false)); assert_eq!(p.debug, Some(cargo_toml::TomlDebugInfo::Limited)); } #[cargo_test] fn config_deserialize_any() { // Some tests to exercise deserialize_any for deserializers that need to // be told the format. write_config_toml( "\ a = true b = ['b'] c = ['c'] ", ); // advanced-env let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_ENVB", "false") .env("CARGO_C", "['d']") .env("CARGO_ENVL", "['a', 'b']") .build(); assert_eq!(config.get::("a").unwrap(), VSOB::Bool(true)); assert_eq!( config.get::("b").unwrap(), VSOB::VecString(vec!["b".to_string()]) ); assert_eq!( config.get::("c").unwrap(), VSOB::VecString(vec!["c".to_string(), "d".to_string()]) ); assert_eq!(config.get::("envb").unwrap(), VSOB::Bool(false)); assert_eq!( config.get::("envl").unwrap(), VSOB::VecString(vec!["a".to_string(), "b".to_string()]) ); // Demonstrate where merging logic isn't very smart. This could be improved. let config = ConfigBuilder::new().env("CARGO_A", "x y").build(); assert_error( config.get::("a").unwrap_err(), "\ error in environment variable `CARGO_A`: could not load config key `a` Caused by: invalid type: string \"x y\", expected a boolean or vector of strings", ); // Normal env. let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_B", "d e") .env("CARGO_C", "f g") .build(); assert_eq!( config.get::("b").unwrap(), VSOB::VecString(vec!["b".to_string(), "d".to_string(), "e".to_string()]) ); assert_eq!( config.get::("c").unwrap(), VSOB::VecString(vec!["c".to_string(), "f".to_string(), "g".to_string()]) ); // config-cli // This test demonstrates that ConfigValue::merge isn't very smart. // It would be nice if it was smarter. let config = ConfigBuilder::new().config_arg("a = ['a']").build_err(); assert_error( config.unwrap_err(), "\ failed to merge --config key `a` into `[..]/.cargo/config.toml` Caused by: failed to merge config value from `--config cli option` into `[..]/.cargo/config.toml`: \ expected boolean, but found array", ); // config-cli and advanced-env let config = ConfigBuilder::new() .unstable_flag("advanced-env") .config_arg("b=['clib']") .config_arg("c=['clic']") .env("CARGO_B", "env1 env2") .env("CARGO_C", "['e1', 'e2']") .build(); assert_eq!( config.get::("b").unwrap(), VSOB::VecString(vec![ "b".to_string(), "env1".to_string(), "env2".to_string(), "clib".to_string(), ]) ); assert_eq!( config.get::("c").unwrap(), VSOB::VecString(vec![ "c".to_string(), "e1".to_string(), "e2".to_string(), "clic".to_string(), ]) ); } #[cargo_test] fn config_toml_errors() { write_config_toml( "\ [profile.dev] opt-level = 'foo' ", ); let config = new_config(); assert_error( config .get::("profile.dev") .unwrap_err(), "\ error in [..]/.cargo/config.toml: could not load config key `profile.dev.opt-level` Caused by: must be `0`, `1`, `2`, `3`, `s` or `z`, but found the string: \"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` Caused by: must be `0`, `1`, `2`, `3`, `s` or `z`, but found the string: \"asdf\"", ); } #[cargo_test] fn load_nested() { write_config_toml( "\ [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_toml( "\ [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.toml: `S.f2` expected an integer, but found a string", ); assert_error( config.get::("S.big").unwrap_err(), "\ error in [..].cargo/config.toml: could not load config key `S.big` Caused by: 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` Caused by: invalid value: integer `123456789`, expected i8", ); #[derive(Debug, Deserialize)] #[allow(dead_code)] struct S { f1: i64, f2: String, f3: i64, big: i64, } assert_error(config.get::("S").unwrap_err(), "missing field `f3`"); } #[cargo_test] fn config_get_option() { write_config_toml( "\ [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_toml("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.toml` Caused by: TOML parse error at line 1, column 5 | 1 | asdf | ^ expected `.`, `=`", ); } #[cargo_test] fn config_get_list() { write_config_toml( "\ 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.toml", ); 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_eq!(config.get::("env-blank").unwrap(), vec![] as Vec); assert_eq!(config.get::("env-num").unwrap(), vec!["1".to_string()]); assert_error( config.get::("env-num-list").unwrap_err(), "error in environment variable `CARGO_ENV_NUM_LIST`: \ expected string, found integer", ); assert_eq!( config.get::("env-text").unwrap(), vec!["asdf".to_string()] ); // "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: TOML parse error at line 1, column 2 | 1 | [zzz] | ^ invalid array expected `]` ", ); // 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_toml( "\ 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_toml(&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_toml( "\ 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.toml: could not load config key `nneg` Caused by: 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` Caused by: invalid value: integer `-1`, expected u32", ); assert_error( config.get::("npos").unwrap_err(), "\ error in [..].cargo/config.toml: could not load config key `npos` Caused by: 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` Caused by: invalid value: integer `123456789`, expected i8", ); } #[cargo_test] fn config_get_ssl_version_missing() { write_config_toml( "\ [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_toml( "\ [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_toml( "\ [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_toml( "\ [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.toml` Caused by: TOML parse error at line 3, column 1 | 3 | ssl-version.min = 'tlsv1.2' | ^ dotted key `ssl-version` attempted to extend non-table type (string) ", ); } #[cargo_test] /// Assert that unstable options can be configured with the `unstable` table in /// cargo config files fn unstable_table_notation() { write_config_toml( "\ [unstable] print-im-a-teapot = true ", ); let config = ConfigBuilder::new().nightly_features_allowed(true).build(); assert_eq!(config.cli_unstable().print_im_a_teapot, true); } #[cargo_test] /// Assert that dotted notation works for configuring unstable options fn unstable_dotted_notation() { write_config_toml( "\ unstable.print-im-a-teapot = true ", ); let config = ConfigBuilder::new().nightly_features_allowed(true).build(); assert_eq!(config.cli_unstable().print_im_a_teapot, true); } #[cargo_test] /// Assert that Zflags on the CLI take precedence over those from config fn unstable_cli_precedence() { write_config_toml( "\ unstable.print-im-a-teapot = true ", ); let config = ConfigBuilder::new().nightly_features_allowed(true).build(); assert_eq!(config.cli_unstable().print_im_a_teapot, true); let config = ConfigBuilder::new() .unstable_flag("print-im-a-teapot=no") .build(); assert_eq!(config.cli_unstable().print_im_a_teapot, false); } #[cargo_test] /// Assert that attempting to set an unstable flag that doesn't exist via config /// is ignored on stable fn unstable_invalid_flag_ignored_on_stable() { write_config_toml( "\ unstable.an-invalid-flag = 'yes' ", ); assert!(ConfigBuilder::new().build_err().is_ok()); } #[cargo_test] /// Assert that unstable options can be configured with the `unstable` table in /// cargo config files fn unstable_flags_ignored_on_stable() { write_config_toml( "\ [unstable] print-im-a-teapot = true ", ); // Enforce stable channel even when testing on nightly. let config = ConfigBuilder::new().nightly_features_allowed(false).build(); assert_eq!(config.cli_unstable().print_im_a_teapot, false); } #[cargo_test] fn table_merge_failure() { // Config::merge fails to merge entries in two tables. write_config_at( "foo/.cargo/config.toml", " [table] key = ['foo'] ", ); write_config_at( ".cargo/config.toml", " [table] key = 'bar' ", ); #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Table { key: StringList, } let config = ConfigBuilder::new().cwd("foo").build(); assert_error( config.get::("table").unwrap_err(), "\ could not load Cargo configuration Caused by: failed to merge configuration at `[..]/.cargo/config.toml` Caused by: failed to merge key `table` between [..]/foo/.cargo/config.toml and [..]/.cargo/config.toml Caused by: failed to merge key `key` between [..]/foo/.cargo/config.toml and [..]/.cargo/config.toml Caused by: failed to merge config value from `[..]/.cargo/config.toml` into `[..]/foo/.cargo/config.toml`: \ expected array, but found string", ); } #[cargo_test] fn non_string_in_array() { // Currently only strings are supported. write_config_toml("foo = [1, 2, 3]"); let config = new_config(); assert_error( config.get::>("foo").unwrap_err(), "\ could not load Cargo configuration Caused by: failed to load TOML configuration from `[..]/.cargo/config.toml` Caused by: failed to parse key `foo` Caused by: expected string but found integer in list", ); } #[cargo_test] fn struct_with_opt_inner_struct() { // Struct with a key that is Option of another struct. // Check that can be defined with environment variable. #[derive(Deserialize)] struct Inner { value: Option, } #[derive(Deserialize)] struct Foo { inner: Option, } let config = ConfigBuilder::new() .env("CARGO_FOO_INNER_VALUE", "12") .build(); let f: Foo = config.get("foo").unwrap(); assert_eq!(f.inner.unwrap().value.unwrap(), 12); } #[cargo_test] fn struct_with_default_inner_struct() { // Struct with serde defaults. // Check that can be defined with environment variable. #[derive(Deserialize, Default)] #[serde(default)] struct Inner { value: i32, } #[derive(Deserialize, Default)] #[serde(default)] struct Foo { inner: Inner, } let config = ConfigBuilder::new() .env("CARGO_FOO_INNER_VALUE", "12") .build(); let f: Foo = config.get("foo").unwrap(); assert_eq!(f.inner.value, 12); } #[cargo_test] fn overlapping_env_config() { // Issue where one key is a prefix of another. #[derive(Deserialize)] #[serde(rename_all = "kebab-case")] struct Ambig { debug: Option, debug_assertions: Option, } let config = ConfigBuilder::new() .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true") .build(); let s: Ambig = config.get("ambig").unwrap(); assert_eq!(s.debug_assertions, Some(true)); assert_eq!(s.debug, None); let config = ConfigBuilder::new().env("CARGO_AMBIG_DEBUG", "0").build(); let s: Ambig = config.get("ambig").unwrap(); assert_eq!(s.debug_assertions, None); assert_eq!(s.debug, Some(0)); let config = ConfigBuilder::new() .env("CARGO_AMBIG_DEBUG", "1") .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true") .build(); let s: Ambig = config.get("ambig").unwrap(); assert_eq!(s.debug_assertions, Some(true)); assert_eq!(s.debug, Some(1)); } #[cargo_test] fn overlapping_env_with_defaults_errors_out() { // Issue where one key is a prefix of another. // This is a limitation of mapping environment variables on to a hierarchy. // Check that we error out when we hit ambiguity in this way, rather than // the more-surprising defaulting through. // If, in the future, we can handle this more correctly, feel free to delete // this test. #[derive(Deserialize, Default)] #[serde(default, rename_all = "kebab-case")] struct Ambig { debug: u32, debug_assertions: bool, } let config = ConfigBuilder::new() .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true") .build(); let err = config.get::("ambig").err().unwrap(); assert!(format!("{}", err).contains("missing config key `ambig.debug`")); let config = ConfigBuilder::new().env("CARGO_AMBIG_DEBUG", "5").build(); let s: Ambig = config.get("ambig").unwrap(); assert_eq!(s.debug_assertions, bool::default()); assert_eq!(s.debug, 5); let config = ConfigBuilder::new() .env("CARGO_AMBIG_DEBUG", "1") .env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true") .build(); let s: Ambig = config.get("ambig").unwrap(); assert_eq!(s.debug_assertions, true); assert_eq!(s.debug, 1); } #[cargo_test] fn struct_with_overlapping_inner_struct_and_defaults() { // Struct with serde defaults. // Check that can be defined with environment variable. #[derive(Deserialize, Default)] #[serde(default)] struct Inner { value: i32, } // Containing struct with a prefix of inner // // This is a limitation of mapping environment variables on to a hierarchy. // Check that we error out when we hit ambiguity in this way, rather than // the more-surprising defaulting through. // If, in the future, we can handle this more correctly, feel free to delete // this case. #[derive(Deserialize, Default)] #[serde(default)] struct PrefixContainer { inn: bool, inner: Inner, } let config = ConfigBuilder::new() .env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12") .build(); let err = config .get::("prefixcontainer") .err() .unwrap(); assert!(format!("{}", err).contains("missing config key `prefixcontainer.inn`")); let config = ConfigBuilder::new() .env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12") .env("CARGO_PREFIXCONTAINER_INN", "true") .build(); let f: PrefixContainer = config.get("prefixcontainer").unwrap(); assert_eq!(f.inner.value, 12); assert_eq!(f.inn, true); // Containing struct where the inner value's field is a prefix of another // // This is a limitation of mapping environment variables on to a hierarchy. // Check that we error out when we hit ambiguity in this way, rather than // the more-surprising defaulting through. // If, in the future, we can handle this more correctly, feel free to delete // this case. #[derive(Deserialize, Default)] #[serde(default)] struct InversePrefixContainer { inner_field: bool, inner: Inner, } let config = ConfigBuilder::new() .env("CARGO_INVERSEPREFIXCONTAINER_INNER_VALUE", "12") .build(); let f: InversePrefixContainer = config.get("inverseprefixcontainer").unwrap(); assert_eq!(f.inner_field, bool::default()); assert_eq!(f.inner.value, 12); } #[cargo_test] fn string_list_tricky_env() { // Make sure StringList handles typed env values. let config = ConfigBuilder::new() .env("CARGO_KEY1", "123") .env("CARGO_KEY2", "true") .env("CARGO_KEY3", "1 2") .build(); let x = config.get::("key1").unwrap(); assert_eq!(x.as_slice(), &["123".to_string()]); let x = config.get::("key2").unwrap(); assert_eq!(x.as_slice(), &["true".to_string()]); let x = config.get::("key3").unwrap(); assert_eq!(x.as_slice(), &["1".to_string(), "2".to_string()]); } #[cargo_test] fn string_list_wrong_type() { // What happens if StringList is given then wrong type. write_config_toml("some_list = 123"); let config = ConfigBuilder::new().build(); assert_error( config.get::("some_list").unwrap_err(), "\ invalid configuration for key `some_list` expected a string or array of strings, but found a integer for `some_list` in [..]/.cargo/config.toml", ); write_config_toml("some_list = \"1 2\""); let config = ConfigBuilder::new().build(); let x = config.get::("some_list").unwrap(); assert_eq!(x.as_slice(), &["1".to_string(), "2".to_string()]); } #[cargo_test] fn string_list_advanced_env() { // StringList with advanced env. let config = ConfigBuilder::new() .unstable_flag("advanced-env") .env("CARGO_KEY1", "[]") .env("CARGO_KEY2", "['1 2', '3']") .env("CARGO_KEY3", "[123]") .build(); let x = config.get::("key1").unwrap(); assert_eq!(x.as_slice(), &[] as &[String]); let x = config.get::("key2").unwrap(); assert_eq!(x.as_slice(), &["1 2".to_string(), "3".to_string()]); assert_error( config.get::("key3").unwrap_err(), "error in environment variable `CARGO_KEY3`: expected string, found integer", ); } #[cargo_test] fn parse_strip_with_string() { write_config_toml( "\ [profile.release] strip = 'debuginfo' ", ); let config = new_config(); let p: cargo_toml::TomlProfile = config.get("profile.release").unwrap(); let strip = p.strip.unwrap(); assert_eq!( strip, cargo_toml::StringOrBool::String("debuginfo".to_string()) ); } #[cargo_test] fn cargo_target_empty_cfg() { write_config_toml( "\ [build] target-dir = '' ", ); let config = new_config(); assert_error( config.target_dir().unwrap_err(), "the target directory is set to an empty string in [..]/.cargo/config.toml", ); } #[cargo_test] fn cargo_target_empty_env() { let project = project().build(); project.cargo("check") .env("CARGO_TARGET_DIR", "") .with_stderr("error: the target directory is set to an empty string in the `CARGO_TARGET_DIR` environment variable") .with_status(101) .run() } #[cargo_test] fn all_profile_options() { // Check that all profile options can be serialized/deserialized. let base_settings = cargo_toml::TomlProfile { opt_level: Some(cargo_toml::TomlOptLevel("0".to_string())), lto: Some(cargo_toml::StringOrBool::String("thin".to_string())), codegen_backend: Some(String::from("example")), codegen_units: Some(123), debug: Some(cargo_toml::TomlDebugInfo::Limited), split_debuginfo: Some("packed".to_string()), debug_assertions: Some(true), rpath: Some(true), panic: Some("abort".to_string()), overflow_checks: Some(true), incremental: Some(true), dir_name: Some(String::from("dir_name")), inherits: Some(String::from("debug")), strip: Some(cargo_toml::StringOrBool::String("symbols".to_string())), package: None, build_override: None, rustflags: None, trim_paths: None, }; let mut overrides = BTreeMap::new(); let key = cargo_toml::ProfilePackageSpec::Spec(PackageIdSpec::parse("foo").unwrap()); overrides.insert(key, base_settings.clone()); let profile = cargo_toml::TomlProfile { build_override: Some(Box::new(base_settings.clone())), package: Some(overrides), ..base_settings }; let profile_toml = toml::to_string(&profile).unwrap(); let roundtrip: cargo_toml::TomlProfile = toml::from_str(&profile_toml).unwrap(); let roundtrip_toml = toml::to_string(&roundtrip).unwrap(); compare::assert_match_exact(&profile_toml, &roundtrip_toml); } #[cargo_test] fn value_in_array() { // Value in an array should work let root_path = paths::root().join(".cargo/config.toml"); write_config_at( &root_path, "\ [net.ssh] known-hosts = [ \"example.com ...\", \"example.net ...\", ] ", ); let foo_path = paths::root().join("foo/.cargo/config.toml"); write_config_at( &foo_path, "\ [net.ssh] known-hosts = [ \"example.org ...\", ] ", ); let config = ConfigBuilder::new() .cwd("foo") // environment variables don't actually work for known-hosts due to // space splitting, but this is included here just to validate that // they work (particularly if other Vec config vars are added // in the future). .env("CARGO_NET_SSH_KNOWN_HOSTS", "env-example") .build(); let net_config = config.net_config().unwrap(); let kh = net_config .ssh .as_ref() .unwrap() .known_hosts .as_ref() .unwrap(); assert_eq!(kh.len(), 4); assert_eq!(kh[0].val, "example.com ..."); assert_eq!(kh[0].definition, Definition::Path(root_path.clone())); assert_eq!(kh[1].val, "example.net ..."); assert_eq!(kh[1].definition, Definition::Path(root_path.clone())); assert_eq!(kh[2].val, "example.org ..."); assert_eq!(kh[2].definition, Definition::Path(foo_path.clone())); assert_eq!(kh[3].val, "env-example"); assert_eq!( kh[3].definition, Definition::Environment("CARGO_NET_SSH_KNOWN_HOSTS".to_string()) ); } #[cargo_test] fn debuginfo_parsing() { let config = ConfigBuilder::new().build(); let p: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); assert_eq!(p.debug, None); let env_test_cases = [ (TomlDebugInfo::None, ["false", "0", "none"].as_slice()), (TomlDebugInfo::LineDirectivesOnly, &["line-directives-only"]), (TomlDebugInfo::LineTablesOnly, &["line-tables-only"]), (TomlDebugInfo::Limited, &["1", "limited"]), (TomlDebugInfo::Full, &["true", "2", "full"]), ]; for (expected, config_strs) in env_test_cases { for &val in config_strs { let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_DEBUG", val) .build(); let debug: TomlDebugInfo = config.get("profile.dev.debug").unwrap(); assert_eq!(debug, expected, "failed to parse {val}"); } } let toml_test_cases = [ (TomlDebugInfo::None, ["false", "0", "\"none\""].as_slice()), ( TomlDebugInfo::LineDirectivesOnly, &["\"line-directives-only\""], ), (TomlDebugInfo::LineTablesOnly, &["\"line-tables-only\""]), (TomlDebugInfo::Limited, &["1", "\"limited\""]), (TomlDebugInfo::Full, &["true", "2", "\"full\""]), ]; for (expected, config_strs) in toml_test_cases { for &val in config_strs { let config = ConfigBuilder::new() .config_arg(format!("profile.dev.debug={val}")) .build(); let debug: TomlDebugInfo = config.get("profile.dev.debug").unwrap(); assert_eq!(debug, expected, "failed to parse {val}"); } } let toml_err_cases = ["\"\"", "\"unrecognized\"", "3"]; for err_val in toml_err_cases { let config = ConfigBuilder::new() .config_arg(format!("profile.dev.debug={err_val}")) .build(); let err = config .get::("profile.dev.debug") .unwrap_err(); assert!(err .to_string() .ends_with("could not load config key `profile.dev.debug`")); } } #[cargo_test] fn build_jobs_missing() { write_config_toml( "\ [build] ", ); let config = new_config(); assert!(config .get::>("build.jobs") .unwrap() .is_none()); } #[cargo_test] fn build_jobs_default() { write_config_toml( "\ [build] jobs = \"default\" ", ); let config = new_config(); let a = config .get::>("build.jobs") .unwrap() .unwrap(); match a { JobsConfig::String(v) => assert_eq!(&v, "default"), JobsConfig::Integer(_) => panic!("Did not except an integer."), } } #[cargo_test] fn build_jobs_integer() { write_config_toml( "\ [build] jobs = 2 ", ); let config = new_config(); let a = config .get::>("build.jobs") .unwrap() .unwrap(); match a { JobsConfig::String(_) => panic!("Did not except an integer."), JobsConfig::Integer(v) => assert_eq!(v, 2), } } #[cargo_test] fn trim_paths_parsing() { let config = ConfigBuilder::new().build(); let p: cargo_toml::TomlProfile = config.get("profile.dev").unwrap(); assert_eq!(p.trim_paths, None); let test_cases = [ (TomlTrimPathsValue::Diagnostics.into(), "diagnostics"), (TomlTrimPathsValue::Macro.into(), "macro"), (TomlTrimPathsValue::Object.into(), "object"), ]; for (expected, val) in test_cases { // env let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_TRIM_PATHS", val) .build(); let trim_paths: TomlTrimPaths = config.get("profile.dev.trim-paths").unwrap(); assert_eq!(trim_paths, expected, "failed to parse {val}"); // config.toml let config = ConfigBuilder::new() .config_arg(format!("profile.dev.trim-paths='{val}'")) .build(); let trim_paths: TomlTrimPaths = config.get("profile.dev.trim-paths").unwrap(); assert_eq!(trim_paths, expected, "failed to parse {val}"); } let test_cases = [(TomlTrimPaths::none(), false), (TomlTrimPaths::All, true)]; for (expected, val) in test_cases { // env let config = ConfigBuilder::new() .env("CARGO_PROFILE_DEV_TRIM_PATHS", format!("{val}")) .build(); let trim_paths: TomlTrimPaths = config.get("profile.dev.trim-paths").unwrap(); assert_eq!(trim_paths, expected, "failed to parse {val}"); // config.toml let config = ConfigBuilder::new() .config_arg(format!("profile.dev.trim-paths={val}")) .build(); let trim_paths: TomlTrimPaths = config.get("profile.dev.trim-paths").unwrap(); assert_eq!(trim_paths, expected, "failed to parse {val}"); } let expected = vec![ TomlTrimPathsValue::Diagnostics, TomlTrimPathsValue::Macro, TomlTrimPathsValue::Object, ] .into(); let val = r#"["diagnostics", "macro", "object"]"#; // config.toml let config = ConfigBuilder::new() .config_arg(format!("profile.dev.trim-paths={val}")) .build(); let trim_paths: TomlTrimPaths = config.get("profile.dev.trim-paths").unwrap(); assert_eq!(trim_paths, expected, "failed to parse {val}"); }