cargo-component 0.21.1

A cargo extension for authoring WebAssembly components
Documentation
use std::fs;

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

use crate::support::*;

mod support;

#[test]
fn help() {
    for arg in ["help update", "update -h", "update --help"] {
        cargo_component(arg.split_whitespace())
            .assert()
            .stdout(contains(
                "Update dependencies as recorded in the component lock file",
            ))
            .success();
    }
}

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

    publish_wit(
        config,
        "test:bar",
        "1.0.0",
        r#"package test:bar@1.0.0;
world foo {
    import foo: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    let project = server.project("component", true, ["--target", "test:bar@1.0.0"])?;
    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    project
        .cargo_component(["update"])
        .assert()
        .success()
        .stderr(contains("test:bar").not());

    Ok(())
}

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

    publish_wit(
        config.clone(),
        "test:bar",
        "1.0.0",
        r#"package test:bar@1.0.0;
world foo {
    import foo: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    let project = server.project("component", true, ["--target", "test:bar@1.0.0"])?;
    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    publish_wit(
        config,
        "test:bar",
        "2.0.0",
        r#"package test:bar@2.0.0;
world foo {
    export bar: func() -> string;
}"#,
    )
    .await?;

    project
        .cargo_component(["update"])
        .assert()
        .success()
        .stderr(contains("test:bar").not());

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    Ok(())
}

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

    publish_wit(
        config.clone(),
        "test:bar",
        "1.0.0",
        r#"package test:bar@1.0.0;
world foo {
    import foo: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    let project = server.project("component", true, ["--target", "test:bar@1.0.0"])?;
    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    publish_wit(
        config,
        "test:bar",
        "1.1.0",
        r#"package test:bar@1.1.0;
world foo {
    import foo: func() -> string;
    import baz: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    project
        .cargo_component(["update"])
        .assert()
        .success()
        .stderr(contains("`test:bar` v1.0.0 -> v1.1.0"));

    let source = r#"
#[allow(warnings)]
mod bindings;
use bindings::{baz, Guest};
struct Component;
impl Guest for Component {
    fn bar() -> String {
        baz()
    }
}

bindings::export!(Component with_types_in bindings);
"#;

    fs::write(project.root().join("src/lib.rs"), source)?;

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    Ok(())
}

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

    publish_wit(
        config.clone(),
        "test:bar",
        "1.0.0",
        r#"package test:bar@1.0.0;
world foo {
    import foo: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    let project = server.project("component", true, ["--target", "test:bar@1.0.0"])?;
    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();
    validate_component(&project.debug_wasm("component"))?;

    publish_wit(
        config.clone(),
        "test:bar",
        "1.1.0",
        r#"package test:bar@1.1.0;
world foo {
    import foo: func() -> string;
    import baz: func() -> string;
    export bar: func() -> string;
}"#,
    )
    .await?;

    project
        .cargo_component(["update", "--dry-run"])
        .assert()
        .success()
        .stderr(contains(
            "Would update dependency `test:bar` v1.0.0 -> v1.1.0",
        ));

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();

    validate_component(&project.debug_wasm("component"))?;

    Ok(())
}

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

    publish_component(config.clone(), "test:bar", "1.0.0", "(component)").await?;
    publish_component(config, "test:baz", "1.0.0", "(component)").await?;

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

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();

    validate_component(&project.debug_wasm("foo"))?;

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

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();

    project.update_manifest(|mut doc| {
        let deps = doc["package"]["metadata"]["component"]["dependencies"]
            .as_table_mut()
            .context("missing deps table")?;
        deps.remove("test:bar").context("missing dependency")?;
        deps.insert("test:baz", value("1.0.0"));
        Ok(doc)
    })?;

    project
        .cargo_component(["update"])
        .assert()
        .stderr(
            contains("Removing dependency `test:bar` v1.0.0")
                .and(contains("Adding dependency `test:baz` v1.0.0")),
        )
        .success();

    project
        .cargo_component(["build"])
        .assert()
        .stderr(contains(
            "Finished `dev` profile [unoptimized + debuginfo] target(s)",
        ))
        .success();

    Ok(())
}