Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions TUnit.Core/Helpers/ClassConstructorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ public static class ClassConstructorHelper
return null;
}

// Use the ClassConstructor to create the instance
var classConstructorType = classConstructorAttribute.ClassConstructorType;
var classConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorType)!;
// Reuse existing ClassConstructor if already set, otherwise create new instance
var classConstructor = testBuilderContext.ClassConstructor
?? (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!;

testBuilderContext.ClassConstructor = classConstructor;

Expand Down
6 changes: 5 additions & 1 deletion TUnit.Core/TestBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ internal static TestBuilderContext FromTestContext(TestContext testContext, IDat
{
return new TestBuilderContext
{
Events = testContext.InternalEvents, TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, DataSourceAttribute = dataSourceAttribute, StateBag = testContext.StateBag.Items,
Events = testContext.InternalEvents,
TestMetadata = testContext.Metadata.TestDetails.MethodMetadata,
DataSourceAttribute = dataSourceAttribute,
StateBag = testContext.StateBag.Items,
ClassConstructor = testContext.ClassConstructor,
};
}
}
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
Events = new TestContextEvents(),
StateBag = new ConcurrentDictionary<string, object?>(),
DataSourceAttribute = methodDataSource,
InitializedAttributes = testBuilderContext.InitializedAttributes // Preserve attributes from parent context
InitializedAttributes = testBuilderContext.InitializedAttributes, // Preserve attributes from parent context
ClassConstructor = testBuilderContext.ClassConstructor // Preserve ClassConstructor for instance creation
};

classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []);
Expand Down
53 changes: 53 additions & 0 deletions TUnit.TestProject/Bugs/3939/Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using TUnit.Core.Interfaces;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs._3939;

/// <summary>
/// Regression test for issue #3939: IClassConstructor and ITestEndEventReceiver
/// should use the same instance so that state can be shared (e.g., DI scope disposal).
/// </summary>
public sealed class ScopedClassConstructor : IClassConstructor, ITestEndEventReceiver
{
private object? _scope;

public Task<object> Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, ClassConstructorMetadata classConstructorMetadata)
{
// Simulate creating a DI scope - this should be available in OnTestEnd
_scope = new object();
return Task.FromResult<object>(new Tests());
}

public ValueTask OnTestEnd(TestContext context)
{
// This should be called on the SAME instance that Create() was called on
// If _scope is null, it means a different instance was used - this is the bug!
if (_scope == null)
{
throw new InvalidOperationException(
"Issue #3939: _scope was null in OnTestEnd(), indicating a different IClassConstructor " +
"instance was used than the one where Create() was called. " +
"IClassConstructor and ITestEndEventReceiver must use the same instance.");
}

// Simulate disposing the scope
_scope = null;
return default;
}

public int Order => 0;
}

[EngineTest(ExpectedResult.Pass)]
[ClassConstructor<ScopedClassConstructor>]
public sealed class Tests
{
[Test]
public Task TestMethod()
{
// The actual test - just verifies the test runs
// The real assertion is in OnTestEnd - it will throw if different instances are used
return Task.CompletedTask;
}
}
Loading