From f39d3b4dbf5dabc6b7a344f8e71e34be671bb004 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Sat, 18 Jan 2025 12:01:59 -0800 Subject: [PATCH 1/8] wip --- Cargo.toml | 1 + src/commands/run.rs | 37 ++++++++++++++++++++----------------- tests/all/cli_tests.rs | 26 ++++++++++++++++++-------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87c750ab8e2c..2c71e1ecf735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -494,6 +494,7 @@ compile = ["cranelift"] run = [ "dep:wasmtime-wasi", "wasmtime/runtime", + "wasmtime/wave", "dep:listenfd", "dep:wasi-common", "dep:tokio", diff --git a/src/commands/run.rs b/src/commands/run.rs index bd45e2daa5c2..b2b1e96beef9 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -466,10 +466,6 @@ impl RunCommand { } #[cfg(feature = "component-model")] CliLinker::Component(linker) => { - if self.invoke.is_some() { - bail!("using `--invoke` with components is not supported"); - } - let component = module.unwrap_component(); let command = wasmtime_wasi::bindings::Command::instantiate_async( @@ -478,19 +474,26 @@ impl RunCommand { linker, ) .await?; - let result = command - .wasi_cli_run() - .call_run(&mut *store) - .await - .context("failed to invoke `run` function") - .map_err(|e| self.handle_core_dump(&mut *store, e)); - - // Translate the `Result<(),()>` produced by wasm into a feigned - // explicit exit here with status 1 if `Err(())` is returned. - result.and_then(|wasm_result| match wasm_result { - Ok(()) => Ok(()), - Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), - }) + + if let Some(invoke) = &self.invoke { + let untyped_call = + wasmtime::component::wasm_wave::untyped::UntypedFuncCall::parse(&invoke)?; + todo!("lookup '{}' in component", untyped_call.name()); + } else { + let result = command + .wasi_cli_run() + .call_run(&mut *store) + .await + .context("failed to invoke `run` function") + .map_err(|e| self.handle_core_dump(&mut *store, e)); + + // Translate the `Result<(),()>` produced by wasm into a feigned + // explicit exit here with status 1 if `Err(())` is returned. + result.and_then(|wasm_result| match wasm_result { + Ok(()) => Ok(()), + Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), + }) + } } }; finish_epoch_handler(store); diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 2c6f48f0c747..b0bb6b90850b 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -1139,14 +1139,7 @@ mod test_programs { #[test] fn cli_hello_stdout() -> Result<()> { - run_wasmtime(&[ - "run", - "-Wcomponent-model", - CLI_HELLO_STDOUT_COMPONENT, - "gussie", - "sparky", - "willa", - ])?; + run_wasmtime(&["run", "-Wcomponent-model", CLI_HELLO_STDOUT_COMPONENT])?; Ok(()) } @@ -2070,6 +2063,23 @@ after empty ])?; Ok(()) } + + mod invoke { + use super::*; + + #[test] + fn cli_hello_stdout() -> Result<()> { + println!("{CLI_HELLO_STDOUT_COMPONENT}"); + run_wasmtime(&[ + "run", + "-Wcomponent-model", + CLI_HELLO_STDOUT_COMPONENT, + "--invoke", + "run()", + ])?; + Ok(()) + } + } } #[test] From 2d97d92d83a12f68bbbab1fe3846aa289ae3c850 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Sat, 18 Jan 2025 12:41:10 -0800 Subject: [PATCH 2/8] wasmtime::component::Component: add iterator of exports --- .../src/runtime/component/component.rs | 117 +++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 10af60dc94ec..cf74fdebdc7f 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -651,14 +651,13 @@ impl Component { /// skip string lookups at runtime and instead use a more efficient /// index-based lookup. /// - /// This method takes a few arguments: + /// This method takes two arguments: /// - /// * `engine` - the engine that was used to compile this component. /// * `instance` - an optional "parent instance" for the export being looked /// up. If this is `None` then the export is looked up on the root of the /// component itself, and otherwise the export is looked up on the /// `instance` specified. Note that `instance` must have come from a - /// previous invocation of this method. + /// previous invocation of this method, or from `Component::exports`. /// * `name` - the name of the export that's being looked up. /// /// If the export is located then two values are returned: a @@ -731,6 +730,118 @@ impl Component { )) } + /// Iterates over the exports of a component, yielding each exported + /// item's name, type, and export index. + /// + /// Returns `Some(impl Iterator...)` when the `instance` argument points + /// to a valid instance in the component, and `None` otherwise. + /// + /// The argument `instance` is an optional "parent instance" to iterate + /// over the exports of. If this is `None` then the exports iterated over + /// are from the root of the component itself, and otherwise the exports + /// iterated over are from the `instance` specified. Note that `instance` + /// must have come from a previous invocation of this method, or from + /// `Component::export_index`. + /// + /// # Examples + /// + /// ``` + /// use wasmtime::Engine; + /// use wasmtime::component::Component; + /// use wasmtime::component::types::ComponentItem; + /// + /// # fn main() -> wasmtime::Result<()> { + /// let engine = Engine::default(); + /// let component = Component::new( + /// &engine, + /// r#" + /// (component + /// (core module $m + /// (func (export "f")) + /// (func (export "g")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "f") + /// (canon lift (core func $i "f"))) + /// (func (export "g") + /// (canon lift (core func $i "g"))) + /// (component $c + /// (core module $m + /// (func (export "h")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "h") + /// (canon lift (core func $i "h"))) + /// ) + /// (instance (export "i") (instantiate $c)) + /// ) + /// "#, + /// )?; + /// + /// // Get all items exported by the component root: + /// let exports = component + /// .exports(None) + /// .expect("root") + /// .collect::>(); + /// assert_eq!(exports.len(), 3); + /// assert_eq!(exports[0].0, "f"); + /// assert!(matches!(exports[0].1, ComponentItem::ComponentFunc(_))); + /// assert_eq!(exports[1].0, "g"); + /// assert_eq!(exports[2].0, "i"); + /// assert!(matches!(exports[2].1, ComponentItem::ComponentInstance(_))); + /// let i = exports[2].2; + /// let i_exports = component + /// .exports(Some(&i)) + /// .expect("export instance `i` looked up above") + /// .collect::>(); + /// assert_eq!(i_exports.len(), 1); + /// assert_eq!(i_exports[0].0, "h"); + /// assert!(matches!(i_exports[0].1, ComponentItem::ComponentFunc(_))); + /// + /// // ... + /// # Ok(()) + /// # } + /// ``` + /// + pub fn exports<'a>( + &'a self, + instance: Option<&'_ ComponentExportIndex>, + ) -> Option + use<'a>> + { + let info = self.env_component(); + let exports = match instance { + Some(idx) => { + if idx.id != self.inner.id { + return None; + } + match &info.export_items[idx.index] { + Export::Instance { exports, .. } => exports, + _ => return None, + } + } + None => &info.exports, + }; + Some(exports.raw_iter().map(|(name, index)| { + let index = *index; + let ty = match info.export_items[index] { + Export::Instance { ty, .. } => TypeDef::ComponentInstance(ty), + Export::LiftedFunction { ty, .. } => TypeDef::ComponentFunc(ty), + Export::ModuleStatic { ty, .. } | Export::ModuleImport { ty, .. } => { + TypeDef::Module(ty) + } + Export::Type(ty) => ty, + }; + let item = self.with_uninstantiated_instance_type(|instance| { + types::ComponentItem::from(&self.inner.engine, &ty, instance) + }); + let export = ComponentExportIndex { + id: self.inner.id, + index, + }; + (name.as_str(), item, export) + })) + } + pub(crate) fn lookup_export_index( &self, instance: Option<&ComponentExportIndex>, From 4692fbf034a5bc2225c7d04e2177818218d8238a Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Sat, 18 Jan 2025 13:37:54 -0800 Subject: [PATCH 3/8] components_rec --- .../src/runtime/component/component.rs | 87 ++++++++++++++++++- src/commands/run.rs | 4 + 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index cf74fdebdc7f..260a287f5d95 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -798,7 +798,6 @@ impl Component { /// assert_eq!(i_exports[0].0, "h"); /// assert!(matches!(i_exports[0].1, ComponentItem::ComponentFunc(_))); /// - /// // ... /// # Ok(()) /// # } /// ``` @@ -842,6 +841,92 @@ impl Component { })) } + /// Like `exports` but recursive: for each ComponentInstance yielded, so + /// will all of its contents. + /// + /// # Examples + /// + /// ``` + /// use wasmtime::Engine; + /// use wasmtime::component::Component; + /// use wasmtime::component::types::ComponentItem; + /// + /// # fn main() -> wasmtime::Result<()> { + /// let engine = Engine::default(); + /// let component = Component::new( + /// &engine, + /// r#" + /// (component + /// (core module $m + /// (func (export "f")) + /// (func (export "g")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "f") + /// (canon lift (core func $i "f"))) + /// (func (export "g") + /// (canon lift (core func $i "g"))) + /// (component $c + /// (core module $m + /// (func (export "h")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "h") + /// (canon lift (core func $i "h"))) + /// ) + /// (instance (export "i") (instantiate $c)) + /// ) + /// "#, + /// )?; + /// + /// // Get all items exported by the component root: + /// let exports = component + /// .exports_rec(None) + /// .expect("root") + /// .collect::>(); + /// assert_eq!(exports.len(), 4); + /// assert_eq!(exports[0].0, "f"); + /// assert!(matches!(exports[0].1, ComponentItem::ComponentFunc(_))); + /// assert_eq!(exports[1].0, "g"); + /// assert_eq!(exports[2].0, "i"); + /// assert!(matches!(exports[2].1, ComponentItem::ComponentInstance(_))); + /// assert_eq!(exports[3].0, "i/h"); + /// assert!(matches!(exports[3].1, ComponentItem::ComponentFunc(_))); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// FIXME: want a fully qualified name datatype here instead of String, so + /// we can mechanically check the suffix rather than parsing the string. + pub fn exports_rec<'a>( + &'a self, + instance: Option<&'_ ComponentExportIndex>, + ) -> Option + use<'a>> + { + self.exports(instance).map(|i| { + i.flat_map(|(name, item, index)| { + let name = name.to_owned(); + let base = std::iter::once((name.clone(), item.clone(), index.clone())); + match item { + types::ComponentItem::ComponentInstance(_) => Box::new( + base.chain( + self.exports_rec(Some(&index)) + .unwrap() + .map(move |(n, item, index)| (format!("{name}/{n}"), item, index)), + ), + ) + as Box< + dyn Iterator< + Item = (String, types::ComponentItem, ComponentExportIndex), + > + 'a, + >, + _ => Box::new(base), + } + }) + }) + } + pub(crate) fn lookup_export_index( &self, instance: Option<&ComponentExportIndex>, diff --git a/src/commands/run.rs b/src/commands/run.rs index b2b1e96beef9..7bdeb54b3671 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -478,6 +478,10 @@ impl RunCommand { if let Some(invoke) = &self.invoke { let untyped_call = wasmtime::component::wasm_wave::untyped::UntypedFuncCall::parse(&invoke)?; + println!( + "component exports: {:?}", + component.exports_rec(None).unwrap().collect::>() + ); todo!("lookup '{}' in component", untyped_call.name()); } else { let result = command From 9765505feac27e6bae7722933b49e400cd6f1c7f Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Sat, 18 Jan 2025 13:51:24 -0800 Subject: [PATCH 4/8] exports_rec gives fully qualified name --- .../src/runtime/component/component.rs | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 260a287f5d95..eb0a9e2309c8 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -885,43 +885,47 @@ impl Component { /// .expect("root") /// .collect::>(); /// assert_eq!(exports.len(), 4); - /// assert_eq!(exports[0].0, "f"); + /// assert_eq!(exports[0].0, ["f"]); /// assert!(matches!(exports[0].1, ComponentItem::ComponentFunc(_))); - /// assert_eq!(exports[1].0, "g"); - /// assert_eq!(exports[2].0, "i"); + /// assert_eq!(exports[1].0, ["g"]); + /// assert_eq!(exports[2].0, ["i"]); /// assert!(matches!(exports[2].1, ComponentItem::ComponentInstance(_))); - /// assert_eq!(exports[3].0, "i/h"); + /// assert_eq!(exports[3].0, ["i", "h"]); /// assert!(matches!(exports[3].1, ComponentItem::ComponentFunc(_))); /// /// # Ok(()) /// # } /// ``` - /// - /// FIXME: want a fully qualified name datatype here instead of String, so - /// we can mechanically check the suffix rather than parsing the string. pub fn exports_rec<'a>( &'a self, instance: Option<&'_ ComponentExportIndex>, - ) -> Option + use<'a>> - { + ) -> Option< + impl Iterator, types::ComponentItem, ComponentExportIndex)> + use<'a>, + > { self.exports(instance).map(|i| { i.flat_map(|(name, item, index)| { - let name = name.to_owned(); + let name = vec![name.to_owned()]; let base = std::iter::once((name.clone(), item.clone(), index.clone())); match item { - types::ComponentItem::ComponentInstance(_) => Box::new( - base.chain( - self.exports_rec(Some(&index)) - .unwrap() - .map(move |(n, item, index)| (format!("{name}/{n}"), item, index)), - ), - ) + types::ComponentItem::ComponentInstance(_) => { + Box::new(base.chain(self.exports_rec(Some(&index)).unwrap().map( + move |(mut suffix, item, index)| { + let mut name = name.clone(); + name.append(&mut suffix); + (name, item, index) + }, + ))) + } + _ => Box::new(base) as Box< dyn Iterator< - Item = (String, types::ComponentItem, ComponentExportIndex), + Item = ( + Vec, + types::ComponentItem, + ComponentExportIndex, + ), > + 'a, >, - _ => Box::new(base), } }) }) From 5ae7094309ae9ed811e43c56f50595a59995cec7 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Sat, 18 Jan 2025 19:36:05 -0800 Subject: [PATCH 5/8] invoke works!!! --- src/commands/run.rs | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/commands/run.rs b/src/commands/run.rs index 7bdeb54b3671..306d4dc36974 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -468,22 +468,52 @@ impl RunCommand { CliLinker::Component(linker) => { let component = module.unwrap_component(); - let command = wasmtime_wasi::bindings::Command::instantiate_async( - &mut *store, - component, - linker, - ) - .await?; - if let Some(invoke) = &self.invoke { let untyped_call = wasmtime::component::wasm_wave::untyped::UntypedFuncCall::parse(&invoke)?; - println!( - "component exports: {:?}", - component.exports_rec(None).unwrap().collect::>() - ); - todo!("lookup '{}' in component", untyped_call.name()); + let name = untyped_call.name(); + let matches = component + .exports_rec(None) + .expect("at root") + .filter(|(names, _, _)| { + names.last().expect("always at least one name") == name + }) + .collect::>(); + match matches.len() { + 0 => bail!("No export named `{name}` in component."), + 1 => {} + _ => bail!("Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names"), + }; + use wasmtime::component::types::ComponentItem; + use wasmtime::component::wasm_wave::wasm::{DisplayFuncResults, WasmFunc}; + use wasmtime::component::Val; + let (params, result_len, export) = match &matches[0] { + (_names, ComponentItem::ComponentFunc(func), export) => { + let param_types = WasmFunc::params(func).collect::>(); + let params = untyped_call.to_wasm_params(¶m_types)?; + (params, func.results().len(), export) + } + (names, ty, _) => { + bail!("Cannot invoke export {names:?}: expected ComponentFunc, got type {ty:?}"); + } + }; + + let instance = linker.instantiate_async(&mut *store, component).await?; + let func = instance + .get_func(&mut *store, export) + .expect("found export index"); + let mut results = vec![Val::Bool(false); result_len]; + func.call_async(&mut *store, ¶ms, &mut results).await?; + println!("{}", DisplayFuncResults(&results)); + Ok(()) } else { + let command = wasmtime_wasi::bindings::Command::instantiate_async( + &mut *store, + component, + linker, + ) + .await?; + let result = command .wasi_cli_run() .call_run(&mut *store) From 34412ac5fe5368c4b254fdd32011bb181d0877c9 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 20 Jan 2025 10:33:04 -0800 Subject: [PATCH 6/8] code motion --- src/commands/run.rs | 95 ++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/src/commands/run.rs b/src/commands/run.rs index 306d4dc36974..c546d48c2fe8 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -467,45 +467,8 @@ impl RunCommand { #[cfg(feature = "component-model")] CliLinker::Component(linker) => { let component = module.unwrap_component(); - - if let Some(invoke) = &self.invoke { - let untyped_call = - wasmtime::component::wasm_wave::untyped::UntypedFuncCall::parse(&invoke)?; - let name = untyped_call.name(); - let matches = component - .exports_rec(None) - .expect("at root") - .filter(|(names, _, _)| { - names.last().expect("always at least one name") == name - }) - .collect::>(); - match matches.len() { - 0 => bail!("No export named `{name}` in component."), - 1 => {} - _ => bail!("Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names"), - }; - use wasmtime::component::types::ComponentItem; - use wasmtime::component::wasm_wave::wasm::{DisplayFuncResults, WasmFunc}; - use wasmtime::component::Val; - let (params, result_len, export) = match &matches[0] { - (_names, ComponentItem::ComponentFunc(func), export) => { - let param_types = WasmFunc::params(func).collect::>(); - let params = untyped_call.to_wasm_params(¶m_types)?; - (params, func.results().len(), export) - } - (names, ty, _) => { - bail!("Cannot invoke export {names:?}: expected ComponentFunc, got type {ty:?}"); - } - }; - - let instance = linker.instantiate_async(&mut *store, component).await?; - let func = instance - .get_func(&mut *store, export) - .expect("found export index"); - let mut results = vec![Val::Bool(false); result_len]; - func.call_async(&mut *store, ¶ms, &mut results).await?; - println!("{}", DisplayFuncResults(&results)); - Ok(()) + if self.invoke.is_some() { + self.invoke_component(&mut *store, component, linker).await } else { let command = wasmtime_wasi::bindings::Command::instantiate_async( &mut *store, @@ -535,6 +498,60 @@ impl RunCommand { result } + #[cfg(feature = "component-model")] + async fn invoke_component( + &self, + store: &mut Store, + component: &wasmtime::component::Component, + linker: &mut wasmtime::component::Linker, + ) -> Result<()> { + use wasmtime::component::{ + types::ComponentItem, + wasm_wave::{ + untyped::UntypedFuncCall, + wasm::{DisplayFuncResults, WasmFunc}, + }, + Val, + }; + + let invoke = self.invoke.as_ref().unwrap(); + let untyped_call = UntypedFuncCall::parse(invoke)?; + let name = untyped_call.name(); + let matches = component + .exports_rec(None) + .expect("at root") + .filter(|(names, _, _)| names.last().expect("always at least one name") == name) + .collect::>(); + match matches.len() { + 0 => bail!("No export named `{name}` in component."), + 1 => {} + _ => bail!("Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names"), + }; + let (params, result_len, export) = match &matches[0] { + (_names, ComponentItem::ComponentFunc(func), export) => { + let param_types = WasmFunc::params(func).collect::>(); + let params = untyped_call.to_wasm_params(¶m_types)?; + (params, func.results().len(), export) + } + (names, ty, _) => { + bail!("Cannot invoke export {names:?}: expected ComponentFunc, got type {ty:?}"); + } + }; + + let instance = linker.instantiate_async(&mut *store, component).await?; + + let func = instance + .get_func(&mut *store, export) + .expect("found export index"); + + let mut results = vec![Val::Bool(false); result_len]; + func.call_async(&mut *store, ¶ms, &mut results).await?; + + println!("{}", DisplayFuncResults(&results)); + + Ok(()) + } + async fn invoke_func(&self, store: &mut Store, func: Func) -> Result<()> { let ty = func.ty(&store); if ty.params().len() > 0 { From 0cb3fbb26e6bb3d0311a3e5711da766b4fad8b2f Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 20 Jan 2025 11:59:49 -0800 Subject: [PATCH 7/8] more context in errors --- src/commands/run.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/run.rs b/src/commands/run.rs index c546d48c2fe8..4e33c4d44a4c 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -515,7 +515,8 @@ impl RunCommand { }; let invoke = self.invoke.as_ref().unwrap(); - let untyped_call = UntypedFuncCall::parse(invoke)?; + let untyped_call = UntypedFuncCall::parse(invoke) + .with_context(|| format!("parsing invoke \"{invoke}\""))?; let name = untyped_call.name(); let matches = component .exports_rec(None) @@ -530,7 +531,9 @@ impl RunCommand { let (params, result_len, export) = match &matches[0] { (_names, ComponentItem::ComponentFunc(func), export) => { let param_types = WasmFunc::params(func).collect::>(); - let params = untyped_call.to_wasm_params(¶m_types)?; + let params = untyped_call.to_wasm_params(¶m_types).with_context(|| { + format!("while interpreting parameters in invoke \"{invoke}\"") + })?; (params, func.results().len(), export) } (names, ty, _) => { From 2c60577413c9c0765a4f95aafb92d7cfbce17fd0 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 20 Jan 2025 12:23:47 -0800 Subject: [PATCH 8/8] fix test of invoke --- tests/all/cli_tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index b0bb6b90850b..2da751c2e11e 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -2070,13 +2070,16 @@ after empty #[test] fn cli_hello_stdout() -> Result<()> { println!("{CLI_HELLO_STDOUT_COMPONENT}"); - run_wasmtime(&[ + let output = run_wasmtime(&[ "run", "-Wcomponent-model", - CLI_HELLO_STDOUT_COMPONENT, "--invoke", "run()", + CLI_HELLO_STDOUT_COMPONENT, ])?; + // First this component prints "hello, world", then the invoke + // result is printed as "ok". + assert_eq!(output, "hello, world\nok\n"); Ok(()) } }