cargo-component 0.21.1

A cargo extension for authoring WebAssembly components
Documentation
use std::{fs, rc::Rc};

use anyhow::{Context, Result};
use assert_cmd::prelude::*;
use predicates::{prelude::*, str::contains};
use tempfile::TempDir;
use toml_edit::value;

use crate::support::*;

mod support;

#[test]
fn help() {
    for arg in ["help add", "add -h", "add --help"] {
        cargo_component(arg.split_whitespace())
            .assert()
            .stdout(contains("Add a dependency for a WebAssembly component"))
            .success();
    }
}

#[test]
fn requires_package() {
    cargo_component(["add"])
        .assert()
        .stderr(contains("cargo component add <PACKAGE>"))
        .failure();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn validate_the_package_exists() -> Result<()> {
    let (server, _, _) = spawn_server(["foo"]).await?;

    let project = server.project("foo", true, Vec::<String>::new())?;

    project
        .cargo_component(["add", "foo:bar"])
        .assert()
        .stderr(contains("package `foo:bar` was not found"))
        .failure();

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn validate_the_version_exists() -> Result<()> {
    let (server, config, _) = spawn_server(Vec::<String>::new()).await?;

    publish_component(config, "test:bar", "1.1.0", "(component)").await?;

    let project = server.project("foo", true, Vec::<String>::new())?;

    project
        .cargo_component(["add", "test:bar"])
        .assert()
        .stderr(contains("Added dependency `test:bar` with version `1.1.0`"))
        .success();

    let manifest = fs::read_to_string(project.root().join("Cargo.toml"))?;
    assert!(contains(r#""test:bar" = "1.1.0""#).eval(&manifest));

    project
        .cargo_component(["add", "--name", "test:bar2", "test:bar@2.0.0"])
        .assert()
        .stderr(contains(
            "component registry package `test:bar` has no release matching version requirement `^2.0.0`",
        ))
        .failure();

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn adds_dependencies_to_target_component() -> Result<()> {
    let (server, config, _) = spawn_server(Vec::<String>::new()).await?;

    publish_component(config, "test:bar", "1.1.0", "(component)").await?;

    let project = server.project("foo_target", true, Vec::<String>::new())?;

    let manifest = fs::read_to_string(project.root().join("Cargo.toml"))?;
    assert!(!contains("package.metadata.component.target.dependencies").eval(&manifest));

    project
        .cargo_component(["add", "test:bar", "--target"])
        .assert()
        .stderr(contains("Added dependency `test:bar` with version `1.1.0`"));

    let manifest = fs::read_to_string(project.root().join("Cargo.toml"))?;
    assert!(contains(r#""test:bar" = "1.1.0""#).eval(&manifest));
    assert!(contains("package.metadata.component.target.dependencies").eval(&manifest));

    project
        .cargo_component(["add", "test:bar", "--target"])
        .assert()
        .stderr(contains(
            "cannot add dependency `test:bar` as it conflicts with an existing dependency",
        ));

    Ok(())
}

#[test]
fn checks_for_duplicate_dependencies() -> Result<()> {
    let project = Project::new("foo", true)?;
    project.update_manifest(|mut doc| {
        doc["package"]["metadata"]["component"]["dependencies"]["foo:bar"] = value("1.2.3");
        Ok(doc)
    })?;

    project
        .cargo_component(["add", "foo:bar"])
        .assert()
        .stderr(contains(
            "cannot add dependency `foo:bar` as it conflicts with an existing dependency",
        ))
        .failure();

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn prints_modified_manifest_for_dry_run() -> Result<()> {
    let (server, config, _) = spawn_server(Vec::<String>::new()).await?;

    publish_component(config, "test:bar", "1.2.3", "(component)").await?;

    let project = server.project("foo", true, Vec::<String>::new())?;

    project
        .cargo_component(["add", "--dry-run", "test:bar"])
        .assert()
        .stderr(contains(
            r#"Added dependency `test:bar` with version `1.2.3`"#,
        ))
        .success();

    let manifest = fs::read_to_string(project.root().join("Cargo.toml"))?;

    // Assert the dependency was added to the manifest
    assert!(!contains(r#"\"test:bar\" = "1.2.3""#).eval(&manifest));

    Ok(())
}

fn validate_add_from_path(project: &Project) -> Result<()> {
    project
        .cargo_component(["add", "--path", "foo/baz", "foo:baz"])
        .assert()
        .stderr(contains("Added dependency `foo:baz` from path `foo/baz`"));

    project
        .cargo_component(["add", "--target", "--path", "foo/qux", "foo:qux"])
        .assert()
        .stderr(contains("Added dependency `foo:qux` from path `foo/qux`"));

    let manifest = fs::read_to_string(project.root().join("Cargo.toml"))?;
    assert!(contains(r#""foo:baz" = { path = "foo/baz" }"#).eval(&manifest));
    assert!(contains(r#""foo:qux" = { path = "foo/qux" }"#).eval(&manifest));
    Ok(())
}

#[test]
fn test_validate_add_from_path() -> Result<()> {
    let project = Project::new("foo", true)?;
    validate_add_from_path(&project)?;
    Ok(())
}

#[test]
fn two_projects_in_one_workspace_validate_add_from_path() -> Result<()> {
    let temp_dir = Rc::new(TempDir::new()?);
    let cargo_workspace = temp_dir.path().join("Cargo.toml");
    fs::write(
        &cargo_workspace,
        r#"
[workspace]
resolver = "2"
"#,
    )
    .with_context(|| {
        format!(
            "failed to write `{cargo_workspace}`",
            cargo_workspace = cargo_workspace.display()
        )
    })?;
    let p1 = Project::with_dir(temp_dir.clone(), "foo", true, Vec::<String>::new())?;
    let p2 = Project::with_dir(temp_dir.clone(), "bar", true, Vec::<String>::new())?;

    validate_add_from_path(&p1)?;
    validate_add_from_path(&p2)?;
    Ok(())
}