From 70bd72750d1117061d971c196292152fde827de7 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 30 Jun 2026 13:32:29 -0400 Subject: [PATCH] test(csharp): cover map runtime exports Signed-off-by: Yordis Prieto --- crates/csharp/src/csproj.rs | 16 ++++- crates/test/src/csharp.rs | 17 ++++- crates/test/src/lib.rs | 26 +++++++- tests/runtime/map/test.cs | 123 ++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tests/runtime/map/test.cs diff --git a/crates/csharp/src/csproj.rs b/crates/csharp/src/csproj.rs index 76b8c5d57..9be60a0f1 100644 --- a/crates/csharp/src/csproj.rs +++ b/crates/csharp/src/csproj.rs @@ -12,6 +12,7 @@ pub struct CSProjectLLVMBuilder { clean_targets: bool, world_name: String, binary: bool, + skip_wit_component: bool, } pub struct CSProjectMonoBuilder { @@ -31,6 +32,7 @@ impl CSProject { clean_targets: false, world_name: world_name.to_string(), binary: false, + skip_wit_component: false, } } @@ -69,6 +71,14 @@ impl CSProjectLLVMBuilder { "Library" }; + let component_type_linker_arg = if self.skip_wit_component { + "".to_string() + } else { + format!( + "" + ) + }; + let mut csproj = format!( " @@ -94,7 +104,7 @@ impl CSProjectLLVMBuilder { - + {component_type_linker_arg} " ); @@ -178,6 +188,10 @@ impl CSProjectLLVMBuilder { self.binary = true; } + pub fn skip_wit_component(&mut self) { + self.skip_wit_component = true; + } + pub fn clean(&mut self) -> &mut Self { self.clean_targets = true; diff --git a/crates/test/src/csharp.rs b/crates/test/src/csharp.rs index 8e11ca7f7..53032296e 100644 --- a/crates/test/src/csharp.rs +++ b/crates/test/src/csharp.rs @@ -1,6 +1,7 @@ use crate::{Compile, LanguageMethods, Runner, Verify}; use anyhow::Result; use heck::*; +use serde::Deserialize; use std::env; use std::fs; use std::path::Path; @@ -8,6 +9,12 @@ use std::process::Command; pub struct Csharp; +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +struct LangConfig { + skip_wit_component: bool, +} + fn dotnet() -> Command { let dotnet_cmd = match env::var("DOTNET_ROOT") { Ok(val) => Path::new(&val).join("dotnet"), @@ -59,6 +66,7 @@ impl LanguageMethods for Csharp { } fn compile(&self, runner: &Runner, compile: &Compile<'_>) -> Result<()> { + let config = compile.component.deserialize_lang_config::()?; let world_name = &compile.component.bindgen.world; let path = &compile.component.path; let test_dir = &compile.bindings_dir; @@ -75,6 +83,9 @@ impl LanguageMethods for Csharp { let mut csproj = wit_bindgen_csharp::CSProject::new(test_dir.to_path_buf(), &assembly_name, world_name); csproj.aot(); + if config.skip_wit_component { + csproj.skip_wit_component(); + } csproj.generate()?; let mut cmd = dotnet(); @@ -109,7 +120,11 @@ impl LanguageMethods for Csharp { runner.run_command(&mut cmd)?; - fs::copy(&wasm_filename, &compile.output)?; + if config.skip_wit_component { + runner.convert_p1_to_component_appending_type_metadata(&wasm_filename, compile)?; + } else { + fs::copy(&wasm_filename, &compile.output)?; + } Ok(()) } diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 615fb9ce0..e65f91606 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -1081,6 +1081,30 @@ status: {}", /// /// Stores the output at `compile.output`. fn convert_p1_to_component(&self, p1: &Path, compile: &Compile<'_>) -> Result<()> { + self.convert_p1_to_component_impl(p1, compile, false) + } + + /// Converts the WASIp1 module at `p1` to a component, appending component + /// type metadata from the test WIT even if existing metadata is present. + /// + /// Some toolchains emit incomplete or stale component type metadata even + /// when asked to skip componentization, while still needing their own + /// metadata for WASI imports. Use this when the test harness must add + /// guest-world metadata without discarding toolchain metadata. + fn convert_p1_to_component_appending_type_metadata( + &self, + p1: &Path, + compile: &Compile<'_>, + ) -> Result<()> { + self.convert_p1_to_component_impl(p1, compile, true) + } + + fn convert_p1_to_component_impl( + &self, + p1: &Path, + compile: &Compile<'_>, + append_component_type: bool, + ) -> Result<()> { let mut resolve = wit_parser::Resolve::default(); let (pkg, _) = resolve .push_path(&compile.component.bindgen.wit_path) @@ -1088,7 +1112,7 @@ status: {}", let world = resolve.select_world(&[pkg], Some(&compile.component.bindgen.world))?; let mut module = fs::read(&p1).context("failed to read wasm file")?; - if !has_component_type_sections(&module) { + if append_component_type || !has_component_type_sections(&module) { let encoded = wit_component::metadata::encode(&resolve, world, StringEncoding::UTF8, None)?; let section = wasm_encoder::CustomSection { diff --git a/tests/runtime/map/test.cs b/tests/runtime/map/test.cs new file mode 100644 index 000000000..e4e63bf5a --- /dev/null +++ b/tests/runtime/map/test.cs @@ -0,0 +1,123 @@ +//@ wasmtime-flags = '-Wcomponent-model-map' +//@ [lang] +//@ skip-wit-component = true + +using System.Diagnostics; +using System.Text; + +namespace TestWorld.wit.Exports.test.maps +{ + public class ToTestExportsImpl : IToTestExports + { + public static Dictionary NamedRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 2); + Debug.Assert(a[1] == "uno"); + Debug.Assert(a[2] == "two"); + + return a.ToDictionary(entry => entry.Value, entry => entry.Key); + } + + public static Dictionary BytesRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 2); + Debug.Assert(a["hello"].SequenceEqual(Encoding.UTF8.GetBytes("world"))); + Debug.Assert(a["bin"].SequenceEqual(new byte[] { 0, 1, 2 })); + + return a; + } + + public static Dictionary EmptyRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 0); + return a; + } + + public static Dictionary OptionRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 2); + Debug.Assert(a["some"] == 42); + Debug.Assert(a["none"] == null); + + return a; + } + + public static IToTestExports.LabeledEntry RecordRoundtrip(IToTestExports.LabeledEntry a) + { + Debug.Assert(a.label == "test-label"); + Debug.Assert(a.values.Count == 2); + Debug.Assert(a.values[10] == "ten"); + Debug.Assert(a.values[20] == "twenty"); + + return a; + } + + public static Dictionary InlineRoundtrip(Dictionary a) + { + return a.ToDictionary(entry => entry.Value, entry => entry.Key); + } + + public static Dictionary LargeRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 100); + return a; + } + + public static (Dictionary, Dictionary) MultiParamRoundtrip( + Dictionary a, + Dictionary b) + { + Debug.Assert(a.Count == 2); + Debug.Assert(b.Count == 1); + + return (a.ToDictionary(entry => entry.Value, entry => entry.Key), b); + } + + public static Dictionary> NestedRoundtrip( + Dictionary> a) + { + Debug.Assert(a.Count == 2); + Debug.Assert(a["group-a"].Count == 2); + Debug.Assert(a["group-a"][1] == "one"); + Debug.Assert(a["group-a"][2] == "two"); + Debug.Assert(a["group-b"].Count == 1); + Debug.Assert(a["group-b"][10] == "ten"); + + return a; + } + + public static IToTestExports.MapOrString VariantRoundtrip(IToTestExports.MapOrString a) + { + return a; + } + + public static Dictionary ResultRoundtrip( + Result, string> a) + { + if (a.IsErr) + { + throw new WitException(a.AsErr, 0); + } + + return a.AsOk; + } + + public static (Dictionary, ulong) TupleRoundtrip( + (Dictionary, ulong) a) + { + Debug.Assert(a.Item1.Count == 1); + Debug.Assert(a.Item1[7] == "seven"); + Debug.Assert(a.Item2 == 42); + + return a; + } + + public static Dictionary SingleEntryRoundtrip(Dictionary a) + { + Debug.Assert(a.Count == 1); + Debug.Assert(a[99] == "ninety-nine"); + + return a; + } + } +}