blob: d6af8614fead7a2f4184028f8cbc063057e88c56 [file] [log] [blame]
use std::borrow::Borrow;
use std::collections;
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_test_support::{paths, project, t};
use serde::Deserialize;
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();
}
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 new_config(env: &[(&str, &str)]) -> Config {
enable_nightly_features(); // -Z advanced-env
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 env = env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let mut config = Config::new(shell, cwd, homedir);
config.set_env(env);
config
.configure(
0,
None,
&None,
false,
false,
false,
&None,
&["advanced-env".into()],
)
.unwrap();
config
}
fn assert_error<E: Borrow<failure::Error>>(error: E, msgs: &str) {
let causes = error
.borrow()
.iter_chain()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.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<i64>,
}
let s: S = config.get("S").unwrap();
assert_eq!(s, S { f1: Some(123) });
let config = new_config(&[("CARGO_S_F1", "456")]);
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::<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 !got_symlink_permission() {
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.
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::<Option<i32>>("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 = new_config(&[("CARGO_S_UNUSED2", "1"), ("CARGO_S2_UNUSED", "2")]);
#[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.
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 file `[..]/.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 = new_config(&[
("CARGO_PROFILE_DEV_CODEGEN_UNITS", "5"),
("CARGO_PROFILE_DEV_BUILD_OVERRIDE_CODEGEN_UNITS", "11"),
("CARGO_PROFILE_DEV_PACKAGE_env_CODEGEN_UNITS", "13"),
("CARGO_PROFILE_DEV_PACKAGE_bar_OPT_LEVEL", "2"),
]);
// TODO: don't use actual `tomlprofile`.
let p: toml::TomlProfile = config.get("profile.dev").unwrap();
let mut packages = collections::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 = new_config(&[
("CARGO_ENVB", "false"),
("CARGO_C", "['d']"),
("CARGO_ENVL", "['a', 'b']"),
]);
let a = config.get::<VSOB>("a").unwrap();
match a {
VSOB::VecString(_) => panic!("expected bool"),
VSOB::Bool(b) => assert_eq!(b, true),
}
let b = config.get::<VSOB>("b").unwrap();
match b {
VSOB::VecString(l) => assert_eq!(l, vec!["b".to_string()]),
VSOB::Bool(_) => panic!("expected list"),
}
let c = config.get::<VSOB>("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::<VSOB>("envb").unwrap();
match envb {
VSOB::VecString(_) => panic!("expected bool"),
VSOB::Bool(b) => assert_eq!(b, false),
}
let envl = config.get::<VSOB>("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::<toml::TomlProfile>("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 = new_config(&[("CARGO_PROFILE_DEV_OPT_LEVEL", "asdf")]);
assert_error(
config.get::<toml::TomlProfile>("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 = new_config(&[
("CARGO_NEST_foo_f2", "3"),
("CARGO_NESTE_foo_f1", "1"),
("CARGO_NESTE_foo_f2", "3"),
("CARGO_NESTE_bar_asdf", "3"),
]);
type Nested = collections::HashMap<String, collections::HashMap<String, u8>>;
let n: Nested = config.get("nest").unwrap();
let mut expected = collections::HashMap::new();
let mut foo = collections::HashMap::new();
foo.insert("f1".to_string(), 1);
foo.insert("f2".to_string(), 3);
expected.insert("foo".to_string(), foo);
let mut bar = collections::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 = new_config(&[("CARGO_E_S", "asdf"), ("CARGO_E_BIG", "123456789")]);
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`: \
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`: \
invalid value: integer `123456789`, expected i8",
);
#[derive(Debug, Deserialize)]
struct S {
f1: i64,
f2: String,
f3: i64,
big: i64,
}
assert_error(
config.get::<S>("S").unwrap_err(),
"missing config key `S.f3`",
);
}
#[cargo_test]
fn config_get_option() {
write_config(
"\
[foo]
f1 = 1
",
);
let config = new_config(&[("CARGO_BAR_ASDF", "3")]);
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:
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<String>;
let config = new_config(&[
("CARGO_L4", "['three', 'four']"),
("CARGO_L5", "['a']"),
("CARGO_ENV_EMPTY", "[]"),
("CARGO_ENV_BLANK", ""),
("CARGO_ENV_NUM", "1"),
("CARGO_ENV_NUM_LIST", "[1]"),
("CARGO_ENV_TEXT", "asdf"),
("CARGO_LEPAIR", "['a', 'b']"),
("CARGO_NESTED2_L", "['z']"),
("CARGO_NESTEDE_L", "['env']"),
("CARGO_BAD_ENV", "[zzz]"),
]);
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_error(
config.get::<L>("env-blank").unwrap_err(),
"error in environment variable `CARGO_ENV_BLANK`: \
should have TOML list syntax, found ``",
);
assert_error(
config.get::<L>("env-num").unwrap_err(),
"error in environment variable `CARGO_ENV_NUM`: \
should have TOML list syntax, found `1`",
);
assert_error(
config.get::<L>("env-num-list").unwrap_err(),
"error in environment variable `CARGO_ENV_NUM_LIST`: \
expected string, found integer",
);
assert_error(
config.get::<L>("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::<L>("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::<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 = new_config(&[("CARGO_NSE", "987"), ("CARGO_NS2", "654")]);
#[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 = new_config(&[("CARGO_EPATH", "a/b"), ("CARGO_P3", "d/e")]);
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 = new_config(&[
("CARGO_EPOS", "123456789"),
("CARGO_ENEG", "-1"),
("CARGO_EI64MAX", "9223372036854775807"),
]);
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`: \
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`: \
invalid value: integer `-1`, expected u32",
);
assert_error(
config.get::<i8>("npos").unwrap_err(),
"error in [..].cargo/config: \
could not load config key `npos`: \
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`: \
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!(config.get::<SslVersionConfig>("http.ssl-version").is_err());
assert!(config
.get::<Option<SslVersionConfig>>("http.ssl-version")
.unwrap()
.is_none());
}