ros_add 0.1.7

The Purpose of the Package is to provide the `cargo ros_add` command to add dependencies to `Cargo.toml` and the `package.xml`
Documentation
//! A cargo subcommand for adding dependencies to both Cargo.toml and ROS package.xml files.
//!
//! This tool helps manage dependencies for ROS 2 packages written in Rust by
//! synchronizing dependencies between Cargo.toml and package.xml manifest files.

use ros_add::{DependencyType, PackageName, PathDoc, SplitRaw, XMLHelper, env_to_matches};
use std::{
    fs::{self},
    io::ErrorKind,
};
use tempfile::tempdir;

#[test]
fn test_dependency_type_cargo_toml_type() {
    assert_eq!(
        DependencyType::BuildDepend.cargo_toml_type(),
        "build-dependencies"
    );
    assert_eq!(DependencyType::Depend.cargo_toml_type(), "dependencies");
    assert_eq!(DependencyType::TestDepend.cargo_toml_type(), "dependencies");
    assert_eq!(DependencyType::ExecDepend.cargo_toml_type(), "dependencies");
}

#[test]
fn test_dependency_type_package_xml_type() {
    assert_eq!(
        DependencyType::BuildDepend.package_xml_type(),
        "build_depend"
    );
    assert_eq!(
        DependencyType::BuildExportDepend.package_xml_type(),
        "build_export_depend"
    );
    assert_eq!(
        DependencyType::BuildtoolDepend.package_xml_type(),
        "buildtool-depend"
    );
    assert_eq!(DependencyType::ExecDepend.package_xml_type(), "exec-depend");
    assert_eq!(DependencyType::Depend.package_xml_type(), "depend");
    assert_eq!(DependencyType::TestDepend.package_xml_type(), "test-depend");
    assert_eq!(DependencyType::Conflict.package_xml_type(), "conflict");
    assert_eq!(DependencyType::Replace.package_xml_type(), "replace");
}

#[test]
fn test_package_name_from_str() {
    let package = PackageName::from("serde");
    assert_eq!(package.name, "serde");
    assert_eq!(package.version, "*");

    let package = PackageName::from("serde@1.0.0");
    assert_eq!(package.name, "serde");
    assert_eq!(package.version, "1.0.0");

    let package = PackageName::from("serde@=1.0.38");
    assert_eq!(package.name, "serde");
    assert_eq!(package.version, "=1.0.38");
}

#[test]
fn test_package_name_display() {
    let package = PackageName {
        name: "serde",
        version: "=1.0.38",
    };
    assert_eq!(format!("{}", package), "Package:serde, Version:=1.0.38");
}

#[test]
fn test_pathdoc_new_valid_toml() {
    let temp_dir = tempdir().unwrap();
    let toml_path = temp_dir.path().join("Cargo.toml");

    let toml_content = r#"
[package]
name = "test_package"
version = "0.1.0"
"#;

    fs::write(&toml_path, toml_content).unwrap();

    let pathdoc = PathDoc::new(toml_path.to_str().unwrap(), DependencyType::Depend);
    assert!(pathdoc.is_ok());

    temp_dir.close().unwrap();
}

#[test]
fn test_pathdoc_new_invalid_toml() {
    let temp_dir = tempdir().unwrap();
    let toml_path = temp_dir.path().join("Cargo.toml");

    let invalid_toml = "invalid toml content";
    fs::write(&toml_path, invalid_toml).unwrap();

    let pathdoc = PathDoc::new(toml_path.to_str().unwrap(), DependencyType::Depend);
    assert!(matches!(pathdoc, Err(ErrorKind::InvalidData)));

    temp_dir.close().unwrap();
}

#[test]
fn test_pathdoc_add_dependency() {
    let temp_dir = tempdir().unwrap();
    let toml_path = temp_dir.path().join("Cargo.toml");

    let toml_content = r#"
[package]
name = "test_package"
version = "0.1.0"

[dependencies]
existing_dep = "1.0.0"
"#;

    fs::write(&toml_path, toml_content).unwrap();

    let pathdoc = PathDoc::new(toml_path.to_str().unwrap(), DependencyType::Depend).unwrap();
    let package = PackageName::from("serde@1.0.0");

    let result = pathdoc.add_dependency_to_cargo_toml(package);
    assert!(result.is_ok());

    // Verify the dependency was added
    let updated_content = fs::read_to_string(&toml_path).unwrap();
    assert!(updated_content.contains("serde = \"1.0.0\""));

    temp_dir.close().unwrap();
}

#[test]
fn test_pathdoc_add_duplicate_dependency() {
    let temp_dir = tempdir().unwrap();
    let toml_path = temp_dir.path().join("Cargo.toml");

    let toml_content = r#"
[package]
name = "test_package"
version = "0.1.0"

[dependencies]
serde = "1.0.0"
"#;

    fs::write(&toml_path, toml_content).unwrap();

    let pathdoc = PathDoc::new(toml_path.to_str().unwrap(), DependencyType::Depend).unwrap();
    let package = PackageName::from("serde@2.0.0");

    let result = pathdoc.add_dependency_to_cargo_toml(package);
    assert!(matches!(result, Err(ErrorKind::AlreadyExists)));

    temp_dir.close().unwrap();
}

#[test]
fn test_pathdoc_add_build_dependency() {
    let temp_dir = tempdir().unwrap();
    let toml_path = temp_dir.path().join("Cargo.toml");

    let toml_content = r#"
[package]
name = "test_package"
version = "0.1.0"
"#;

    fs::write(&toml_path, toml_content).unwrap();

    let pathdoc = PathDoc::new(toml_path.to_str().unwrap(), DependencyType::BuildDepend).unwrap();
    let package = PackageName::from("build_helper@0.1.0");

    let result = pathdoc.add_dependency_to_cargo_toml(package);
    assert!(result.is_ok());

    // Verify the build dependency was added to the correct section
    let updated_content = fs::read_to_string(&toml_path).unwrap();
    assert!(updated_content.contains("[build-dependencies]"));
    assert!(updated_content.contains("build_helper = \"0.1.0\""));

    temp_dir.close().unwrap();
}

#[test]
fn test_xml_helper_new_valid_xml() {
    let temp_dir = tempdir().unwrap();
    let xml_path = temp_dir.path().join("package.xml");

    let xml_content = r#"<?xml version="1.0"?>
<package format="3">
  <name>test_package</name>
  <version>0.0.0</version>
  <description>Test package</description>
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>"#;

    fs::write(&xml_path, xml_content).unwrap();

    let xml_helper = XMLHelper::new(xml_path.to_str().unwrap(), DependencyType::BuildDepend);
    assert!(xml_helper.is_ok());

    temp_dir.close().unwrap();
}

#[test]
fn test_xml_helper_new_invalid_xml() {
    let temp_dir = tempdir().unwrap();
    let xml_path = temp_dir.path().join("package.xml");

    // XMLHelper::new only checks if file exists, not if content is valid XML
    // So this test should expect success, and the XML validation should happen
    // in add_dependency_to_package_xml
    let invalid_xml = "invalid xml content";
    fs::write(&xml_path, invalid_xml).unwrap();

    let xml_helper = XMLHelper::new(xml_path.to_str().unwrap(), DependencyType::BuildDepend);
    assert!(xml_helper.is_ok()); // This should succeed because file exists

    temp_dir.close().unwrap();
}

#[test]
fn test_xml_helper_add_dependency_invalid_xml() {
    let temp_dir = tempdir().unwrap();
    let xml_path = temp_dir.path().join("package.xml");

    // Use XML that's definitely malformed (unclosed tag, invalid characters)
    let invalid_xml = r#"<?xml version="1.0"?>
<package format="3">
  <name>test_package
  <version>0.0.0</version>
  <description>Test & invalid & characters here
</package>"#;

    fs::write(&xml_path, invalid_xml).unwrap();

    let xml_helper =
        XMLHelper::new(xml_path.to_str().unwrap(), DependencyType::BuildDepend).unwrap();
    let package = PackageName::from("rclcpp");

    let result = xml_helper.add_dependency_to_package_xml(package);
    assert!(matches!(result, Err(ErrorKind::InvalidData)));

    temp_dir.close().unwrap();
}

#[test]
fn test_xml_helper_add_dependency() {
    let temp_dir = tempdir().unwrap();
    let xml_path = temp_dir.path().join("package.xml");

    let xml_content = r#"<?xml version="1.0"?>
<package format="3">
  <name>test_package</name>
  <version>0.0.0</version>
  <description>Test package</description>
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>"#;

    fs::write(&xml_path, xml_content).unwrap();

    let xml_helper =
        XMLHelper::new(xml_path.to_str().unwrap(), DependencyType::BuildDepend).unwrap();
    let package = PackageName::from("rclcpp");

    let result = xml_helper.add_dependency_to_package_xml(package);
    assert!(result.is_ok());

    // Verify the dependency was added
    let updated_content = fs::read_to_string(&xml_path).unwrap();
    assert!(updated_content.contains("<build_depend>rclcpp</build_depend>"));

    temp_dir.close().unwrap();
}

#[test]
fn test_xml_helper_add_duplicate_dependency() {
    let temp_dir = tempdir().unwrap();
    let xml_path = temp_dir.path().join("package.xml");

    let xml_content = r#"<?xml version="1.0"?>
<package format="3">
  <name>test_package</name>
  <version>0.0.0</version>
  <description>Test package</description>
  <build_depend>rclcpp</build_depend>
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>"#;

    fs::write(&xml_path, xml_content).unwrap();

    let xml_helper =
        XMLHelper::new(xml_path.to_str().unwrap(), DependencyType::BuildDepend).unwrap();
    let package = PackageName::from("rclcpp");

    let result = xml_helper.add_dependency_to_package_xml(package);
    assert!(matches!(result, Err(ErrorKind::AlreadyExists)));

    temp_dir.close().unwrap();
}

#[test]
fn test_env_to_matches_basic() {
    let args = vec!["ros_add".to_string(), "serde".to_string()];
    let matches = env_to_matches(args);

    assert_eq!(matches.get_one::<String>("dependency").unwrap(), "serde");
    assert!(!matches.get_flag("build"));
    assert!(!matches.get_flag("test"));
}

#[test]
fn test_env_to_matches_with_flags() {
    let args = vec![
        "ros_add".to_string(),
        "serde".to_string(),
        "--build".to_string(),
        "--test".to_string(),
    ];
    let matches = env_to_matches(args);

    assert_eq!(matches.get_one::<String>("dependency").unwrap(), "serde");
    assert!(matches.get_flag("build"));
    assert!(matches.get_flag("test"));
}

#[test]
fn test_env_to_matches_cargo_subcommand() {
    // Simulate cargo subcommand args: cargo ros-add serde
    let args = vec![
        "cargo".to_string(),
        "ros-add".to_string(),
        "serde".to_string(),
    ];
    let matches = env_to_matches(args);

    assert_eq!(matches.get_one::<String>("dependency").unwrap(), "serde");
}

#[test]
fn test_dependency_type_from_arg_matches() {
    let args = vec![
        "ros_add".to_string(),
        "serde".to_string(),
        "--build".to_string(),
    ];
    let matches = env_to_matches(args);
    let dep_type: DependencyType = matches.into();

    assert_eq!(dep_type, DependencyType::BuildDepend);
}

#[test]
fn test_dependency_type_from_arg_matches_default() {
    let args = vec!["ros_add".to_string(), "serde".to_string()];
    let matches = env_to_matches(args);
    let dep_type: DependencyType = matches.into();

    assert_eq!(dep_type, DependencyType::Depend);
}

#[test]
fn test_package_name_from_split_raw() {
    let split_raw = SplitRaw::from("package@1.0.0");
    let package_name = PackageName::from(split_raw);

    assert_eq!(package_name.name, "package");
    assert_eq!(package_name.version, "1.0.0");
}

#[test]
fn test_package_name_from_split_raw_no_version() {
    let split_raw = SplitRaw::from("package");
    let package_name = PackageName::from(split_raw);

    assert_eq!(package_name.name, "package");
    assert_eq!(package_name.version, "*");
}