diff --git a/Cargo.lock b/Cargo.lock index 655867f60..920abb2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,6 +3081,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -3425,7 +3434,7 @@ name = "test-helpers" version = "0.0.0" dependencies = [ "codegen-macro", - "wit-bindgen-core 0.40.0", + "wit-bindgen-core 0.41.0", "wit-parser 0.227.1", ] @@ -3700,6 +3709,18 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -3708,13 +3729,22 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -3724,6 +3754,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow 0.7.15", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -4521,7 +4564,7 @@ dependencies = [ "rayon", "serde", "serde_derive", - "toml", + "toml 0.9.12+spec-1.1.0", "wasmtime", ] @@ -4570,7 +4613,7 @@ dependencies = [ "serde", "serde_derive", "sha2 0.10.9", - "toml", + "toml 0.9.12+spec-1.1.0", "wasmtime-environ", "windows-sys 0.61.2", "zstd", @@ -5125,6 +5168,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -5168,9 +5214,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398c650cec1278cfb72e214ba26ef3440ab726e66401bd39c04f465ee3979e6b" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" dependencies = [ "anyhow", "heck", @@ -5284,8 +5330,7 @@ dependencies = [ "anyhow", "clap", "heck", - "test-helpers", - "wit-bindgen-core 0.40.0", + "wit-bindgen-core 0.41.0", "wrpc-introspect", ] @@ -5302,9 +5347,8 @@ dependencies = [ "serde", "serde_json", "syn", - "test-helpers", "tokio", - "wit-bindgen-core 0.40.0", + "wit-bindgen-core 0.41.0", "wit-bindgen-wrpc", "wrpc-introspect", "wrpc-transport", @@ -5319,10 +5363,25 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wit-bindgen-core 0.40.0", + "wit-bindgen-core 0.41.0", "wit-bindgen-wrpc-rust", ] +[[package]] +name = "wit-bindgen-wrpc-test" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "heck", + "rayon", + "regex", + "serde", + "serde_json", + "toml 0.8.23", + "wit-parser 0.227.1", +] + [[package]] name = "wit-component" version = "0.238.1" @@ -5496,10 +5555,11 @@ dependencies = [ "wasmtime", "wasmtime-cli-flags", "wasmtime-wasi", - "wit-bindgen-core 0.40.0", + "wit-bindgen-core 0.41.0", "wit-bindgen-wrpc", "wit-bindgen-wrpc-go", "wit-bindgen-wrpc-rust", + "wit-bindgen-wrpc-test", "wrpc-cli", "wrpc-nats", "wrpc-quic", diff --git a/Cargo.toml b/Cargo.toml index 96b8d30dd..49b13e223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ bin-bindgen = [ "dep:clap", "dep:wit-bindgen-core", "dep:wit-bindgen-wrpc-go", + "dep:wit-bindgen-wrpc-test", "wit-bindgen-wrpc-go/clap", "wit-bindgen-wrpc-rust/clap", ] @@ -82,6 +83,7 @@ wit-bindgen-core = { workspace = true, optional = true } wit-bindgen-wrpc = { workspace = true } wit-bindgen-wrpc-go = { workspace = true, optional = true } wit-bindgen-wrpc-rust = { workspace = true, optional = true } +wit-bindgen-wrpc-test = { workspace = true, optional = true } wrpc-cli = { workspace = true, optional = true } wrpc-nats = { workspace = true, optional = true } wrpc-quic = { workspace = true, optional = true } @@ -148,8 +150,10 @@ prettyplease = { version = "0.2.37", default-features = false } proc-macro2 = { version = "1", default-features = false } quinn = { version = "0.11.6", default-features = false } quote = { version = "1", default-features = false } +rayon = { version = "1", default-features = false } rcgen = { version = "0.14", default-features = false } redis = { version = "1.2", default-features = false } +regex = { version = "1", default-features = false } reqwest = { version = "0.13", default-features = false } rustls = { version = "0.23", default-features = false } semver = { version = "1", default-features = false } @@ -163,6 +167,7 @@ tokio = { version = "1", default-features = false } tokio-stream = { version = "0.1", default-features = false } tokio-util = { version = "0.7", default-features = false } tokio-websockets = { version = "0.13", default-features = false } +toml = { version = "0.8", default-features = false } tower = { version = "0.5", default-features = false } tower-http = { version = "0.7", default-features = false } tracing = { version = "0.1", default-features = false } @@ -179,11 +184,12 @@ wasmtime-cli-flags = { version = "45", default-features = false } wasmtime-wasi = { version = "45", default-features = false } wasmtime-wasi-http = { version = "45", default-features = false } wit-bindgen = { version = "0.45", default-features = false } -wit-bindgen-core = { version = "0.40", default-features = false } +wit-bindgen-core = { version = "0.41", default-features = false } wit-bindgen-wrpc = { version = "0.11", default-features = false, path = "./crates/wit-bindgen" } wit-bindgen-wrpc-go = { version = "0.13", default-features = false, path = "./crates/wit-bindgen-go" } wit-bindgen-wrpc-rust = { version = "0.11", default-features = false, path = "./crates/wit-bindgen-rust" } wit-bindgen-wrpc-rust-macro = { version = "0.11", default-features = false, path = "./crates/wit-bindgen-rust-macro" } +wit-bindgen-wrpc-test = { version = "0.1", default-features = false, path = "./crates/wit-bindgen-test" } wit-component = { version = "0.252", default-features = false } wit-parser = { version = "0.227", default-features = false } wrpc-cli = { version = "0.8", path = "./crates/cli", default-features = false } diff --git a/crates/wit-bindgen-go/Cargo.toml b/crates/wit-bindgen-go/Cargo.toml index 4d1d2c40c..a160b0b1a 100644 --- a/crates/wit-bindgen-go/Cargo.toml +++ b/crates/wit-bindgen-go/Cargo.toml @@ -22,6 +22,3 @@ clap = { workspace = true, features = ["derive"], optional = true } heck = { workspace = true } wit-bindgen-core = { workspace = true } wrpc-introspect = { workspace = true } - -[dev-dependencies] -test-helpers = { workspace = true } diff --git a/crates/wit-bindgen-go/src/lib.rs b/crates/wit-bindgen-go/src/lib.rs index 53f35b29a..dec8f3f07 100644 --- a/crates/wit-bindgen-go/src/lib.rs +++ b/crates/wit-bindgen-go/src/lib.rs @@ -226,7 +226,7 @@ impl From for InterfaceGeneration { } #[derive(Debug, Clone)] -#[cfg_attr(feature = "clap", derive(clap::Args))] +#[cfg_attr(feature = "clap", derive(clap::Parser))] pub struct Opts { /// Whether or not `gofmt` is executed to format generated code. #[cfg_attr(feature = "clap", arg(long, default_missing_value = "true", default_value_t = true, num_args = 0..=1, require_equals = true, action = clap::ArgAction::Set))] diff --git a/crates/wit-bindgen-go/tests/codegen.rs b/crates/wit-bindgen-go/tests/codegen.rs deleted file mode 100644 index c4e54474e..000000000 --- a/crates/wit-bindgen-go/tests/codegen.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::fs; -use std::path::Path; -use std::process::Command; - -macro_rules! codegen_test { - (issue668 $name:tt $test:tt) => {}; - (multiversion $name:tt $test:tt) => {}; - - // TODO: implement support for stream, future, and error-context in the - // wRPC Go generator, and then remove these lines: - (streams $name:tt $test:tt) => {}; - (futures $name:tt $test:tt) => {}; - (resources_with_streams $name:tt $test:tt) => {}; - (resources_with_futures $name:tt $test:tt) => {}; - (error_context $name:tt $test:tt) => {}; - - ($id:ident $name:tt $test:tt) => { - #[test] - fn $id() { - test_helpers::run_world_codegen_test( - "go", - $test.as_ref(), - |resolve, world, files| { - wit_bindgen_wrpc_go::Opts { - gofmt: false, - package: "bindings".to_string(), - with: Vec::new(), - generate_all: true, - } - .build() - .generate(resolve, world, files) - .unwrap() - }, - verify, - ) - } - }; -} - -test_helpers::codegen_tests!(); - -fn verify(dir: &Path, _name: &str) { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap(); - let go_work = dir.join("go.work"); - fs::write( - &go_work, - r"go 1.22.2 -use .", - ) - .unwrap_or_else(|_| panic!("failed to write `{}`", go_work.display())); - let go_mod = dir.join("go.mod"); - fs::write( - &go_mod, - format!( - r"module bindings - -go 1.22.2 - -require wrpc.io/go v0.0.0-unpublished - -replace wrpc.io/go v0.0.0-unpublished => {}", - root.join("go").display(), - ), - ) - .unwrap_or_else(|_| panic!("failed to write `{}`", go_mod.display())); - - test_helpers::run_command(Command::new("go").args(["test", "./..."]).current_dir(dir)); -} diff --git a/crates/wit-bindgen-rust-macro/src/lib.rs b/crates/wit-bindgen-rust-macro/src/lib.rs index 2d15ce01e..5062fa72e 100644 --- a/crates/wit-bindgen-rust-macro/src/lib.rs +++ b/crates/wit-bindgen-rust-macro/src/lib.rs @@ -106,6 +106,10 @@ impl Parse for Config { .map(|p| p.into_token_stream().to_string()) .collect(); } + Opt::AdditionalDerivesIgnore(list) => { + opts.additional_derive_ignore = + list.into_iter().map(|i| i.value()).collect() + } Opt::With(with) => opts.with.extend(with), Opt::GenerateAll => { opts.generate_all = true; @@ -298,6 +302,7 @@ mod kw { syn::custom_keyword!(bitflags_path); syn::custom_keyword!(exports); syn::custom_keyword!(additional_derives); + syn::custom_keyword!(additional_derives_ignore); syn::custom_keyword!(with); syn::custom_keyword!(generate_all); syn::custom_keyword!(generate_unused_types); @@ -321,6 +326,7 @@ enum Opt { BitflagsPath(syn::LitStr), // Parse as paths so we can take the concrete types/macro names rather than raw strings AdditionalDerives(Vec), + AdditionalDerivesIgnore(Vec), With(HashMap), GenerateAll, GenerateUnusedTypes(syn::LitBool), @@ -380,6 +386,13 @@ impl Parse for Opt { syn::bracketed!(contents in input); let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; Ok(Opt::AdditionalDerives(list.iter().cloned().collect())) + } else if l.peek(kw::additional_derives_ignore) { + input.parse::()?; + input.parse::()?; + let contents; + syn::bracketed!(contents in input); + let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?; + Ok(Opt::AdditionalDerivesIgnore(list.iter().cloned().collect())) } else if l.peek(kw::with) { input.parse::()?; input.parse::()?; diff --git a/crates/wit-bindgen-rust/Cargo.toml b/crates/wit-bindgen-rust/Cargo.toml index c2c613d2c..1be24d3e0 100644 --- a/crates/wit-bindgen-rust/Cargo.toml +++ b/crates/wit-bindgen-rust/Cargo.toml @@ -31,7 +31,6 @@ bytes = { workspace = true } futures = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -test-helpers = { workspace = true } tokio = { workspace = true } wit-bindgen-wrpc = { path = "../wit-bindgen" } wrpc-transport = { workspace = true } diff --git a/crates/wit-bindgen-rust/src/interface.rs b/crates/wit-bindgen-rust/src/interface.rs index d44649907..726183d3a 100644 --- a/crates/wit-bindgen-rust/src/interface.rs +++ b/crates/wit-bindgen-rust/src/interface.rs @@ -2,7 +2,7 @@ use crate::{ full_wit_type_name, int_repr, to_rust_ident, to_upper_camel_case, FnSig, Identifier, InterfaceName, RustFlagsRepr, RustWrpc, TypeGeneration, }; -use heck::{ToShoutySnakeCase, ToUpperCamelCase}; +use heck::{ToKebabCase, ToShoutySnakeCase, ToUpperCamelCase}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::mem; @@ -17,7 +17,7 @@ pub struct InterfaceGenerator<'a> { pub src: Source, pub(super) identifier: Identifier<'a>, pub in_import: bool, - pub(super) gen: &'a mut RustWrpc, + pub(super) r#gen: &'a mut RustWrpc, pub resolve: &'a Resolve, } @@ -44,7 +44,7 @@ impl InterfaceGenerator<'_> { } for func in funcs { - if self.gen.skip.contains(&func.name) { + if self.r#gen.skip.contains(&func.name) { continue; } @@ -74,14 +74,14 @@ impl InterfaceGenerator<'_> { uwrite!( self.src, " -> impl ::core::future::Future> + ::core::marker::Send", - self.gen.anyhow_path() + self.r#gen.anyhow_path() ); } 1 => { uwrite!( self.src, " -> impl ::core::future::Future { uwrite!( self.src, " -> impl ::core::future::Future( async move {{ let ("#, resource_traits = trait_names.join(""), - anyhow = self.gen.anyhow_path(), - futures = self.gen.futures_path(), - wrpc_transport = self.gen.wrpc_transport_path() + anyhow = self.r#gen.anyhow_path(), + futures = self.r#gen.futures_path(), + wrpc_transport = self.r#gen.wrpc_transport_path() ); for Function { name, kind, .. } in &funcs_to_export { uwriteln!( @@ -194,7 +194,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, ") = {tokio}::try_join!(", - tokio = self.gen.tokio_path() + tokio = self.r#gen.tokio_path() ); let instance = match identifier { Identifier::Interface(id, name) => { @@ -258,8 +258,8 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( "{}", ::std::sync::Arc::from("#, rpc_func_name(func), - anyhow = self.gen.anyhow_path(), - wrpc_transport = self.gen.wrpc_transport_path(), + anyhow = self.r#gen.anyhow_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), ); if paths.is_empty() { self.src.push_str( @@ -292,7 +292,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( self.src, r" {anyhow}::Ok([", - anyhow = self.gen.anyhow_path(), + anyhow = self.r#gen.anyhow_path(), ); for func in &funcs_to_export { let name = to_rust_ident(&func.name); @@ -312,7 +312,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( FunctionKind::Constructor(_) => "c", FunctionKind::Static(_) | FunctionKind::AsyncStatic(_) => "s", }, - futures = self.gen.futures_path(), + futures = self.r#gen.futures_path(), wit_name = func.name, ); for i in 0..func.params.len() { @@ -349,8 +349,8 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( let rx = rx.map({tracing}::Instrument::in_current_span).map({tokio}::spawn); {tracing}::trace!(instance = "{instance}", func = "{wit_name}", "calling handler"); match {trait_name}::{name}(&handler, cx"#, - tokio = self.gen.tokio_path(), - tracing = self.gen.tracing_path(), + tokio = self.r#gen.tokio_path(), + tracing = self.r#gen.tracing_path(), wit_name = func.name, ); for i in 0..func.params.len() { @@ -430,9 +430,9 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( > ) }},"#, - anyhow = self.gen.anyhow_path(), - futures = self.gen.futures_path(), - tracing = self.gen.tracing_path(), + anyhow = self.r#gen.anyhow_path(), + futures = self.r#gen.futures_path(), + tracing = self.r#gen.tracing_path(), wit_name = func.name, ); } @@ -512,15 +512,15 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( ", ); let map = if self.in_import { - &mut self.gen.import_modules + &mut self.r#gen.import_modules } else { - &mut self.gen.export_modules + &mut self.r#gen.export_modules }; map.push((module, module_path)); } fn generate_guest_import(&mut self, instance: &str, func: &Function) { - if self.gen.skip.contains(&func.name) { + if self.r#gen.skip.contains(&func.name) { return; } @@ -577,7 +577,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( paths }); - let anyhow = self.gen.anyhow_path().to_string(); + let anyhow = self.r#gen.anyhow_path().to_string(); let params = self.print_docs_and_params(func, &sig); match func.result.iter().collect::>().as_slice() { @@ -600,7 +600,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, ", ::core::option::Option> + ::core::marker::Send + 'static + {wrpc_transport}::Captures<'a>>)", - wrpc_transport = self.gen.wrpc_transport_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), ); } uwrite!(self.src, ">> + Send + 'a"); @@ -618,7 +618,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, "::core::option::Option> + ::core::marker::Send + 'static + {wrpc_transport}::Captures<'a>>", - wrpc_transport = self.gen.wrpc_transport_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), ); } uwrite!(self.src, ")>> + Send + 'a"); @@ -637,7 +637,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( let wrpc__ = {anyhow}::Context::context( {wrpc_transport}::InvokeExt::invoke_values_blocking(wrpc__, cx__, "{instance}", "{}", ({params}), "#, rpc_func_name(func), - wrpc_transport = self.gen.wrpc_transport_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), params = { let s = params.join(", "); if params.len() == 1 { @@ -674,7 +674,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( let (wrpc__, io__) = {anyhow}::Context::context( {wrpc_transport}::InvokeExt::invoke_values(wrpc__, cx__, "{instance}", "{}", ({params}), "#, rpc_func_name(func), - wrpc_transport = self.gen.wrpc_transport_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), params = { let s = params.join(", "); if params.len() == 1 { @@ -809,7 +809,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, "<'a, C: {wrpc_transport}::Invoke>(wrpc__: &'a C, cx__: C::Context,", - wrpc_transport = self.gen.wrpc_transport_path(), + wrpc_transport = self.r#gen.wrpc_transport_path(), ); } else { self.push_str("("); @@ -982,7 +982,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( fn print_list(&mut self, ty: &Type, owned: bool, submodule: bool) { if owned { if is_ty(self.resolve, Type::U8, ty) { - uwrite!(self.src, "{bytes}::Bytes", bytes = self.gen.bytes_path()); + uwrite!(self.src, "{bytes}::Bytes", bytes = self.r#gen.bytes_path()); } else { self.push_str("Vec<"); self.print_ty(ty, true, submodule); @@ -992,7 +992,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, "&'a {bytes}::Bytes", - bytes = self.gen.bytes_path() + bytes = self.r#gen.bytes_path() ); } else { self.push_str("&'a ["); @@ -1015,7 +1015,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( uwrite!( self.src, "::core::pin::Pin<::std::boxed::Box"); } fn print_borrow(&mut self, id: TypeId, submodule: bool) { - self.src.push_str(self.gen.wrpc_transport_path()); + self.src.push_str(self.r#gen.wrpc_transport_path()); self.push_str("::ResourceBorrow<"); self.print_tyid(id, true, submodule); self.push_str(">"); @@ -1044,7 +1044,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( if let Some(name) = &ty.name { let full_wit_type_name = full_wit_type_name(self.resolve, id); if let Some(TypeGeneration::Remap(remapped_path)) = - self.gen.with.get(&full_wit_type_name) + self.r#gen.with.get(&full_wit_type_name) { let remapped_path = remapped_path.clone(); self.push_str(&remapped_path); @@ -1078,7 +1078,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( } fn name_of(&self, ty: TypeId) -> Option { - (self.gen.opts.generate_unused_types + (self.r#gen.opts.generate_unused_types // If this type isn't actually used, no need to generate it. || matches!( self.info(ty), @@ -1132,7 +1132,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( } fn path_to_interface(&self, interface: InterfaceId) -> Option { - let InterfaceName { path, remapped } = &self.gen.interface_names[&interface]; + let InterfaceName { path, remapped } = &self.r#gen.interface_names[&interface]; if *remapped { let mut path_to_root = self.path_to_root(); path_to_root.push_str(path); @@ -1165,7 +1165,7 @@ pub fn serve_interface<'a, T: {wrpc_transport}::Serve>( } fn info(&self, ty: TypeId) -> TypeInfo { - self.gen.types.get(ty) + self.r#gen.types.get(ty) } } @@ -1184,7 +1184,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { let info = self.info(id); // We use a BTree set to make sure we don't have any duplicates and we have a stable order let additional_derives: BTreeSet = self - .gen + .r#gen .opts .additional_derive_attributes .iter() @@ -1194,7 +1194,15 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { let (paths, _) = async_paths_tyid(self.resolve, id); self.rustdoc(docs); - let mut derives = additional_derives.clone(); + let mut derives = BTreeSet::new(); + if !self + .r#gen + .opts + .additional_derive_ignore + .contains(&name.to_kebab_case()) + { + derives.extend(additional_derives.clone()); + } if info.is_copy() && paths.is_empty() { self.push_str("#[repr(C)]\n"); if !derives.contains("Clone") @@ -1235,10 +1243,10 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { let mod_name = to_rust_ident(ty_name); - let bytes = self.gen.bytes_path().to_string(); - let tokio = self.gen.tokio_path().to_string(); - let tokio_util = self.gen.tokio_util_path().to_string(); - let wrpc_transport = self.gen.wrpc_transport_path().to_string(); + let bytes = self.r#gen.bytes_path().to_string(); + let tokio = self.r#gen.tokio_path().to_string(); + let tokio_util = self.r#gen.tokio_util_path().to_string(); + let wrpc_transport = self.r#gen.wrpc_transport_path().to_string(); let (paths, _) = async_paths_tyid(self.resolve, id); if paths.is_empty() { @@ -1643,11 +1651,11 @@ pub struct {}(());", fn type_flags(&mut self, id: TypeId, ty_name: &str, flags: &Flags, docs: &Docs) { if let Some(name) = self.name_of(id) { - let bitflags = self.gen.bitflags_path().to_string(); - let bytes = self.gen.bytes_path().to_string(); - let tokio_util = self.gen.tokio_util_path().to_string(); - let wasm_tokio = self.gen.wasm_tokio_path().to_string(); - let wrpc_transport = self.gen.wrpc_transport_path().to_string(); + let bitflags = self.r#gen.bitflags_path().to_string(); + let bytes = self.r#gen.bytes_path().to_string(); + let tokio_util = self.r#gen.tokio_util_path().to_string(); + let wasm_tokio = self.r#gen.wasm_tokio_path().to_string(); + let wrpc_transport = self.r#gen.wrpc_transport_path().to_string(); let mod_name = to_rust_ident(ty_name); @@ -1773,7 +1781,7 @@ mod {mod_name} {{ let info = self.info(id); // We use a BTree set to make sure we don't have any duplicates and have a stable order let additional_derives: BTreeSet = self - .gen + .r#gen .opts .additional_derive_attributes .iter() @@ -1783,7 +1791,15 @@ mod {mod_name} {{ let (paths, _) = async_paths_tyid(self.resolve, id); self.rustdoc(docs); - let mut derives = additional_derives.clone(); + let mut derives = BTreeSet::new(); + if !self + .r#gen + .opts + .additional_derive_ignore + .contains(&name.to_kebab_case()) + { + derives.extend(additional_derives.clone()); + } if info.is_copy() && paths.is_empty() { derives.extend( ["::core::marker::Copy", "::core::clone::Clone"] @@ -1837,10 +1853,10 @@ mod {mod_name} {{ self.push_str(" {}\n"); } - let bytes = self.gen.bytes_path().to_string(); - let tokio_util = self.gen.tokio_util_path().to_string(); - let wasm_tokio = self.gen.wasm_tokio_path().to_string(); - let wrpc_transport = self.gen.wrpc_transport_path().to_string(); + let bytes = self.r#gen.bytes_path().to_string(); + let tokio_util = self.r#gen.tokio_util_path().to_string(); + let wasm_tokio = self.r#gen.wasm_tokio_path().to_string(); + let wrpc_transport = self.r#gen.wrpc_transport_path().to_string(); let mod_name = to_rust_ident(ty_name); @@ -1936,7 +1952,7 @@ mod {mod_name} {{ }}"#, ); } else { - let tokio = self.gen.tokio_path().to_string(); + let tokio = self.r#gen.tokio_path().to_string(); let (paths, _) = async_paths_tyid(self.resolve, id); if paths.is_empty() { @@ -2124,7 +2140,7 @@ mod {mod_name} {{ uwrite!( self.src, r" - Self::Payload(Some(PayloadDecoder::{case}(ref mut dec))) => dec.take_deferred()," + Self::Payload(Some(PayloadDecoder::{case}(dec))) => dec.take_deferred()," ); } } @@ -2149,7 +2165,7 @@ mod {mod_name} {{ type Error = ::std::io::Error; fn decode(&mut self, src: &mut {bytes}::BytesMut) -> ::core::result::Result<::core::option::Option, Self::Error> {{ - let state = if let Self::Payload(Some(ref mut state)) = self {{ + let state = if let Self::Payload(Some(state)) = self {{ state }} else {{ let Some(disc) = {wasm_tokio}::Leb128DecoderU32.decode(src)? else {{ @@ -2173,7 +2189,7 @@ mod {mod_name} {{ r" {i} => {{ *self = Self::Payload(::core::option::Option::default()); - let Self::Payload(ref mut dec) = self else {{ + let Self::Payload(dec) = self else {{ unreachable!() }}; dec.insert(PayloadDecoder::{case}(::core::default::Default::default())) @@ -2256,13 +2272,15 @@ mod {mod_name} {{ self.int_repr(enum_.tag()); self.push_str(")]\n"); // We use a BTree set to make sure we don't have any duplicates and a stable order - let mut derives: BTreeSet = self - .gen + let mut derives: BTreeSet = BTreeSet::new(); + if !self + .r#gen .opts - .additional_derive_attributes - .iter() - .cloned() - .collect(); + .additional_derive_ignore + .contains(&name.to_kebab_case()) + { + derives.extend(self.r#gen.opts.additional_derive_attributes.to_vec()); + } derives.extend( [ ":: core :: clone :: Clone", @@ -2360,10 +2378,10 @@ mod {mod_name} {{ ); } - let bytes = self.gen.bytes_path().to_string(); - let tokio_util = self.gen.tokio_util_path().to_string(); - let wasm_tokio = self.gen.wasm_tokio_path().to_string(); - let wrpc_transport = self.gen.wrpc_transport_path().to_string(); + let bytes = self.r#gen.bytes_path().to_string(); + let tokio_util = self.r#gen.tokio_util_path().to_string(); + let wasm_tokio = self.r#gen.wasm_tokio_path().to_string(); + let wrpc_transport = self.r#gen.wrpc_transport_path().to_string(); let mod_name = to_rust_ident(ty_name); diff --git a/crates/wit-bindgen-rust/src/lib.rs b/crates/wit-bindgen-rust/src/lib.rs index be9549da7..d01cceaa8 100644 --- a/crates/wit-bindgen-rust/src/lib.rs +++ b/crates/wit-bindgen-rust/src/lib.rs @@ -100,7 +100,7 @@ fn parse_with(s: &str) -> Result<(String, WithOption), String> { } #[derive(Default, Debug, Clone)] -#[cfg_attr(feature = "clap", derive(clap::Args))] +#[cfg_attr(feature = "clap", derive(clap::Parser))] pub struct Opts { /// Whether or not a formatter is executed to format generated code. #[cfg_attr(feature = "clap", arg(long))] @@ -120,9 +120,18 @@ pub struct Opts { /// specified multiple times to add multiple attributes. /// /// These derive attributes will be added to any generated structs or enums - #[cfg_attr(feature = "clap", arg(long = "additional_derive_attribute", short = 'd', default_values_t = Vec::::new()))] + #[cfg_attr(feature = "clap", arg(long, short = 'd'))] pub additional_derive_attributes: Vec, + /// Variants and records to ignore when applying additional derive attributes. + /// + /// These names are specified as they are listed in the wit file, i.e. in kebab case. + /// This feature allows some variants and records to use types for which adding traits will cause + /// compilation to fail, such as serde::Deserialize on wasi:io/streams. + /// + #[cfg_attr(feature = "clap", arg(long))] + pub additional_derive_ignore: Vec, + /// Remapping of interface names to rust module names. /// /// Argument must be of the form `k=v` and this option can be passed @@ -217,7 +226,7 @@ impl RustWrpc { identifier, src: Source::default(), in_import, - gen: self, + r#gen: self, resolve, } } @@ -482,8 +491,15 @@ impl WorldGenerator for RustWrpc { self.opts.additional_derive_attributes ); } + if !self.opts.additional_derive_ignore.is_empty() { + uwriteln!( + self.src_preamble, + "// * additional derives ignored {:?}", + self.opts.additional_derive_ignore + ); + } for (k, v) in &self.opts.with { - uwriteln!(self.src_preamble, "// * with {k:?} = {v:?}"); + uwriteln!(self.src_preamble, "// * with {k:?} = {v}"); } self.types.analyze(resolve); self.world = Some(world); @@ -527,14 +543,14 @@ impl WorldGenerator for RustWrpc { self.generated_types.insert(full_name); } - let mut gen = self.interface(Identifier::Interface(id, name), resolve, true); - let (snake, module_path) = gen.start_append_submodule(name); - if gen.gen.name_interface(resolve, id, name, false)? { + let mut r#gen = self.interface(Identifier::Interface(id, name), resolve, true); + let (snake, module_path) = r#gen.start_append_submodule(name); + if r#gen.r#gen.name_interface(resolve, id, name, false)? { return Ok(()); } for (name, ty_id) in to_define { - gen.define_type(name, *ty_id); + r#gen.define_type(name, *ty_id); } let interface = &resolve.interfaces[id]; @@ -551,11 +567,11 @@ impl WorldGenerator for RustWrpc { } else { name }; - gen.generate_imports(&instance, resolve.interfaces[id].functions.values()); + r#gen.generate_imports(&instance, resolve.interfaces[id].functions.values()); let docs = &resolve.interfaces[id].docs; - gen.finish_append_submodule(&snake, module_path, docs); + r#gen.finish_append_submodule(&snake, module_path, docs); Ok(()) } @@ -569,7 +585,7 @@ impl WorldGenerator for RustWrpc { ) { self.import_funcs_called = true; - let mut gen = self.interface(Identifier::World(world), resolve, true); + let mut r#gen = self.interface(Identifier::World(world), resolve, true); let World { ref name, package, .. } = resolve.worlds[world]; @@ -578,9 +594,9 @@ impl WorldGenerator for RustWrpc { } else { name.to_string() }; - gen.generate_imports(&instance, funcs.iter().map(|(_, func)| *func)); + r#gen.generate_imports(&instance, funcs.iter().map(|(_, func)| *func)); - let src = gen.finish(); + let src = r#gen.finish(); self.src.push_str(&src); } @@ -605,24 +621,24 @@ impl WorldGenerator for RustWrpc { self.generated_types.insert(full_name); } - let mut gen = self.interface(Identifier::Interface(id, name), resolve, false); - let (snake, module_path) = gen.start_append_submodule(name); - if gen.gen.name_interface(resolve, id, name, true)? { + let mut r#gen = self.interface(Identifier::Interface(id, name), resolve, false); + let (snake, module_path) = r#gen.start_append_submodule(name); + if r#gen.r#gen.name_interface(resolve, id, name, true)? { return Ok(()); } for (name, ty_id) in to_define { - gen.define_type(name, *ty_id); + r#gen.define_type(name, *ty_id); } - let exports = gen.generate_exports( + let exports = r#gen.generate_exports( Identifier::Interface(id, name), resolve.interfaces[id].functions.values(), ); let docs = &resolve.interfaces[id].docs; - gen.finish_append_submodule(&snake, module_path, docs); + r#gen.finish_append_submodule(&snake, module_path, docs); if exports { self.export_paths .push(self.interface_names[&id].path.clone()); @@ -637,9 +653,9 @@ impl WorldGenerator for RustWrpc { funcs: &[(&str, &Function)], _files: &mut Files, ) -> Result<()> { - let mut gen = self.interface(Identifier::World(world), resolve, false); - let exports = gen.generate_exports(Identifier::World(world), funcs.iter().map(|f| f.1)); - let src = gen.finish(); + let mut r#gen = self.interface(Identifier::World(world), resolve, false); + let exports = r#gen.generate_exports(Identifier::World(world), funcs.iter().map(|f| f.1)); + let src = r#gen.finish(); self.src.push_str(&src); if exports { self.export_paths.push(String::new()); @@ -667,11 +683,11 @@ impl WorldGenerator for RustWrpc { } self.generated_types.insert(full_name); } - let mut gen = self.interface(Identifier::World(world), resolve, true); + let mut r#gen = self.interface(Identifier::World(world), resolve, true); for (name, ty) in to_define { - gen.define_type(name, *ty); + r#gen.define_type(name, *ty); } - let src = gen.finish(); + let src = r#gen.finish(); self.src.push_str(&src); } diff --git a/crates/wit-bindgen-rust/tests/codegen.rs b/crates/wit-bindgen-rust/tests/codegen.rs deleted file mode 100644 index c20fba403..000000000 --- a/crates/wit-bindgen-rust/tests/codegen.rs +++ /dev/null @@ -1,1119 +0,0 @@ -#![allow(unused_macros)] -#![allow(dead_code, unused_variables)] - -mod codegen_tests { - macro_rules! codegen_test { - (wasi_cli $name:tt $test:tt) => {}; - (wasi_http $name:tt $test:tt) => {}; - - // TODO: implement support for stream, future, and error-context in the - // wRPC generator, and then remove these lines: - (streams $name:tt $test:tt) => {}; - (futures $name:tt $test:tt) => {}; - (resources_with_streams $name:tt $test:tt) => {}; - (resources_with_futures $name:tt $test:tt) => {}; - (error_context $name:tt $test:tt) => {}; - - ($id:ident $name:tt $test:tt) => { - mod $id { - wit_bindgen_wrpc::generate!({ - path: $test, - generate_all - }); - - // This empty module named 'core' is here to catch module path - // conflicts with 'core' modules used in code generated by the - // wit_bindgen_wrpc::generate macro. - // Ref: https://github.com/bytecodealliance/wit-bindgen/pull/568 - mod core {} - - #[test] - fn works() {} - - mod borrowed { - wit_bindgen_wrpc::generate!({ - path: $test, - generate_all - }); - - #[test] - fn works() {} - } - - mod duplicate { - wit_bindgen_wrpc::generate!({ - path: $test, - generate_all - }); - - #[test] - fn works() {} - } - } - - }; - } - test_helpers::codegen_tests!(); -} - -mod strings { - wit_bindgen_wrpc::generate!({ - inline: " - package my:strings; - - world not-used-name { - import cat: interface { - foo: func(x: string); - bar: func() -> string; - } - } - ", - }); - - #[allow(dead_code)] - async fn test( - wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, - ) -> anyhow::Result<()> { - // Test the argument is `&str`. - cat::foo(wrpc, (), "hello").await?; - - // Test the return type is `String`. - let _t: String = cat::bar(wrpc, ()).await?; - - Ok(()) - } -} - -/// Like `strings` but with a type alias. -mod aliased_strings { - wit_bindgen_wrpc::generate!({ - inline: " - package my:strings; - - world not-used-name { - import cat: interface { - type my-string = string; - foo: func(x: my-string); - bar: func() -> my-string; - } - } - ", - }); - - #[allow(dead_code)] - async fn test( - wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, - ) -> anyhow::Result<()> { - // Test the argument is `&str`. - cat::foo(wrpc, (), "hello").await?; - - // Test the return type is `String`. - let _t: String = cat::bar(wrpc, ()).await?; - - Ok(()) - } -} - -/// Like `aliased_strings` but with lists instead of strings. -mod aliased_lists { - wit_bindgen_wrpc::generate!({ - inline: " - package my:lists; - - world not-used-name { - import cat: interface { - type my-list = list; - foo: func(x: my-list); - bar: func() -> my-list; - } - } - ", - }); - - #[allow(dead_code)] - async fn test( - wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, - ) -> anyhow::Result<()> { - // Test the argument is `&[u32]`. - cat::foo(wrpc, (), &[1, 2, 3]).await?; - - // Test the return type is `Vec`. - let _t: Vec = cat::bar(wrpc, ()).await?; - - Ok(()) - } -} - -mod skip { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - world baz { - export exports: interface { - foo: func(); - bar: func(); - } - } - ", - skip: ["foo"], - }); - - #[derive(Clone)] - struct Component; - - impl exports::exports::Handler for Component { - async fn bar(&self, cx: Ctx) -> anyhow::Result<()> { - Ok(()) - } - } - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -mod symbol_does_not_conflict { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo1 { - foo: func(); - } - - interface foo2 { - foo: func(); - } - - interface bar1 { - bar: func() -> string; - } - - interface bar2 { - bar: func() -> string; - } - - world foo { - export foo1; - export foo2; - export bar1; - export bar2; - } - ", - }); - - #[derive(Clone)] - struct Component; - - impl exports::my::inline::foo1::Handler for Component { - async fn foo(&self, cx: Ctx) -> anyhow::Result<()> { - Ok(()) - } - } - - impl exports::my::inline::foo2::Handler for Component { - async fn foo(&self, cx: Ctx) -> anyhow::Result<()> { - Ok(()) - } - } - - impl exports::my::inline::bar1::Handler for Component { - async fn bar(&self, cx: Ctx) -> anyhow::Result { - Ok(String::new()) - } - } - - impl exports::my::inline::bar2::Handler for Component { - async fn bar(&self, cx: Ctx) -> anyhow::Result { - Ok(String::new()) - } - } - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -mod alternative_bitflags_path { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - world foo { - flags bar { - foo, - bar, - baz - } - export get-flag: func() -> bar; - } - ", - bitflags_path: "my_bitflags", - }); - - pub(crate) use wit_bindgen_wrpc::bitflags as my_bitflags; - - #[derive(Clone)] - struct Component; - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } - - impl Handler for Component { - async fn get_flag(&self, cx: Ctx) -> anyhow::Result { - Ok(Bar::BAZ) - } - } -} - -mod owned_resource_deref_mut { - use exports::my::inline::foo::Bar; - use wit_bindgen_wrpc::bytes::Bytes; - use wrpc_transport::{ResourceBorrow, ResourceOwn}; - - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo { - resource bar { - constructor(data: u32); - get-data: func() -> u32; - consume: static func(%self: bar) -> u32; - } - } - - world baz { - export foo; - } - ", - }); - - pub struct MyResource { - data: u32, - } - - impl exports::my::inline::foo::HandlerBar for Component { - async fn new(&self, cx: Ctx, data: u32) -> anyhow::Result> { - Ok(ResourceOwn::from(Bytes::default())) - } - - async fn get_data(&self, cx: Ctx, self_: ResourceBorrow) -> anyhow::Result { - Ok(42) - } - - async fn consume(&self, cx: Ctx, mut _this: ResourceOwn) -> anyhow::Result { - Ok(42) - } - } - - #[derive(Clone)] - struct Component; - - impl exports::my::inline::foo::Handler for Component {} - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -mod package_with_versions { - use exports::my::inline::foo::Bar; - use wit_bindgen_wrpc::bytes::Bytes; - use wrpc_transport::ResourceOwn; - - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline@0.0.0; - - interface foo { - resource bar { - constructor(); - } - } - - world baz { - export foo; - } - ", - }); - - pub struct MyResource; - - impl exports::my::inline::foo::HandlerBar for Component { - async fn new(&self, cx: Ctx) -> anyhow::Result> { - Ok(ResourceOwn::from(Bytes::default())) - } - } - - #[derive(Clone)] - struct Component; - - impl exports::my::inline::foo::Handler for Component {} - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -mod custom_derives { - use std::collections::{hash_map::RandomState, HashSet}; - - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface blah { - record foo { - field1: string, - field2: list - } - - bar: func(cool: foo); - } - - world baz { - export blah; - } - ", - - // Clone is included by default almost everywhere, so include it here to make sure it - // doesn't conflict - additional_derives: [serde::Serialize, serde::Deserialize, ::core::hash::Hash, core::clone::Clone, ::core::cmp::PartialEq, ::core::cmp::Eq], - }); - - use exports::my::inline::blah::Foo; - - #[derive(Clone)] - struct Component; - - impl exports::my::inline::blah::Handler for Component { - async fn bar(&self, cx: Ctx, cool: Foo) -> anyhow::Result<()> { - // Check that built in derives that I've added actually work by seeing that this hashes - let _blah: HashSet = HashSet::from_iter([Foo { - field1: "hello".to_string(), - field2: vec![1, 2, 3], - }]); - - // Check that the attributes from an external crate actually work. If they don't work, - // compilation will fail here - let _ = serde_json::to_string(&cool); - Ok(()) - } - } - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, Component).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -mod with { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo { - record msg { - field: string, - } - } - - interface bar { - use foo.{msg}; - - bar: func(m: msg); - } - - world baz { - import bar; - } - ", - with: { - "my:inline/foo": other::my::inline::foo, - }, - }); - - pub mod other { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo { - record msg { - field: string, - } - } - - world dummy { - use foo.{msg}; - import bar: func(m: msg); - } - ", - }); - } - - #[allow(dead_code)] - async fn test( - wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, - ) -> anyhow::Result<()> { - let msg = other::my::inline::foo::Msg { - field: "hello".to_string(), - }; - my::inline::bar::bar(wrpc, (), &msg).await?; - Ok(()) - } -} - -mod with_and_resources { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo { - resource a; - } - - interface bar { - use foo.{a}; - - bar: func(m: a) -> list; - } - - world baz { - import bar; - } - ", - with: { - "my:inline/foo": other::my::inline::foo, - }, - }); - - pub mod other { - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - - interface foo { - resource a; - } - - world dummy { - use foo.{a}; - import bar: func(m: a); - } - ", - }); - } -} - -mod with_type { - use wit_bindgen_wrpc::bytes::Bytes; - use wrpc_transport::ResourceOwn; - - mod my_types { - use std::marker::PhantomData; - - use wit_bindgen_wrpc::bytes::BytesMut; - use wit_bindgen_wrpc::tokio_util::codec::{Decoder, Encoder}; - use wit_bindgen_wrpc::wasm_tokio::CoreVecDecoder; - use wit_bindgen_wrpc::wrpc_transport::{Decode, Deferred, DeferredFn, Encode}; - - #[derive(Debug, Clone, Copy)] - pub struct MyA { - pub inner: f64, - } - - #[derive(Debug, Clone, Copy)] - pub struct MyB; - - pub enum MyC { - A(MyA), - B(MyB), - } - - pub struct MyD { - pub inner: u32, - } - - pub struct MyE { - pub inner: u32, - } - - pub struct Codec(PhantomData); - - impl Default for Codec { - fn default() -> Self { - Self(PhantomData) - } - } - - impl Encoder for Codec { - type Error = std::io::Error; - - fn encode(&mut self, _: T, dst: &mut BytesMut) -> std::io::Result<()> { - Ok(()) - } - } - - impl Encoder<&T> for Codec { - type Error = std::io::Error; - - fn encode(&mut self, _: &T, _: &mut BytesMut) -> std::io::Result<()> { - Ok(()) - } - } - - impl Decoder for Codec { - type Item = T; - type Error = std::io::Error; - - fn decode(&mut self, _: &mut BytesMut) -> Result, Self::Error> { - Ok(None) - } - } - - impl Deferred for Codec { - fn take_deferred(&mut self) -> Option> { - None - } - } - - macro_rules! impl_codec { - ($t:ty) => { - impl Encode for $t { - type Encoder = Codec<$t>; - } - - impl Encode for &$t { - type Encoder = Codec<$t>; - } - - impl Decode for $t { - type Decoder = Codec<$t>; - type ListDecoder = CoreVecDecoder; - } - }; - } - - impl_codec!(MyA); - impl_codec!(MyB); - impl_codec!(MyC); - impl_codec!(MyD); - impl_codec!(MyE); - } - - wit_bindgen_wrpc::generate!({ - inline: " - package my:inline; - interface foo { - record a { - inner: f64, - } - resource b; - variant c { - a(a), - b(b), - } - // test type definition generation with `generate` option - record f { - inner: u32, - } - // test type definition generation without `generate` option - record g { - inner: u32, - } - func1: func(v: a) -> a; - func2: func(v: b) -> b; - func3: func(v: list) -> list; - func4: func(v: option) -> option; - func5: func() -> result; - func6: func() -> result; - func7: func() -> result; - } - interface bar { - record e { - inner: u32, - } - func6: func(v: e) -> e; - } - world dummy-type-remap { - // test remapping with importing type directly - use foo.{c}; - import func7: func(v: c) -> c; - // test remapping the type defined in world - record d { - inner: u32, - } - import func8: func(v: d) -> d; - export bar; - } - ", - with: { - "my:inline/foo/a": crate::with_type::my_types::MyA, - "my:inline/foo/b": crate::with_type::my_types::MyB, - "my:inline/foo/c": crate::with_type::my_types::MyC, - "dummy-type-remap/d": crate::with_type::my_types::MyD, - "my:inline/bar/e": crate::with_type::my_types::MyE, - "my:inline/foo/f": generate, - }, - }); - - struct Component; - - impl exports::my::inline::bar::Handler for Component { - async fn func6(&self, _: Ctx, v: my_types::MyE) -> anyhow::Result { - Ok(v) - } - } - - async fn test( - wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, - ) -> anyhow::Result<()> { - let a = my_types::MyA { inner: 0.0 }; - let _ = my::inline::foo::func1(wrpc, (), &a).await?; - - let _ = my::inline::foo::func2(wrpc, (), &ResourceOwn::from(Bytes::default())); - - let c = my_types::MyC::A(a); - let _ = func7(wrpc, (), &c).await?; - - let a_list = vec![a, a]; - let _ = my::inline::foo::func3(wrpc, (), &a_list).await?; - - let _ = my::inline::foo::func4(wrpc, (), Some(a)).await?; - - let _ = my::inline::foo::func5(wrpc, ()).await?; - - let d = my_types::MyD { inner: 0 }; - let _ = func8(wrpc, (), &d).await?; - Ok(()) - } -} - -#[allow(unused)] -mod generate_unused_types { - use exports::foo::bar::component::UnusedEnum; - use exports::foo::bar::component::UnusedRecord; - use exports::foo::bar::component::UnusedVariant; - - wit_bindgen_wrpc::generate!({ - inline: " - package foo:bar; - - world bindings { - export component; - } - - interface component { - variant unused-variant { - %enum(unused-enum), - %record(unused-record) - } - enum unused-enum { - unused - } - record unused-record { - x: u32 - } - } - ", - generate_unused_types: true, - }); -} - -#[allow(unused)] -mod gated_features { - wit_bindgen_wrpc::generate!({ - inline: r#" - package foo:bar@1.2.3; - - world bindings { - @unstable(feature = x) - import x: func(); - @unstable(feature = y) - import y: func(); - @since(version = 1.2.3) - import z: func(); - } - "#, - features: ["y"], - }); - - fn _foo(wrpc: &impl wit_bindgen_wrpc::wrpc_transport::Invoke) { - y(wrpc, ()); - z(wrpc, ()); - } -} - -#[allow(unused)] -mod simple_with_option { - mod a { - wit_bindgen_wrpc::generate!({ - inline: r#" - package foo:bar; - - interface a { - x: func(); - } - - package foo:baz { - world w { - import foo:bar/a; - } - } - "#, - world: "foo:baz/w", - generate_all, - }); - } - - mod b { - wit_bindgen_wrpc::generate!({ - inline: r#" - package foo:bar; - - interface a { - x: func(); - } - - package foo:baz { - world w { - import foo:bar/a; - } - } - "#, - world: "foo:baz/w", - with: { "foo:bar/a": generate }, - }); - } -} - -#[allow(unused)] -mod multiple_paths { - wit_bindgen_wrpc::generate!({ - inline: r#" - package test:paths; - - world test { - import paths:path1/test; - export paths:path2/test; - } - "#, - path: ["tests/wit/path1", "tests/wit/path2"], - generate_all, - }); -} - -#[allow(unused)] -mod interface_export_example { - wit_bindgen_wrpc::generate!({ - inline: r#" - package my:test; - - interface a { - type my-type = u32; - } - - world my-world { - export b: interface { - use a.{my-type}; - - foo: func() -> my-type; - } - } - "#, - }); - - #[derive(Clone)] - struct MyComponent; - - impl exports::b::Handler for MyComponent { - async fn foo(&self, cx: Ctx) -> anyhow::Result { - Ok(42) - } - } - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use wit_bindgen_wrpc::futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, MyComponent).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - wit_bindgen_wrpc::tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} -#[allow(unused)] -mod resource_example { - use std::sync::{Arc, RwLock}; - - use anyhow::Context as _; - use bytes::Bytes; - use wrpc_transport::{ResourceBorrow, ResourceOwn}; - - wit_bindgen_wrpc::generate!({ - inline: r#" - package my:test; - - interface logging { - enum level { - debug, - info, - error, - } - - resource logger { - constructor(level: level); - log: func(level: level, msg: string); - level: func() -> level; - set-level: func(level: level); - } - } - - world my-world { - export logging; - } - "#, - }); - - use exports::my::test::logging::{Handler, HandlerLogger, Level, Logger}; - - #[derive(Clone, Default)] - struct MyComponent { - loggers: Arc>>, - } - - // Note that the `logging` interface has no methods of its own but a trait - // is required to be implemented here to specify the type of `Logger`. - impl Handler for MyComponent {} - - struct MyLogger { - level: RwLock, - contents: RwLock, - } - - impl HandlerLogger for MyComponent { - async fn new(&self, cx: Ctx, level: Level) -> anyhow::Result> { - let mut loggers = self.loggers.write().unwrap(); - let handle = loggers.len().to_le_bytes(); - loggers.push(MyLogger { - level: RwLock::new(level), - contents: RwLock::new(String::new()), - }); - Ok(ResourceOwn::from(Bytes::copy_from_slice(&handle))) - } - - async fn log( - &self, - cx: Ctx, - logger: ResourceBorrow, - level: Level, - msg: String, - ) -> anyhow::Result<()> { - let i = Bytes::from(logger).as_ref().try_into()?; - let i = usize::from_le_bytes(i); - let loggers = self.loggers.read().unwrap(); - let logger = loggers.get(i).context("invalid resource handle")?; - if level as u32 <= *logger.level.read().unwrap() as u32 { - let mut contents = logger.contents.write().unwrap(); - contents.push_str(&msg); - contents.push('\n'); - } - Ok(()) - } - - async fn level(&self, cx: Ctx, logger: ResourceBorrow) -> anyhow::Result { - let i = Bytes::from(logger).as_ref().try_into()?; - let i = usize::from_le_bytes(i); - let loggers = self.loggers.read().unwrap(); - let logger = loggers.get(i).context("invalid resource handle")?; - let level = logger.level.read().unwrap(); - Ok(*level) - } - - async fn set_level( - &self, - cx: Ctx, - logger: ResourceBorrow, - level: Level, - ) -> anyhow::Result<()> { - let i = Bytes::from(logger).as_ref().try_into()?; - let i = usize::from_le_bytes(i); - let loggers = self.loggers.read().unwrap(); - let logger = loggers.get(i).context("invalid resource handle")?; - *logger.level.write().unwrap() = level; - Ok(()) - } - } - - async fn serve_exports(wrpc: &impl wrpc_transport::Serve) { - use futures::stream::TryStreamExt as _; - - let invocations = serve(wrpc, MyComponent::default()).await.unwrap(); - let invocations = std::thread::spawn(|| invocations).join().unwrap(); - for (instance, name, st) in invocations { - tokio::spawn(async move { - eprintln!("serving {instance} {name}"); - st.try_collect::>().await.unwrap(); - }); - } - } -} - -#[allow(unused)] -mod example_4 { - wit_bindgen_wrpc::generate!({ - inline: r#" - package example:exported-resources; - - world import-some-resources { - export logging; - } - - interface logging { - enum level { - debug, - info, - warn, - error, - } - resource logger { - constructor(max-level: level); - - get-max-level: func() -> level; - set-max-level: func(level: level); - - log: func(level: level, msg: string); - } - } - "#, - }); -} - -#[allow(unused)] -mod async_test { - wit_bindgen_wrpc::generate!({ - inline: r#" - package wrpc-test:%async; - - world %async { - import handler; - export handler; - } - - interface handler { - use types.{request, response}; - - handle: func(request: request) -> result; - } - - interface types { - type fields = list>>>; - - record request { - body: stream, - trailers: future>, - path-with-query: option, - authority: option, - headers: fields, - } - - record response { - body: stream, - trailers: future>, - status: u16, - headers: fields, - } - } - "#, - }); -} - -#[allow(unused)] -mod top_level_example { - wit_bindgen_wrpc::generate!({ - inline: r" - package a:b; - - world the-world { - record fahrenheit { - degrees: f32, - } - - import what-temperature-is-it: func() -> fahrenheit; - - record celsius { - degrees: f32, - } - - import convert-to-celsius: func(a: fahrenheit) -> celsius; - } - ", - }); - - async fn test(wrpc: &impl wrpc_transport::Invoke) -> anyhow::Result<()> { - let current_temp = what_temperature_is_it(wrpc, ()).await?; - println!("current temp in fahrenheit is {}", current_temp.degrees); - let in_celsius: Celsius = convert_to_celsius(wrpc, (), ¤t_temp).await?; - println!("current temp in celsius is {}", in_celsius.degrees); - Ok(()) - } -} diff --git a/crates/wit-bindgen-test/Cargo.toml b/crates/wit-bindgen-test/Cargo.toml new file mode 100644 index 000000000..d54b3113c --- /dev/null +++ b/crates/wit-bindgen-test/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wit-bindgen-wrpc-test" +version = "0.1.0" +description = """ +Test runner for the wRPC bindings generators, exposed as `wit-bindgen-wrpc test`. +""" + +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +doctest = false + +[dependencies] +anyhow = { workspace = true, features = ["std"] } +clap = { workspace = true, features = ["derive", "std"] } +heck = { workspace = true } +rayon = { workspace = true } +regex = { workspace = true, features = ["std", "perf", "unicode"] } +serde = { workspace = true, features = ["derive", "std"] } +serde_json = { workspace = true, features = ["std"] } +toml = { workspace = true, features = ["parse"] } +wit-parser = { workspace = true } diff --git a/crates/wit-bindgen-test/src/config.rs b/crates/wit-bindgen-test/src/config.rs new file mode 100644 index 000000000..90661dc13 --- /dev/null +++ b/crates/wit-bindgen-test/src/config.rs @@ -0,0 +1,121 @@ +//! Configuration support for tests. +//! +//! This module contains the various structures and type definitions which are +//! used to configure both runtime tests and codegen tests. +//! +//! Test configuration happens by parsing TOML-in-comments at the start of +//! source files. Configuration is delimited by being at the top of a source +//! file and prefixed with a language's line-comment syntax followed by `@`. For +//! example in Rust that would look like: +//! +//! ```text +//! //@ some-key = 'some-value' +//! +//! include!(...); +//! +//! // ... rest of the test here +//! ``` +//! +//! Here `some-key = 'some-value'` is the TOML to parse into configuration. +//! There are two kinds of configuration here defined in this file: +//! +//! * `RuntimeTestConfig` - this is for runtime tests or `test.rs` and +//! `runner.rs` for example. This configures per-language and per-compilation +//! options. +//! +//! * `WitConfig` - this is per-`*.wit` file either as a codegen test or a +//! `test.wit` input for runtime tests. + +use anyhow::Context; +use anyhow::Result; +use serde::de::DeserializeOwned; +use serde::Deserialize; + +/// Configuration that can be placed at the top of runtime tests in source +/// language files. This is currently language-agnostic. +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct RuntimeTestConfig { + /// Extra command line arguments to pass to the language-specific bindings + /// generator. + /// + /// This is either a string which is whitespace delimited or it's an array + /// of strings. By default no extra arguments are passed. + #[serde(default)] + pub args: StringList, + // + // Maybe add something like this eventually if necessary? For example plumb + // arbitrary configuration from tests to the "compile" backend. This would + // then thread through as `Compile` and could be used to pass compiler flags + // for example. + // + // lang: HashMap, + + // ... + // + // or alternatively could also have something dedicated like: + // compile_flags: StringList, + // + // unclear! This should be expanded on over time as necessary. +} + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum StringList { + String(String), + List(Vec), +} + +impl From for Vec { + fn from(list: StringList) -> Vec { + match list { + StringList::String(s) => s.split_whitespace().map(|s| s.to_string()).collect(), + StringList::List(s) => s, + } + } +} + +impl Default for StringList { + fn default() -> StringList { + StringList::List(Vec::new()) + } +} + +/// Configuration found in `*.wit` file either in codegen tests or in `test.wit` +/// files for runtime tests. +#[derive(Clone, Default, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[expect( + dead_code, + reason = "fields are carried from upstream; wRPC drives expected failures by test name in should_fail_verify" +)] +pub struct WitConfig { + /// Indicates that this WIT test uses the component model async features + /// and/or proposal. + /// + /// This can be used to help expect failure in languages that do not yet + /// support this proposal. + #[serde(default, rename = "async")] + pub async_: bool, + + /// When set to `true` disables the passing of per-language default bindgen + /// arguments. For example with Rust it avoids passing `--generate-all` by + /// default to bindings generation. + pub default_bindgen_args: Option, +} + +/// Parses the configuration `T` from `contents` in comments at the start of the +/// file where comments are lines prefixed by `comment`. +pub fn parse_test_config(contents: &str, comment: &str) -> Result +where + T: DeserializeOwned, +{ + let config_lines: Vec<_> = contents + .lines() + .take_while(|l| l.starts_with(comment)) + .map(|l| &l[comment.len()..]) + .collect(); + let config_text = config_lines.join("\n"); + + toml::from_str(&config_text).context("failed to parse the test configuration") +} diff --git a/crates/wit-bindgen-test/src/go.rs b/crates/wit-bindgen-test/src/go.rs new file mode 100644 index 000000000..11e750799 --- /dev/null +++ b/crates/wit-bindgen-test/src/go.rs @@ -0,0 +1,73 @@ +use crate::{LanguageMethods, Runner, Verify}; +use anyhow::{Context, Result}; +use clap::Parser; +use std::env; +use std::process::Command; + +#[derive(Default, Debug, Clone, Parser)] +pub struct GoOpts { + /// Path to the in-tree `wrpc.io/go` module that generated Go bindings + /// depend on. Defaults to `go` relative to the current directory. + #[clap(long, value_name = "PATH")] + go_wrpc_path: Option, +} + +pub struct Go; + +impl LanguageMethods for Go { + fn display(&self) -> &str { + "go" + } + + fn comment_prefix_for_test_config(&self) -> Option<&str> { + Some("//@") + } + + fn default_bindgen_args(&self) -> &[&str] { + &["--generate-all", "--package", "bindings", "--gofmt=false"] + } + + fn should_fail_verify( + &self, + name: &str, + _config: &crate::config::WitConfig, + _args: &[String], + ) -> bool { + // The wRPC Go generator does not yet support bare `stream`/`future` + // types, `error-context`, or futures held by resources. + matches!( + name, + "streams.wit" | "futures.wit" | "error-context.wit" | "resources-with-futures.wit" + ) + } + + fn prepare(&self, _runner: &mut Runner<'_>) -> Result<()> { + // Go modules are resolved at build time; nothing to prepare. + Ok(()) + } + + fn verify(&self, runner: &Runner<'_>, verify: &Verify<'_>) -> Result<()> { + let cwd = env::current_dir()?; + let go_module = match &runner.opts.go.go_wrpc_path { + Some(path) => cwd.join(path), + None => cwd.join("go"), + }; + + crate::write_if_different(&verify.bindings_dir.join("go.work"), "go 1.22.2\nuse .\n")?; + crate::write_if_different( + &verify.bindings_dir.join("go.mod"), + format!( + "module bindings\n\ngo 1.22.2\n\nrequire wrpc.io/go v0.0.0-unpublished\n\nreplace wrpc.io/go v0.0.0-unpublished => {}\n", + go_module.display(), + ), + )?; + + runner + .run_command( + Command::new("go") + .args(["build", "./..."]) + .current_dir(verify.bindings_dir), + ) + .context("failed to compile generated Go bindings") + } +} diff --git a/crates/wit-bindgen-test/src/lib.rs b/crates/wit-bindgen-test/src/lib.rs new file mode 100644 index 000000000..6fd2bddc4 --- /dev/null +++ b/crates/wit-bindgen-test/src/lib.rs @@ -0,0 +1,962 @@ +// This crate is vendored from upstream `wit-bindgen`'s `crates/test` (with the +// guest-Wasm-specific pieces removed); these lints fire on the upstream code, +// which is kept verbatim to minimize the diff. +#![allow( + clippy::needless_borrow, + clippy::needless_borrows_for_generic_args, + clippy::println_empty_string, + clippy::flat_map_identity, + clippy::nonminimal_bool +)] + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Parser; +use rayon::prelude::*; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::Arc; + +mod config; +mod go; +mod rust; + +/// Tool to run tests that exercise the `wit-bindgen` bindings generator. +/// +/// This tool is used to (a) generate bindings for a target language, (b) +/// compile the bindings and source code to a wasm component, (c) compose a +/// "runner" and a "test" component together, and (d) execute this component to +/// ensure that it passes. This process is guided by filesystem structure which +/// must adhere to some conventions. +/// +/// * Tests are located in any directory that contains a `test.wit` description +/// of the WIT being tested. The `` argument to this command is walked +/// recursively to find `test.wit` files. +/// +/// * The `test.wit` file must have a `runner` world and a `test` world. The +/// "runner" should import interfaces that are exported by "test". +/// +/// * Adjacent to `test.wit` should be a number of `runner*.*` files. There is +/// one runner per source language, for example `runner.rs` and `runner.c`. +/// These are source files for the `runner` world. Source files can start with +/// `//@ ...` comments to deserialize into `config::RuntimeTestConfig`, +/// currently that supports: +/// +/// ```text +/// //@ args = ['--arguments', 'to', '--the', 'bindings', '--generator'] +/// ``` +/// +/// or +/// +/// ```text +/// //@ args = '--arguments to --the bindings --generator' +/// ``` +/// +/// * Adjacent to `test.wit` should also be a number of `test*.*` files. Like +/// runners there is one per source language. Note that you can have multiple +/// implementations of tests in the same language too, for example +/// `test-foo.rs` and `test-bar.rs`. All tests must export the same `test` +/// world from `test.wit`, however. +/// +/// This tool will discover `test.wit` files, discover runners/tests, and then +/// compile everything and run the combinatorial matrix of runners against +/// tests. It's expected that each `runner.*` and `test.*` perform the same +/// functionality and only differ in source language. +#[derive(Default, Debug, Clone, Parser)] +pub struct Opts { + /// Directory containing the test being run or all tests being run. + test: Vec, + + /// Path to where binary artifacts for tests are stored. + #[clap(long, value_name = "PATH")] + artifacts: PathBuf, + + /// Optional filter to use on test names to only run some tests. + /// + /// This is a regular expression defined by the `regex` Rust crate. + #[clap(short, long, value_name = "REGEX")] + filter: Option, + + #[clap(flatten)] + rust: rust::RustOpts, + + #[clap(flatten)] + go: go::GoOpts, + + /// Whether or not the calling process's stderr is inherited into child + /// processes. + /// + /// This helps preserving color in compiler error messages but can also + /// jumble up output if there are multiple errors. + #[clap(short, long)] + inherit_stderr: bool, + + /// Configuration of which languages are tested. + /// + /// Passing `--lang rust` will only test Rust for example. Passing + /// `--lang=-rust` will test everything except Rust. + #[clap(short, long)] + languages: Vec, +} + +impl Opts { + pub fn run(&self, wit_bindgen: &Path) -> Result<()> { + Runner { + opts: self, + rust_state: None, + wit_bindgen, + } + .run() + } +} + +/// Helper structure representing a discovered `test.wit` file. +struct Test { + /// The name of this test, unique amongst all tests. + /// + /// Inferred from the directory name. + name: String, + + kind: TestKind, +} + +enum TestKind { + Runtime(Vec), + Codegen(PathBuf), +} + +/// Helper structure representing a single component found in a test directory. +struct Component { + /// The name of this component, inferred from the file stem. + /// + /// May be shared across different languages. + name: String, + + /// The path to the source file for this component. + path: PathBuf, + + /// Whether or not this component is a "runner" or a "test" + kind: Kind, + + /// The detected language for this component. + language: Language, + + /// The WIT world that's being used with this component, loaded from + /// `test.wit`. + bindgen: Bindgen, +} + +#[derive(Clone)] +struct Bindgen { + /// The arguments to the bindings generator that this component will be + /// using. + args: Vec, + /// The path to the `*.wit` file or files that are having bindings + /// generated. + wit_path: PathBuf, + /// The name of the world within `wit_path` that's having bindings generated + /// for it. + world: String, + /// Configuration found in `wit_path` + wit_config: config::WitConfig, +} + +#[derive(PartialEq)] +enum Kind { + Runner, + Test, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Language { + Rust, + Go, +} + +/// Helper structure to package up arguments when sent to language-specific +/// compilation backends for `LanguageMethods::verify` +struct Verify<'a> { + // `wit_test` and `args` are carried from upstream but only consulted by the + // guest-Wasm/no_std backends that wRPC does not vendor. + #[expect(dead_code)] + wit_test: &'a Path, + bindings_dir: &'a Path, + artifacts_dir: &'a Path, + #[expect(dead_code)] + args: &'a [String], + world: &'a str, +} + +/// Helper structure to package up runtime state associated with executing tests. +struct Runner<'a> { + opts: &'a Opts, + rust_state: Option, + wit_bindgen: &'a Path, +} + +impl Runner<'_> { + /// Executes all tests. + fn run(&mut self) -> Result<()> { + // First step, discover all tests in the specified test directory. + let mut tests = HashMap::new(); + for test in self.opts.test.iter() { + self.discover_tests(&mut tests, test) + .with_context(|| format!("failed to discover tests in {test:?}"))?; + } + if tests.is_empty() { + bail!( + "no `test.wit` files found were found in {:?}", + self.opts.test, + ); + } + + self.prepare_languages(&tests)?; + self.run_codegen_tests(&tests)?; + self.run_runtime_tests(&tests)?; + + println!("PASSED"); + + Ok(()) + } + + /// Walks over `dir`, recursively, inserting located cases into `tests`. + fn discover_tests(&self, tests: &mut HashMap, path: &Path) -> Result<()> { + if path.is_file() { + if path.extension().and_then(|s| s.to_str()) == Some("wit") { + return self.insert_test(&path, TestKind::Codegen(path.to_owned()), tests); + } + + return Ok(()); + } + + let runtime_candidate = path.join("test.wit"); + if runtime_candidate.is_file() { + let components = self + .load_test(&runtime_candidate, path) + .with_context(|| format!("failed to load test in {path:?}"))?; + return self.insert_test(path, TestKind::Runtime(components), tests); + } + + let codegen_candidate = path.join("wit"); + if codegen_candidate.is_dir() { + return self.insert_test(path, TestKind::Codegen(codegen_candidate), tests); + } + + for entry in path.read_dir().context("failed to read test directory")? { + let entry = entry.context("failed to read test directory entry")?; + let path = entry.path(); + + self.discover_tests(tests, &path)?; + } + + Ok(()) + } + + fn insert_test( + &self, + path: &Path, + kind: TestKind, + tests: &mut HashMap, + ) -> Result<()> { + let test_name = path + .file_name() + .and_then(|s| s.to_str()) + .context("non-utf-8 filename")?; + let prev = tests.insert( + test_name.to_string(), + Test { + name: test_name.to_string(), + kind, + }, + ); + if prev.is_some() { + bail!("duplicate test name `{test_name}` found"); + } + Ok(()) + } + + /// Loads a test from `dir` using the `wit` file in the directory specified. + /// + /// Returns a list of components that were found within this directory. + fn load_test(&self, wit: &Path, dir: &Path) -> Result> { + let mut resolve = wit_parser::Resolve::default(); + let pkg = resolve + .push_file(&wit) + .context("failed to load `test.wit` in test directory")?; + let resolve = Arc::new(resolve); + resolve + .select_world(pkg, Some("runner")) + .context("failed to find expected `runner` world to generate bindings")?; + resolve + .select_world(pkg, Some("test")) + .context("failed to find expected `test` world to generate bindings")?; + + let wit_contents = std::fs::read_to_string(wit)?; + let wit_config: config::WitConfig = config::parse_test_config(&wit_contents, "//@") + .context("failed to parse WIT test config")?; + + let mut components = Vec::new(); + let mut any_runner = false; + let mut any_test = false; + + for entry in dir.read_dir().context("failed to read test directory")? { + let entry = entry.context("failed to read test directory entry")?; + let path = entry.path(); + + let Some(name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + let kind = if name.starts_with("runner") { + any_runner = true; + Kind::Runner + } else if name != "test.wit" && name.starts_with("test") { + any_test = true; + Kind::Test + } else { + continue; + }; + + let bindgen = Bindgen { + args: Vec::new(), + wit_config: wit_config.clone(), + world: kind.to_string(), + wit_path: wit.to_path_buf(), + }; + + let component = self + .parse_component(&path, kind, bindgen) + .with_context(|| format!("failed to parse component source file {path:?}"))?; + components.push(component); + } + + if !any_runner { + bail!("no `runner*` test files found in test directory"); + } + if !any_test { + bail!("no `test*` test files found in test directory"); + } + + Ok(components) + } + + /// Parsers the component located at `path` and creates all information + /// necessary for a `Component` return value. + fn parse_component(&self, path: &Path, kind: Kind, mut bindgen: Bindgen) -> Result { + let extension = path + .extension() + .and_then(|s| s.to_str()) + .context("non-utf-8 path extension")?; + + let language = match extension { + "rs" => Language::Rust, + "go" => Language::Go, + other => bail!("unsupported test source file extension `{other}`"), + }; + + let contents = fs::read_to_string(&path)?; + let config = match language.obj().comment_prefix_for_test_config() { + Some(comment) => { + config::parse_test_config::(&contents, comment)? + } + None => Default::default(), + }; + assert!(bindgen.args.is_empty()); + bindgen.args = config.args.into(); + + Ok(Component { + name: path.file_stem().unwrap().to_str().unwrap().to_string(), + path: path.to_path_buf(), + language, + bindgen, + kind, + }) + } + + /// Prepares all languages in use in `test` as part of a one-time + /// initialization step. + fn prepare_languages(&mut self, tests: &HashMap) -> Result<()> { + let all_languages = self.all_languages(); + + let mut prepared = HashSet::new(); + let mut prepare = |lang: &Language| -> Result<()> { + if !self.include_language(lang) || !prepared.insert(lang.clone()) { + return Ok(()); + } + lang.obj() + .prepare(self) + .with_context(|| format!("failed to prepare language {lang}")) + }; + + for test in tests.values() { + match &test.kind { + TestKind::Runtime(c) => { + for component in c { + prepare(&component.language)? + } + } + TestKind::Codegen(_) => { + for lang in all_languages.iter() { + prepare(lang)?; + } + } + } + } + + Ok(()) + } + + fn all_languages(&self) -> Vec { + Language::ALL.to_vec() + } + + /// Executes all tests that are `TestKind::Codegen`. + fn run_codegen_tests(&mut self, tests: &HashMap) -> Result<()> { + let mut codegen_tests = Vec::new(); + let languages = self.all_languages(); + for (name, test) in tests.iter().filter_map(|(name, t)| match &t.kind { + TestKind::Runtime(_) => None, + TestKind::Codegen(p) => Some((name, p)), + }) { + let config = match fs::read_to_string(test) { + Ok(wit) => config::parse_test_config::(&wit, "//@") + .with_context(|| format!("failed to parse test config from {test:?}"))?, + Err(_) => Default::default(), + }; + for language in languages.iter() { + // If the CLI arguments filter out this language, then discard + // the test case. + if !self.include_language(&language) { + continue; + } + + codegen_tests.push(( + language.clone(), + test, + name.to_string(), + Vec::new(), + config.clone(), + )); + + for (args_kind, args) in language.obj().codegen_test_variants() { + codegen_tests.push(( + language.clone(), + test, + format!("{name}-{args_kind}"), + args.iter().map(|s| s.to_string()).collect::>(), + config.clone(), + )); + } + } + } + + if codegen_tests.is_empty() { + return Ok(()); + } + + println!("Running {} codegen tests:", codegen_tests.len()); + + let results = codegen_tests + .par_iter() + .map(|(language, test, args_kind, args, config)| { + let should_fail = language.obj().should_fail_verify(args_kind, config, args); + let result = self + .codegen_test(language, test, &args_kind, args, config) + .with_context(|| { + format!("failed to codegen test for `{language}` over {test:?}") + }); + self.update_status(&result, should_fail); + (result, should_fail, language, test, args_kind) + }) + .collect::>(); + + println!(""); + + self.render_errors(results.into_iter().map( + |(result, should_fail, language, test, args_kind)| { + StepResult::new(test.to_str().unwrap(), result) + .should_fail(should_fail) + .metadata("language", language) + .metadata("variant", args_kind) + }, + )); + + Ok(()) + } + + /// Runs a single codegen test. + /// + /// This will generate bindings for `test` in the `language` specified. The + /// test name is mangled by `args_kind` and the `args` are arguments to pass + /// to the bindings generator. + fn codegen_test( + &self, + language: &Language, + test: &Path, + args_kind: &str, + args: &[String], + config: &config::WitConfig, + ) -> Result<()> { + let mut resolve = wit_parser::Resolve::default(); + let (pkg, _) = resolve.push_path(test).context("failed to load WIT")?; + let world = resolve + .select_world(pkg, None) + .or_else(|err| resolve.select_world(pkg, Some("imports")).map_err(|_| err)) + .context("failed to select a world for bindings generation")?; + let world = resolve.worlds[world].name.clone(); + + let artifacts_dir = std::env::current_dir()? + .join(&self.opts.artifacts) + .join("codegen") + .join(language.to_string()) + .join(args_kind); + let bindings_dir = artifacts_dir.join("bindings"); + let bindgen = Bindgen { + args: args.to_vec(), + wit_path: test.to_path_buf(), + world: world.clone(), + wit_config: config.clone(), + }; + language + .obj() + .generate_bindings(self, &bindgen, &bindings_dir) + .context("failed to generate bindings")?; + + language + .obj() + .verify( + self, + &Verify { + world: &world, + artifacts_dir: &artifacts_dir, + bindings_dir: &bindings_dir, + wit_test: test, + args: &bindgen.args, + }, + ) + .context("failed to verify generated bindings")?; + + Ok(()) + } + + /// Execute all `TestKind::Runtime` tests + fn run_runtime_tests(&mut self, tests: &HashMap) -> Result<()> { + let components = tests + .values() + .filter(|t| match &self.opts.filter { + Some(filter) => filter.is_match(&t.name), + None => true, + }) + .filter_map(|t| match &t.kind { + TestKind::Runtime(c) => Some(c.iter().map(move |c| (t, c))), + TestKind::Codegen(_) => None, + }) + .flat_map(|i| i) + // Discard components that are unrelated to the languages being + // tested. + .filter(|(_test, component)| self.include_language(&component.language)) + .collect::>(); + + println!("Compiling {} components:", components.len()); + + // In parallel compile all sources to their binary component + // form. + let compile_results = components + .par_iter() + .map(|(test, component)| { + let path = self + .compile_component(test, component) + .with_context(|| format!("failed to compile component {:?}", component.path)); + self.update_status(&path, false); + (test, component, path) + }) + .collect::>(); + println!(""); + + let mut compilations = Vec::new(); + self.render_errors( + compile_results + .into_iter() + .map(|(test, component, result)| match result { + Ok(path) => { + compilations.push((test, component, path)); + StepResult::new("", Ok(())) + } + Err(e) => StepResult::new(&test.name, Err(e)) + .metadata("component", &component.name) + .metadata("path", component.path.display()), + }), + ); + + // Next, massage the data a bit. Create a map of all tests to where + // their components are located. Then perform a product of runners/tests + // to generate a list of test cases. Finally actually execute the testj + // cases. + let mut compiled_components = HashMap::new(); + for (test, component, path) in compilations { + let list = compiled_components.entry(&test.name).or_insert(Vec::new()); + list.push((component, path)); + } + + let mut to_run = Vec::new(); + for (test, components) in compiled_components.iter() { + for a in components.iter().filter(|(c, _)| c.kind == Kind::Runner) { + // Unlike upstream's component composition, wRPC links the runner + // and test into one host binary, so they must share a language. + for b in components + .iter() + .filter(|(c, _)| c.kind == Kind::Test && c.language == a.0.language) + { + to_run.push((test, a, b)); + } + } + } + + println!("Running {} runtime tests:", to_run.len()); + + let results = to_run + .par_iter() + .map(|(case_name, (runner, runner_path), (test, test_path))| { + let case = &tests[case_name.as_str()]; + let result = self + .runtime_test(case, runner, runner_path, test, test_path) + .with_context(|| { + format!( + "failed to run `{}` with runner `{}` and test `{}`", + case.name, runner.language, test.language, + ) + }); + self.update_status(&result, false); + (result, case_name, runner, runner_path, test, test_path) + }) + .collect::>(); + + println!(""); + + self.render_errors(results.into_iter().map( + |(result, case_name, runner, runner_path, test, test_path)| { + StepResult::new(case_name, result) + .metadata("runner", runner.path.display()) + .metadata("test", test.path.display()) + .metadata("compiled runner", runner_path.display()) + .metadata("compiled test", test_path.display()) + }, + )); + + Ok(()) + } + + /// Generates bindings for the `component` specified in the `test` given. + /// + /// Unlike upstream `wit-bindgen` this does not compile a standalone guest + /// component — wRPC bindings are `Invoke`/`Serve` stubs that are linked into + /// a host binary per `runner`/`test` pair in `runtime_test`. Returns the + /// directory the bindings were generated into. + fn compile_component(&self, test: &Test, component: &Component) -> Result { + let root_dir = std::env::current_dir()? + .join(&self.opts.artifacts) + .join(&test.name); + let artifacts_dir = root_dir.join(format!("{}-{}", component.name, component.language)); + let bindings_dir = artifacts_dir.join("bindings"); + component + .language + .obj() + .generate_bindings(self, &component.bindgen, &bindings_dir)?; + Ok(bindings_dir) + } + + /// Executes a single test case. + /// + /// Unlike upstream — which composes a `runner` and `test` guest component + /// and runs them in a component runtime — wRPC links the `runner` (a wRPC + /// client) and `test` (a wRPC server) into a single host binary connected + /// over an in-process TCP transport, and runs it to completion. + fn runtime_test( + &self, + case: &Test, + runner: &Component, + runner_bindings: &Path, + test: &Component, + test_bindings: &Path, + ) -> Result<()> { + match runner.language { + Language::Rust => { + self.rust_runtime_test(case, runner, runner_bindings, test, test_bindings) + } + Language::Go => bail!("Go runtime tests are not yet supported"), + } + } + + /// Helper to execute an external process and generate a helpful error + /// message on failure. + fn run_command(&self, cmd: &mut Command) -> Result<()> { + if self.opts.inherit_stderr { + cmd.stderr(Stdio::inherit()); + } + let output = cmd + .output() + .with_context(|| format!("failed to spawn {cmd:?}"))?; + if output.status.success() { + return Ok(()); + } + + let mut error = format!( + "\ +command execution failed +command: {cmd:?} +status: {}", + output.status, + ); + + if !output.stdout.is_empty() { + error.push_str(&format!( + "\nstdout:\n {}", + String::from_utf8_lossy(&output.stdout).replace("\n", "\n ") + )); + } + if !output.stderr.is_empty() { + error.push_str(&format!( + "\nstderr:\n {}", + String::from_utf8_lossy(&output.stderr).replace("\n", "\n ") + )); + } + + bail!("{error}") + } + + /// "poor man's test output progress" + fn update_status(&self, result: &Result, should_fail: bool) { + if result.is_ok() == !should_fail { + print!("."); + } else { + print!("F"); + } + let _ = std::io::stdout().flush(); + } + + /// Returns whether `languages` is included in this testing session. + fn include_language(&self, language: &Language) -> bool { + let lang = language.obj().display(); + let mut any_positive = false; + let mut any_negative = false; + for opt in self.opts.languages.iter() { + for name in opt.split(',') { + if let Some(suffix) = name.strip_prefix('-') { + any_negative = true; + // If explicitly asked to not include this, don't include + // it. + if suffix == lang { + return false; + } + } else { + any_positive = true; + // If explicitly asked to include this, then include it. + if name == lang { + return true; + } + } + } + } + + // By default include all languages. + if self.opts.languages.is_empty() { + return true; + } + + // If any language was explicitly included then assume any non-mentioned + // language should be omitted. + if any_positive { + return false; + } + + // And if there are only negative mentions (e.g. `-foo`) then assume + // everything else is allowed. + assert!(any_negative); + true + } + + fn render_errors<'a>(&self, results: impl Iterator>) { + let mut failures = 0; + for result in results { + let err = match (result.result, result.should_fail) { + (Ok(()), false) | (Err(_), true) => continue, + (Err(e), false) => e, + (Ok(()), true) => anyhow!("test should have failed, but passed"), + }; + failures += 1; + + println!("------ Failure: {} --------", result.name); + for (k, v) in result.metadata { + println!(" {k}: {v}"); + } + println!(" error: {}", format!("{err:?}").replace("\n", "\n ")); + } + + if failures > 0 { + println!("{failures} tests FAILED"); + std::process::exit(1); + } + } +} + +struct StepResult<'a> { + result: Result<()>, + should_fail: bool, + name: &'a str, + metadata: Vec<(&'a str, String)>, +} + +impl<'a> StepResult<'a> { + fn new(name: &'a str, result: Result<()>) -> StepResult<'a> { + StepResult { + name, + result, + should_fail: false, + metadata: Vec::new(), + } + } + + fn should_fail(mut self, fail: bool) -> Self { + self.should_fail = fail; + self + } + + fn metadata(mut self, name: &'a str, value: impl fmt::Display) -> Self { + self.metadata.push((name, value.to_string())); + self + } +} + +/// Helper trait for each language to implement which encapsulates +/// language-specific logic. +trait LanguageMethods { + /// Display name for this language, used in filenames. + fn display(&self) -> &str; + + /// Returns the prefix that this language uses to annotate configuration in + /// the top of source files. + /// + /// This should be the language's line-comment syntax followed by `@`, e.g. + /// `//@` for Rust or `;;@` for WebAssembly Text. + fn comment_prefix_for_test_config(&self) -> Option<&str>; + + /// Returns the extra permutations, if any, of arguments to use with codegen + /// tests. + /// + /// This is used to run all codegen tests with a variety of bindings + /// generator options. The first element in the tuple is a descriptive + /// string that should be unique (used in file names) and the second elemtn + /// is the list of arguments for that variant to pass to the bindings + /// generator. + fn codegen_test_variants(&self) -> &[(&str, &[&str])] { + &[] + } + + /// Performs any one-time preparation necessary for this language, such as + /// downloading or caching dependencies. + fn prepare(&self, runner: &mut Runner<'_>) -> Result<()>; + + /// Generates bindings for `component` into `dir`. + /// + /// Runs `wit-bindgen` in aa subprocess to catch failures such as panics. + fn generate_bindings(&self, runner: &Runner<'_>, bindgen: &Bindgen, dir: &Path) -> Result<()> { + let name = match self.bindgen_name() { + Some(name) => name, + None => return Ok(()), + }; + let mut cmd = Command::new(runner.wit_bindgen); + cmd.arg(name) + .arg(&bindgen.wit_path) + .arg("--world") + .arg(format!("%{}", bindgen.world)) + .arg("--out-dir") + .arg(dir); + + match bindgen.wit_config.default_bindgen_args { + Some(true) | None => { + for arg in self.default_bindgen_args() { + cmd.arg(arg); + } + } + Some(false) => {} + } + + for arg in bindgen.args.iter() { + cmd.arg(arg); + } + + runner.run_command(&mut cmd) + } + + /// Returns the default set of arguments that will be passed to + /// `wit-bindgen`. + /// + /// Defaults to empty, but each language can override it. + fn default_bindgen_args(&self) -> &[&str] { + &[] + } + + /// Returns the name of this bindings generator when passed to + /// `wit-bindgen`. + /// + /// By default this is `Some(self.display())`, but it can be overridden if + /// necessary. Returning `None` here means that no bindings generator is + /// supported. + fn bindgen_name(&self) -> Option<&str> { + Some(self.display()) + } + + /// Returns whether this language is supposed to fail this codegen tests + /// given the `config` and `args` for the test. + fn should_fail_verify(&self, name: &str, config: &config::WitConfig, args: &[String]) -> bool; + + /// Performs a "check" or a verify that the generated bindings described by + /// `Verify` are indeed valid. + fn verify(&self, runner: &Runner<'_>, verify: &Verify) -> Result<()>; +} + +impl Language { + const ALL: &[Language] = &[Language::Rust, Language::Go]; + + fn obj(&self) -> &dyn LanguageMethods { + match self { + Language::Rust => &rust::Rust, + Language::Go => &go::Go, + } + } +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.obj().display().fmt(f) + } +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Kind::Runner => "runner".fmt(f), + Kind::Test => "test".fmt(f), + } + } +} + +/// Returns `true` if the file was written, or `false` if the file is the same +/// as it was already on disk. +fn write_if_different(path: &Path, contents: impl AsRef<[u8]>) -> Result { + let contents = contents.as_ref(); + if let Ok(prev) = fs::read(path) { + if prev == contents { + return Ok(false); + } + } + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create directory {parent:?}"))?; + } + fs::write(path, contents).with_context(|| format!("failed to write {path:?}"))?; + Ok(true) +} diff --git a/crates/wit-bindgen-test/src/rust.rs b/crates/wit-bindgen-test/src/rust.rs new file mode 100644 index 000000000..7018e1abe --- /dev/null +++ b/crates/wit-bindgen-test/src/rust.rs @@ -0,0 +1,356 @@ +use crate::{Component, LanguageMethods, Runner, Test, Verify}; +use anyhow::{bail, Context, Result}; +use clap::Parser; +use heck::ToSnakeCase; +use std::collections::HashMap; +use std::env; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Default, Debug, Clone, Parser)] +pub struct RustOpts { + /// A custom `path` dependency to use for `wit-bindgen-wrpc`. + #[clap(long, conflicts_with = "rust_wit_bindgen_version", value_name = "PATH")] + rust_wit_bindgen_path: Option, + + /// A custom version to use for the `wit-bindgen-wrpc` dependency. + #[clap(long, conflicts_with = "rust_wit_bindgen_path", value_name = "X.Y.Z")] + rust_wit_bindgen_version: Option, + + /// A custom `path` dependency to use for `wrpc-transport`. + #[clap( + long, + conflicts_with = "rust_wrpc_transport_version", + value_name = "PATH" + )] + rust_wrpc_transport_path: Option, + + /// A custom version to use for the `wrpc-transport` dependency. + #[clap( + long, + conflicts_with = "rust_wrpc_transport_path", + value_name = "X.Y.Z" + )] + rust_wrpc_transport_version: Option, +} + +pub struct Rust; + +#[derive(Default)] +pub struct State { + /// Directory containing all compiled dependency rlibs (the `deps` + /// subdirectory of the helper crate's target directory). + deps_dir: PathBuf, + /// Map from crate name to its compiled `.rlib`, captured from `cargo build + /// --message-format=json`, used for exact `--extern` paths. + rlibs: HashMap, +} + +impl LanguageMethods for Rust { + fn display(&self) -> &str { + "rust" + } + + fn comment_prefix_for_test_config(&self) -> Option<&str> { + Some("//@") + } + + fn should_fail_verify( + &self, + name: &str, + _config: &crate::config::WitConfig, + _args: &[String], + ) -> bool { + // The wRPC Rust generator does not yet support bare `stream`/`future` + // types. Other async constructs (`error-context`, futures/streams behind + // resources) generate successfully. + matches!(name, "streams.wit" | "futures.wit") + } + + fn default_bindgen_args(&self) -> &[&str] { + &["--generate-all"] + } + + fn prepare(&self, runner: &mut Runner<'_>) -> Result<()> { + let cwd = env::current_dir()?; + let opts = &runner.opts.rust; + + let wit_bindgen_dep = match &opts.rust_wit_bindgen_path { + Some(path) => format!("path = {:?}", cwd.join(path)), + None => { + let version = opts + .rust_wit_bindgen_version + .as_deref() + .unwrap_or(env!("CARGO_PKG_VERSION")); + format!("version = \"{version}\"") + } + }; + let transport_dep = match &opts.rust_wrpc_transport_path { + Some(path) => format!("path = {:?}", cwd.join(path)), + None => { + let version = opts + .rust_wrpc_transport_version + .as_deref() + .unwrap_or(env!("CARGO_PKG_VERSION")); + format!("version = \"{version}\"") + } + }; + + let dir = cwd.join(&runner.opts.artifacts).join("rust"); + let helper = dir.join("deps-crate"); + + super::write_if_different( + &helper.join("Cargo.toml"), + &format!( + r#" +[package] +name = "wrpc-test-rust-deps" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[lib] +path = "lib.rs" + +[dependencies] +wit-bindgen-wrpc = {{ {wit_bindgen_dep} }} +wrpc-transport = {{ {transport_dep}, features = ["net"] }} +tokio = {{ version = "1", features = ["macros", "rt-multi-thread", "net", "sync", "io-util", "time"] }} +futures = "0.3" +anyhow = "1" +serde = {{ version = "1", features = ["derive"] }} +serde_json = "1" +"#, + ), + )?; + super::write_if_different(&helper.join("lib.rs"), "")?; + + println!("Building wRPC test dependencies..."); + let output = Command::new("cargo") + .current_dir(&helper) + .arg("build") + .arg("--message-format=json") + .output() + .context("failed to spawn `cargo build`")?; + if !output.status.success() { + bail!( + "failed to build wRPC test dependencies:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + } + + // Capture the `.rlib` path for each compiled crate so that runtime-test + // drivers and codegen verification can pass exact `--extern` paths. + let mut rlibs = HashMap::new(); + for line in String::from_utf8_lossy(&output.stdout).lines() { + let Ok(value) = serde_json::from_str::(line) else { + continue; + }; + if value["reason"] != "compiler-artifact" { + continue; + } + let Some(name) = value["target"]["name"].as_str() else { + continue; + }; + if let Some(files) = value["filenames"].as_array() { + for file in files.iter().filter_map(|f| f.as_str()) { + if file.ends_with(".rlib") { + rlibs.insert(name.to_string(), PathBuf::from(file)); + } + } + } + } + + let deps_dir = helper.join("target/debug/deps"); + anyhow::ensure!( + deps_dir.is_dir(), + "dependency directory {deps_dir:?} was not produced" + ); + + runner.rust_state = Some(State { deps_dir, rlibs }); + Ok(()) + } + + fn verify(&self, runner: &Runner<'_>, verify: &Verify<'_>) -> Result<()> { + let bindings = verify + .bindings_dir + .join(format!("{}.rs", verify.world.to_snake_case())); + let test_edition = |edition: Edition| -> Result<()> { + let mut cmd = runner.rustc(edition); + cmd.arg(&bindings); + runner.extern_arg(&mut cmd, "wit_bindgen_wrpc")?; + cmd.arg("--crate-type=rlib") + .arg("-o") + .arg(verify.artifacts_dir.join("tmp")); + runner.run_command(&mut cmd)?; + Ok(()) + }; + + test_edition(Edition::E2021)?; + test_edition(Edition::E2024)?; + Ok(()) + } +} + +enum Edition { + E2021, + E2024, +} + +/// The fixed driver linking a `runner` (client) and `test` (server) into one +/// binary, connected over an in-process TCP transport. +const DRIVER: &str = r##"#![allow(warnings)] + +pub mod test { + include!(env!("WRPC_TEST_BINDINGS")); + include!(env!("WRPC_TEST_SRC")); +} + +pub mod runner { + include!(env!("WRPC_RUNNER_BINDINGS")); + include!(env!("WRPC_RUNNER_SRC")); +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + use core::net::Ipv6Addr; + use core::time::Duration; + use std::sync::Arc; + + use futures::{stream, StreamExt as _}; + use wrpc_transport::frame::{tcp, Server}; + + let lis = tokio::net::TcpListener::bind((Ipv6Addr::LOCALHOST, 0)) + .await + .expect("failed to start TCP listener"); + let addr = lis.local_addr().expect("failed to get server address"); + + let srv = Arc::new(Server::default()); + + // Register the `test` world's exported handlers, then serve them forever in + // the background. + let invocations = test::serve(srv.as_ref(), test::Component) + .await + .expect("failed to serve `test` world exports"); + tokio::spawn(async move { + let mut invocations = stream::select_all(invocations.into_iter().map( + |(instance, name, invocations)| invocations.map(move |res| (instance, name, res)), + )); + while let Some((instance, name, invocation)) = invocations.next().await { + invocation + .unwrap_or_else(|err| panic!("failed to accept `{instance}#{name}` invocation: {err:?}")) + .await + .expect("failed to serve invocation"); + } + }); + + // Accept connections forever in the background. Each client invocation opens + // a new connection. + { + let srv = Arc::clone(&srv); + tokio::spawn(async move { + loop { + let (stream, _) = lis.accept().await.expect("failed to accept connection"); + let (rx, tx) = stream.into_split(); + let srv = Arc::clone(&srv); + tokio::spawn(async move { + srv.accept((), tx, rx) + .await + .expect("failed to accept connection"); + }); + } + }); + } + + // Run the `runner` world (client) to completion. + let clt = tcp::Client::from(addr); + tokio::time::timeout(Duration::from_secs(60), runner::run(&clt)) + .await + .expect("runtime test timed out")?; + Ok(()) +} +"##; + +impl Runner<'_> { + fn rustc(&self, edition: Edition) -> Command { + let state = self.rust_state.as_ref().unwrap(); + let mut cmd = Command::new("rustc"); + cmd.arg(match edition { + Edition::E2021 => "--edition=2021", + Edition::E2024 => "--edition=2024", + }) + .arg("-L") + .arg(format!("dependency={}", state.deps_dir.display())); + cmd + } + + /// Adds `--extern =` to `cmd` using the exact rlib path + /// captured during `prepare`. + fn extern_arg(&self, cmd: &mut Command, krate: &str) -> Result<()> { + let state = self.rust_state.as_ref().unwrap(); + let rlib = state + .rlibs + .get(krate) + .with_context(|| format!("no compiled rlib found for crate `{krate}`"))?; + cmd.arg("--extern") + .arg(format!("{krate}={}", rlib.display())); + Ok(()) + } + + /// Builds and runs a single Rust runtime test by linking the `runner` and + /// `test` into one binary connected over an in-process TCP transport. + pub(crate) fn rust_runtime_test( + &self, + case: &Test, + runner: &Component, + runner_bindings: &std::path::Path, + tst: &Component, + test_bindings: &std::path::Path, + ) -> Result<()> { + let artifacts_dir = env::current_dir()? + .join(&self.opts.artifacts) + .join(&case.name) + .join(format!("{}-{}", runner.name, tst.name)); + + let runner_rs = + runner_bindings.join(format!("{}.rs", runner.bindgen.world.to_snake_case())); + let test_rs = test_bindings.join(format!("{}.rs", tst.bindgen.world.to_snake_case())); + + let driver = artifacts_dir.join("driver.rs"); + super::write_if_different(&driver, DRIVER)?; + + let bin = artifacts_dir.join("driver"); + let mut cmd = self.rustc(Edition::E2021); + cmd.arg(&driver) + .arg("--crate-type=bin") + .arg("-Dwarnings") + .arg("-o") + .arg(&bin) + .env("WRPC_TEST_BINDINGS", &test_rs) + .env("WRPC_RUNNER_BINDINGS", &runner_rs) + .env("WRPC_TEST_SRC", tst.path.canonicalize()?) + .env("WRPC_RUNNER_SRC", runner.path.canonicalize()?) + // Set so a `wit_bindgen_wrpc::generate!` embedded in a runner/test + // source (e.g. the `with` tests' `mod other`) doesn't panic looking + // for the manifest dir. + .env("CARGO_MANIFEST_DIR", &artifacts_dir); + for extern_ in [ + "wit_bindgen_wrpc", + "wrpc_transport", + "tokio", + "futures", + "anyhow", + "serde", + "serde_json", + ] { + self.extern_arg(&mut cmd, extern_)?; + } + self.run_command(&mut cmd) + .context("failed to compile runtime test driver")?; + + self.run_command(&mut Command::new(&bin)) + .context("runtime test driver failed") + } +} diff --git a/src/bin/wit-bindgen-wrpc.rs b/src/bin/wit-bindgen-wrpc.rs index 3ecc9ec5d..cb868aed6 100644 --- a/src/bin/wit-bindgen-wrpc.rs +++ b/src/bin/wit-bindgen-wrpc.rs @@ -29,6 +29,12 @@ enum Opt { #[clap(flatten)] args: Common, }, + + // doc-comments are present on `wit_bindgen_wrpc_test::Opts` for clap to use. + Test { + #[clap(flatten)] + opts: wit_bindgen_wrpc_test::Opts, + }, } #[derive(Debug, Parser)] @@ -82,6 +88,10 @@ fn main() -> Result<()> { let (generator, opt) = match Opt::parse() { Opt::Rust { opts, args } => (opts.build(), args), Opt::Go { opts, args } => (opts.build(), args), + Opt::Test { opts } => { + let exe = std::env::args_os().next().unwrap(); + return opts.run(std::path::Path::new(&exe)); + } }; gen_world(generator, &opt, &mut files).map_err(attach_with_context)?; diff --git a/tests/bindgen.rs b/tests/bindgen.rs new file mode 100644 index 000000000..7d3e0f85e --- /dev/null +++ b/tests/bindgen.rs @@ -0,0 +1,29 @@ +//! Drives the `wit-bindgen-wrpc test` subcommand over the in-tree codegen and +//! runtime test suites. Runs as an ordinary integration test so it is exercised +//! by `cargo test`/`nextest`, locating the freshly-built CLI via the +//! `CARGO_BIN_EXE_wit-bindgen-wrpc` environment variable Cargo provides. +#![cfg(feature = "bin-bindgen")] + +use std::process::Command; + +#[test] +fn bindgen() { + let status = Command::new(env!("CARGO_BIN_EXE_wit-bindgen-wrpc")) + .args([ + "test", + "tests/codegen", + "tests/runtime", + "--artifacts", + concat!(env!("CARGO_TARGET_TMPDIR"), "/bindgen"), + "--rust-wit-bindgen-path", + "crates/wit-bindgen", + "--rust-wrpc-transport-path", + "crates/transport", + ]) + .status() + .expect("failed to run `wit-bindgen-wrpc test`"); + assert!( + status.success(), + "`wit-bindgen-wrpc test` reported failures" + ); +} diff --git a/tests/codegen/error-context.wit b/tests/codegen/error-context.wit index d76f89685..8e9e06e97 100644 --- a/tests/codegen/error-context.wit +++ b/tests/codegen/error-context.wit @@ -1,3 +1,5 @@ +//@ async = true + package foo:foo; interface error-contexts { diff --git a/tests/codegen/futures.wit b/tests/codegen/futures.wit index ff24a5098..47237eeec 100644 --- a/tests/codegen/futures.wit +++ b/tests/codegen/futures.wit @@ -1,3 +1,5 @@ +//@ async = true + package foo:foo; interface futures { diff --git a/tests/codegen/resources-with-futures.wit b/tests/codegen/resources-with-futures.wit index 33a2b2aeb..a48e1e523 100644 --- a/tests/codegen/resources-with-futures.wit +++ b/tests/codegen/resources-with-futures.wit @@ -1,3 +1,5 @@ +//@ async = true + package my:resources; interface with-futures { diff --git a/tests/codegen/resources-with-streams.wit b/tests/codegen/resources-with-streams.wit index d9e3620fc..b78cb173c 100644 --- a/tests/codegen/resources-with-streams.wit +++ b/tests/codegen/resources-with-streams.wit @@ -1,3 +1,5 @@ +//@ async = true + package my:resources; interface with-streams { diff --git a/tests/codegen/streams.wit b/tests/codegen/streams.wit index 96ccfeed2..271c8200b 100644 --- a/tests/codegen/streams.wit +++ b/tests/codegen/streams.wit @@ -1,3 +1,5 @@ +//@ async = true + package foo:foo; interface transmit { diff --git a/tests/runtime/rust/alternative-bitflags/runner.rs b/tests/runtime/rust/alternative-bitflags/runner.rs new file mode 100644 index 000000000..6bc7c8999 --- /dev/null +++ b/tests/runtime/rust/alternative-bitflags/runner.rs @@ -0,0 +1,7 @@ +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + let flag = my::inline::flags_iface::get_flag(clt, ()).await?; + assert_eq!(flag, my::inline::flags_iface::Bar::BAZ); + Ok(()) +} diff --git a/tests/runtime/rust/alternative-bitflags/test.rs b/tests/runtime/rust/alternative-bitflags/test.rs new file mode 100644 index 000000000..851ef1e96 --- /dev/null +++ b/tests/runtime/rust/alternative-bitflags/test.rs @@ -0,0 +1,12 @@ +//@ args = '--bitflags-path=wit_bindgen_wrpc::bitflags' + +use exports::my::inline::flags_iface::Bar; + +#[derive(Clone)] +pub struct Component; + +impl exports::my::inline::flags_iface::Handler for Component { + async fn get_flag(&self, _cx: Ctx) -> anyhow::Result { + Ok(Bar::BAZ) + } +} diff --git a/tests/runtime/rust/alternative-bitflags/test.wit b/tests/runtime/rust/alternative-bitflags/test.wit new file mode 100644 index 000000000..b06c9888b --- /dev/null +++ b/tests/runtime/rust/alternative-bitflags/test.wit @@ -0,0 +1,18 @@ +package my:inline; + +interface flags-iface { + flags bar { + foo, + bar, + baz, + } + get-flag: func() -> bar; +} + +world test { + export flags-iface; +} + +world runner { + import flags-iface; +} diff --git a/tests/runtime/rust/custom-derives/runner.rs b/tests/runtime/rust/custom-derives/runner.rs new file mode 100644 index 000000000..d2d70a374 --- /dev/null +++ b/tests/runtime/rust/custom-derives/runner.rs @@ -0,0 +1,16 @@ +use my::inline::blah::{bar, Foo}; + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + bar( + clt, + (), + &Foo { + field1: "x".to_string(), + field2: vec![2, 3, 3, 4], + }, + ) + .await?; + Ok(()) +} diff --git a/tests/runtime/rust/custom-derives/test.rs b/tests/runtime/rust/custom-derives/test.rs new file mode 100644 index 000000000..34716fff2 --- /dev/null +++ b/tests/runtime/rust/custom-derives/test.rs @@ -0,0 +1,44 @@ +//@ args = [ +//@ '-dHash', +//@ '-dClone', +//@ '-d::core::cmp::PartialEq', +//@ '-d::core::cmp::Eq', +//@ '-dserde::Serialize', +//@ '-dserde::Deserialize', +//@ '--additional-derive-ignore=ignoreme', +//@ ] + +use std::collections::{hash_map::RandomState, HashSet}; + +use exports::my::inline::blah::Foo; + +#[derive(Clone)] +pub struct Component; + +impl exports::my::inline::blah::Handler for Component { + async fn bar(&self, _cx: Ctx, cool: Foo) -> anyhow::Result<()> { + // The added `Hash`/`Eq` derives must apply to `foo`, so it can be used + // as a `HashSet` element. + let _blah: HashSet = HashSet::from_iter([Foo { + field1: "hello".to_string(), + field2: vec![1, 2, 3], + }]); + + // The added `serde` derives must apply too, otherwise this fails to + // compile. + let _ = serde_json::to_string(&cool); + Ok(()) + } + + async fn barry( + &self, + _cx: Ctx, + warm: exports::my::inline::blah::Ignoreme, + ) -> anyhow::Result<()> { + // Compilation would fail here if `serde::Deserialize` were applied to + // `ignoreme`, since it holds a resource handle. `--additional-derive-ignore` + // must have excluded it. + let _ = warm; + Ok(()) + } +} diff --git a/tests/runtime/rust/custom-derives/test.wit b/tests/runtime/rust/custom-derives/test.wit new file mode 100644 index 000000000..d3b493db8 --- /dev/null +++ b/tests/runtime/rust/custom-derives/test.wit @@ -0,0 +1,32 @@ +package my:inline; + +interface blag { + resource input-stream { + read: func(len: u64) -> list; + } +} + +interface blah { + use blag.{input-stream}; + + record foo { + field1: string, + field2: list, + } + + bar: func(cool: foo); + + variant ignoreme { + stream-type(input-stream), + } + + barry: func(warm: ignoreme); +} + +world test { + export blah; +} + +world runner { + import blah; +} diff --git a/tests/runtime/rust/gated-features/runner.rs b/tests/runtime/rust/gated-features/runner.rs new file mode 100644 index 000000000..ea41f4ccc --- /dev/null +++ b/tests/runtime/rust/gated-features/runner.rs @@ -0,0 +1,9 @@ +//@ args = '--features y' + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + foo::bar::iface::y(clt, ()).await?; + foo::bar::iface::z(clt, ()).await?; + Ok(()) +} diff --git a/tests/runtime/rust/gated-features/test.rs b/tests/runtime/rust/gated-features/test.rs new file mode 100644 index 000000000..9475139d9 --- /dev/null +++ b/tests/runtime/rust/gated-features/test.rs @@ -0,0 +1,13 @@ +//@ args = '--features y' + +#[derive(Clone)] +pub struct Component; + +impl exports::foo::bar::iface::Handler for Component { + async fn y(&self, _cx: Ctx) -> anyhow::Result<()> { + Ok(()) + } + async fn z(&self, _cx: Ctx) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/tests/runtime/rust/gated-features/test.wit b/tests/runtime/rust/gated-features/test.wit new file mode 100644 index 000000000..a4dc06a56 --- /dev/null +++ b/tests/runtime/rust/gated-features/test.wit @@ -0,0 +1,18 @@ +package foo:bar@1.2.3; + +interface iface { + @unstable(feature = x) + x: func(); + @unstable(feature = y) + y: func(); + @since(version = 1.2.3) + z: func(); +} + +world test { + export iface; +} + +world runner { + import iface; +} diff --git a/tests/runtime/rust/skip/runner.rs b/tests/runtime/rust/skip/runner.rs new file mode 100644 index 000000000..5a855280d --- /dev/null +++ b/tests/runtime/rust/skip/runner.rs @@ -0,0 +1,6 @@ +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + my::test::exports_iface::bar(clt, ()).await?; + Ok(()) +} diff --git a/tests/runtime/rust/skip/test.rs b/tests/runtime/rust/skip/test.rs new file mode 100644 index 000000000..3137e2cef --- /dev/null +++ b/tests/runtime/rust/skip/test.rs @@ -0,0 +1,10 @@ +//@ args = '--skip foo' + +#[derive(Clone)] +pub struct Component; + +impl exports::my::test::exports_iface::Handler for Component { + async fn bar(&self, _cx: Ctx) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/tests/runtime/rust/skip/test.wit b/tests/runtime/rust/skip/test.wit new file mode 100644 index 000000000..03c3fa8d0 --- /dev/null +++ b/tests/runtime/rust/skip/test.wit @@ -0,0 +1,14 @@ +package my:test; + +interface exports-iface { + foo: func(); + bar: func(); +} + +world test { + export exports-iface; +} + +world runner { + import exports-iface; +} diff --git a/tests/runtime/rust/unused-types/runner.rs b/tests/runtime/rust/unused-types/runner.rs new file mode 100644 index 000000000..bc8fa0a26 --- /dev/null +++ b/tests/runtime/rust/unused-types/runner.rs @@ -0,0 +1,6 @@ +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + foo::bar::component::ping(clt, ()).await?; + Ok(()) +} diff --git a/tests/runtime/rust/unused-types/test.rs b/tests/runtime/rust/unused-types/test.rs new file mode 100644 index 000000000..a3ee062fd --- /dev/null +++ b/tests/runtime/rust/unused-types/test.rs @@ -0,0 +1,10 @@ +//@ args = '--generate-unused-types' + +#[derive(Clone)] +pub struct Component; + +impl exports::foo::bar::component::Handler for Component { + async fn ping(&self, _cx: Ctx) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/tests/runtime/rust/unused-types/test.wit b/tests/runtime/rust/unused-types/test.wit new file mode 100644 index 000000000..da10a8a31 --- /dev/null +++ b/tests/runtime/rust/unused-types/test.wit @@ -0,0 +1,23 @@ +package foo:bar; + +interface component { + variant unused-variant { + %enum(unused-enum), + %record(unused-record), + } + enum unused-enum { + unused, + } + record unused-record { + x: u32, + } + ping: func(); +} + +world test { + export component; +} + +world runner { + import component; +} diff --git a/tests/runtime/rust/with-option-generate/runner-generate-all.rs b/tests/runtime/rust/with-option-generate/runner-generate-all.rs new file mode 100644 index 000000000..2bf48c033 --- /dev/null +++ b/tests/runtime/rust/with-option-generate/runner-generate-all.rs @@ -0,0 +1,8 @@ +//@ args = '--generate-all' + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + foo::baz::a::x(clt, ()).await?; + Ok(()) +} diff --git a/tests/runtime/rust/with-option-generate/runner-generate-one.rs b/tests/runtime/rust/with-option-generate/runner-generate-one.rs new file mode 100644 index 000000000..cd0140df7 --- /dev/null +++ b/tests/runtime/rust/with-option-generate/runner-generate-one.rs @@ -0,0 +1,8 @@ +//@ args = '--with=foo:baz/a=generate' + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + foo::baz::a::x(clt, ()).await?; + Ok(()) +} diff --git a/tests/runtime/rust/with-option-generate/test.rs b/tests/runtime/rust/with-option-generate/test.rs new file mode 100644 index 000000000..d4fcd82c7 --- /dev/null +++ b/tests/runtime/rust/with-option-generate/test.rs @@ -0,0 +1,10 @@ +//@ args = '--generate-all' + +#[derive(Clone)] +pub struct Component; + +impl exports::foo::baz::a::Handler for Component { + async fn x(&self, _cx: Ctx) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/tests/runtime/rust/with-option-generate/test.wit b/tests/runtime/rust/with-option-generate/test.wit new file mode 100644 index 000000000..74653a7af --- /dev/null +++ b/tests/runtime/rust/with-option-generate/test.wit @@ -0,0 +1,17 @@ +//@ default-bindgen-args = false + +package foo:bar; + +world test { + export foo:baz/a; +} + +world runner { + import foo:baz/a; +} + +package foo:baz { + interface a { + x: func(); + } +} diff --git a/tests/runtime/rust/with-types/runner.rs b/tests/runtime/rust/with-types/runner.rs new file mode 100644 index 000000000..4e1057662 --- /dev/null +++ b/tests/runtime/rust/with-types/runner.rs @@ -0,0 +1,32 @@ +//@ args = [ +//@ '--with=my:inline/foo/a=crate::runner::other::my::inline::foo::A', +//@ '--with=my:inline/foo/c=crate::runner::other::my::inline::foo::C', +//@ ] + +mod other { + wit_bindgen_wrpc::generate!({ + inline: " + package my:inline; + interface foo { + record a { inner: f64, } + variant c { a(a), other(u32), } + } + world dummy { + use foo.{a, c}; + import f: func(v: a, w: c); + } + ", + }); +} + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + let a = other::my::inline::foo::A { inner: 1.5 }; + let got = my::inline::foo::func1(clt, (), &a).await?; + assert_eq!(got.inner, 1.5); + + let c = other::my::inline::foo::C::A(a); + my::inline::foo::func3(clt, (), &c).await?; + Ok(()) +} diff --git a/tests/runtime/rust/with-types/test.rs b/tests/runtime/rust/with-types/test.rs new file mode 100644 index 000000000..9c99f2dea --- /dev/null +++ b/tests/runtime/rust/with-types/test.rs @@ -0,0 +1,13 @@ +use exports::my::inline::foo::{A, C}; + +#[derive(Clone)] +pub struct Component; + +impl exports::my::inline::foo::Handler for Component { + async fn func1(&self, _cx: Ctx, v: A) -> anyhow::Result { + Ok(v) + } + async fn func3(&self, _cx: Ctx, v: C) -> anyhow::Result { + Ok(v) + } +} diff --git a/tests/runtime/rust/with-types/test.wit b/tests/runtime/rust/with-types/test.wit new file mode 100644 index 000000000..2a6e6e228 --- /dev/null +++ b/tests/runtime/rust/with-types/test.wit @@ -0,0 +1,21 @@ +package my:inline; + +interface foo { + record a { + inner: f64, + } + variant c { + a(a), + other(u32), + } + func1: func(v: a) -> a; + func3: func(v: c) -> c; +} + +world test { + export foo; +} + +world runner { + import foo; +} diff --git a/tests/runtime/rust/with/runner.rs b/tests/runtime/rust/with/runner.rs new file mode 100644 index 000000000..30cc8c376 --- /dev/null +++ b/tests/runtime/rust/with/runner.rs @@ -0,0 +1,26 @@ +//@ args = '--with=my:inline/foo=other::my::inline::foo' + +mod other { + wit_bindgen_wrpc::generate!({ + inline: " + package my:inline; + interface foo { + record msg { field: string, } + } + world dummy { + use foo.{msg}; + import bar: func(m: msg); + } + ", + }); +} + +pub async fn run( + clt: &impl wit_bindgen_wrpc::wrpc_transport::Invoke, +) -> anyhow::Result<()> { + let msg = other::my::inline::foo::Msg { + field: "hello".to_string(), + }; + my::inline::bar::bar(clt, (), &msg).await?; + Ok(()) +} diff --git a/tests/runtime/rust/with/test.rs b/tests/runtime/rust/with/test.rs new file mode 100644 index 000000000..43011b397 --- /dev/null +++ b/tests/runtime/rust/with/test.rs @@ -0,0 +1,11 @@ +use exports::my::inline::bar::Msg; + +#[derive(Clone)] +pub struct Component; + +impl exports::my::inline::bar::Handler for Component { + async fn bar(&self, _cx: Ctx, m: Msg) -> anyhow::Result<()> { + assert_eq!(m.field, "hello"); + Ok(()) + } +} diff --git a/tests/runtime/rust/with/test.wit b/tests/runtime/rust/with/test.wit new file mode 100644 index 000000000..c70024504 --- /dev/null +++ b/tests/runtime/rust/with/test.wit @@ -0,0 +1,20 @@ +package my:inline; + +interface foo { + record msg { + field: string, + } +} + +interface bar { + use foo.{msg}; + bar: func(m: msg); +} + +world test { + export bar; +} + +world runner { + import bar; +}