| //! Tests for `include` config field. |
| |
| use crate::prelude::*; |
| use cargo_test_support::str; |
| |
| use super::config::GlobalContextBuilder; |
| use super::config::assert_error; |
| use super::config::write_config_at; |
| use super::config::write_config_toml; |
| |
| #[cargo_test] |
| fn simple() { |
| // Simple test. |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| include = ['other.toml'] |
| key1 = 1 |
| key2 = 2 |
| ", |
| ); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| key2 = 3 |
| key3 = 4 |
| ", |
| ); |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| assert_eq!(gctx.get::<i32>("key3").unwrap(), 4); |
| } |
| |
| #[cargo_test] |
| fn mix_of_hierarchy_and_include() { |
| write_config_at( |
| "foo/.cargo/config.toml", |
| " |
| include = ['other.toml'] |
| key1 = 1 |
| |
| # also make sure unstable flags merge in the correct order |
| [unstable] |
| features = ['1'] |
| ", |
| ); |
| write_config_at( |
| "foo/.cargo/other.toml", |
| " |
| key1 = 2 |
| key2 = 2 |
| |
| [unstable] |
| features = ['2'] |
| ", |
| ); |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| include = ['other.toml'] |
| key1 = 3 |
| key2 = 3 |
| key3 = 3 |
| |
| [unstable] |
| features = ['3'] |
| ", |
| ); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| key1 = 4 |
| key2 = 4 |
| key3 = 4 |
| key4 = 4 |
| |
| [unstable] |
| features = ['4'] |
| ", |
| ); |
| let gctx = GlobalContextBuilder::new() |
| .cwd("foo") |
| .nightly_features_allowed(true) |
| .build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| assert_eq!(gctx.get::<i32>("key3").unwrap(), 3); |
| assert_eq!(gctx.get::<i32>("key4").unwrap(), 4); |
| assert_eq!( |
| gctx.get::<Vec<String>>("unstable.features").unwrap(), |
| vec![ |
| "4".to_string(), |
| "3".to_string(), |
| "2".to_string(), |
| "1".to_string() |
| ] |
| ); |
| } |
| |
| #[cargo_test] |
| fn left_to_right_bottom_to_top() { |
| // How it merges multiple nested includes. |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| include = ['left-middle.toml', 'right-middle.toml'] |
| top = 1 |
| ", |
| ); |
| write_config_at( |
| ".cargo/right-middle.toml", |
| " |
| include = ['right-bottom.toml'] |
| top = 0 |
| right-middle = 0 |
| ", |
| ); |
| write_config_at( |
| ".cargo/right-bottom.toml", |
| " |
| top = -1 |
| right-middle = -1 |
| right-bottom = -1 |
| ", |
| ); |
| write_config_at( |
| ".cargo/left-middle.toml", |
| " |
| include = ['left-bottom.toml'] |
| top = -2 |
| right-middle = -2 |
| right-bottom = -2 |
| left-middle = -2 |
| ", |
| ); |
| write_config_at( |
| ".cargo/left-bottom.toml", |
| " |
| top = -3 |
| right-middle = -3 |
| right-bottom = -3 |
| left-middle = -3 |
| left-bottom = -3 |
| ", |
| ); |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("top").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("right-middle").unwrap(), 0); |
| assert_eq!(gctx.get::<i32>("right-bottom").unwrap(), -1); |
| assert_eq!(gctx.get::<i32>("left-middle").unwrap(), -2); |
| assert_eq!(gctx.get::<i32>("left-bottom").unwrap(), -3); |
| } |
| |
| #[cargo_test] |
| fn nested_include_resolves_relative_to_including_file() { |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| include = ['../config/cargo.toml'] |
| ", |
| ); |
| write_config_at( |
| "config/cargo.toml", |
| " |
| include = ['other.toml'] |
| middle = 10 |
| ", |
| ); |
| write_config_at( |
| "config/other.toml", |
| " |
| nested = 42 |
| ", |
| ); |
| |
| // This should not be included, |
| // because `include` path is resolved relative to the including config |
| // (where the `include` is defined) |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| INVALID = SYNTAX |
| ", |
| ); |
| |
| let gctx = GlobalContextBuilder::new().build(); |
| |
| assert_eq!(gctx.get::<i32>("nested").unwrap(), 42); |
| assert_eq!(gctx.get::<i32>("middle").unwrap(), 10); |
| } |
| |
| #[cargo_test] |
| fn missing_file() { |
| // Error when there's a missing file. |
| write_config_toml("include=['missing.toml']"); |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml` |
| |
| Caused by: |
| failed to read configuration file `[ROOT]/.cargo/missing.toml` |
| |
| Caused by: |
| [NOT_FOUND] |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn wrong_file_extension() { |
| // Error when it doesn't end with `.toml`. |
| write_config_toml("include=['config.png']"); |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| expected a config include path ending with `.toml`, but found `config.png` from `[ROOT]/.cargo/config.toml` |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn cycle() { |
| // Detects a cycle. |
| write_config_at(".cargo/config.toml", "include=['one.toml']"); |
| write_config_at(".cargo/one.toml", "include=['two.toml']"); |
| write_config_at(".cargo/two.toml", "include=['config.toml']"); |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| failed to load config include `one.toml` from `[ROOT]/.cargo/config.toml` |
| |
| Caused by: |
| failed to load config include `two.toml` from `[ROOT]/.cargo/one.toml` |
| |
| Caused by: |
| failed to load config include `config.toml` from `[ROOT]/.cargo/two.toml` |
| |
| Caused by: |
| config `include` cycle detected with path `[ROOT]/.cargo/config.toml` |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn cli_include() { |
| // Using --config with include. |
| // CLI takes priority over files. |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| foo = 1 |
| bar = 2 |
| ", |
| ); |
| write_config_at(".cargo/config-foo.toml", "foo = 2"); |
| let gctx = GlobalContextBuilder::new() |
| .config_arg("include=['.cargo/config-foo.toml']") |
| .build(); |
| assert_eq!(gctx.get::<i32>("foo").unwrap(), 2); |
| assert_eq!(gctx.get::<i32>("bar").unwrap(), 2); |
| } |
| |
| #[cargo_test] |
| fn bad_format() { |
| // Not a valid format. |
| write_config_toml("include = 1"); |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| expected a list of strings or a list of tables, but found integer at `include` in `[ROOT]/.cargo/config.toml |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn cli_include_failed() { |
| // Error message when CLI include fails to load. |
| let gctx = GlobalContextBuilder::new() |
| .config_arg("include=['foobar.toml']") |
| .build_err(); |
| assert_error( |
| gctx.unwrap_err(), |
| str![[r#" |
| failed to load --config include |
| |
| Caused by: |
| failed to load config include `foobar.toml` from `--config cli option` |
| |
| Caused by: |
| failed to read configuration file `[ROOT]/foobar.toml` |
| |
| Caused by: |
| [NOT_FOUND] |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn cli_merge_failed() { |
| // Error message when CLI include merge fails. |
| write_config_toml("foo = ['a']"); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| foo = 'b' |
| ", |
| ); |
| let gctx = GlobalContextBuilder::new() |
| .config_arg("include=['.cargo/other.toml']") |
| .build_err(); |
| // Maybe this error message should mention it was from an include file? |
| assert_error( |
| gctx.unwrap_err(), |
| str![[r#" |
| failed to merge key `foo` between [ROOT]/.cargo/config.toml and [ROOT]/.cargo/other.toml |
| |
| Caused by: |
| failed to merge config value from `[ROOT]/.cargo/other.toml` into `[ROOT]/.cargo/config.toml`: expected array, but found string |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn cli_include_take_priority_over_env() { |
| write_config_at(".cargo/include.toml", "k='include'"); |
| |
| // k=env |
| let gctx = GlobalContextBuilder::new().env("CARGO_K", "env").build(); |
| assert_eq!(gctx.get::<String>("k").unwrap(), "env"); |
| |
| // k=env |
| // --config 'include=".cargo/include.toml"' |
| let gctx = GlobalContextBuilder::new() |
| .env("CARGO_K", "env") |
| .config_arg("include=['.cargo/include.toml']") |
| .build(); |
| assert_eq!(gctx.get::<String>("k").unwrap(), "include"); |
| |
| // k=env |
| // --config '.cargo/foo.toml' |
| write_config_at(".cargo/foo.toml", "include=['include.toml']"); |
| let gctx = GlobalContextBuilder::new() |
| .env("CARGO_K", "env") |
| .config_arg(".cargo/foo.toml") |
| .build(); |
| assert_eq!(gctx.get::<String>("k").unwrap(), "include"); |
| } |
| |
| #[cargo_test] |
| fn inline_table_style() { |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| include = ['simple.toml', { path = 'other.toml' }] |
| key1 = 1 |
| key2 = 2 |
| ", |
| ); |
| write_config_at( |
| ".cargo/simple.toml", |
| " |
| key2 = 3 |
| key3 = 4 |
| ", |
| ); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| key3 = 5 |
| key4 = 6 |
| ", |
| ); |
| |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| assert_eq!(gctx.get::<i32>("key3").unwrap(), 5); |
| assert_eq!(gctx.get::<i32>("key4").unwrap(), 6); |
| } |
| |
| #[cargo_test] |
| fn array_of_tables_style() { |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| key1 = 1 |
| key2 = 2 |
| |
| [[include]] |
| path = 'other1.toml' |
| |
| [[include]] |
| path = 'other2.toml' |
| ", |
| ); |
| write_config_at( |
| ".cargo/other1.toml", |
| " |
| key2 = 3 |
| key3 = 4 |
| ", |
| ); |
| write_config_at( |
| ".cargo/other2.toml", |
| " |
| key3 = 5 |
| key4 = 6 |
| ", |
| ); |
| |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| assert_eq!(gctx.get::<i32>("key3").unwrap(), 5); |
| assert_eq!(gctx.get::<i32>("key4").unwrap(), 6); |
| } |
| |
| #[cargo_test] |
| fn table_with_unknown_fields() { |
| // Unknown fields should be ignored for forward compatibility |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| key1 = 1 |
| |
| [[include]] |
| path = 'other.toml' |
| unknown_foo = true |
| unknown_bar = 123 |
| ", |
| ); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| key2 = 2 |
| ", |
| ); |
| |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| } |
| |
| #[cargo_test] |
| fn table_missing_required_field() { |
| // Missing required field should fail |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| key1 = 1 |
| [[include]] |
| random_field = true |
| ", |
| ); |
| |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| missing field `path` at `include[0]` in `[ROOT]/.cargo/config.toml` |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn optional_include_missing_and_existing() { |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| key1 = 1 |
| |
| [[include]] |
| path = 'missing.toml' |
| optional = true |
| |
| [[include]] |
| path = 'other.toml' |
| optional = true |
| ", |
| ); |
| write_config_at( |
| ".cargo/other.toml", |
| " |
| key2 = 2 |
| ", |
| ); |
| |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_eq!(gctx.get::<i32>("key1").unwrap(), 1); |
| assert_eq!(gctx.get::<i32>("key2").unwrap(), 2); |
| } |
| |
| #[cargo_test] |
| fn optional_false_missing_file() { |
| write_config_at( |
| ".cargo/config.toml", |
| " |
| key1 = 1 |
| |
| [[include]] |
| path = 'missing.toml' |
| optional = false |
| ", |
| ); |
| |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml` |
| |
| Caused by: |
| failed to read configuration file `[ROOT]/.cargo/missing.toml` |
| |
| Caused by: |
| [NOT_FOUND] |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn disallow_glob_syntax() { |
| // Reserved for future extension |
| write_config_toml("include = ['config-*.toml']"); |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| expected a config include path without glob patterns, but found `config-*.toml` from `[ROOT]/.cargo/config.toml` |
| "#]], |
| ); |
| } |
| |
| #[cargo_test] |
| fn disallow_template_syntax() { |
| // Reserved for future extension |
| write_config_toml("include = ['{workspace-root}/config.toml']"); |
| // Config values are lazy loaded and fail at first retrieval. |
| // This ensures simple commands like `cargo --version` working even when |
| let gctx = GlobalContextBuilder::new().build(); |
| assert_error( |
| gctx.get::<bool>("foo").unwrap_err(), |
| str![[r#" |
| could not load Cargo configuration |
| |
| Caused by: |
| expected a config include path without template braces, but found `{workspace-root}/config.toml` from `[ROOT]/.cargo/config.toml` |
| "#]], |
| ); |
| } |