diff --git a/src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs b/src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs index 54075fab09..3092386fce 100644 --- a/src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs +++ b/src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs @@ -184,7 +184,10 @@ public static string GenerateTypeInfo(ITypeSymbol typeSymbol) /// private static string GetTypeOfExpression(ITypeSymbol typeSymbol) { - var fullyQualifiedName = typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix); + // typeof(dynamic) does not compile (CS1962); dynamic is object at runtime. + var fullyQualifiedName = typeSymbol.TypeKind == TypeKind.Dynamic + ? "object" + : typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix); return $"typeof({fullyQualifiedName})"; } diff --git a/src/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs b/src/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs index 7a5fe268e8..a1300cb836 100644 --- a/src/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs +++ b/src/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs @@ -125,6 +125,14 @@ public static bool IsIEnumerable(this ITypeSymbol namedTypeSymbol, Compilation c public static string GloballyQualified(this ISymbol typeSymbol) { + // `dynamic` is `object` at runtime, and `typeof(dynamic)` does not compile (CS1962). Rendering it as `object` + // also binds generated calls statically instead of through the runtime binder. Nested uses such as + // `List` or `dynamic[]` are valid everywhere, including inside typeof, so only the top level changes. + if (typeSymbol is ITypeSymbol { TypeKind: TypeKind.Dynamic }) + { + return "object"; + } + // Handle open generic types where type arguments are type parameters // This prevents invalid C# like List, Dictionary, T? where type parameters are undefined if (typeSymbol is INamedTypeSymbol { IsGenericType: true, Arity: > 0 } namedTypeSymbol) diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7165/Tests7165.cs b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7165/Tests7165.cs new file mode 100644 index 0000000000..fe5914f6ad --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/Bugs/7165/Tests7165.cs @@ -0,0 +1,25 @@ +namespace TUnit.Core.SourceGenerator.Tests; + +/// +/// Snapshot coverage for https://github.com/thomhurst/TUnit/issues/7165: top-level dynamic parameters, +/// constructor parameters and properties are rendered as object, while nested uses such as +/// List<dynamic> and dynamic[] keep their spelling. +/// checks that the generated code compiles. +/// +internal class Tests7165 : TestsBase +{ + [Test] + public Task Test() => RunTest(Path.Combine( + Git.TestsDirectory.FullName, + "TUnit.TestProject", + "Bugs", + "_7165", + "DynamicParameterTests.cs"), + async generatedFiles => + { + var source = string.Join(Environment.NewLine, generatedFiles); + + await Assert.That(source).DoesNotContain("typeof(dynamic)"); + await Assert.That(source).Contains("typeof(object)"); + }); +} diff --git a/tests/TUnit.Core.SourceGenerator.Tests/DynamicParameterCompilationTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/DynamicParameterCompilationTests.cs new file mode 100644 index 0000000000..93219aaea6 --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/DynamicParameterCompilationTests.cs @@ -0,0 +1,122 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Core.SourceGenerator.CodeGenerators; +using TUnit.Core.SourceGenerator.Generators; + +namespace TUnit.Core.SourceGenerator.Tests; + +/// +/// Regression test for https://github.com/thomhurst/TUnit/issues/7165. +/// +/// dynamic test parameters, constructor parameters and data-source properties used to be rendered as typeof(dynamic) +/// in the generated metadata, which does not compile (CS1962). dynamic is object at runtime, so +/// the generator must render it as typeof(object). The snapshot harness only checks generator +/// diagnostics, so this test compiles the generated output and asserts there are no compiler errors. +/// +internal class DynamicParameterCompilationTests +{ + private const string Source = + """ + global using global::System; + global using global::System.Collections.Generic; + global using global::System.Threading.Tasks; + global using global::TUnit.Core; + + namespace MyTests; + + public class DynamicTests + { + [Test] + [Arguments("x")] + public void Takes_Dynamic(dynamic value) + { + } + + [Test] + [Arguments(1, "a", "b")] + public void Takes_Dynamic_Params(int first, params dynamic[] values) + { + } + + [Test] + [MethodDataSource(nameof(Data))] + public void Takes_Dynamic_Generic(List values, dynamic[] array) + { + } + + public static IEnumerable, dynamic[])>> Data() + { + yield return () => (new List { 1 }, new dynamic[] { "a" }); + } + } + + [Arguments("x")] + public class DynamicConstructorTests(dynamic value) + { + [MethodDataSource(nameof(PropertyData))] + public dynamic Property { get; set; } = null!; + + [MethodDataSource(nameof(PropertyData))] + public static dynamic StaticProperty { get; set; } = null!; + + public static object PropertyData() => "y"; + + [Test] + public void Test() + { + } + } + """; + + [Test] + public async Task Generated_metadata_for_dynamic_parameters_compiles() + { + // Generated code (and the project) targets the preview language version. + var parseOptions = new CSharpParseOptions(LanguageVersion.Preview); + + var compilation = CSharpCompilation.Create( + "DynamicParameterRegression", + [CSharpSyntaxTree.ParseText(Source, parseOptions)], + ReferencesHelper.References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new TestMetadataGenerator().AsSourceGenerator(), new PropertyInjectionSourceGenerator().AsSourceGenerator(), new StaticPropertyInitializationGenerator().AsSourceGenerator()], + parseOptions: parseOptions); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out var generatorDiagnostics); + + // A generator that throws reports a "failed to generate source" warning and emits nothing, and the input + // alone still compiles. Fail on generator diagnostics and require every generator to produce output, so + // the compilation check below cannot pass vacuously. + var generatorFailures = generatorDiagnostics + .Where(d => d.Severity == DiagnosticSeverity.Error + || (d.Severity == DiagnosticSeverity.Warning && d.GetMessage().Contains("failed to generate source"))) + .ToArray(); + + await Assert.That(generatorFailures) + .IsEmpty() + .Because(string.Join(Environment.NewLine, generatorFailures.Select(d => d.ToString()))); + + foreach (var result in driver.GetRunResult().Results) + { + var generatorName = result.Generator.GetGeneratorType().Name; + await Assert.That(result.Exception).IsNull().Because($"{generatorName} threw"); + await Assert.That(result.GeneratedSources).IsNotEmpty().Because($"{generatorName} must emit source for the dynamic members"); + } + + var errors = output.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToArray(); + + // Surface the full generated source if anything fails, so failures are diagnosable. + var generated = string.Join( + Environment.NewLine + "----" + Environment.NewLine, + output.SyntaxTrees.Select(t => t.GetText().ToString())); + + await Assert.That(errors) + .IsEmpty() + .Because($"generated code for dynamic parameters must compile. Errors:{Environment.NewLine}" + + string.Join(Environment.NewLine, errors.Select(e => e.ToString())) + + Environment.NewLine + generated); + } +} diff --git a/tests/TUnit.Core.SourceGenerator.Tests/Tests7165.Test.verified.txt b/tests/TUnit.Core.SourceGenerator.Tests/Tests7165.Test.verified.txt new file mode 100644 index 0000000000..2f8abacd73 --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/Tests7165.Test.verified.txt @@ -0,0 +1,285 @@ +// +#pragma warning disable + +#nullable enable +namespace TUnit.Generated; +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] +[global::System.CodeDom.Compiler.GeneratedCode("TUnit", "VERSION_SCRUBBED")] +internal static class TUnit_TestProject_Bugs__7165_DynamicParameterTests__TestSource +{ + #if NET8_0_OR_GREATER + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "k__BackingField")] + private static extern ref object GetPropertyBackingField(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests instance); + #endif + private static readonly global::TUnit.Core.ClassMetadata __classMetadata = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.Bugs._7165.DynamicParameterTests", new global::TUnit.Core.ClassMetadata + { + Type = typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests), + TypeInfo = new global::TUnit.Core.ConcreteType(typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests)), + Name = "DynamicParameterTests", + Namespace = "TUnit.TestProject.Bugs._7165", + Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", "TestsBase`1"), + Parameters = global::TUnit.Core.ParameterMetadataFactory.ForConstructor(typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests), false, new global::TUnit.Core.ParameterMetadata[] + { + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "constructorValue", new global::TUnit.Core.ConcreteType(typeof(object)), false) + }), + Properties = new global::TUnit.Core.PropertyMetadata[] + { + new global::TUnit.Core.PropertyMetadata + { + ReflectionInfo = typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests).GetProperty("Property", typeof(object)), + Type = typeof(object), + Name = "Property", + IsStatic = false, + IsNullable = false, + Getter = o => ((global::TUnit.TestProject.Bugs._7165.DynamicParameterTests)o).Property, + ClassMetadata = null!, + ContainingTypeMetadata = null! + } + }, + Parent = null + }); + private static readonly global::System.Type __classType = typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests); + private static global::TUnit.TestProject.Bugs._7165.DynamicParameterTests __CreateInstance(global::System.Type[] typeArgs, object?[] args) + { + return new global::TUnit.TestProject.Bugs._7165.DynamicParameterTests(global::TUnit.Core.Helpers.CastHelper.Cast(args[0])) + { + Property = default!, + }; + } + private static global::System.Threading.Tasks.ValueTask __Invoke(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests instance, int methodIndex, object?[] args, global::System.Threading.CancellationToken cancellationToken) + { + try + { + switch (methodIndex) + { + case 0: + { + switch (args.Length) + { + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Parameter_Receives_Argument(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } + default: + throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); + } + } + case 1: + { + switch (args.Length) + { + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), global::System.Array.Empty())); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is dynamic[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[1])))); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + case 7: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) })); + } + default: + if (args.Length < 1) + { + throw new global::System.ArgumentException($"Expected at least 1 argument, but got {args.Length}"); + } + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Params_Array_Receives_Arguments(global::TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args.Length > 1 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(1, args.Length - 1), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + case 2: + { + switch (args.Length) + { + case 1: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), global::System.Array.Empty())); + } + case 2: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), (args[1] is null ? null : args[1] is dynamic[] arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray(args[1])))); + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]) })); + } + case 4: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]) })); + } + case 5: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]) })); + } + case 6: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]) })); + } + case 7: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), new dynamic[] { global::TUnit.Core.Helpers.CastHelper.Cast(args[1]), global::TUnit.Core.Helpers.CastHelper.Cast(args[2]), global::TUnit.Core.Helpers.CastHelper.Cast(args[3]), global::TUnit.Core.Helpers.CastHelper.Cast(args[4]), global::TUnit.Core.Helpers.CastHelper.Cast(args[5]), global::TUnit.Core.Helpers.CastHelper.Cast(args[6]) })); + } + default: + if (args.Length < 1) + { + throw new global::System.ArgumentException($"Expected at least 1 argument, but got {args.Length}"); + } + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Generic_And_Array_Parameters_Receive_Data(global::TUnit.Core.Helpers.CastHelper.Cast>(args[0]), (args.Length > 1 ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range(1, args.Length - 1), i => global::TUnit.Core.Helpers.CastHelper.Cast(args[i]))) : global::System.Array.Empty()))); + } + } + case 3: + { + return new global::System.Threading.Tasks.ValueTask(instance.Dynamic_Constructor_Parameter_And_Property_Are_Set()); + } + default: + throw new global::System.ArgumentOutOfRangeException(nameof(methodIndex)); + } + } + catch (global::System.Exception ex) + { + return new global::System.Threading.Tasks.ValueTask(global::System.Threading.Tasks.Task.FromException(ex)); + } + } + private static global::System.Attribute[] __Attributes(int groupIndex) + { + switch (groupIndex) + { + case 0: + { + return + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass), + new global::TUnit.Core.ArgumentsAttribute("ctor") + ]; + } + default: + throw new global::System.ArgumentOutOfRangeException(nameof(groupIndex)); + } + } + private static readonly global::System.Func __createInstance = __CreateInstance; + private static readonly global::System.Func __invoke = __Invoke; + private static readonly global::System.Func __attributes = __Attributes; + public static readonly global::TUnit.Core.TestEntry[] Entries = new global::TUnit.Core.TestEntry[] + { + global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata( + methodName: "Dynamic_Parameter_Receives_Argument", + fullyQualifiedName: "TUnit.TestProject.Bugs._7165.DynamicParameterTests.Dynamic_Parameter_Receives_Argument", + filePath: "", + lineNumber: 23, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute("x"), + new global::TUnit.Core.ArgumentsAttribute(5), +}, + classDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute("ctor"), +}, + returnType: typeof(global::System.Threading.Tasks.Task), + parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests), "Dynamic_Parameter_Receives_Argument", false, new global::TUnit.Core.ParameterMetadata[] + { + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(object), "value", new global::TUnit.Core.ConcreteType(typeof(object)), false) + }), + classMetadata: __classMetadata, + createInstance: __createInstance, + invokeBody: __invoke, + methodIndex: 0, + createAttributes: __attributes, + attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata( + methodName: "Dynamic_Params_Array_Receives_Arguments", + fullyQualifiedName: "TUnit.TestProject.Bugs._7165.DynamicParameterTests.Dynamic_Params_Array_Receives_Arguments", + filePath: "", + lineNumber: 32, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute(1, "a", "b"), +}, + classDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute("ctor"), +}, + returnType: typeof(global::System.Threading.Tasks.Task), + parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests), "Dynamic_Params_Array_Receives_Arguments", false, new global::TUnit.Core.ParameterMetadata[] + { + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(int), "first", new global::TUnit.Core.ConcreteType(typeof(int)), false), + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(dynamic[]), "values", new global::TUnit.Core.ConcreteType(typeof(dynamic[])), false) + }), + classMetadata: __classMetadata, + createInstance: __createInstance, + invokeBody: __invoke, + methodIndex: 1, + createAttributes: __attributes, + attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata( + methodName: "Dynamic_Generic_And_Array_Parameters_Receive_Data", + fullyQualifiedName: "TUnit.TestProject.Bugs._7165.DynamicParameterTests.Dynamic_Generic_And_Array_Parameters_Receive_Data", + filePath: "", + lineNumber: 40, + hasDataSource: true, + testDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.MethodDataSourceAttribute("CollectionData") + { + Factory = static dataGeneratorMetadata => global::TUnit.Core.Helpers.DataSourceFactories.FromEnumerable(static () => global::TUnit.TestProject.Bugs._7165.DynamicParameterTests.CollectionData()), + }, +}, + classDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute("ctor"), +}, + returnType: typeof(global::System.Threading.Tasks.Task), + parameters: global::TUnit.Core.ParameterMetadataFactory.ForMethod(typeof(global::TUnit.TestProject.Bugs._7165.DynamicParameterTests), "Dynamic_Generic_And_Array_Parameters_Receive_Data", false, new global::TUnit.Core.ParameterMetadata[] + { + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(global::System.Collections.Generic.List), "list", new global::TUnit.Core.ConcreteType(typeof(global::System.Collections.Generic.List)), false), + global::TUnit.Core.ParameterMetadataFactory.Create(typeof(dynamic[]), "array", new global::TUnit.Core.ConcreteType(typeof(dynamic[])), false) + }), + classMetadata: __classMetadata, + createInstance: __createInstance, + invokeBody: __invoke, + methodIndex: 2, + createAttributes: __attributes, + attributeGroupIndex: 0), + global::TUnit.Core.TestEntryFactory.CreateWithClassMetadata( + methodName: "Dynamic_Constructor_Parameter_And_Property_Are_Set", + fullyQualifiedName: "TUnit.TestProject.Bugs._7165.DynamicParameterTests.Dynamic_Constructor_Parameter_And_Property_Are_Set", + filePath: "", + lineNumber: 48, + hasDataSource: true, + classDataSources: new global::TUnit.Core.IDataSourceAttribute[] +{ + new global::TUnit.Core.ArgumentsAttribute("ctor"), +}, + returnType: typeof(global::System.Threading.Tasks.Task), + classMetadata: __classMetadata, + createInstance: __createInstance, + invokeBody: __invoke, + methodIndex: 3, + createAttributes: __attributes, + attributeGroupIndex: 0), + }; +} +internal static partial class TUnit_TestRegistration +{ + static readonly int _r_TUnit_TestProject_Bugs__7165_DynamicParameterTests__TestSource = global::TUnit.Core.SourceRegistrar.RegisterEntries(static () => TUnit_TestProject_Bugs__7165_DynamicParameterTests__TestSource.Entries); +} diff --git a/tests/TUnit.Engine.Tests/Issue7165Tests.cs b/tests/TUnit.Engine.Tests/Issue7165Tests.cs new file mode 100644 index 0000000000..e424e70c25 --- /dev/null +++ b/tests/TUnit.Engine.Tests/Issue7165Tests.cs @@ -0,0 +1,24 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +/// +/// Test methods and classes with dynamic parameters and properties compile in source-generated mode +/// and receive their data in every mode (#7165). +/// +public class Issue7165Tests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Dynamic_Parameters_Receive_Their_Data() + { + await RunTestsWithFilter( + "/*/TUnit.TestProject.Bugs._7165/*/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(5), + result => result.ResultSummary.Counters.Passed.ShouldBe(5), + result => result.ResultSummary.Counters.Failed.ShouldBe(0) + ]); + } +} diff --git a/tests/TUnit.TestProject/Bugs/_7165/DynamicParameterTests.cs b/tests/TUnit.TestProject/Bugs/_7165/DynamicParameterTests.cs new file mode 100644 index 0000000000..fcee522a09 --- /dev/null +++ b/tests/TUnit.TestProject/Bugs/_7165/DynamicParameterTests.cs @@ -0,0 +1,56 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._7165; + +// `dynamic` test parameters, constructor parameters and data-source properties used to generate +// `typeof(dynamic)`, which does not compile (CS1962). Values are read back through `object` so the +// test bodies stay statically bound (#7165). + +[EngineTest(ExpectedResult.Pass)] +[Arguments("ctor")] +public class DynamicParameterTests(dynamic constructorValue) +{ + [MethodDataSource(nameof(PropertyValue))] + public required dynamic Property { get; init; } + + public static string PropertyValue() => "property"; + + public static IEnumerable, dynamic[])>> CollectionData() + { + yield return () => ([1, "two"], ["three"]); + } + + [Test] + [Arguments("x")] + [Arguments(5)] + public async Task Dynamic_Parameter_Receives_Argument(dynamic value) + { + object received = value; + await Assert.That(received is "x" or 5).IsTrue(); + } + + [Test] + [Arguments(1, "a", "b")] + public async Task Dynamic_Params_Array_Receives_Arguments(int first, params dynamic[] values) + { + await Assert.That(first).IsEqualTo(1); + await Assert.That(values.Cast()).IsEquivalentTo(new object[] { "a", "b" }); + } + + [Test] + [MethodDataSource(nameof(CollectionData))] + public async Task Dynamic_Generic_And_Array_Parameters_Receive_Data(List list, dynamic[] array) + { + await Assert.That(list.Cast()).IsEquivalentTo(new object[] { 1, "two" }); + await Assert.That(array.Cast()).IsEquivalentTo(new object[] { "three" }); + } + + [Test] + public async Task Dynamic_Constructor_Parameter_And_Property_Are_Set() + { + object constructor = constructorValue; + object property = Property; + await Assert.That(constructor).IsEqualTo("ctor"); + await Assert.That(property).IsEqualTo("property"); + } +}