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
8 changes: 2 additions & 6 deletions TUnit.Engine/Services/PropertyInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,8 @@ public Task ResolveAndCachePropertiesAsync(
TestContext testContext,
CancellationToken cancellationToken = default)
{
// Skip property resolution if this test is reusing the discovery instance (already initialized)
if (testContext.IsDiscoveryInstanceReused)
{
return Task.CompletedTask;
}

// Even when the first data row reuses a discovery instance, this test still
// needs its own cached property values so shared fixtures get ref-counted.
var plan = PropertyInjectionCache.GetOrCreatePlan(testClassType);

if (!plan.HasProperties)
Expand Down
87 changes: 87 additions & 0 deletions TUnit.TestProject/Bugs/5982/FixtureLifetimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace TUnit.TestProject.Bugs._5982;

using TUnit.TestProject.Attributes;

public sealed class MyFixture : IAsyncDisposable
{
public bool Disposed { get; private set; }

public ValueTask DisposeAsync()
{
Disposed = true;
return ValueTask.CompletedTask;
}
}

[EngineTest(ExpectedResult.Pass)]
public class AnotherTest
{
[ClassDataSource<MyFixture>(Shared = SharedType.PerTestSession)]
public required MyFixture My { get; set; }

[Test]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
public async Task Wait(int delay)
{
await Task.Delay(delay);
}
}

[EngineTest(ExpectedResult.Pass)]
[NotInParallel]
public class SomeTest
{
[ClassDataSource<MyFixture>(Shared = SharedType.PerTestSession)]
public required MyFixture My { get; set; }

[Test]
[MethodDataSource(nameof(Methods))]
public async Task TheLastCaseFails(int delay)
{
await Assert.That(My.Disposed).IsFalse();
await Task.Delay(delay);
}

public IEnumerable<Func<int>> Methods()
{
yield return () => 100;
yield return () => 100;
yield return () => 100;
yield return () => 100;
}

[Test]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
[Arguments(100)]
public async Task Wait1(int delay)
{
await Task.Delay(delay);
}

[Test]
public async Task Wait2()
{
await Task.Delay(100);
}

[Test]
public async Task Wait3()
{
await Task.Delay(500);
}

[Test]
public async Task Wait4()
{
await Task.Delay(2_000);
}
}
115 changes: 115 additions & 0 deletions TUnit.UnitTests/PropertyInjectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Collections.Concurrent;
using TUnit.Core.Interfaces;
using TUnit.Engine.Services;

namespace TUnit.UnitTests;

public class PropertyInjectorTests
{
[Test]
public async Task ReusedDiscoveryInstanceStillCachesInjectedProperties()
{
var context = CreateContext<ReusedDiscoveryInstanceTestClass>();
context.IsDiscoveryInstanceReused = true;

var injector = new PropertyInjector(new Lazy<IInitializationCallback>(() => new PassthroughInitializationCallback()), "session");

await injector.ResolveAndCachePropertiesAsync(
typeof(ReusedDiscoveryInstanceTestClass),
context.StateBag.Items,
context.Metadata.TestDetails.MethodMetadata,
context.InternalEvents,
context);

await Assert.That(context.Metadata.TestDetails.TestClassInjectedPropertyArguments).Count().IsEqualTo(1);
await Assert.That(context.Metadata.TestDetails.TestClassInjectedPropertyArguments.Values.Single())
.IsTypeOf<ReusedDiscoveryInstanceFixture>();
}

private static TestContext CreateContext<T>() where T : class
{
var classMetadata = new ClassMetadata
{
Type = typeof(T),
TypeInfo = new ConcreteType(typeof(T)),
Name = typeof(T).Name,
Namespace = typeof(T).Namespace ?? string.Empty,
Assembly = new AssemblyMetadata
{
Name = typeof(T).Assembly.GetName().Name ?? string.Empty
},
Parent = null,
Parameters = [],
Properties = []
};

var methodMetadata = MethodMetadataFactory.Create(
"Test",
typeof(T),
typeof(Task),
classMetadata);

var beforeDiscoveryContext = new BeforeTestDiscoveryContext { TestFilter = null };
var discoveryContext = new TestDiscoveryContext(beforeDiscoveryContext) { TestFilter = null };
var sessionContext = new TestSessionContext(discoveryContext)
{
Id = Guid.NewGuid().ToString(),
TestFilter = null
};
var assemblyContext = new AssemblyHookContext(sessionContext)
{
Assembly = typeof(T).Assembly
};
var classContext = new ClassHookContext(assemblyContext)
{
ClassType = typeof(T)
};
var builderContext = new TestBuilderContext
{
TestMetadata = methodMetadata
};

var context = new TestContext("Test", new EmptyServiceProvider(), classContext, builderContext, CancellationToken.None);
context.TestDetails = new TestDetails<T>([])
{
TestId = "Test",
TestName = "Test",
ClassType = typeof(T),
MethodName = "Test",
ClassInstance = PlaceholderInstance.Instance,
TestMethodArguments = [],
TestClassArguments = [],
MethodMetadata = methodMetadata,
ReturnType = typeof(Task),
AttributesByType = new Dictionary<Type, IReadOnlyList<Attribute>>()
};

return context;
}

private sealed class ReusedDiscoveryInstanceFixture;

private sealed class ReusedDiscoveryInstanceTestClass
{
[ClassDataSource<ReusedDiscoveryInstanceFixture>(Shared = SharedType.PerTestSession)]
public required ReusedDiscoveryInstanceFixture Fixture { get; set; }
}

private sealed class PassthroughInitializationCallback : IInitializationCallback
{
public ValueTask<T> EnsureInitializedAsync<T>(
T obj,
ConcurrentDictionary<string, object?>? objectBag = null,
MethodMetadata? methodMetadata = null,
TestContextEvents? events = null,
CancellationToken cancellationToken = default) where T : notnull
{
return ValueTask.FromResult(obj);
}
}

private sealed class EmptyServiceProvider : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
}
Loading