summaryrefslogtreecommitdiffstats
path: root/src/tools/cargo/tests/testsuite/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/cargo/tests/testsuite/config.rs')
-rw-r--r--src/tools/cargo/tests/testsuite/config.rs1596
1 files changed, 1596 insertions, 0 deletions
diff --git a/src/tools/cargo/tests/testsuite/config.rs b/src/tools/cargo/tests/testsuite/config.rs
new file mode 100644
index 000000000..92e1f4264
--- /dev/null
+++ b/src/tools/cargo/tests/testsuite/config.rs
@@ -0,0 +1,1596 @@
+//! Tests for config settings.
+
+use cargo::core::{PackageIdSpec, Shell};
+use cargo::util::config::{self, Config, Definition, SslVersionConfig, StringList};
+use cargo::util::interning::InternedString;
+use cargo::util::toml::{self as cargo_toml, VecStringOrBool as VSOB};
+use cargo::CargoResult;
+use cargo_test_support::compare;
+use cargo_test_support::{panic_error, paths, project, symlink_supported, t};
+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<String, String>,
+ unstable: Vec<String>,
+ config_args: Vec<String>,
+ cwd: Option<PathBuf>,
+ enable_nightly_features: bool,
+}
+
+impl ConfigBuilder {
+ pub fn new() -> ConfigBuilder {
+ ConfigBuilder {
+ env: HashMap::new(),
+ unstable: Vec::new(),
+ config_args: Vec::new(),
+ cwd: None,
+ enable_nightly_features: false,
+ }
+ }
+
+ /// Passes a `-Z` flag.
+ pub fn unstable_flag(&mut self, s: impl Into<String>) -> &mut Self {
+ self.unstable.push(s.into());
+ self
+ }
+
+ /// Sets an environment variable.
+ pub fn env(&mut self, key: impl Into<String>, val: impl Into<String>) -> &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<String>) -> &mut Self {
+ self.config_args.push(arg.into());
+ self
+ }
+
+ /// Sets the current working directory where config files will be loaded.
+ pub fn cwd(&mut self, path: impl AsRef<Path>) -> &mut Self {
+ self.cwd = Some(paths::root().join(path.as_ref()));
+ 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<Config> {
+ let output = Box::new(fs::File::create(paths::root().join("shell.out")).unwrap());
+ let shell = Shell::from_write(output);
+ let cwd = self.cwd.clone().unwrap_or_else(|| paths::root());
+ let homedir = paths::home();
+ 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(paths::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(config: &str) {
+ write_config_at(paths::root().join(".cargo/config"), config);
+}
+
+pub fn write_config_at(path: impl AsRef<Path>, 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<E: Borrow<anyhow::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::<Vec<_>>()
+ .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(
+ "\
+[S]
+f1 = 123
+",
+ );
+
+ let config = new_config();
+
+ #[derive(Debug, Deserialize, Eq, PartialEq)]
+ struct S {
+ f1: Option<i64>,
+ }
+ 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_with_extension() {
+ write_config_toml(
+ "\
+[foo]
+f1 = 1
+",
+ );
+
+ let config = new_config();
+
+ assert_eq!(config.get::<Option<i32>>("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 !symlink_supported() {
+ return;
+ };
+
+ write_config_toml(
+ "\
+[foo]
+f1 = 1
+",
+ );
+
+ symlink_config_to_config_toml();
+
+ let config = new_config();
+
+ assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
+
+ // It should NOT have warned for the symlink.
+ let output = read_output(config);
+ assert_eq!(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::<Option<i32>>("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(
+ "\
+[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<i64>,
+ }
+ // 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`
+";
+ assert_match(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: 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::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(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(InternedString::new("without-lto")),
+ inherits: Some(InternedString::new("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::U32OrBool::U32(1)));
+
+ 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::U32OrBool::U32(1)));
+}
+
+#[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']
+",
+ );
+
+ // 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::<VSOB>("a").unwrap(), VSOB::Bool(true));
+ assert_eq!(
+ config.get::<VSOB>("b").unwrap(),
+ VSOB::VecString(vec!["b".to_string()])
+ );
+ assert_eq!(
+ config.get::<VSOB>("c").unwrap(),
+ VSOB::VecString(vec!["c".to_string(), "d".to_string()])
+ );
+ assert_eq!(config.get::<VSOB>("envb").unwrap(), VSOB::Bool(false));
+ assert_eq!(
+ config.get::<VSOB>("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::<VSOB>("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::<VSOB>("b").unwrap(),
+ VSOB::VecString(vec!["b".to_string(), "d".to_string(), "e".to_string()])
+ );
+ assert_eq!(
+ config.get::<VSOB>("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`
+
+Caused by:
+ failed to merge config value from `--config cli option` into `[..]/.cargo/config`: \
+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::<VSOB>("b").unwrap(),
+ VSOB::VecString(vec![
+ "b".to_string(),
+ "clib".to_string(),
+ "env1".to_string(),
+ "env2".to_string()
+ ])
+ );
+ assert_eq!(
+ config.get::<VSOB>("c").unwrap(),
+ VSOB::VecString(vec![
+ "c".to_string(),
+ "clic".to_string(),
+ "e1".to_string(),
+ "e2".to_string()
+ ])
+ );
+}
+
+#[cargo_test]
+fn config_toml_errors() {
+ write_config(
+ "\
+[profile.dev]
+opt-level = 'foo'
+",
+ );
+
+ let config = new_config();
+
+ assert_error(
+ config
+ .get::<cargo_toml::TomlProfile>("profile.dev")
+ .unwrap_err(),
+ "\
+error in [..]/.cargo/config: 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::<cargo_toml::TomlProfile>("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(
+ "\
+[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<String, HashMap<String, u8>>;
+
+ 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::<i64>("foo").unwrap_err(),
+ "missing config key `foo`",
+ );
+ assert_error(
+ config.get::<i64>("foo.bar").unwrap_err(),
+ "missing config key `foo.bar`",
+ );
+ assert_error(
+ config.get::<i64>("S.f2").unwrap_err(),
+ "error in [..]/.cargo/config: `S.f2` expected an integer, but found a string",
+ );
+ assert_error(
+ config.get::<u8>("S.big").unwrap_err(),
+ "\
+error in [..].cargo/config: could not load config key `S.big`
+
+Caused by:
+ invalid value: integer `123456789`, expected u8",
+ );
+
+ // Environment variable type errors.
+ assert_error(
+ config.get::<i64>("e.s").unwrap_err(),
+ "error in environment variable `CARGO_E_S`: invalid digit found in string",
+ );
+ assert_error(
+ config.get::<i8>("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>("S").unwrap_err(), "missing field `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::<Option<i32>>("a").unwrap(), None);
+ assert_eq!(config.get::<Option<i32>>("a.b").unwrap(), None);
+ assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
+ assert_eq!(config.get::<Option<i32>>("bar.asdf").unwrap(), Some(3));
+ assert_eq!(config.get::<Option<i32>>("bar.zzzz").unwrap(), None);
+}
+
+#[cargo_test]
+fn config_bad_toml() {
+ write_config("asdf");
+ let config = new_config();
+ assert_error(
+ config.get::<i32>("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:
+ TOML parse error at line 1, column 5
+ |
+1 | asdf
+ | ^
+expected `.`, `=`",
+ );
+}
+
+#[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<String>;
+
+ 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::<L>("unset").unwrap(), vec![] as Vec<String>);
+ assert_eq!(config.get::<L>("l1").unwrap(), vec![] as Vec<String>);
+ assert_eq!(config.get::<L>("l2").unwrap(), vec!["one", "two"]);
+ assert_error(
+ config.get::<L>("l3").unwrap_err(),
+ "\
+invalid configuration for key `l3`
+expected a list, but found a integer for `l3` in [..]/.cargo/config",
+ );
+ assert_eq!(
+ config.get::<L>("l4").unwrap(),
+ vec!["one", "two", "three", "four"]
+ );
+ assert_eq!(config.get::<L>("l5").unwrap(), vec!["a"]);
+ assert_eq!(config.get::<L>("env-empty").unwrap(), vec![] as Vec<String>);
+ assert_eq!(config.get::<L>("env-blank").unwrap(), vec![] as Vec<String>);
+ assert_eq!(config.get::<L>("env-num").unwrap(), vec!["1".to_string()]);
+ assert_error(
+ config.get::<L>("env-num-list").unwrap_err(),
+ "error in environment variable `CARGO_ENV_NUM_LIST`: \
+ expected string, found integer",
+ );
+ assert_eq!(
+ config.get::<L>("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::<L>("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::<TupS>("lepair").unwrap(),
+ TupS("a".to_string(), "b".to_string())
+ );
+
+ // Nested with an option.
+ #[derive(Debug, Deserialize, Eq, PartialEq)]
+ struct S {
+ l: Option<Vec<String>>,
+ }
+ assert_eq!(config.get::<S>("nested-empty").unwrap(), S { l: None });
+ assert_eq!(
+ config.get::<S>("nested").unwrap(),
+ S {
+ l: Some(vec!["x".to_string()]),
+ }
+ );
+ assert_eq!(
+ config.get::<S>("nested2").unwrap(),
+ S {
+ l: Some(vec!["y".to_string(), "z".to_string()]),
+ }
+ );
+ assert_eq!(
+ config.get::<S>("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::<NewS>("ns").unwrap(), NewS(123));
+ assert_eq!(config.get::<NewS>("ns2").unwrap(), NewS(654));
+ assert_eq!(config.get::<NewS>("nse").unwrap(), NewS(987));
+ assert_error(
+ config.get::<NewS>("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::<config::ConfigRelativePath>("p1")
+ .unwrap()
+ .resolve_path(&config),
+ paths::root().join("foo/bar")
+ );
+ assert_eq!(
+ config
+ .get::<config::ConfigRelativePath>("p2")
+ .unwrap()
+ .resolve_path(&config),
+ paths::root().join("../abc")
+ );
+ assert_eq!(
+ config
+ .get::<config::ConfigRelativePath>("p3")
+ .unwrap()
+ .resolve_path(&config),
+ paths::root().join("d/e")
+ );
+ assert_eq!(
+ config
+ .get::<config::ConfigRelativePath>("abs")
+ .unwrap()
+ .resolve_path(&config),
+ paths::home()
+ );
+ assert_eq!(
+ config
+ .get::<config::ConfigRelativePath>("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::<u64>("i64max").unwrap(),
+ 9_223_372_036_854_775_807
+ );
+ assert_eq!(
+ config.get::<i64>("i64max").unwrap(),
+ 9_223_372_036_854_775_807
+ );
+ assert_eq!(
+ config.get::<u64>("ei64max").unwrap(),
+ 9_223_372_036_854_775_807
+ );
+ assert_eq!(
+ config.get::<i64>("ei64max").unwrap(),
+ 9_223_372_036_854_775_807
+ );
+
+ assert_error(
+ config.get::<u32>("nneg").unwrap_err(),
+ "\
+error in [..].cargo/config: could not load config key `nneg`
+
+Caused by:
+ invalid value: integer `-123456789`, expected u32",
+ );
+ assert_error(
+ config.get::<u32>("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::<i8>("npos").unwrap_err(),
+ "\
+error in [..].cargo/config: could not load config key `npos`
+
+Caused by:
+ invalid value: integer `123456789`, expected i8",
+ );
+ assert_error(
+ config.get::<i8>("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(
+ "\
+[http]
+hello = 'world'
+",
+ );
+
+ let config = new_config();
+
+ assert!(config
+ .get::<Option<SslVersionConfig>>("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::<Option<SslVersionConfig>>("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::<Option<SslVersionConfig>>("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::<SslVersionConfig>("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:
+ 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(
+ "\
+[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(
+ "\
+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(
+ "\
+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(
+ "\
+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(
+ "\
+[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",
+ "
+ [table]
+ key = ['foo']
+ ",
+ );
+ write_config_at(
+ ".cargo/config",
+ "
+ [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>("table").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to merge configuration at `[..]/.cargo/config`
+
+Caused by:
+ failed to merge key `table` between [..]/foo/.cargo/config and [..]/.cargo/config
+
+Caused by:
+ failed to merge key `key` between [..]/foo/.cargo/config and [..]/.cargo/config
+
+Caused by:
+ failed to merge config value from `[..]/.cargo/config` into `[..]/foo/.cargo/config`: \
+ expected array, but found string",
+ );
+}
+
+#[cargo_test]
+fn non_string_in_array() {
+ // Currently only strings are supported.
+ write_config("foo = [1, 2, 3]");
+ let config = new_config();
+ assert_error(
+ config.get::<Vec<i32>>("foo").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to load TOML configuration from `[..]/.cargo/config`
+
+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<i32>,
+ }
+ #[derive(Deserialize)]
+ struct Foo {
+ inner: Option<Inner>,
+ }
+ 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<u32>,
+ debug_assertions: Option<bool>,
+ }
+ 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>("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>("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::<StringList>("key1").unwrap();
+ assert_eq!(x.as_slice(), &["123".to_string()]);
+ let x = config.get::<StringList>("key2").unwrap();
+ assert_eq!(x.as_slice(), &["true".to_string()]);
+ let x = config.get::<StringList>("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("some_list = 123");
+ let config = ConfigBuilder::new().build();
+ assert_error(
+ config.get::<StringList>("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",
+ );
+
+ write_config("some_list = \"1 2\"");
+ let config = ConfigBuilder::new().build();
+ let x = config.get::<StringList>("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::<StringList>("key1").unwrap();
+ assert_eq!(x.as_slice(), &[] as &[String]);
+ let x = config.get::<StringList>("key2").unwrap();
+ assert_eq!(x.as_slice(), &["1 2".to_string(), "3".to_string()]);
+ assert_error(
+ config.get::<StringList>("key3").unwrap_err(),
+ "error in environment variable `CARGO_KEY3`: expected string, found integer",
+ );
+}
+
+#[cargo_test]
+fn parse_strip_with_string() {
+ write_config(
+ "\
+[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(
+ "\
+[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",
+ );
+}
+
+#[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(InternedString::new("example")),
+ codegen_units: Some(123),
+ debug: Some(cargo_toml::U32OrBool::U32(1)),
+ 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(InternedString::new("dir_name")),
+ inherits: Some(InternedString::new("debug")),
+ strip: Some(cargo_toml::StringOrBool::String("symbols".to_string())),
+ package: None,
+ build_override: None,
+ rustflags: 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<String> 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<Value> 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.org ...");
+ assert_eq!(kh[0].definition, Definition::Path(foo_path.clone()));
+ assert_eq!(kh[1].val, "example.com ...");
+ assert_eq!(kh[1].definition, Definition::Path(root_path.clone()));
+ assert_eq!(kh[2].val, "example.net ...");
+ assert_eq!(kh[2].definition, Definition::Path(root_path.clone()));
+ assert_eq!(kh[3].val, "env-example");
+ assert_eq!(
+ kh[3].definition,
+ Definition::Environment("CARGO_NET_SSH_KNOWN_HOSTS".to_string())
+ );
+}