use anyhow::Result;
use serde::Deserialize;
use tera::{Context, Tera};
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Config {
pub vars: Option<serde_json::Value>,
pub options: Options,
#[serde(default)]
pub plugins: Vec<Plugin>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, Default)]
#[serde(rename_all = "lowercase")]
pub enum IconStyle {
#[default]
Nerd,
Unicode,
Ascii,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Default, Clone)]
pub struct Options {
pub config_root: Option<String>,
pub concurrency: Option<usize>,
pub cache_root: Option<String>,
#[serde(default)]
pub icons: IconStyle,
#[serde(default)]
pub chezmoi: bool,
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct MapSpec {
pub lhs: String,
pub mode: Vec<String>,
pub desc: Option<String>,
}
impl MapSpec {
pub fn modes_or_default(&self) -> Vec<String> {
if self.mode.is_empty() {
vec!["n".to_string()]
} else {
self.mode.clone()
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Default, Clone)]
pub struct Plugin {
pub name: Option<String>,
pub url: String,
pub dst: Option<String>,
#[serde(default, rename = "lazy")]
pub lazy_raw: Option<bool>,
#[serde(skip)]
pub lazy: bool,
#[serde(default = "default_merge")]
pub merge: bool,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub on_cmd: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub on_ft: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_map_specs")]
pub on_map: Option<Vec<MapSpec>>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub on_event: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub on_path: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub on_source: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub build: Option<String>,
pub rev: Option<String>,
pub cond: Option<String>,
#[serde(default)]
pub dev: bool,
}
fn deserialize_string_or_vec<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
let opt = Option::<StringOrVec>::deserialize(deserializer)?;
Ok(opt.map(|v| match v {
StringOrVec::String(s) => vec![s],
StringOrVec::Vec(v) => v,
}))
}
fn deserialize_string_or_vec_vec<'de, D>(
deserializer: D,
) -> std::result::Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SV {
S(String),
V(Vec<String>),
}
let sv: SV = Deserialize::deserialize(deserializer)?;
Ok(match sv {
SV::S(s) => vec![s],
SV::V(v) => v,
})
}
fn deserialize_map_specs<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Vec<MapSpec>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct MapSpecFull {
lhs: String,
#[serde(default, deserialize_with = "deserialize_string_or_vec_vec")]
mode: Vec<String>,
desc: Option<String>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum MapSpecRaw {
Simple(String),
Full(MapSpecFull),
}
let opt = Option::<Vec<MapSpecRaw>>::deserialize(deserializer)?;
Ok(opt.map(|v| {
v.into_iter()
.map(|raw| match raw {
MapSpecRaw::Simple(lhs) => MapSpec {
lhs,
mode: Vec::new(),
desc: None,
},
MapSpecRaw::Full(full) => MapSpec {
lhs: full.lhs,
mode: full.mode,
desc: full.desc,
},
})
.collect()
}))
}
fn default_merge() -> bool {
true
}
impl Plugin {
pub fn canonical_path(&self) -> String {
let url = self.url.trim_end_matches(".git");
if url.contains("://") {
let parts: Vec<&str> = url.split("://").collect();
let path = parts[1];
path.to_string()
} else if url.contains("@") {
let parts: Vec<&str> = url.split("@").collect();
let path = parts[1].replace(":", "/");
path.to_string()
} else {
if url.contains("/") {
format!("github.com/{}", url)
} else {
url.to_string()
}
}
}
pub fn default_name(&self) -> String {
let url = self.url.trim_end_matches(".git");
let normalized = url.replace(':', "/");
normalized.rsplit('/').next().unwrap_or(url).to_string()
}
pub fn display_name(&self) -> String {
self.name.clone().unwrap_or_else(|| self.default_name())
}
}
const MAX_VARS_RESOLVE_ITERATIONS: usize = 10;
fn extract_vars_section(toml_str: &str) -> String {
let mut in_vars = false;
let mut vars_lines = vec!["[vars]".to_string()];
for line in toml_str.lines() {
let trimmed = line.trim();
if !in_vars {
let stripped = trimmed
.split('#')
.next()
.unwrap_or("")
.trim()
.replace(' ', "");
if stripped == "[vars]" {
in_vars = true;
continue;
}
continue;
}
if trimmed.starts_with('[') {
let section_name = trimmed
.split('#')
.next()
.unwrap_or("")
.trim()
.replace(' ', "");
if section_name.starts_with("[vars.") || section_name.starts_with("[vars]") {
vars_lines.push(line.to_string());
continue;
}
break;
}
if trimmed.starts_with("{%") {
break;
}
vars_lines.push(line.to_string());
}
vars_lines.join("\n")
}
pub fn parse_config(toml_str: &str) -> Result<Config> {
let vars_toml = extract_vars_section(toml_str);
#[derive(Deserialize)]
struct VarsOnly {
vars: Option<serde_json::Value>,
}
let vars_parsed: VarsOnly = toml::from_str(&vars_toml)
.map_err(|e| anyhow::anyhow!("Failed to parse [vars] section: {}", e))?;
let mut env_map = std::collections::HashMap::new();
for (key, value) in std::env::vars() {
env_map.insert(key, value);
}
let mut vars_value = vars_parsed
.vars
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let mut converged = false;
for _ in 0..MAX_VARS_RESOLVE_ITERATIONS {
let mut ctx = Context::new();
ctx.insert("vars", &vars_value);
ctx.insert("env", &env_map);
ctx.insert("is_windows", &cfg!(windows));
let vars_str = serde_json::to_string(&vars_value)?;
let rendered_str = Tera::one_off(&vars_str, &ctx, false)?;
let new_value: serde_json::Value = serde_json::from_str(&rendered_str)?;
if new_value == vars_value {
converged = true;
break;
}
vars_value = new_value;
}
if !converged {
eprintln!(
"Warning: [vars] cross-references did not converge after {} iterations",
MAX_VARS_RESOLVE_ITERATIONS
);
}
let mut context = Context::new();
context.insert("vars", &vars_value);
context.insert("is_windows", &cfg!(windows));
context.insert("env", &env_map);
let rendered = Tera::one_off(toml_str, &context, false)?;
let mut config: Config = toml::from_str(&rendered)?;
for plugin in config.plugins.iter_mut() {
let has_trigger = plugin.on_cmd.is_some()
|| plugin.on_ft.is_some()
|| plugin.on_map.is_some()
|| plugin.on_event.is_some()
|| plugin.on_path.is_some()
|| plugin.on_source.is_some();
plugin.lazy = match plugin.lazy_raw {
Some(v) => v, None if has_trigger => true, None => false, };
}
Ok(config)
}
pub fn sort_plugins(plugins: &mut Vec<Plugin>) -> Result<()> {
let mut sorted = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut visiting = std::collections::HashSet::new();
let mut plugin_map: std::collections::HashMap<String, &Plugin> =
std::collections::HashMap::new();
for p in plugins.iter() {
plugin_map.insert(p.url.clone(), p);
plugin_map.insert(p.display_name(), p);
}
fn visit(
key: &str,
plugin_map: &std::collections::HashMap<String, &Plugin>,
visited: &mut std::collections::HashSet<String>,
visiting: &mut std::collections::HashSet<String>,
sorted: &mut Vec<Plugin>,
) -> Result<()> {
if visited.contains(key) {
return Ok(());
}
if visiting.contains(key) {
eprintln!("Warning: Cyclic dependency detected: {}", key);
return Ok(());
}
visiting.insert(key.to_string());
if let Some(plugin) = plugin_map.get(key) {
if visited.contains(&plugin.url) {
visiting.remove(key);
return Ok(());
}
if let Some(deps) = &plugin.depends {
for dep in deps {
visit(dep, plugin_map, visited, visiting, sorted)?;
}
}
visited.insert(plugin.url.clone());
visited.insert(plugin.display_name());
visiting.remove(key);
sorted.push((*plugin).clone());
} else {
eprintln!("Warning: Dependency not found in config: {}", key);
visited.insert(key.to_string());
visiting.remove(key);
}
Ok(())
}
for plugin in plugins.iter() {
visit(
&plugin.url,
&plugin_map,
&mut visited,
&mut visiting,
&mut sorted,
)?;
}
*plugins = sorted;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config_accepts_on_cmd_as_string() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_cmd = "Telescope"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(
config.plugins[0].on_cmd,
Some(vec!["Telescope".to_string()])
);
}
#[test]
fn test_parse_config_accepts_on_cmd_as_array() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_cmd = ["Telescope", "Grep"]
"#;
let config = parse_config(toml).unwrap();
assert_eq!(
config.plugins[0].on_cmd,
Some(vec!["Telescope".to_string(), "Grep".to_string()])
);
}
#[test]
fn test_parse_config_accepts_cache_root_option() {
let toml = r#"
[options]
cache_root = "~/dotfiles/nvim/rvpm"
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(
config.options.cache_root.as_deref(),
Some("~/dotfiles/nvim/rvpm")
);
}
#[test]
fn test_parse_config_cache_root_defaults_to_none() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.options.cache_root, None);
}
#[test]
fn test_parse_config_chezmoi_defaults_to_false() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert!(!config.options.chezmoi);
}
#[test]
fn test_parse_config_accepts_chezmoi_true() {
let toml = r#"
[options]
chezmoi = true
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert!(config.options.chezmoi);
}
#[test]
fn test_parse_config_icons_defaults_to_nerd() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.options.icons, IconStyle::Nerd);
}
#[test]
fn test_parse_config_accepts_icons_unicode() {
let toml = r#"
[options]
icons = "unicode"
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.options.icons, IconStyle::Unicode);
}
#[test]
fn test_parse_config_accepts_icons_ascii() {
let toml = r#"
[options]
icons = "ascii"
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.options.icons, IconStyle::Ascii);
}
#[test]
fn test_parse_config_accepts_on_map_simple_string() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_map = ["<leader>f"]
"#;
let config = parse_config(toml).unwrap();
let maps = config.plugins[0].on_map.as_ref().unwrap();
assert_eq!(maps.len(), 1);
assert_eq!(maps[0].lhs, "<leader>f");
assert!(
maps[0].mode.is_empty(),
"simple form leaves mode empty (defaults to 'n' at generate)"
);
assert_eq!(maps[0].desc, None);
}
#[test]
fn test_parse_config_accepts_on_map_table_form() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_map = [
{ lhs = "<leader>v", mode = ["n", "x"], desc = "visual thing" },
]
"#;
let config = parse_config(toml).unwrap();
let maps = config.plugins[0].on_map.as_ref().unwrap();
assert_eq!(maps.len(), 1);
assert_eq!(maps[0].lhs, "<leader>v");
assert_eq!(maps[0].mode, vec!["n".to_string(), "x".to_string()]);
assert_eq!(maps[0].desc.as_deref(), Some("visual thing"));
}
#[test]
fn test_parse_config_accepts_on_map_mixed_forms() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_map = [
"<leader>a",
{ lhs = "<leader>b", mode = "x" },
{ lhs = "<leader>c", mode = ["n", "v"], desc = "C" },
]
"#;
let config = parse_config(toml).unwrap();
let maps = config.plugins[0].on_map.as_ref().unwrap();
assert_eq!(maps.len(), 3);
assert_eq!(maps[0].lhs, "<leader>a");
assert!(maps[0].mode.is_empty());
assert_eq!(maps[1].lhs, "<leader>b");
assert_eq!(maps[1].mode, vec!["x".to_string()]);
assert_eq!(maps[2].lhs, "<leader>c");
assert_eq!(maps[2].mode, vec!["n".to_string(), "v".to_string()]);
assert_eq!(maps[2].desc.as_deref(), Some("C"));
}
#[test]
fn test_lazy_auto_true_when_on_cmd_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_cmd = "Foo"
"#;
let config = parse_config(toml).unwrap();
assert!(
config.plugins[0].lazy,
"on_cmd が設定されていれば lazy = true になるべき"
);
}
#[test]
fn test_lazy_auto_true_when_on_event_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_event = "BufReadPre"
"#;
let config = parse_config(toml).unwrap();
assert!(
config.plugins[0].lazy,
"on_event が設定されていれば lazy = true になるべき"
);
}
#[test]
fn test_lazy_auto_true_when_on_ft_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_ft = ["rust"]
"#;
let config = parse_config(toml).unwrap();
assert!(config.plugins[0].lazy);
}
#[test]
fn test_lazy_auto_true_when_on_map_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_map = ["<leader>f"]
"#;
let config = parse_config(toml).unwrap();
assert!(config.plugins[0].lazy);
}
#[test]
fn test_lazy_auto_true_when_on_source_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_source = "telescope.nvim"
"#;
let config = parse_config(toml).unwrap();
assert!(config.plugins[0].lazy);
}
#[test]
fn test_lazy_auto_true_when_on_path_set() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_path = "*.rs"
"#;
let config = parse_config(toml).unwrap();
assert!(config.plugins[0].lazy);
}
#[test]
fn test_lazy_explicit_false_overrides_auto() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
lazy = false
on_cmd = "Foo"
"#;
let config = parse_config(toml).unwrap();
assert!(
!config.plugins[0].lazy,
"lazy = false が明示されていればそちらを尊重"
);
}
#[test]
fn test_no_trigger_stays_eager() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert!(!config.plugins[0].lazy, "トリガーなしは eager のまま");
}
#[test]
fn test_parse_config_accepts_on_event_as_string() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
on_event = "BufReadPre"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(
config.plugins[0].on_event,
Some(vec!["BufReadPre".to_string()])
);
}
#[test]
fn test_sort_plugins_dependencies() {
let mut plugins = vec![
Plugin {
url: "A".to_string(),
depends: Some(vec!["B".to_string()]),
..Default::default()
},
Plugin {
url: "B".to_string(),
..Default::default()
},
];
sort_plugins(&mut plugins).unwrap();
assert_eq!(plugins[0].url, "B");
assert_eq!(plugins[1].url, "A");
}
#[test]
fn test_sort_plugins_depends_by_display_name() {
let mut plugins = vec![
Plugin {
url: "nvim-telescope/telescope.nvim".to_string(),
depends: Some(vec!["plenary.nvim".to_string()]),
..Default::default()
},
Plugin {
url: "nvim-lua/plenary.nvim".to_string(),
..Default::default()
},
];
sort_plugins(&mut plugins).unwrap();
assert_eq!(plugins[0].url, "nvim-lua/plenary.nvim");
assert_eq!(plugins[1].url, "nvim-telescope/telescope.nvim");
}
#[test]
fn test_sort_plugins_depends_by_url_still_works() {
let mut plugins = vec![
Plugin {
url: "nvim-telescope/telescope.nvim".to_string(),
depends: Some(vec!["nvim-lua/plenary.nvim".to_string()]),
..Default::default()
},
Plugin {
url: "nvim-lua/plenary.nvim".to_string(),
..Default::default()
},
];
sort_plugins(&mut plugins).unwrap();
assert_eq!(plugins[0].url, "nvim-lua/plenary.nvim");
assert_eq!(plugins[1].url, "nvim-telescope/telescope.nvim");
}
#[test]
fn test_sort_plugins_cycle_resilience() {
let mut plugins = vec![
Plugin {
url: "A".to_string(),
depends: Some(vec!["B".to_string()]),
..Default::default()
},
Plugin {
url: "B".to_string(),
depends: Some(vec!["A".to_string()]),
..Default::default()
},
Plugin {
url: "C".to_string(),
..Default::default()
},
];
let result = sort_plugins(&mut plugins);
assert!(result.is_ok());
assert!(plugins.iter().any(|p| p.url == "C"));
}
#[test]
fn test_sort_plugins_missing_dependency_resilience() {
let mut plugins = vec![Plugin {
url: "A".to_string(),
depends: Some(vec!["NOT_FOUND".to_string()]),
..Default::default()
}];
let result = sort_plugins(&mut plugins);
assert!(result.is_ok());
assert_eq!(plugins.len(), 1);
assert_eq!(plugins[0].url, "A");
}
#[test]
fn test_parse_config_with_tera() {
let toml_content = r#"
[vars]
base = "/tmp/rvpm"
[options]
[[plugins]]
name = "plenary"
url = "nvim-lua/plenary.nvim"
dst = "{{ vars.base }}/plenary"
"#;
let config = parse_config(toml_content).unwrap();
assert_eq!(config.plugins[0].dst, Some("/tmp/rvpm/plenary".to_string()));
}
#[test]
fn test_parse_config_with_env_and_os() {
unsafe {
std::env::set_var("RVPM_TEST_ENV", "hello");
}
let toml_content = r#"
[options]
[[plugins]]
name = "test"
url = "repo"
dst = "{{ env.RVPM_TEST_ENV }}_{{ is_windows }}"
"#;
let config = parse_config(toml_content).unwrap();
let expected_dst = format!("hello_{}", cfg!(windows));
assert_eq!(config.plugins[0].dst, Some(expected_dst));
}
#[test]
fn test_parse_complex_config() {
let toml_content = r#"
[options]
[[plugins]]
url = "nvim-telescope/telescope.nvim"
lazy = true
on_cmd = ["Telescope"]
on_path = ["*.rs"]
on_source = ["plenary.nvim"]
depends = ["plenary"]
merge = false
"#;
let config = parse_config(toml_content).unwrap();
let p = &config.plugins[0];
assert_eq!(p.url, "nvim-telescope/telescope.nvim");
assert!(p.lazy);
assert_eq!(p.on_cmd, Some(vec!["Telescope".to_string()]));
assert_eq!(p.on_path, Some(vec!["*.rs".to_string()]));
assert_eq!(p.on_source, Some(vec!["plenary.nvim".to_string()]));
assert_eq!(p.depends, Some(vec!["plenary".to_string()]));
assert!(!p.merge);
}
#[test]
fn test_plugin_canonical_path() {
let p1 = Plugin {
url: "https://github.com/owner/repo".to_string(),
..Default::default()
};
assert_eq!(p1.canonical_path(), "github.com/owner/repo");
let p2 = Plugin {
url: "owner/repo".to_string(),
..Default::default()
};
assert_eq!(p2.canonical_path(), "github.com/owner/repo");
let p3 = Plugin {
url: "git@github.com:owner/repo.git".to_string(),
..Default::default()
};
assert_eq!(p3.canonical_path(), "github.com/owner/repo");
}
#[test]
fn test_plugin_default_name() {
let p1 = Plugin {
url: "nvim-lua/plenary.nvim".to_string(),
..Default::default()
};
assert_eq!(p1.default_name(), "plenary.nvim");
let p2 = Plugin {
url: "https://github.com/yukimemi/chronicle.vim".to_string(),
..Default::default()
};
assert_eq!(p2.default_name(), "chronicle.vim");
let p3 = Plugin {
url: "https://github.com/owner/repo.git".to_string(),
..Default::default()
};
assert_eq!(p3.default_name(), "repo");
let p4 = Plugin {
url: "git@github.com:owner/telescope.nvim.git".to_string(),
..Default::default()
};
assert_eq!(p4.default_name(), "telescope.nvim");
let p5 = Plugin {
url: "https://github.com/owner/long-ugly-name".to_string(),
name: Some("short".to_string()),
..Default::default()
};
assert_eq!(p5.display_name(), "short");
assert_eq!(p1.display_name(), "plenary.nvim");
}
#[test]
fn test_tera_if_block_excludes_plugin() {
let toml = r#"
[vars]
use_blink = false
[options]
[[plugins]]
url = "owner/always"
{% if vars.use_blink %}
[[plugins]]
url = "owner/blink"
{% endif %}
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.plugins.len(), 1);
assert_eq!(config.plugins[0].url, "owner/always");
}
#[test]
fn test_tera_if_block_includes_plugin_when_true() {
let toml = r#"
[vars]
use_blink = true
[options]
[[plugins]]
url = "owner/always"
{% if vars.use_blink %}
[[plugins]]
url = "owner/blink"
{% endif %}
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.plugins.len(), 2);
}
#[test]
fn test_vars_reference_other_vars() {
let toml = r#"
[vars]
base = "/tmp"
full = "{{ vars.base }}/plugins"
[options]
[[plugins]]
url = "owner/repo"
dst = "{{ vars.full }}/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.plugins[0].dst, Some("/tmp/plugins/repo".to_string()));
}
#[test]
fn test_vars_forward_reference() {
let toml = r#"
[vars]
full = "{{ vars.base }}/plugins"
base = "/tmp"
[options]
[[plugins]]
url = "owner/repo"
dst = "{{ vars.full }}/repo"
"#;
let config = parse_config(toml).unwrap();
assert_eq!(config.plugins[0].dst, Some("/tmp/plugins/repo".to_string()));
}
#[test]
fn test_tera_is_windows_in_if_block() {
let toml = r#"
[options]
[[plugins]]
url = "owner/always"
{% if is_windows %}
[[plugins]]
url = "owner/win-only"
{% endif %}
"#;
let config = parse_config(toml).unwrap();
if cfg!(windows) {
assert_eq!(config.plugins.len(), 2);
} else {
assert_eq!(config.plugins.len(), 1);
}
}
#[test]
fn test_parse_config_dev_defaults_to_false() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert!(!config.plugins[0].dev);
}
#[test]
fn test_parse_config_dev_option() {
let toml = r#"
[options]
[[plugins]]
url = "owner/repo"
dev = true
dst = "~/src/owner/repo"
"#;
let config = parse_config(toml).unwrap();
assert!(config.plugins[0].dev);
}
}