From 41de930b41878695e053fe4f63cdb17d3348fa69 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Jun 2026 13:20:14 -0700 Subject: [PATCH 01/14] Fix custom-modifier type erasure in the WinMD generator type mapping MapTypeSignatureToOutput had no handling for CustomModifierTypeSignature, so any modifier-wrapped type fell through to the 'System.Object' fallback and was silently erased. This affects 'in' parameters on abstract/virtual/interface/delegate members, where the C# compiler emits a 'modreq(InAttribute)' on the by-reference type. For example, a COM interop 'in Guid riid' was emitted as '[in] object' instead of '[in] Guid&'. Strip leading custom modifiers before dispatching on the underlying type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Writers/WinMDWriter.TypeMapping.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs index bda4732b5..0b34eb127 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs @@ -38,6 +38,14 @@ internal sealed partial class WinMDWriter /// The mapped for use in the output WinMD. private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSignature) { + // Strip custom modifiers ('modreq'/'modopt') before dispatching on the underlying type. + // For example, 'in' parameters on abstract, virtual, interface, or delegate members carry a + // 'modreq(System.Runtime.InteropServices.InAttribute)' on their by-reference type. Custom + // modifiers have no Windows Runtime representation, and without stripping them a modifier-wrapped + // type would fall through to the 'System.Object' fallback at the end of this method, silently + // erasing the real signature. + inputSignature = StripCustomModifiers(inputSignature); + // Handle 'CorLib' types if (inputSignature is CorLibTypeSignature corLib) { @@ -139,6 +147,26 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSignature) return _outputModule.CorLibTypeFactory.Object; } + /// + /// Strips any leading custom modifiers (modreq/modopt) from a type signature. + /// + /// + /// Custom modifiers carry no Windows Runtime meaning. They are emitted by the C# compiler in a few + /// cases, most notably the modreq(System.Runtime.InteropServices.InAttribute) applied to the + /// by-reference type of an in parameter on abstract, virtual, interface, or delegate members. + /// + /// The type signature to unwrap. + /// The underlying with all leading custom modifiers removed. + private static TypeSignature StripCustomModifiers(TypeSignature signature) + { + while (signature is CustomModifierTypeSignature customModifier) + { + signature = customModifier.BaseType; + } + + return signature; + } + /// /// Imports a type reference from the input module to the output module. /// From cca2e3bcc1b5cf0962bad6e4c1228bcd01239e37 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Jun 2026 13:20:27 -0700 Subject: [PATCH 02/14] Validate unsupported array/span parameter conventions in the WinMD generator Windows Runtime arrays use one of three conventions (ReadOnlySpan for PassArray, Span for FillArray, or 'out T[]' for ReceiveArray), and spans are always passed by value. The generator previously emitted invalid metadata for a few shapes: 'ref T[]'/'in T[]' became '[in] T[]&', and 'out Span'/'out ReadOnlySpan' became a ReceiveArray. Reject these with clear errors (CSWINRTWINMDGEN0011 for by-reference arrays, CSWINRTWINMDGEN0012 for by-reference spans), while preserving the deliberate COM interop 'ref'/'in' scalar behavior (e.g. 'ref Guid riid' -> '[in]') and the by-value array PassArray default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Errors/WellKnownWinMDExceptions.cs | 16 ++++ .../Writers/WinMDWriter.Members.cs | 77 +++++++++++++++++-- .../Writers/WinMDWriter.Types.cs | 2 +- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 641d4c741..b776768d7 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -73,6 +73,22 @@ public static Exception InputAssemblyRuntimeVersionNotFound(string path) return Exception(7, $"Failed to probe the .NET runtime version from the input assembly '{path}'."); } + /// + /// A method has a ref or in array parameter, which is not a valid Windows Runtime array convention. + /// + public static Exception ByReferenceArrayParameterNotSupported(string declaringTypeName, string methodName, string parameterName) + { + return Exception(11, $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), 'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); + } + + /// + /// A method has a by-reference span parameter (e.g. out Span<T>), which has no Windows Runtime representation. + /// + public static Exception ByReferenceSpanParameterNotSupported(string declaringTypeName, string methodName, string parameterName) + { + return Exception(12, $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'. Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' (FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); + } + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs index 5562f0130..4ffd0748b 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs @@ -6,6 +6,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.WinMDGenerator.Errors; using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; using MethodImplAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodImplAttributes; using MethodSemanticsAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodSemanticsAttributes; @@ -144,6 +145,11 @@ private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputM /// Any other by-reference type (e.g. ref Guid on a COM interop interface) → [in], matching the MIDL convention for ref const T parameters. /// All other params → [in]. /// + /// + /// Array and span shapes that have no Windows Runtime representation are rejected with a well-known + /// error: a by-reference span (e.g. out Span<T>), or a ref/in array (only + /// out T[] is a valid by-reference array). See . + /// /// private static void AddParameterDefinitions(MethodDefinition outputMethod, MethodDefinition inputMethod) { @@ -157,7 +163,7 @@ private static void AddParameterDefinitions(MethodDefinition outputMethod, Metho if (sigIndex < inputParamTypes.Count) { - paramattributes = GetWinRTParameterAttributes(inputParam, inputParamTypes[sigIndex]); + paramattributes = GetWinRTParameterAttributes(inputMethod, inputParam, inputParamTypes[sigIndex]); } outputMethod.ParameterDefinitions.Add(new ParameterDefinition( @@ -168,15 +174,57 @@ private static void AddParameterDefinitions(MethodDefinition outputMethod, Metho } /// - /// Determines the Windows Runtime parameter attributes based on the input parameter and its type. + /// Determines the Windows Runtime parameter attributes based on the input parameter and its type, + /// validating that the parameter uses a supported Windows Runtime calling convention. /// /// /// If the input parameter already has or /// set, those flags are preserved unchanged. Otherwise, /// the type drives the default per the rules documented on . + /// By-reference spans (e.g. out Span<T>) and ref/in arrays have no + /// Windows Runtime representation and throw a . /// - private static ParameterAttributes GetWinRTParameterAttributes(ParameterDefinition inputParam, TypeSignature inputParamType) + /// The method that declares the parameter (used for error context). + /// The input (provides the In/Out direction flags). + /// The input parameter . + /// The Windows Runtime for the parameter. + /// Thrown if the parameter uses an unsupported array or span convention. + private static ParameterAttributes GetWinRTParameterAttributes( + MethodDefinition inputMethod, + ParameterDefinition inputParam, + TypeSignature inputParamType) { + // Look through any custom modifiers (e.g. the 'modreq(InAttribute)' emitted for 'in' parameters + // on abstract, virtual, interface, or delegate members) to inspect the real underlying signature + TypeSignature parameterType = StripCustomModifiers(inputParamType); + + // Validate by-reference parameters that wrap a span or array. Windows Runtime spans are always + // passed by value, and Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' + // (PassArray), 'Span' (FillArray), or 'out T[]' (ReceiveArray). A by-reference span, or a + // 'ref'/'in' array, has no Windows Runtime representation and is rejected here. + if (parameterType is ByReferenceTypeSignature byReference) + { + TypeSignature elementType = StripCustomModifiers(byReference.BaseType); + + if (IsSpan(elementType) || IsReadOnlySpan(elementType)) + { + throw WellKnownWinMDExceptions.ByReferenceSpanParameterNotSupported( + inputMethod.DeclaringType!.FullName, + inputMethod.Name!.Value, + inputParam.Name!.Value); + } + + // 'out T[]' (ReceiveArray) is valid, but 'ref T[]'/'in T[]' are not. By-reference non-array + // types (e.g. 'ref Guid riid' on a COM interop interface) are still allowed and handled below. + if (elementType is SzArrayTypeSignature && !inputParam.IsOut) + { + throw WellKnownWinMDExceptions.ByReferenceArrayParameterNotSupported( + inputMethod.DeclaringType!.FullName, + inputMethod.Name!.Value, + inputParam.Name!.Value); + } + } + // Preserve any 'In'/'Out' direction flags the input parameter already carries ParameterAttributes inputDirectionFlags = inputParam.Attributes & (ParameterAttributes.In | ParameterAttributes.Out); @@ -186,8 +234,7 @@ private static ParameterAttributes GetWinRTParameterAttributes(ParameterDefiniti } // 'Span' → 'FillArray' pattern: '[out]' without 'BYREF' - if (inputParamType is GenericInstanceTypeSignature genericInstanceSignature && - genericInstanceSignature.GenericType.FullName == "System.Span`1") + if (IsSpan(parameterType)) { return ParameterAttributes.Out; } @@ -198,6 +245,26 @@ private static ParameterAttributes GetWinRTParameterAttributes(ParameterDefiniti return ParameterAttributes.In; } + /// + /// Determines whether a type signature is . + /// + /// The type signature to test. + /// if is ; otherwise, . + private static bool IsSpan(TypeSignature signature) + { + return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.Span`1" }; + } + + /// + /// Determines whether a type signature is . + /// + /// The type signature to test. + /// if is ; otherwise, . + private static bool IsReadOnlySpan(TypeSignature signature) + { + return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.ReadOnlySpan`1" }; + } + /// /// Adds a property definition to a WinMD type (interface or class). /// diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Types.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Types.cs index 15854de6b..95fed8425 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Types.cs @@ -713,7 +713,7 @@ private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeD for (int i = 0; i < paramNames.Length; i++) { ParameterAttributes paramAttr = i < parameterTypes.Length && i < method.ParameterDefinitions.Count - ? GetWinRTParameterAttributes(method.ParameterDefinitions[i], method.Signature!.ParameterTypes[i]) + ? GetWinRTParameterAttributes(method, method.ParameterDefinitions[i], method.Signature!.ParameterTypes[i]) : ParameterAttributes.In; outputMethod.ParameterDefinitions.Add(new ParameterDefinition( From e9bae0833970a3e981a4fef992a923ead880f005 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Jun 2026 13:20:38 -0700 Subject: [PATCH 03/14] Add WinMD generator parameter-convention failure tests Add a WinMDGeneratorTest project that runs the actual cswinrtwinmdgen tool end-to-end: it compiles small C# inputs with Roslyn, invokes the generator as a subprocess, and asserts a non-zero exit code plus the expected CSWINRTWINMDGEN error. The project deliberately focuses on failure cases (unsupported 'ref'/'in' arrays and 'out' spans), which can be exercised here without failing the build; the supported '.winmd' shapes (PassArray/FillArray/ReceiveArray) are covered by the authoring tests. The generator exposes no internals to the test project. Registers the project in the solution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/WinMDGeneratorTestHelper.cs | 138 ++++++++++++++++++ .../Test_ParameterConventions.cs | 94 ++++++++++++ .../WinMDGeneratorTest.csproj | 62 ++++++++ src/cswinrt.slnx | 8 + 4 files changed, 302 insertions(+) create mode 100644 src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs create mode 100644 src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs create mode 100644 src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj diff --git a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs new file mode 100644 index 000000000..671f05eb8 --- /dev/null +++ b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + +namespace WinMDGeneratorTest.Helpers; + +/// +/// Helpers to drive the WinMD generator (cswinrtwinmdgen) end-to-end: compile a small C# input, +/// run the actual tool against it as a separate process, and capture its result. +/// +internal static class WinMDGeneratorTestHelper +{ + /// + /// Compiles the given C# into an assembly and runs the WinMD generator + /// against it, returning the process exit code and combined output. + /// + /// The C# source defining the authored component types. + /// Whether to use the UWP (Windows.UI.Xaml) projections. + /// The generator process exit code and its combined standard output and error. + public static (int ExitCode, string Output) RunGenerator(string source, bool useWindowsUIXamlProjections = false) + { + string toolPath = GetGeneratorPath(); + string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; + + try + { + string inputAssemblyPath = Path.Combine(temporaryDirectory, "TestInput.dll"); + string outputWinmdPath = Path.Combine(temporaryDirectory, "TestInput.winmd"); + string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); + + CompileInputAssembly(source, inputAssemblyPath); + + // Each line of the response file is a single ' ' pair. The input + // assembly is also passed as its own reference path (its directory seeds the resolver). + File.WriteAllLines(responseFilePath, + [ + $"--input-assembly-path {inputAssemblyPath}", + $"--reference-assembly-paths {inputAssemblyPath}", + $"--output-winmd-path {outputWinmdPath}", + "--assembly-version 1.0.0.0", + $"--use-windows-ui-xaml-projections {useWindowsUIXamlProjections}", + ]); + + return RunTool(toolPath, responseFilePath); + } + finally + { + try + { + Directory.Delete(temporaryDirectory, recursive: true); + } + catch (IOException) + { + // Best effort cleanup; a leftover temp directory must not fail the test + } + } + } + + /// + /// Compiles the given C# into an assembly on disk. + /// + private static void CompileInputAssembly(string source, string outputPath) + { + CSharpParseOptions parseOptions = new(LanguageVersion.Preview); + + // The target framework attribute lets the generator probe the .NET runtime version. It is added + // in a separate syntax tree so it does not interfere with any 'using' directives in the test + // source (an assembly attribute must precede type declarations but follow 'using' directives). + SyntaxTree sourceTree = CSharpSyntaxTree.ParseText(source, parseOptions); + SyntaxTree assemblyInfoTree = CSharpSyntaxTree.ParseText( + """[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0")]""", + parseOptions); + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "TestInput", + syntaxTrees: [sourceTree, assemblyInfoTree], + references: Net100.References.All, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + + EmitResult result = compilation.Emit(outputPath); + + Assert.IsTrue(result.Success, $"Input compilation failed:\n{string.Join("\n", result.Diagnostics)}"); + } + + /// + /// Runs dotnet exec <tool> <responseFile> and captures the exit code and output. + /// + private static (int ExitCode, string Output) RunTool(string toolPath, string responseFilePath) + { + ProcessStartInfo startInfo = new("dotnet") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + startInfo.ArgumentList.Add("exec"); + startInfo.ArgumentList.Add(toolPath); + startInfo.ArgumentList.Add(responseFilePath); + + using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start the WinMD generator process."); + + string standardOutput = process.StandardOutput.ReadToEnd(); + string standardError = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + return (process.ExitCode, standardOutput + standardError); + } + + /// + /// Resolves the path to the built cswinrtwinmdgen tool from assembly metadata. + /// + private static string GetGeneratorPath() + { + string? path = typeof(WinMDGeneratorTestHelper).Assembly + .GetCustomAttributes() + .FirstOrDefault(static attribute => attribute.Key == "WinMDGeneratorAssemblyPath")?.Value; + + Assert.IsFalse(string.IsNullOrEmpty(path), "The 'WinMDGeneratorAssemblyPath' assembly metadata was not found."); + + string fullPath = Path.GetFullPath(path!); + + Assert.IsTrue(File.Exists(fullPath), $"The WinMD generator was not found at '{fullPath}'."); + + return fullPath; + } +} diff --git a/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs new file mode 100644 index 000000000..7398c2781 --- /dev/null +++ b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WinMDGeneratorTest.Helpers; + +namespace WinMDGeneratorTest; + +/// +/// End-to-end tests for the WinMD generator's validation of unsupported method parameter conventions. +/// +/// +/// These tests run the actual cswinrtwinmdgen tool and assert that unsupported array/span +/// parameter shapes are rejected with the expected error. Keeping the failure cases here lets us +/// exercise them without breaking the build. The supported conventions that produce a valid +/// .winmd (PassArray/FillArray/ReceiveArray) are covered by the authoring tests instead. +/// +[TestClass] +public class Test_ParameterConventions +{ + [TestMethod] + public void ValidComponent_GeneratesSuccessfully() + { + // Smoke test: a valid component generates a '.winmd' (exit code 0). This validates the + // end-to-end harness so the failure assertions below are meaningful. + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + public interface IComponent + { + int Method(int value); + } + """); + + Assert.AreEqual(0, exitCode, output); + } + + [TestMethod] + public void RefArrayParameter_IsRejected() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + public interface IComponent + { + void Method(ref int[] values); + } + """); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0011"); + } + + [TestMethod] + public void InArrayParameter_IsRejected() + { + // 'in int[]' carries a 'modreq(InAttribute)' on interface members; the generator must still + // see through it to detect the by-reference array. + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + public interface IComponent + { + void Method(in int[] values); + } + """); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0011"); + } + + [TestMethod] + public void OutSpanParameter_IsRejected() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + using System; + public interface IComponent + { + void Method(out Span values); + } + """); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0012"); + } + + [TestMethod] + public void OutReadOnlySpanParameter_IsRejected() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + using System; + public interface IComponent + { + void Method(out ReadOnlySpan values); + } + """); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0012"); + } +} diff --git a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj new file mode 100644 index 000000000..c70c6a443 --- /dev/null +++ b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj @@ -0,0 +1,62 @@ + + + Exe + net10.0 + x64;x86 + preview + enable + false + + + false + + + + win-x86 + x86 + + + + win-x64 + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 67c2cb9bc..b8c76d9ee 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -273,6 +273,14 @@ + + + + + + + + From ea5200d62b3dc854e78d28aa469fb33eb4c202e2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 14 Jun 2026 17:19:14 -0700 Subject: [PATCH 04/14] Add WinMD generator invocation and invalid-input failure tests Extend the WinMDGeneratorTest harness with a custom-response-file overload and a public CompileComponent helper, then add Test_InvalidInputs covering the generator's remaining end-to-end failure modes: a missing response file (CSWINRTWINMDGEN0001), a malformed or duplicate-argument response file (0002), a missing required or unparseable argument (0003), a missing or corrupt input assembly (0004), an unwritable output path (0006), and a missing debug-repro directory (0008). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/WinMDGeneratorTestHelper.cs | 99 ++++++--- .../WinMDGeneratorTest/Test_InvalidInputs.cs | 188 ++++++++++++++++++ 2 files changed, 259 insertions(+), 28 deletions(-) create mode 100644 src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs diff --git a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs index 671f05eb8..75dc8dc24 100644 --- a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs +++ b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -21,55 +22,80 @@ internal static class WinMDGeneratorTestHelper { /// /// Compiles the given C# into an assembly and runs the WinMD generator - /// against it, returning the process exit code and combined output. + /// against it with a standard response file, returning the process exit code and combined output. /// /// The C# source defining the authored component types. /// Whether to use the UWP (Windows.UI.Xaml) projections. /// The generator process exit code and its combined standard output and error. public static (int ExitCode, string Output) RunGenerator(string source, bool useWindowsUIXamlProjections = false) { - string toolPath = GetGeneratorPath(); - string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; - - try + return RunGenerator(temporaryDirectory => { - string inputAssemblyPath = Path.Combine(temporaryDirectory, "TestInput.dll"); - string outputWinmdPath = Path.Combine(temporaryDirectory, "TestInput.winmd"); - string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); + string inputAssemblyPath = CompileComponent(source, temporaryDirectory); - CompileInputAssembly(source, inputAssemblyPath); - - // Each line of the response file is a single ' ' pair. The input - // assembly is also passed as its own reference path (its directory seeds the resolver). - File.WriteAllLines(responseFilePath, + return [ $"--input-assembly-path {inputAssemblyPath}", $"--reference-assembly-paths {inputAssemblyPath}", - $"--output-winmd-path {outputWinmdPath}", + $"--output-winmd-path {Path.Combine(temporaryDirectory, "TestInput.winmd")}", "--assembly-version 1.0.0.0", $"--use-windows-ui-xaml-projections {useWindowsUIXamlProjections}", - ]); + ]; + }); + } + + /// + /// Runs the WinMD generator against a caller-provided response file, returning the process exit + /// code and combined output. + /// + /// + /// The receives a fresh temporary working directory and + /// returns the response file lines to run with. It can use to produce + /// an input assembly, or reference any other path under the directory (e.g. to test invalid inputs). + /// + /// Builds the response file lines for a given temporary directory. + /// The generator process exit code and its combined standard output and error. + public static (int ExitCode, string Output) RunGenerator(Func> responseFileFactory) + { + string toolPath = GetGeneratorPath(); + string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; + + try + { + IReadOnlyList responseFileLines = responseFileFactory(temporaryDirectory); + string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); + + File.WriteAllLines(responseFilePath, responseFileLines); return RunTool(toolPath, responseFilePath); } finally { - try - { - Directory.Delete(temporaryDirectory, recursive: true); - } - catch (IOException) - { - // Best effort cleanup; a leftover temp directory must not fail the test - } + TryDeleteDirectory(temporaryDirectory); } } /// - /// Compiles the given C# into an assembly on disk. + /// Runs the WinMD generator pointed at a response file path that does not exist. /// - private static void CompileInputAssembly(string source, string outputPath) + /// The generator process exit code and its combined standard output and error. + public static (int ExitCode, string Output) RunGeneratorWithMissingResponseFile() { + string missingResponseFilePath = Path.Combine(Path.GetTempPath(), $"WinMDGeneratorTest_{Guid.NewGuid():N}.rsp"); + + return RunTool(GetGeneratorPath(), missingResponseFilePath); + } + + /// + /// Compiles the given C# into TestInput.dll in . + /// + /// The C# source defining the authored component types. + /// The directory to emit the assembly into. + /// The full path to the compiled assembly. + public static string CompileComponent(string source, string directory) + { + string outputPath = Path.Combine(directory, "TestInput.dll"); + CSharpParseOptions parseOptions = new(LanguageVersion.Preview); // The target framework attribute lets the generator probe the .NET runtime version. It is added @@ -89,12 +115,14 @@ private static void CompileInputAssembly(string source, string outputPath) EmitResult result = compilation.Emit(outputPath); Assert.IsTrue(result.Success, $"Input compilation failed:\n{string.Join("\n", result.Diagnostics)}"); + + return outputPath; } /// - /// Runs dotnet exec <tool> <responseFile> and captures the exit code and output. + /// Runs dotnet exec <tool> <argument> and captures the exit code and output. /// - private static (int ExitCode, string Output) RunTool(string toolPath, string responseFilePath) + private static (int ExitCode, string Output) RunTool(string toolPath, string argument) { ProcessStartInfo startInfo = new("dotnet") { @@ -106,7 +134,7 @@ private static (int ExitCode, string Output) RunTool(string toolPath, string res startInfo.ArgumentList.Add("exec"); startInfo.ArgumentList.Add(toolPath); - startInfo.ArgumentList.Add(responseFilePath); + startInfo.ArgumentList.Add(argument); using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start the WinMD generator process."); @@ -135,4 +163,19 @@ private static string GetGeneratorPath() return fullPath; } + + /// + /// Deletes a directory recursively, ignoring failures (best effort cleanup). + /// + private static void TryDeleteDirectory(string directory) + { + try + { + Directory.Delete(directory, recursive: true); + } + catch (IOException) + { + // Best effort cleanup; a leftover temp directory must not fail the test + } + } } diff --git a/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs new file mode 100644 index 000000000..35d9b6349 --- /dev/null +++ b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using WinMDGeneratorTest.Helpers; + +namespace WinMDGeneratorTest; + +/// +/// End-to-end tests for the WinMD generator's handling of invalid invocations: malformed or missing +/// response files, bad arguments, missing or corrupt input assemblies, an unwritable output path, and +/// a missing debug-repro directory. +/// +/// +/// Each test runs the actual cswinrtwinmdgen tool and asserts a non-zero exit code and the +/// expected CSWINRTWINMDGEN error in the output. +/// +[TestClass] +public class Test_InvalidInputs +{ + [TestMethod] + public void MissingResponseFile_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGeneratorWithMissingResponseFile(); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0001"); + } + + [TestMethod] + public void MalformedResponseFile_IsReported() + { + // A response file line must be a ' ' pair; a line without a space is invalid + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + [ + "--assembly-version 1.0.0.0", + "this-line-has-no-space", + ]); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0002"); + } + + [TestMethod] + public void DuplicateArgument_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + [ + "--assembly-version 1.0.0.0", + "--assembly-version 2.0.0.0", + ]); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0002"); + } + + [TestMethod] + public void MissingRequiredArgument_IsReported() + { + // The required '--output-winmd-path' argument is omitted + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + [ + "--input-assembly-path input.dll", + "--reference-assembly-paths input.dll", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections False", + ]); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0003"); + } + + [TestMethod] + public void InvalidBooleanArgument_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + [ + "--input-assembly-path input.dll", + "--reference-assembly-paths input.dll", + "--output-winmd-path output.winmd", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections not-a-boolean", + ]); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0003"); + } + + [TestMethod] + public void MissingInputAssembly_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + { + string missingInput = Path.Combine(temporaryDirectory, "DoesNotExist.dll"); + + return + [ + $"--input-assembly-path {missingInput}", + $"--reference-assembly-paths {missingInput}", + $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections False", + ]; + }); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0004"); + } + + [TestMethod] + public void InvalidInputAssembly_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + { + string invalidInput = Path.Combine(temporaryDirectory, "NotAPortableExecutable.dll"); + + File.WriteAllText(invalidInput, "This is not a valid PE image."); + + return + [ + $"--input-assembly-path {invalidInput}", + $"--reference-assembly-paths {invalidInput}", + $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections False", + ]; + }); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0004"); + } + + [TestMethod] + public void UnwritableOutputPath_IsReported() + { + // The output path points at an existing directory, so the '.winmd' cannot be written + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + { + string inputAssemblyPath = WinMDGeneratorTestHelper.CompileComponent(""" + public interface IComponent + { + int Method(int value); + } + """, temporaryDirectory); + + return + [ + $"--input-assembly-path {inputAssemblyPath}", + $"--reference-assembly-paths {inputAssemblyPath}", + $"--output-winmd-path {temporaryDirectory}", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections False", + ]; + }); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0006"); + } + + [TestMethod] + public void MissingDebugReproDirectory_IsReported() + { + (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + { + string inputAssemblyPath = WinMDGeneratorTestHelper.CompileComponent(""" + public interface IComponent + { + int Method(int value); + } + """, temporaryDirectory); + + string missingDebugReproDirectory = Path.Combine(temporaryDirectory, "does-not-exist"); + + return + [ + $"--input-assembly-path {inputAssemblyPath}", + $"--reference-assembly-paths {inputAssemblyPath}", + $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", + "--assembly-version 1.0.0.0", + "--use-windows-ui-xaml-projections False", + $"--debug-repro-directory {missingDebugReproDirectory}", + ]; + }); + + Assert.AreNotEqual(0, exitCode, output); + StringAssert.Contains(output, "CSWINRTWINMDGEN0008"); + } +} From cec048bfa114636ba8dbcc28424c786b07bdc785 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 12:02:57 -0700 Subject: [PATCH 05/14] Move WinMD generator TypeSignature helpers into an extension file Move StripCustomModifiers (previously a private method in WinMDWriter.TypeMapping) and the span predicates into a new Extensions/TypeSignatureExtensions.cs, matching the existing TypeDefinitionExtensions pattern. Rename IsSpan/IsReadOnlySpan to IsTypeOfSpan/IsTypeOfReadOnlySpan as extension methods, consistent with the interop generator's IsTypeOf* extensions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/TypeSignatureExtensions.cs | 54 +++++++++++++++++++ .../Writers/WinMDWriter.Members.cs | 28 ++-------- .../Writers/WinMDWriter.TypeMapping.cs | 22 +------- 3 files changed, 59 insertions(+), 45 deletions(-) create mode 100644 src/WinRT.WinMD.Generator/Extensions/TypeSignatureExtensions.cs diff --git a/src/WinRT.WinMD.Generator/Extensions/TypeSignatureExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/TypeSignatureExtensions.cs new file mode 100644 index 000000000..20e00e309 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Extensions/TypeSignatureExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.WinMDGenerator; + +/// +/// Extension methods for . +/// +internal static class TypeSignatureExtensions +{ + extension(TypeSignature signature) + { + /// + /// Strips any leading custom modifiers (modreq/modopt) from the signature. + /// + /// + /// Custom modifiers carry no Windows Runtime meaning. They are emitted by the C# compiler in a few + /// cases, most notably the modreq(System.Runtime.InteropServices.InAttribute) applied to the + /// by-reference type of an in parameter on abstract, virtual, interface, or delegate members. + /// + /// The underlying with all leading custom modifiers removed. + public TypeSignature StripCustomModifiers() + { + TypeSignature current = signature; + + while (current is CustomModifierTypeSignature customModifier) + { + current = customModifier.BaseType; + } + + return current; + } + + /// + /// Checks whether the signature is some type. + /// + /// if the signature is ; otherwise, . + public bool IsTypeOfSpan() + { + return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.Span`1" }; + } + + /// + /// Checks whether the signature is some type. + /// + /// if the signature is ; otherwise, . + public bool IsTypeOfReadOnlySpan() + { + return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.ReadOnlySpan`1" }; + } + } +} diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs index 4ffd0748b..dd89151f1 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs @@ -196,7 +196,7 @@ private static ParameterAttributes GetWinRTParameterAttributes( { // Look through any custom modifiers (e.g. the 'modreq(InAttribute)' emitted for 'in' parameters // on abstract, virtual, interface, or delegate members) to inspect the real underlying signature - TypeSignature parameterType = StripCustomModifiers(inputParamType); + TypeSignature parameterType = inputParamType.StripCustomModifiers(); // Validate by-reference parameters that wrap a span or array. Windows Runtime spans are always // passed by value, and Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' @@ -204,9 +204,9 @@ private static ParameterAttributes GetWinRTParameterAttributes( // 'ref'/'in' array, has no Windows Runtime representation and is rejected here. if (parameterType is ByReferenceTypeSignature byReference) { - TypeSignature elementType = StripCustomModifiers(byReference.BaseType); + TypeSignature elementType = byReference.BaseType.StripCustomModifiers(); - if (IsSpan(elementType) || IsReadOnlySpan(elementType)) + if (elementType.IsTypeOfSpan() || elementType.IsTypeOfReadOnlySpan()) { throw WellKnownWinMDExceptions.ByReferenceSpanParameterNotSupported( inputMethod.DeclaringType!.FullName, @@ -234,7 +234,7 @@ private static ParameterAttributes GetWinRTParameterAttributes( } // 'Span' → 'FillArray' pattern: '[out]' without 'BYREF' - if (IsSpan(parameterType)) + if (parameterType.IsTypeOfSpan()) { return ParameterAttributes.Out; } @@ -245,26 +245,6 @@ private static ParameterAttributes GetWinRTParameterAttributes( return ParameterAttributes.In; } - /// - /// Determines whether a type signature is . - /// - /// The type signature to test. - /// if is ; otherwise, . - private static bool IsSpan(TypeSignature signature) - { - return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.Span`1" }; - } - - /// - /// Determines whether a type signature is . - /// - /// The type signature to test. - /// if is ; otherwise, . - private static bool IsReadOnlySpan(TypeSignature signature) - { - return signature is GenericInstanceTypeSignature { GenericType.FullName: "System.ReadOnlySpan`1" }; - } - /// /// Adds a property definition to a WinMD type (interface or class). /// diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs index 0b34eb127..c96986017 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs @@ -44,7 +44,7 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSignature) // modifiers have no Windows Runtime representation, and without stripping them a modifier-wrapped // type would fall through to the 'System.Object' fallback at the end of this method, silently // erasing the real signature. - inputSignature = StripCustomModifiers(inputSignature); + inputSignature = inputSignature.StripCustomModifiers(); // Handle 'CorLib' types if (inputSignature is CorLibTypeSignature corLib) @@ -147,26 +147,6 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSignature) return _outputModule.CorLibTypeFactory.Object; } - /// - /// Strips any leading custom modifiers (modreq/modopt) from a type signature. - /// - /// - /// Custom modifiers carry no Windows Runtime meaning. They are emitted by the C# compiler in a few - /// cases, most notably the modreq(System.Runtime.InteropServices.InAttribute) applied to the - /// by-reference type of an in parameter on abstract, virtual, interface, or delegate members. - /// - /// The type signature to unwrap. - /// The underlying with all leading custom modifiers removed. - private static TypeSignature StripCustomModifiers(TypeSignature signature) - { - while (signature is CustomModifierTypeSignature customModifier) - { - signature = customModifier.BaseType; - } - - return signature; - } - /// /// Imports a type reference from the input module to the output module. /// From 85882ae78967f539d8d2c7ebd92ac95258bcad4e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 13:38:50 -0700 Subject: [PATCH 06/14] Add AssertSuccess/AssertFailure helpers to collapse WinMD generator tests Rename the test helper to WinMDGeneratorRunner and give it AssertSuccess/AssertFailure entry points that run the generator and make the exit-code and error-output assertions, so each test is a single call. The run mechanics (Run, RunTool, GetGeneratorPath, the failure-result assertion, cleanup) are private; only the assertion entry points and CompileComponent (used by the response-file factory scenarios) are public. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rTestHelper.cs => WinMDGeneratorRunner.cs} | 134 ++++++++++++------ .../WinMDGeneratorTest/Test_InvalidInputs.cs | 65 +++------ .../Test_ParameterConventions.cs | 35 ++--- 3 files changed, 119 insertions(+), 115 deletions(-) rename src/Tests/WinMDGeneratorTest/Helpers/{WinMDGeneratorTestHelper.cs => WinMDGeneratorRunner.cs} (65%) diff --git a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs similarity index 65% rename from src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs rename to src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs index 75dc8dc24..5c6f02314 100644 --- a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorTestHelper.cs +++ b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs @@ -15,38 +15,40 @@ namespace WinMDGeneratorTest.Helpers; /// -/// Helpers to drive the WinMD generator (cswinrtwinmdgen) end-to-end: compile a small C# input, -/// run the actual tool against it as a separate process, and capture its result. +/// Runs the WinMD generator (cswinrtwinmdgen) end-to-end and asserts its outcome. /// -internal static class WinMDGeneratorTestHelper +/// +/// Each entry point compiles a small C# input (or builds a raw response file), runs the actual tool as +/// a separate process, and asserts the result. Tests should call or one of +/// the AssertFailure overloads so each scenario stays a single call. +/// +internal static class WinMDGeneratorRunner { /// - /// Compiles the given C# into an assembly and runs the WinMD generator - /// against it with a standard response file, returning the process exit code and combined output. + /// Asserts that the generator succeeds (exit code 0) for the given component source. /// /// The C# source defining the authored component types. /// Whether to use the UWP (Windows.UI.Xaml) projections. - /// The generator process exit code and its combined standard output and error. - public static (int ExitCode, string Output) RunGenerator(string source, bool useWindowsUIXamlProjections = false) + public static void AssertSuccess(string source, bool useWindowsUIXamlProjections = false) { - return RunGenerator(temporaryDirectory => - { - string inputAssemblyPath = CompileComponent(source, temporaryDirectory); + (int exitCode, string output) = Run(source, useWindowsUIXamlProjections); - return - [ - $"--input-assembly-path {inputAssemblyPath}", - $"--reference-assembly-paths {inputAssemblyPath}", - $"--output-winmd-path {Path.Combine(temporaryDirectory, "TestInput.winmd")}", - "--assembly-version 1.0.0.0", - $"--use-windows-ui-xaml-projections {useWindowsUIXamlProjections}", - ]; - }); + Assert.AreEqual(0, exitCode, output); + } + + /// + /// Asserts that the generator fails for the given component source, reporting the expected error. + /// + /// The C# source defining the authored component types. + /// The expected error id (e.g. "CSWINRTWINMDGEN0011") the output must contain. + /// Whether to use the UWP (Windows.UI.Xaml) projections. + public static void AssertFailure(string source, string error, bool useWindowsUIXamlProjections = false) + { + AssertFailureResult(Run(source, useWindowsUIXamlProjections), error); } /// - /// Runs the WinMD generator against a caller-provided response file, returning the process exit - /// code and combined output. + /// Asserts that the generator fails for a caller-provided response file, reporting the expected error. /// /// /// The receives a fresh temporary working directory and @@ -54,41 +56,32 @@ public static (int ExitCode, string Output) RunGenerator(string source, bool use /// an input assembly, or reference any other path under the directory (e.g. to test invalid inputs). /// /// Builds the response file lines for a given temporary directory. - /// The generator process exit code and its combined standard output and error. - public static (int ExitCode, string Output) RunGenerator(Func> responseFileFactory) + /// The expected error id (e.g. "CSWINRTWINMDGEN0002") the output must contain. + public static void AssertFailure(Func> responseFileFactory, string error) { - string toolPath = GetGeneratorPath(); - string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; - - try - { - IReadOnlyList responseFileLines = responseFileFactory(temporaryDirectory); - string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); - - File.WriteAllLines(responseFilePath, responseFileLines); - - return RunTool(toolPath, responseFilePath); - } - finally - { - TryDeleteDirectory(temporaryDirectory); - } + AssertFailureResult(Run(responseFileFactory), error); } /// - /// Runs the WinMD generator pointed at a response file path that does not exist. + /// Asserts that the generator fails when pointed at a response file path that does not exist, + /// reporting the expected error. /// - /// The generator process exit code and its combined standard output and error. - public static (int ExitCode, string Output) RunGeneratorWithMissingResponseFile() + /// The expected error id (e.g. "CSWINRTWINMDGEN0001") the output must contain. + public static void AssertFailureForMissingResponseFile(string error) { string missingResponseFilePath = Path.Combine(Path.GetTempPath(), $"WinMDGeneratorTest_{Guid.NewGuid():N}.rsp"); - return RunTool(GetGeneratorPath(), missingResponseFilePath); + AssertFailureResult(RunTool(GetGeneratorPath(), missingResponseFilePath), error); } /// /// Compiles the given C# into TestInput.dll in . /// + /// + /// This is exposed for the + /// scenarios that need a valid input assembly before triggering a later-stage failure (e.g. an + /// unwritable output path). + /// /// The C# source defining the authored component types. /// The directory to emit the assembly into. /// The full path to the compiled assembly. @@ -119,6 +112,59 @@ public static string CompileComponent(string source, string directory) return outputPath; } + /// + /// Compiles the given C# into an assembly and runs the WinMD generator + /// against it with a standard response file. + /// + private static (int ExitCode, string Output) Run(string source, bool useWindowsUIXamlProjections) + { + return Run(temporaryDirectory => + { + string inputAssemblyPath = CompileComponent(source, temporaryDirectory); + + return + [ + $"--input-assembly-path {inputAssemblyPath}", + $"--reference-assembly-paths {inputAssemblyPath}", + $"--output-winmd-path {Path.Combine(temporaryDirectory, "TestInput.winmd")}", + "--assembly-version 1.0.0.0", + $"--use-windows-ui-xaml-projections {useWindowsUIXamlProjections}", + ]; + }); + } + + /// + /// Runs the WinMD generator against a caller-provided response file. + /// + private static (int ExitCode, string Output) Run(Func> responseFileFactory) + { + string toolPath = GetGeneratorPath(); + string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; + + try + { + IReadOnlyList responseFileLines = responseFileFactory(temporaryDirectory); + string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); + + File.WriteAllLines(responseFilePath, responseFileLines); + + return RunTool(toolPath, responseFilePath); + } + finally + { + TryDeleteDirectory(temporaryDirectory); + } + } + + /// + /// Asserts that a generator run failed (non-zero exit code) and its output contains the expected error. + /// + private static void AssertFailureResult((int ExitCode, string Output) result, string error) + { + Assert.AreNotEqual(0, result.ExitCode, result.Output); + StringAssert.Contains(result.Output, error); + } + /// /// Runs dotnet exec <tool> <argument> and captures the exit code and output. /// @@ -151,7 +197,7 @@ private static (int ExitCode, string Output) RunTool(string toolPath, string arg /// private static string GetGeneratorPath() { - string? path = typeof(WinMDGeneratorTestHelper).Assembly + string? path = typeof(WinMDGeneratorRunner).Assembly .GetCustomAttributes() .FirstOrDefault(static attribute => attribute.Key == "WinMDGeneratorAssemblyPath")?.Value; diff --git a/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs index 35d9b6349..9d434003f 100644 --- a/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs +++ b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs @@ -21,75 +21,60 @@ public class Test_InvalidInputs [TestMethod] public void MissingResponseFile_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGeneratorWithMissingResponseFile(); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0001"); + WinMDGeneratorRunner.AssertFailureForMissingResponseFile(error: "CSWINRTWINMDGEN0001"); } [TestMethod] public void MalformedResponseFile_IsReported() { // A response file line must be a ' ' pair; a line without a space is invalid - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + WinMDGeneratorRunner.AssertFailure(static _ => [ "--assembly-version 1.0.0.0", "this-line-has-no-space", - ]); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0002"); + ], error: "CSWINRTWINMDGEN0002"); } [TestMethod] public void DuplicateArgument_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + WinMDGeneratorRunner.AssertFailure(static _ => [ "--assembly-version 1.0.0.0", "--assembly-version 2.0.0.0", - ]); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0002"); + ], error: "CSWINRTWINMDGEN0002"); } [TestMethod] public void MissingRequiredArgument_IsReported() { // The required '--output-winmd-path' argument is omitted - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + WinMDGeneratorRunner.AssertFailure(static _ => [ "--input-assembly-path input.dll", "--reference-assembly-paths input.dll", "--assembly-version 1.0.0.0", "--use-windows-ui-xaml-projections False", - ]); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0003"); + ], error: "CSWINRTWINMDGEN0003"); } [TestMethod] public void InvalidBooleanArgument_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static _ => + WinMDGeneratorRunner.AssertFailure(static _ => [ "--input-assembly-path input.dll", "--reference-assembly-paths input.dll", "--output-winmd-path output.winmd", "--assembly-version 1.0.0.0", "--use-windows-ui-xaml-projections not-a-boolean", - ]); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0003"); + ], error: "CSWINRTWINMDGEN0003"); } [TestMethod] public void MissingInputAssembly_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + WinMDGeneratorRunner.AssertFailure(static temporaryDirectory => { string missingInput = Path.Combine(temporaryDirectory, "DoesNotExist.dll"); @@ -101,16 +86,13 @@ public void MissingInputAssembly_IsReported() "--assembly-version 1.0.0.0", "--use-windows-ui-xaml-projections False", ]; - }); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0004"); + }, error: "CSWINRTWINMDGEN0004"); } [TestMethod] public void InvalidInputAssembly_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + WinMDGeneratorRunner.AssertFailure(static temporaryDirectory => { string invalidInput = Path.Combine(temporaryDirectory, "NotAPortableExecutable.dll"); @@ -124,19 +106,16 @@ public void InvalidInputAssembly_IsReported() "--assembly-version 1.0.0.0", "--use-windows-ui-xaml-projections False", ]; - }); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0004"); + }, error: "CSWINRTWINMDGEN0004"); } [TestMethod] public void UnwritableOutputPath_IsReported() { // The output path points at an existing directory, so the '.winmd' cannot be written - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + WinMDGeneratorRunner.AssertFailure(static temporaryDirectory => { - string inputAssemblyPath = WinMDGeneratorTestHelper.CompileComponent(""" + string inputAssemblyPath = WinMDGeneratorRunner.CompileComponent(""" public interface IComponent { int Method(int value); @@ -151,18 +130,15 @@ public interface IComponent "--assembly-version 1.0.0.0", "--use-windows-ui-xaml-projections False", ]; - }); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0006"); + }, error: "CSWINRTWINMDGEN0006"); } [TestMethod] public void MissingDebugReproDirectory_IsReported() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(static temporaryDirectory => + WinMDGeneratorRunner.AssertFailure(static temporaryDirectory => { - string inputAssemblyPath = WinMDGeneratorTestHelper.CompileComponent(""" + string inputAssemblyPath = WinMDGeneratorRunner.CompileComponent(""" public interface IComponent { int Method(int value); @@ -180,9 +156,6 @@ public interface IComponent "--use-windows-ui-xaml-projections False", $"--debug-repro-directory {missingDebugReproDirectory}", ]; - }); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0008"); + }, error: "CSWINRTWINMDGEN0008"); } } diff --git a/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs index 7398c2781..049ae24e5 100644 --- a/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs +++ b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs @@ -20,30 +20,24 @@ public class Test_ParameterConventions [TestMethod] public void ValidComponent_GeneratesSuccessfully() { - // Smoke test: a valid component generates a '.winmd' (exit code 0). This validates the - // end-to-end harness so the failure assertions below are meaningful. - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + // Smoke test: a valid component generates a '.winmd', validating the end-to-end harness + WinMDGeneratorRunner.AssertSuccess(""" public interface IComponent { int Method(int value); } """); - - Assert.AreEqual(0, exitCode, output); } [TestMethod] public void RefArrayParameter_IsRejected() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + WinMDGeneratorRunner.AssertFailure(""" public interface IComponent { void Method(ref int[] values); } - """); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0011"); + """, error: "CSWINRTWINMDGEN0011"); } [TestMethod] @@ -51,44 +45,35 @@ public void InArrayParameter_IsRejected() { // 'in int[]' carries a 'modreq(InAttribute)' on interface members; the generator must still // see through it to detect the by-reference array. - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + WinMDGeneratorRunner.AssertFailure(""" public interface IComponent { void Method(in int[] values); } - """); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0011"); + """, error: "CSWINRTWINMDGEN0011"); } [TestMethod] public void OutSpanParameter_IsRejected() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + WinMDGeneratorRunner.AssertFailure(""" using System; public interface IComponent { void Method(out Span values); } - """); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0012"); + """, error: "CSWINRTWINMDGEN0012"); } [TestMethod] public void OutReadOnlySpanParameter_IsRejected() { - (int exitCode, string output) = WinMDGeneratorTestHelper.RunGenerator(""" + WinMDGeneratorRunner.AssertFailure(""" using System; public interface IComponent { void Method(out ReadOnlySpan values); } - """); - - Assert.AreNotEqual(0, exitCode, output); - StringAssert.Contains(output, "CSWINRTWINMDGEN0012"); + """, error: "CSWINRTWINMDGEN0012"); } } From 3d824e4ac6226338787e33f0c6b9ff862ce8af97 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 13:42:52 -0700 Subject: [PATCH 07/14] Take full response file content as a single string in WinMD generator tests Change the AssertFailure response-file factory from Func> to Func so callers provide the entire '.rsp' content as one multiline raw interpolated string instead of a list of per-line expressions. The runner now writes the content verbatim with File.WriteAllText. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/WinMDGeneratorRunner.cs | 33 +++--- .../WinMDGeneratorTest/Test_InvalidInputs.cs | 108 ++++++++---------- 2 files changed, 66 insertions(+), 75 deletions(-) diff --git a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs index 5c6f02314..aaf4516f9 100644 --- a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs +++ b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -52,12 +51,13 @@ public static void AssertFailure(string source, string error, bool useWindowsUIX /// /// /// The receives a fresh temporary working directory and - /// returns the response file lines to run with. It can use to produce - /// an input assembly, or reference any other path under the directory (e.g. to test invalid inputs). + /// returns the full response file content to run with (one --argument value pair per line). It + /// can use to produce an input assembly, or reference any other path + /// under the directory (e.g. to test invalid inputs). /// - /// Builds the response file lines for a given temporary directory. + /// Builds the full response file content for a given temporary directory. /// The expected error id (e.g. "CSWINRTWINMDGEN0002") the output must contain. - public static void AssertFailure(Func> responseFileFactory, string error) + public static void AssertFailure(Func responseFileFactory, string error) { AssertFailureResult(Run(responseFileFactory), error); } @@ -78,7 +78,7 @@ public static void AssertFailureForMissingResponseFile(string error) /// Compiles the given C# into TestInput.dll in . /// /// - /// This is exposed for the + /// This is exposed for the /// scenarios that need a valid input assembly before triggering a later-stage failure (e.g. an /// unwritable output path). /// @@ -122,31 +122,30 @@ private static (int ExitCode, string Output) Run(string source, bool useWindowsU { string inputAssemblyPath = CompileComponent(source, temporaryDirectory); - return - [ - $"--input-assembly-path {inputAssemblyPath}", - $"--reference-assembly-paths {inputAssemblyPath}", - $"--output-winmd-path {Path.Combine(temporaryDirectory, "TestInput.winmd")}", - "--assembly-version 1.0.0.0", - $"--use-windows-ui-xaml-projections {useWindowsUIXamlProjections}", - ]; + return $""" + --input-assembly-path {inputAssemblyPath} + --reference-assembly-paths {inputAssemblyPath} + --output-winmd-path {Path.Combine(temporaryDirectory, "TestInput.winmd")} + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections {useWindowsUIXamlProjections} + """; }); } /// /// Runs the WinMD generator against a caller-provided response file. /// - private static (int ExitCode, string Output) Run(Func> responseFileFactory) + private static (int ExitCode, string Output) Run(Func responseFileFactory) { string toolPath = GetGeneratorPath(); string temporaryDirectory = Directory.CreateTempSubdirectory("WinMDGeneratorTest_").FullName; try { - IReadOnlyList responseFileLines = responseFileFactory(temporaryDirectory); + string responseFileContent = responseFileFactory(temporaryDirectory); string responseFilePath = Path.Combine(temporaryDirectory, "args.rsp"); - File.WriteAllLines(responseFilePath, responseFileLines); + File.WriteAllText(responseFilePath, responseFileContent); return RunTool(toolPath, responseFilePath); } diff --git a/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs index 9d434003f..d48a91e36 100644 --- a/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs +++ b/src/Tests/WinMDGeneratorTest/Test_InvalidInputs.cs @@ -28,47 +28,43 @@ public void MissingResponseFile_IsReported() public void MalformedResponseFile_IsReported() { // A response file line must be a ' ' pair; a line without a space is invalid - WinMDGeneratorRunner.AssertFailure(static _ => - [ - "--assembly-version 1.0.0.0", - "this-line-has-no-space", - ], error: "CSWINRTWINMDGEN0002"); + WinMDGeneratorRunner.AssertFailure(static _ => """ + --assembly-version 1.0.0.0 + this-line-has-no-space + """, error: "CSWINRTWINMDGEN0002"); } [TestMethod] public void DuplicateArgument_IsReported() { - WinMDGeneratorRunner.AssertFailure(static _ => - [ - "--assembly-version 1.0.0.0", - "--assembly-version 2.0.0.0", - ], error: "CSWINRTWINMDGEN0002"); + WinMDGeneratorRunner.AssertFailure(static _ => """ + --assembly-version 1.0.0.0 + --assembly-version 2.0.0.0 + """, error: "CSWINRTWINMDGEN0002"); } [TestMethod] public void MissingRequiredArgument_IsReported() { // The required '--output-winmd-path' argument is omitted - WinMDGeneratorRunner.AssertFailure(static _ => - [ - "--input-assembly-path input.dll", - "--reference-assembly-paths input.dll", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections False", - ], error: "CSWINRTWINMDGEN0003"); + WinMDGeneratorRunner.AssertFailure(static _ => """ + --input-assembly-path input.dll + --reference-assembly-paths input.dll + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections False + """, error: "CSWINRTWINMDGEN0003"); } [TestMethod] public void InvalidBooleanArgument_IsReported() { - WinMDGeneratorRunner.AssertFailure(static _ => - [ - "--input-assembly-path input.dll", - "--reference-assembly-paths input.dll", - "--output-winmd-path output.winmd", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections not-a-boolean", - ], error: "CSWINRTWINMDGEN0003"); + WinMDGeneratorRunner.AssertFailure(static _ => """ + --input-assembly-path input.dll + --reference-assembly-paths input.dll + --output-winmd-path output.winmd + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections not-a-boolean + """, error: "CSWINRTWINMDGEN0003"); } [TestMethod] @@ -78,14 +74,13 @@ public void MissingInputAssembly_IsReported() { string missingInput = Path.Combine(temporaryDirectory, "DoesNotExist.dll"); - return - [ - $"--input-assembly-path {missingInput}", - $"--reference-assembly-paths {missingInput}", - $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections False", - ]; + return $""" + --input-assembly-path {missingInput} + --reference-assembly-paths {missingInput} + --output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")} + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections False + """; }, error: "CSWINRTWINMDGEN0004"); } @@ -98,14 +93,13 @@ public void InvalidInputAssembly_IsReported() File.WriteAllText(invalidInput, "This is not a valid PE image."); - return - [ - $"--input-assembly-path {invalidInput}", - $"--reference-assembly-paths {invalidInput}", - $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections False", - ]; + return $""" + --input-assembly-path {invalidInput} + --reference-assembly-paths {invalidInput} + --output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")} + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections False + """; }, error: "CSWINRTWINMDGEN0004"); } @@ -122,14 +116,13 @@ public interface IComponent } """, temporaryDirectory); - return - [ - $"--input-assembly-path {inputAssemblyPath}", - $"--reference-assembly-paths {inputAssemblyPath}", - $"--output-winmd-path {temporaryDirectory}", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections False", - ]; + return $""" + --input-assembly-path {inputAssemblyPath} + --reference-assembly-paths {inputAssemblyPath} + --output-winmd-path {temporaryDirectory} + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections False + """; }, error: "CSWINRTWINMDGEN0006"); } @@ -147,15 +140,14 @@ public interface IComponent string missingDebugReproDirectory = Path.Combine(temporaryDirectory, "does-not-exist"); - return - [ - $"--input-assembly-path {inputAssemblyPath}", - $"--reference-assembly-paths {inputAssemblyPath}", - $"--output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")}", - "--assembly-version 1.0.0.0", - "--use-windows-ui-xaml-projections False", - $"--debug-repro-directory {missingDebugReproDirectory}", - ]; + return $""" + --input-assembly-path {inputAssemblyPath} + --reference-assembly-paths {inputAssemblyPath} + --output-winmd-path {Path.Combine(temporaryDirectory, "out.winmd")} + --assembly-version 1.0.0.0 + --use-windows-ui-xaml-projections False + --debug-repro-directory {missingDebugReproDirectory} + """; }, error: "CSWINRTWINMDGEN0008"); } } From 6cad433b0fdca017fa0e8cf9322a499e4840950f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 13:47:55 -0700 Subject: [PATCH 08/14] Document the WinMDGeneratorTest project in the testing skill Add a 'WinMD generator tests' section describing the new src/Tests/WinMDGeneratorTest project (end-to-end failure-case tests for cswinrtwinmdgen), add a routing-table row, and bump the primary test area count to 6. Also add a matching per-project verification step to the update-testing-instructions skill and renumber the subsequent steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/testing/SKILL.md | 42 ++++++++++++++++++- .../update-testing-instructions/SKILL.md | 27 ++++++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md index 7875decfe..0349df71b 100644 --- a/.github/skills/testing/SKILL.md +++ b/.github/skills/testing/SKILL.md @@ -13,7 +13,7 @@ Before adding tests, always check whether tests for the same functionality alrea ## Test project overview -CsWinRT 3.0 has 6 primary test project areas, each serving a different purpose. Additional specialized test projects also exist under `src/Tests/`: +CsWinRT 3.0 has 7 primary test project areas, each serving a different purpose. Additional specialized test projects also exist under `src/Tests/`: ### 1. Unit tests (`src/Tests/UnitTest/`) @@ -244,6 +244,45 @@ public async Task InvalidType_Warns() **How they run:** `run-smoke-tests.ps1` (parameterized by `-Test` and `-Runtime`) builds and runs the consumption app (asserting a clean exit code), builds the authoring component and verifies the generated `Authoring.winmd` defines `Authoring.Greeter`, and builds each reference-projection library (`Projection`, `WindowsSdkProjection`, `WindowsSdkXamlProjection`) verifying it produces both a forwarder and a `ref` reference assembly (shared verification). The consumption and authoring tests run on both CoreCLR and Native AOT (`-Runtime`); the three reference-projection tests are build-only and run on CoreCLR only. It is invoked after the `nuget pack` step in `src/build.cmd` (x64 only; skippable via `cswinrt_run_smoke_tests=false`) and as individual steps in `build/AzurePipelineTemplates/CsWinRT-PublishToNuGet-Steps.yml`. +### 7. WinMD generator tests (`src/Tests/WinMDGeneratorTest/`) + +**What it tests:** End-to-end behavior of the `cswinrtwinmdgen` build tool (the WinMD generator). It focuses on **failure cases** — unsupported authored component shapes and invalid tool invocations — that can be asserted without breaking a real build. + +**When to add tests here:** For testing a WinMD generator failure mode — a new `CSWINRTWINMDGEN` validation error, an unsupported authoring pattern, or a malformed/invalid invocation. Authored shapes that produce a valid `.winmd` are covered by the authoring tests (`AuthoringTest/`) instead. + +**Project settings:** +- **Test framework:** MSTest (`[TestClass]`, `[TestMethod]`, `Assert.*`) +- **TFM:** `net10.0`, multi-platform (x64/x86) +- **Output type:** Exe, `CsWinRTEnabled=false` +- **Key dependencies:** MSTest packages, `Basic.Reference.Assemblies.Net100` and `Microsoft.CodeAnalysis.CSharp` (used to compile the C# inputs) +- **References:** `WinRT.WinMD.Generator` via `ProjectReference` with `ReferenceOutputAssembly="false"` and `GlobalPropertiesToRemove="RuntimeIdentifier"` — the tool is **built but not referenced**, and tests invoke it as a separate process. Its path is passed to the tests via an `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`). + +**Test classes:** +| Test class | What it tests | +|------------|---------------| +| `Test_ParameterConventions` | Unsupported array/span parameter shapes (`ref`/`in` arrays, `out` spans) → `CSWINRTWINMDGEN0011`/`0012`, plus a valid smoke test | +| `Test_InvalidInputs` | Invalid invocations: malformed/missing response files, bad arguments, missing/corrupt input assemblies, an unwritable output path, a missing debug-repro directory | + +**Test helper (in `Helpers/`):** +- `WinMDGeneratorRunner` — compiles a C# input, runs the actual tool as a subprocess, and asserts the outcome. Public entry points: `AssertSuccess`, `AssertFailure` (from component source or full response-file content), `AssertFailureForMissingResponseFile`, and `CompileComponent`. + +**Pattern:** +```csharp +[TestMethod] +public void RefArrayParameter_IsRejected() +{ + WinMDGeneratorRunner.AssertFailure(""" + public interface IComponent + { + void Method(ref int[] values); + } + """, error: "CSWINRTWINMDGEN0011"); +} +``` + +- Each test is a single `AssertSuccess`/`AssertFailure` call; the runner makes the exit-code and error-output assertions +- Failure cases assert the tool exits non-zero and its output contains the expected `CSWINRTWINMDGEN` error id + ## Deciding where to add tests | You want to test... | Add test to... | @@ -258,6 +297,7 @@ public async Task InvalidType_Warns() | GC/reference tracking behavior | `ObjectLifetimeTests/` | | XAML visual tree element lifetime | `ObjectLifetimeTests/` | | WinRT component authoring patterns | `AuthoringTest/` | +| A WinMD generator failure mode (a `CSWINRTWINMDGEN` error) | `WinMDGeneratorTest/` (add to `Test_ParameterConventions` or `Test_InvalidInputs`) | | The produced NuGet package works end-to-end (real `ref`/`lib` assemblies, generators) | `SmokeTests/` (`Consumption/`, `Authoring/`, or a reference-projection project) | | Generated projection code patterns or cross-ABI control flow | Update `TestComponentCSharp/` and add tests in `UnitTest/` or `FunctionalTests/` | diff --git a/.github/skills/update-testing-instructions/SKILL.md b/.github/skills/update-testing-instructions/SKILL.md index 5f2b42d2a..785a09d54 100644 --- a/.github/skills/update-testing-instructions/SKILL.md +++ b/.github/skills/update-testing-instructions/SKILL.md @@ -86,7 +86,19 @@ Launch an explore agent to verify: - **"Referenced from" list** is current - **"When to update" guidance** is still correct -### Step 9: verify the "deciding where to add tests" table +### Step 9: verify WinMD generator tests (`src/Tests/WinMDGeneratorTest/`) + +Launch an explore agent to analyze the WinMD generator test project. Verify: + +- **Project settings** are current: test framework, TFM, platforms, output type, `CsWinRTEnabled` setting +- **Tool wiring** is accurate: the `WinRT.WinMD.Generator` `ProjectReference` is built but not referenced (`ReferenceOutputAssembly="false"` + `GlobalPropertiesToRemove="RuntimeIdentifier"`), and the `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`) passes the built tool's path to the tests +- **Key dependencies** are current (MSTest packages, `Basic.Reference.Assemblies.Net100`, `Microsoft.CodeAnalysis.CSharp`) +- **Test class table** is complete and accurate — check all `Test_*.cs` files, verify no classes have been added, removed, or renamed, and verify each class's description matches its actual content +- **Test helper** (`WinMDGeneratorRunner` in `Helpers/`) is accurately described, including its public entry points (`AssertSuccess`, `AssertFailure`, `AssertFailureForMissingResponseFile`, `CompileComponent`) +- **Pattern example** matches the actual single-call `AssertSuccess`/`AssertFailure` style +- **Error ids** referenced (`CSWINRTWINMDGEN*`) are still produced by the generator + +### Step 10: verify the "deciding where to add tests" table Check every row in the routing table against the actual test projects. Verify: @@ -95,7 +107,7 @@ Check every row in the routing table against the actual test projects. Verify: - No new test project categories exist that should be added as rows - The `TestComponentCSharp` guidance is still accurate -### Step 10: verify code examples +### Step 11: verify code examples For each code example in the testing skill, verify it matches the conventions actually used in the test files: @@ -103,8 +115,9 @@ For each code example in the testing skill, verify it matches the conventions ac - **Functional test pattern**: verify the example uses the correct exit codes and top-level statement style - **Generator test pattern**: verify the `VerifySources` call matches the actual method signature and style. Check an actual test method in `Test_CustomPropertyProviderGenerator` to confirm - **Analyzer test pattern**: verify the `VerifyAnalyzerAsync` call matches the actual method signature and style. Check an actual test method in the analyzer test files to confirm +- **WinMD generator test pattern**: verify the single-call `AssertSuccess`/`AssertFailure` example matches the actual style. Check an actual test method in `Test_ParameterConventions` or `Test_InvalidInputs` to confirm -### Step 11: update the testing instructions +### Step 12: update the testing instructions Apply surgical edits to `.github/skills/testing/SKILL.md` to fix any discrepancies found. Typical updates include: @@ -125,17 +138,17 @@ Apply surgical edits to `.github/skills/testing/SKILL.md` to fix any discrepanci - Do not add unnecessary commentary or explanation beyond what's needed for test placement guidance -### Step 12: update this skill if needed +### Step 13: update this skill if needed If significant changes to the test suite were discovered (e.g. test projects added or removed, new categories of tests, changed validation criteria), also update this skill file (`.github/skills/update-testing-instructions/SKILL.md`) to reflect those changes. In particular: -- The **per-project verification steps** (steps 2–8) must stay in sync with the actual test projects. If a test project is added or removed, add or remove its verification step accordingly. +- The **per-project verification steps** (steps 2–9) must stay in sync with the actual test projects. If a test project is added or removed, add or remove its verification step accordingly. - The **verification criteria** for each step should reflect what is actually worth checking. If a project gains new aspects worth validating (e.g. new test helper classes, new build settings), add those to the checklist. -- The **code example verification step** (step 10) should list all code examples present in the testing skill. +- The **code example verification step** (step 11) should list all code examples present in the testing skill. This ensures the skill remains useful and accurate for future runs. -### Step 13: summarize changes +### Step 14: summarize changes After editing, provide a clear summary of what was updated and why, so the user can review the changes before committing. From 8ad0ebdbde0d6dac00a41cdab18041efdeda7cb8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 13:47:55 -0700 Subject: [PATCH 09/14] Fix stale comment in WinMDGeneratorTest project file The tests run the generator tool end-to-end as a subprocess rather than exercising its managed logic in-process, so update the comment that justifies 'CsWinRTEnabled=false' to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj index c70c6a443..0ae7ab50b 100644 --- a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj +++ b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj @@ -7,7 +7,7 @@ enable false - + false From 1598ed3b826e60e91cdd4af2ff7336b343d654e6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 13:57:25 -0700 Subject: [PATCH 10/14] Move new WinMD generator errors after the existing ones Define the by-reference array/span parameter errors (0011/0012) after the debug-repro errors (0008-0010) instead of in the middle, so the methods appear in ascending error-id order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Errors/WellKnownWinMDExceptions.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index b776768d7..6ce08065d 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -73,22 +73,6 @@ public static Exception InputAssemblyRuntimeVersionNotFound(string path) return Exception(7, $"Failed to probe the .NET runtime version from the input assembly '{path}'."); } - /// - /// A method has a ref or in array parameter, which is not a valid Windows Runtime array convention. - /// - public static Exception ByReferenceArrayParameterNotSupported(string declaringTypeName, string methodName, string parameterName) - { - return Exception(11, $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), 'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); - } - - /// - /// A method has a by-reference span parameter (e.g. out Span<T>), which has no Windows Runtime representation. - /// - public static Exception ByReferenceSpanParameterNotSupported(string declaringTypeName, string methodName, string parameterName) - { - return Exception(12, $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'. Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' (FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); - } - /// public static Exception DebugReproDirectoryDoesNotExist(string path) { @@ -119,6 +103,22 @@ public static Exception CannotReadWindowsSdkXml(string path) return Exception(12, WellKnownGeneratorMessages.CannotReadWindowsSdkXml(path)); } + /// + /// A method has a ref or in array parameter, which is not a valid Windows Runtime array convention. + /// + public static Exception ByReferenceArrayParameterNotSupported(string declaringTypeName, string methodName, string parameterName) + { + return Exception(11, $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), 'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); + } + + /// + /// A method has a by-reference span parameter (e.g. out Span<T>), which has no Windows Runtime representation. + /// + public static Exception ByReferenceSpanParameterNotSupported(string declaringTypeName, string methodName, string parameterName) + { + return Exception(12, $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'. Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' (FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); + } + /// /// Creates a new exception with the specified id and message. /// From 2f5534ec4d50df1d895ae4164f6b2d1710b3f04d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Jun 2026 15:23:37 -0700 Subject: [PATCH 11/14] Build the WinMD generator framework-dependent in the test project (NETSDK1151) The generator's PublishAot build is self-contained, which a non self-contained executable cannot reference (NETSDK1151). Replace GlobalPropertiesToRemove for RuntimeIdentifier with UndefineProperties for BuildToolArch, PublishBuildTool, RuntimeIdentifier and SelfContained so the tool is built framework-dependent for the build host, matching the proven WinRT.Internal reference pattern. The output stays under net10.0 (no RID subfolder), so the tool path used by the tests is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinMDGeneratorTest/WinMDGeneratorTest.csproj | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj index 0ae7ab50b..bc2bb655f 100644 --- a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj +++ b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj @@ -41,13 +41,19 @@ + OutputItemType="_CsWinRTToolReference" + UndefineProperties="BuildToolArch;PublishBuildTool;RuntimeIdentifier;SelfContained" + Private="false" /> From cd6c033e1d3665ad382e9f6810e9cc9c56d2b136 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 17 Jun 2026 14:10:14 -0700 Subject: [PATCH 12/14] Wrap long exception messages for readability Split long interpolated exception message strings into multi-line concatenated strings in src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs (ByReferenceArrayParameterNotSupported and ByReferenceSpanParameterNotSupported) to improve readability and line-length compliance; no functional behavior changes. --- .../Errors/WellKnownWinMDExceptions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 6ce08065d..82737674f 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -108,7 +108,10 @@ public static Exception CannotReadWindowsSdkXml(string path) /// public static Exception ByReferenceArrayParameterNotSupported(string declaringTypeName, string methodName, string parameterName) { - return Exception(11, $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), 'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); + return Exception(11, + $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. " + + $"Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), " + + $"'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); } /// @@ -116,7 +119,10 @@ public static Exception ByReferenceArrayParameterNotSupported(string declaringTy /// public static Exception ByReferenceSpanParameterNotSupported(string declaringTypeName, string methodName, string parameterName) { - return Exception(12, $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'. Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' (FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); + return Exception(12, + $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'." + + $"Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' " + + $"(FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); } /// From 67f517ce8f253dbb8143d2a6e8351ae7359a71ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 28 Jul 2026 10:14:57 -0700 Subject: [PATCH 13/14] Adapt the WinMD generator parameter validation to staging/3.0 The custom overload names change (#2454) took error ids 0011 and 0012 for 'WindowsSdkNotFound' and 'CannotReadWindowsSdkXml', so renumber the new by-reference array/span parameter errors to 0013 and 0014 and update the tests and the testing skill accordingly. Also update the new test project for the build changes on staging: drop 'LangVersion' (now centralized in 'src/Directory.Build.props') and drop the removed 'ARM' platform from the solution entry (#2472, #2489). Finally, fix the stale 'GlobalPropertiesToRemove' mention in the testing skill, which the project file replaced with 'UndefineProperties'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ec1fee3-01c2-4213-baf0-bed82d056068 --- .github/skills/testing/SKILL.md | 6 +++--- .github/skills/update-testing-instructions/SKILL.md | 2 +- .../WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs | 2 +- src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs | 8 ++++---- src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj | 1 - .../Errors/WellKnownWinMDExceptions.cs | 4 ++-- src/cswinrt.slnx | 4 +--- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md index 0349df71b..0ca2b5d30 100644 --- a/.github/skills/testing/SKILL.md +++ b/.github/skills/testing/SKILL.md @@ -255,12 +255,12 @@ public async Task InvalidType_Warns() - **TFM:** `net10.0`, multi-platform (x64/x86) - **Output type:** Exe, `CsWinRTEnabled=false` - **Key dependencies:** MSTest packages, `Basic.Reference.Assemblies.Net100` and `Microsoft.CodeAnalysis.CSharp` (used to compile the C# inputs) -- **References:** `WinRT.WinMD.Generator` via `ProjectReference` with `ReferenceOutputAssembly="false"` and `GlobalPropertiesToRemove="RuntimeIdentifier"` — the tool is **built but not referenced**, and tests invoke it as a separate process. Its path is passed to the tests via an `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`). +- **References:** `WinRT.WinMD.Generator` via `ProjectReference` with `ReferenceOutputAssembly="false"` and `UndefineProperties="BuildToolArch;PublishBuildTool;RuntimeIdentifier;SelfContained"` — the tool is **built but not referenced**, and tests invoke it as a separate process. Its path is passed to the tests via an `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`). **Test classes:** | Test class | What it tests | |------------|---------------| -| `Test_ParameterConventions` | Unsupported array/span parameter shapes (`ref`/`in` arrays, `out` spans) → `CSWINRTWINMDGEN0011`/`0012`, plus a valid smoke test | +| `Test_ParameterConventions` | Unsupported array/span parameter shapes (`ref`/`in` arrays, `out` spans) → `CSWINRTWINMDGEN0013`/`0014`, plus a valid smoke test | | `Test_InvalidInputs` | Invalid invocations: malformed/missing response files, bad arguments, missing/corrupt input assemblies, an unwritable output path, a missing debug-repro directory | **Test helper (in `Helpers/`):** @@ -276,7 +276,7 @@ public void RefArrayParameter_IsRejected() { void Method(ref int[] values); } - """, error: "CSWINRTWINMDGEN0011"); + """, error: "CSWINRTWINMDGEN0013"); } ``` diff --git a/.github/skills/update-testing-instructions/SKILL.md b/.github/skills/update-testing-instructions/SKILL.md index 785a09d54..2dee7b2ef 100644 --- a/.github/skills/update-testing-instructions/SKILL.md +++ b/.github/skills/update-testing-instructions/SKILL.md @@ -91,7 +91,7 @@ Launch an explore agent to verify: Launch an explore agent to analyze the WinMD generator test project. Verify: - **Project settings** are current: test framework, TFM, platforms, output type, `CsWinRTEnabled` setting -- **Tool wiring** is accurate: the `WinRT.WinMD.Generator` `ProjectReference` is built but not referenced (`ReferenceOutputAssembly="false"` + `GlobalPropertiesToRemove="RuntimeIdentifier"`), and the `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`) passes the built tool's path to the tests +- **Tool wiring** is accurate: the `WinRT.WinMD.Generator` `ProjectReference` is built but not referenced (`ReferenceOutputAssembly="false"` + `UndefineProperties="BuildToolArch;PublishBuildTool;RuntimeIdentifier;SelfContained"`), and the `AssemblyMetadata` item (`WinMDGeneratorAssemblyPath`) passes the built tool's path to the tests - **Key dependencies** are current (MSTest packages, `Basic.Reference.Assemblies.Net100`, `Microsoft.CodeAnalysis.CSharp`) - **Test class table** is complete and accurate — check all `Test_*.cs` files, verify no classes have been added, removed, or renamed, and verify each class's description matches its actual content - **Test helper** (`WinMDGeneratorRunner` in `Helpers/`) is accurately described, including its public entry points (`AssertSuccess`, `AssertFailure`, `AssertFailureForMissingResponseFile`, `CompileComponent`) diff --git a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs index aaf4516f9..fade9b3c2 100644 --- a/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs +++ b/src/Tests/WinMDGeneratorTest/Helpers/WinMDGeneratorRunner.cs @@ -39,7 +39,7 @@ public static void AssertSuccess(string source, bool useWindowsUIXamlProjections /// Asserts that the generator fails for the given component source, reporting the expected error. /// /// The C# source defining the authored component types. - /// The expected error id (e.g. "CSWINRTWINMDGEN0011") the output must contain. + /// The expected error id (e.g. "CSWINRTWINMDGEN0013") the output must contain. /// Whether to use the UWP (Windows.UI.Xaml) projections. public static void AssertFailure(string source, string error, bool useWindowsUIXamlProjections = false) { diff --git a/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs index 049ae24e5..0daf582a1 100644 --- a/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs +++ b/src/Tests/WinMDGeneratorTest/Test_ParameterConventions.cs @@ -37,7 +37,7 @@ public interface IComponent { void Method(ref int[] values); } - """, error: "CSWINRTWINMDGEN0011"); + """, error: "CSWINRTWINMDGEN0013"); } [TestMethod] @@ -50,7 +50,7 @@ public interface IComponent { void Method(in int[] values); } - """, error: "CSWINRTWINMDGEN0011"); + """, error: "CSWINRTWINMDGEN0013"); } [TestMethod] @@ -62,7 +62,7 @@ public interface IComponent { void Method(out Span values); } - """, error: "CSWINRTWINMDGEN0012"); + """, error: "CSWINRTWINMDGEN0014"); } [TestMethod] @@ -74,6 +74,6 @@ public interface IComponent { void Method(out ReadOnlySpan values); } - """, error: "CSWINRTWINMDGEN0012"); + """, error: "CSWINRTWINMDGEN0014"); } } diff --git a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj index bc2bb655f..16faeafcd 100644 --- a/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj +++ b/src/Tests/WinMDGeneratorTest/WinMDGeneratorTest.csproj @@ -3,7 +3,6 @@ Exe net10.0 x64;x86 - preview enable false diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 82737674f..5edf471ce 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -108,7 +108,7 @@ public static Exception CannotReadWindowsSdkXml(string path) /// public static Exception ByReferenceArrayParameterNotSupported(string declaringTypeName, string methodName, string parameterName) { - return Exception(11, + return Exception(13, $"Method '{declaringTypeName}.{methodName}' has by-reference array parameter '{parameterName}' passed by 'ref' or 'in'. " + $"Windows Runtime arrays use one of three conventions: 'ReadOnlySpan' for a read-only input array (PassArray), " + $"'Span' for a caller-allocated array (FillArray), or 'out T[]' for a callee-allocated array (ReceiveArray)."); @@ -119,7 +119,7 @@ public static Exception ByReferenceArrayParameterNotSupported(string declaringTy /// public static Exception ByReferenceSpanParameterNotSupported(string declaringTypeName, string methodName, string parameterName) { - return Exception(12, + return Exception(14, $"Method '{declaringTypeName}.{methodName}' has by-reference span parameter '{parameterName}'." + $"Windows Runtime spans are passed by value: use 'ReadOnlySpan' (PassArray) or 'Span' " + $"(FillArray) by value, or 'out T[]' for a callee-allocated array (ReceiveArray)."); diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index b8c76d9ee..f5fd28721 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -274,11 +274,9 @@ - - + - From 3ca6d933bebdf367f2de87a9486e867c3589fd1c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 28 Jul 2026 10:30:39 -0700 Subject: [PATCH 14/14] Update the WinMD generator error id range in the instructions The range was already stale at '0001'-'0010': the custom overload names change (#2454) added 0011 and 0012. This adds 0013 and 0014, so bump the documented range to '0001'-'0014'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ec1fee3-01c2-4213-baf0-bed82d056068 --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aec7eaa33..40ef01adf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -737,7 +737,7 @@ All five .NET build tools (`cswinrtprojectionrefgen`, `cswinrtprojectiongen`, `c | Projection Writer (library) | `CSWINRTPROJECTIONGEN5xxx` | `5003`–`5021`, `9999` (shares the `CSWINRTPROJECTIONGEN` prefix with the host; the writer reserves the 5000+ range so the two never collide) | | Impl Generator | `CSWINRTIMPLGENxxxx` | `0001`–`0014`, `9999` | | Interop Generator | `CSWINRTINTEROPGENxxxx` | `0001`–`0100`, `9999` | -| WinMD Generator | `CSWINRTWINMDGENxxxx` | `0001`–`0010`, `9999` | +| WinMD Generator | `CSWINRTWINMDGENxxxx` | `0001`–`0014`, `9999` | | Runtime (obsolete markers) | `CSWINRT3xxx` | `CSWINRT3001` (deriving from `WindowsRuntimeObject`), `CSWINRT3002` (type map group types), `CSWINRT3003` (component authoring attributes), `CSWINRT3004` (`WindowsRuntimeReferenceAssemblyAttribute`) | ---