From d506a9804cffe2d268a33d78e4cb8b919b13d359 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 Aug 2025 21:29:47 +0100
Subject: [PATCH 1/3] Fix abstract class constructor processing in hook
metadata generation (#2888)
* Initial plan
* Fix abstract class constructor processing in hook metadata generation
Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
* test: Add test source for ActualTestClass in Issue 2887
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
---
.../Bugs/Issue2887/Issue2887Tests.cs | 21 ++++
.../Issue2887Tests.Test.verified.txt | 108 ++++++++++++++++++
.../Utilities/MetadataGenerationHelper.cs | 12 +-
TUnit.TestProject/Bugs/Issue2887/ReproTest.cs | 28 +++++
4 files changed, 167 insertions(+), 2 deletions(-)
create mode 100644 TUnit.Core.SourceGenerator.Tests/Bugs/Issue2887/Issue2887Tests.cs
create mode 100644 TUnit.Core.SourceGenerator.Tests/Issue2887Tests.Test.verified.txt
create mode 100644 TUnit.TestProject/Bugs/Issue2887/ReproTest.cs
diff --git a/TUnit.Core.SourceGenerator.Tests/Bugs/Issue2887/Issue2887Tests.cs b/TUnit.Core.SourceGenerator.Tests/Bugs/Issue2887/Issue2887Tests.cs
new file mode 100644
index 0000000000..e3ec0c9219
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/Bugs/Issue2887/Issue2887Tests.cs
@@ -0,0 +1,21 @@
+using TUnit.Core.SourceGenerator.Tests.Options;
+
+namespace TUnit.Core.SourceGenerator.Tests.Bugs._Issue2887;
+
+internal class Issue2887Tests : TestsBase
+{
+ [Test]
+ public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName,
+ "TUnit.TestProject",
+ "Bugs",
+ "Issue2887",
+ "ReproTest.cs"),
+ new RunTestOptions
+ {
+ VerifyConfigurator = settingsTask => settingsTask.ScrubLinesContaining("TestFilePath = ")
+ },
+ async generatedFiles =>
+ {
+ // This test ensures that abstract classes with parameterized constructors and hooks don't cause generation errors
+ });
+}
\ No newline at end of file
diff --git a/TUnit.Core.SourceGenerator.Tests/Issue2887Tests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Issue2887Tests.Test.verified.txt
new file mode 100644
index 0000000000..0a7f5785b8
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/Issue2887Tests.Test.verified.txt
@@ -0,0 +1,108 @@
+//
+#pragma warning disable
+
+//
+#pragma warning disable
+#nullable enable
+namespace TUnit.Generated;
+internal sealed class ActualTestClass_Test1_TestSource_GUID : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource
+{
+ public async global::System.Collections.Generic.IAsyncEnumerable GetTestsAsync(string testSessionId, [global::System.Runtime.CompilerServices.EnumeratorCancellation] global::System.Threading.CancellationToken cancellationToken = default)
+ {
+ var metadata = new global::TUnit.Core.TestMetadata
+ {
+ TestName = "Test1",
+ TestClassType = typeof(global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass),
+ TestMethodName = "Test1",
+ Dependencies = global::System.Array.Empty(),
+ AttributeFactory = () =>
+ [
+ new global::TUnit.Core.TestAttribute(),
+ new global::TUnit.Core.ClassConstructorAttribute()
+ ],
+ DataSources = new global::TUnit.Core.IDataSourceAttribute[]
+ {
+ },
+ ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[]
+ {
+ },
+ PropertyDataSources = new global::TUnit.Core.PropertyDataSource[]
+ {
+ },
+ PropertyInjections = new global::TUnit.Core.PropertyInjectionData[]
+ {
+ },
+ InheritanceDepth = 0,
+ FilePath = @"",
+ LineNumber = 26,
+ MethodMetadata = new global::TUnit.Core.MethodMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass),
+ TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.Bugs._Issue2887.ActualTestClass, TestsBase`1"),
+ Name = "Test1",
+ GenericTypeCount = 0,
+ ReturnType = typeof(void),
+ ReturnTypeReference = global::TUnit.Core.TypeReference.CreateConcrete("void, System.Private.CoreLib"),
+ Parameters = global::System.Array.Empty(),
+ Class = global::TUnit.Core.ClassMetadata.GetOrAdd("TestsBase`1:global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass", () =>
+ {
+ var classMetadata = new global::TUnit.Core.ClassMetadata
+ {
+ Type = typeof(global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass),
+ TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.Bugs._Issue2887.ActualTestClass, TestsBase`1"),
+ Name = "ActualTestClass",
+ Namespace = "TUnit.TestProject.Bugs._Issue2887",
+ Assembly = global::TUnit.Core.AssemblyMetadata.GetOrAdd("TestsBase`1", () => new global::TUnit.Core.AssemblyMetadata { Name = "TestsBase`1" }),
+ Parameters = new global::TUnit.Core.ParameterMetadata[]
+ {
+ new global::TUnit.Core.ParameterMetadata(typeof(global::TUnit.TestProject.Bugs._Issue2887.IServiceProvider))
+ {
+ Name = "serviceProvider",
+ TypeReference = global::TUnit.Core.TypeReference.CreateConcrete("TUnit.TestProject.Bugs._Issue2887.IServiceProvider, TestsBase`1"),
+ IsNullable = false,
+ ReflectionInfo = typeof(global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass).GetConstructor(new global::System.Type[] { typeof(global::TUnit.TestProject.Bugs._Issue2887.IServiceProvider) })!.GetParameters()[0]
+ }
+ },
+ Properties = global::System.Array.Empty(),
+ Parent = null
+ };
+ // Set ClassMetadata and ContainingTypeMetadata references on properties to avoid circular dependency
+ foreach (var prop in classMetadata.Properties)
+ {
+ prop.ClassMetadata = classMetadata;
+ prop.ContainingTypeMetadata = classMetadata;
+ }
+ return classMetadata;
+ })
+ },
+ InstanceFactory = (typeArgs, args) =>
+ {
+ // ClassConstructor attribute is present - instance creation handled at runtime
+ throw new global::System.NotSupportedException("Instance creation for classes with ClassConstructor attribute is handled at runtime");
+ },
+ TestInvoker = async (instance, args) =>
+ {
+ var typedInstance = (global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass)instance;
+ var context = global::TUnit.Core.TestContext.Current;
+ typedInstance.Test1();
+ await global::System.Threading.Tasks.Task.CompletedTask;
+ },
+ InvokeTypedTest = async (instance, args, cancellationToken) =>
+ {
+ instance.Test1();
+ await global::System.Threading.Tasks.Task.CompletedTask;
+ },
+ };
+ metadata.UseRuntimeDataGeneration(testSessionId);
+ yield return metadata;
+ yield break;
+ }
+}
+internal static class ActualTestClass_Test1_ModuleInitializer_GUID
+{
+ [global::System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize()
+ {
+ global::TUnit.Core.SourceRegistrar.Register(typeof(global::TUnit.TestProject.Bugs._Issue2887.ActualTestClass), new ActualTestClass_Test1_TestSource_GUID());
+ }
+}
diff --git a/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index 3f2aa7cd8c..5dba6206f2 100644
--- a/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -146,7 +146,11 @@ private static void WriteClassMetadataGetOrAdd(ICodeWriter writer, INamedTypeSym
writer.AppendLine($"Namespace = \"{typeSymbol.ContainingNamespace?.ToDisplayString() ?? ""}\",");
writer.AppendLine($"Assembly = {GenerateAssemblyMetadataGetOrAdd(typeSymbol.ContainingAssembly)},");
- var constructor = typeSymbol.InstanceConstructors.FirstOrDefault();
+ // For abstract classes, skip constructor processing since they cannot be instantiated directly
+ // For concrete classes, only consider public constructors
+ var constructor = typeSymbol.IsAbstract
+ ? null
+ : typeSymbol.InstanceConstructors.FirstOrDefault(c => c.DeclaredAccessibility == Accessibility.Public);
var constructorParams = constructor?.Parameters ?? ImmutableArray.Empty;
if (constructor != null && constructorParams.Length > 0)
{
@@ -210,7 +214,11 @@ public static string GenerateClassMetadataGetOrAdd(INamedTypeSymbol typeSymbol,
writer.AppendLine($"Name = \"{typeSymbol.Name}\",");
writer.AppendLine($"Namespace = \"{typeSymbol.ContainingNamespace?.ToDisplayString() ?? ""}\",");
writer.AppendLine($"Assembly = {GenerateAssemblyMetadataGetOrAdd(typeSymbol.ContainingAssembly)},");
- var constructor = typeSymbol.InstanceConstructors.FirstOrDefault();
+ // For abstract classes, skip constructor processing since they cannot be instantiated directly
+ // For concrete classes, only consider public constructors
+ var constructor = typeSymbol.IsAbstract
+ ? null
+ : typeSymbol.InstanceConstructors.FirstOrDefault(c => c.DeclaredAccessibility == Accessibility.Public);
var constructorParams = constructor?.Parameters ?? ImmutableArray.Empty;
if (constructor != null && constructorParams.Length > 0)
{
diff --git a/TUnit.TestProject/Bugs/Issue2887/ReproTest.cs b/TUnit.TestProject/Bugs/Issue2887/ReproTest.cs
new file mode 100644
index 0000000000..176c53038e
--- /dev/null
+++ b/TUnit.TestProject/Bugs/Issue2887/ReproTest.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.TestProject.Bugs._Issue2887;
+
+public interface IServiceProvider;
+
+public sealed class DependencyInjectionClassConstructor : IClassConstructor
+{
+ public Task