use std::time::Duration;
use anyhow::{Context, Result};
use serial_test::serial;
use tokio::process::Command;
use wadm_types::api::StatusType;
use wash_lib::app::validate_manifest_file;
use wash_lib::cli::get::parse_watch_interval;
use wash_lib::cli::output::{AppDeployCommandOutput, AppValidateOutput};
mod common;
use common::TestWashInstance;
const TINYGO_HELLO_WORLD_MANIFEST_CONTENT: &str = r#"
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: tinygo-hello-world
annotations:
description: 'HTTP hello world demo in Golang (TinyGo), using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)'
wasmcloud.dev/authors: wasmCloud team
wasmcloud.dev/source-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/golang/components/http-hello-world/wadm.yaml
wasmcloud.dev/readme-md-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/golang/components/http-hello-world/README.md
wasmcloud.dev/homepage: https://github.com/wasmCloud/wasmCloud/tree/main/examples/golang/components/http-hello-world
wasmcloud.dev/categories: |
http,outgoing-http,http-server,tinygo,golang,example
spec:
components:
- name: http-component
type: component
properties:
image: file://./build/http_hello_world_s.wasm
traits:
- type: spreadscaler
properties:
instances: 1
- name: httpserver
type: capability
properties:
image: ghcr.io/wasmcloud/http-server:0.22.0
traits:
- type: link
properties:
target: http-component
namespace: wasi
package: http
interfaces: [incoming-handler]
source_config:
- name: default-http
properties:
address: 127.0.0.1:8080
"#;
#[tokio::test]
async fn app_validate_simple() -> Result<()> {
let pass = "./tests/fixtures/wadm/simple.wadm.yaml";
tokio::fs::try_exists(pass).await?;
let output = Command::new(env!("CARGO_BIN_EXE_wash"))
.args([
"app",
"validate",
"./tests/fixtures/wadm/manifests/simple.wadm.yaml",
"--output",
"json",
])
.kill_on_drop(true)
.output()
.await
.context("failed to execute wash app validate")?;
let cmd_output: AppValidateOutput =
serde_json::from_slice(&output.stdout).context("failed to build JSON from output")?;
assert!(cmd_output.valid, "valid output");
assert!(cmd_output.errors.is_empty(), "no errors");
assert!(cmd_output.warnings.is_empty(), "no warnings");
Ok(())
}
#[tokio::test]
#[cfg_attr(not(can_reach_ghcr_io), ignore = "ghcr.io is not reachable")]
async fn test_validate_complete_wadm_manifest() {
let test_dir = std::env::temp_dir().join("validate_complete_wadm_manifest");
let manifest_file_path = test_dir.join("wadm_manifest.yaml");
tokio::fs::create_dir_all(&test_dir)
.await
.expect("Failed to create test directory");
tokio::fs::write(&manifest_file_path, TINYGO_HELLO_WORLD_MANIFEST_CONTENT)
.await
.expect("Failed to write test manifest file");
let result = validate_manifest_file(&manifest_file_path, true).await;
assert!(result.is_ok(), "Validation failed: {:?}", result.err());
tokio::fs::remove_dir_all(&test_dir)
.await
.expect("Failed to clean up test directory");
}
#[test]
fn test_parse_watch_interval_milliseconds() {
let result = parse_watch_interval("1500").unwrap();
assert_eq!(result, Duration::from_millis(1500));
}
#[test]
fn test_parse_watch_interval_humantime_seconds() {
let result = parse_watch_interval("5s").unwrap();
assert_eq!(result, Duration::from_secs(5));
}
#[test]
fn test_parse_watch_interval_invalid_input() {
let result = parse_watch_interval("invalid");
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid duration: 'invalid'. Expected a duration like '5s', '1m', '100ms', or milliseconds as an integer."
);
}
#[tokio::test]
#[serial]
async fn test_undeploy_all_and_delete_undeployed() -> Result<()> {
let instance = TestWashInstance::create().await?;
let AppDeployCommandOutput {
success,
deployed,
model_name,
model_version,
} = instance
.deploy_app("./tests/fixtures/wadm/manifests/simple.wadm.yaml")
.await?;
assert!(success && deployed);
assert_eq!(model_name, "sample");
assert_eq!(model_version, "v0.0.1");
tokio::time::timeout(Duration::from_secs(30), async {
loop {
if instance.list_apps().await.is_ok_and(|output| {
output.applications.iter().any(|a| {
a.name == "sample" && a.detailed_status.info.status_type == StatusType::Deployed
})
}) {
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(250)).await;
}
})
.await
.context("timed out waiting for app to be deployed")?;
instance.undeploy_all_apps().await?;
tokio::time::timeout(Duration::from_secs(30), async {
loop {
eprintln!("resp: {:#?}", instance.list_apps().await);
if instance.list_apps().await.is_ok_and(|output| {
output.applications.iter().any(|a| {
a.name == "sample"
&& a.detailed_status.info.status_type == StatusType::Undeployed
})
}) {
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(250)).await;
}
})
.await
.context("timed out waiting for app to be undeployed")?;
instance.delete_all_undeployed_apps().await?;
tokio::time::timeout(Duration::from_secs(30), async {
loop {
if instance
.list_apps()
.await
.is_ok_and(|output| output.applications.is_empty())
{
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(250)).await;
}
})
.await
.context("timed out waiting for app to be deleted")?;
Ok(())
}