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
2 changes: 1 addition & 1 deletion TUnit.Core/Contexts/TestRegisteredContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void SetHookExecutor(IHookExecutor executor)
/// </summary>
public void SetParallelLimiter(IParallelLimit parallelLimit)
{
TestContext.Parallelism.SetLimiter(parallelLimit);
TestContext.ParallelLimiter = parallelLimit;
}

/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Core/Extensions/ServiceProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace TUnit.Core.Extensions;

/// <summary>
/// Extension methods for <see cref="IServiceProvider"/>.
/// </summary>
internal static class ServiceProviderExtensions
{
/// <summary>
/// Gets a service of the specified type from the service provider.
/// </summary>
/// <typeparam name="T">The type of service to retrieve</typeparam>
/// <param name="serviceProvider">The service provider</param>
/// <returns>The service instance, or null if not found</returns>
public static T? GetService<T>(this IServiceProvider serviceProvider) where T : class
{
return serviceProvider.GetService(typeof(T)) as T;
}
}
9 changes: 2 additions & 7 deletions TUnit.Core/Extensions/TestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ namespace TUnit.Core.Extensions;

public static class TestContextExtensions
{
public static T? GetService<T>(this TestContext context) where T : class
{
return context.GetService<T>();
}

public static string GetClassTypeName(this TestContext context)
{
var parameters = context.Metadata.TestDetails.MethodMetadata.Class.Parameters;
Expand Down Expand Up @@ -51,7 +46,7 @@ public static string GetClassTypeName(this TestContext context)
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields)] T>(this TestContext context, DynamicTest<T> dynamicTest) where T : class
{
await context.GetService<ITestRegistry>()!.AddDynamicTest(context, dynamicTest);;
await context.Services.GetService<ITestRegistry>()!.AddDynamicTest(context, dynamicTest);;
}

/// <summary>
Expand All @@ -75,6 +70,6 @@ public static async Task CreateTestVariant(
Enums.TestRelationship relationship = Enums.TestRelationship.Derived,
string? displayName = null)
{
await context.GetService<ITestRegistry>()!.CreateTestVariant(context, arguments, properties, relationship, displayName);
await context.Services.GetService<ITestRegistry>()!.CreateTestVariant(context, arguments, properties, relationship, displayName);
}
}
2 changes: 1 addition & 1 deletion TUnit.Core/Helpers/DataSourceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ public static void RegisterTypeCreator<T>(Func<MethodMetadata, string, Task<T>>
dataSourceAttribute,
TestContext.Current,
TestContext.Current?.Metadata.TestDetails.ClassInstance,
TestContext.Current?.Events,
TestContext.Current?.InternalEvents,
TestContext.Current?.StateBag.Items ?? new ConcurrentDictionary<string, object?>()
);

Expand Down
19 changes: 13 additions & 6 deletions TUnit.Core/Interfaces/ITestDependencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,24 @@ public interface ITestDependencies
TestRelationship Relationship { get; }

/// <summary>
/// Gets tests matching the specified predicate.
/// Gets all registered tests that match the specified predicate.
/// </summary>
IEnumerable<TestContext> GetTests(Func<TestContext, bool> predicate);
/// <param name="predicate">The predicate to filter tests by.</param>
/// <returns>A read-only list of matching test contexts.</returns>
IReadOnlyList<TestContext> GetTests(Func<TestContext, bool> predicate);

/// <summary>
/// Gets all tests with the specified name.
/// Gets all registered tests that match the specified test name.
/// </summary>
List<TestContext> GetTests(string testName);
/// <param name="testName">The name of the test method.</param>
/// <returns>A read-only list of matching test contexts.</returns>
IReadOnlyList<TestContext> GetTests(string testName);

/// <summary>
/// Gets all tests with the specified name and class type.
/// Gets all registered tests that match the specified test name and class type.
/// </summary>
List<TestContext> GetTests(string testName, Type classType);
/// <param name="testName">The name of the test method.</param>
/// <param name="classType">The type of the test class.</param>
/// <returns>A read-only list of matching test contexts.</returns>
IReadOnlyList<TestContext> GetTests(string testName, Type classType);
}
34 changes: 32 additions & 2 deletions TUnit.Core/Interfaces/ITestEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,37 @@ namespace TUnit.Core.Interfaces;
public interface ITestEvents
{
/// <summary>
/// Gets the event manager for this test, providing hooks for custom test lifecycle integration.
/// Gets the event that is raised when the test context is disposed.
/// </summary>
TestContextEvents Events { get; }
AsyncEvent<TestContext>? OnDispose { get; }

/// <summary>
/// Gets the event that is raised when the test has been registered with the test runner.
/// </summary>
AsyncEvent<TestContext>? OnTestRegistered { get; }

/// <summary>
/// Gets the event that is raised before the test is initialized.
/// </summary>
AsyncEvent<TestContext>? OnInitialize { get; }

/// <summary>
/// Gets the event that is raised before the test method is invoked.
/// </summary>
AsyncEvent<TestContext>? OnTestStart { get; }

/// <summary>
/// Gets the event that is raised after the test method has completed.
/// </summary>
AsyncEvent<TestContext>? OnTestEnd { get; }

/// <summary>
/// Gets the event that is raised if the test was skipped.
/// </summary>
AsyncEvent<TestContext>? OnTestSkipped { get; }

/// <summary>
/// Gets the event that is raised before a test is retried.
/// </summary>
AsyncEvent<(TestContext TestContext, int RetryAttempt)>? OnTestRetry { get; }
}
5 changes: 3 additions & 2 deletions TUnit.Core/Interfaces/ITestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ namespace TUnit.Core.Interfaces;
public interface ITestMetadata
{
/// <summary>
/// Gets the unique identifier for this test instance.
/// Gets the unique identifier for the test definition (template/source) that generated this test.
/// This ID is shared across all instances of parameterized tests.
/// </summary>
Guid Id { get; }
string DefinitionId { get; }

/// <summary>
/// Gets the detailed metadata about this test, including class type, method info, and arguments.
Expand Down
16 changes: 16 additions & 0 deletions TUnit.Core/Interfaces/ITestOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ public interface ITestOutput
/// </summary>
/// <returns>The accumulated error output</returns>
string GetErrorOutput();

/// <summary>
/// Writes a line of text to standard output.
/// Convenience method for StandardOutput.WriteLine(message).
/// Thread-safe for concurrent calls.
/// </summary>
/// <param name="message">The message to write</param>
void WriteLine(string message);

/// <summary>
/// Writes a line of text to error output.
/// Convenience method for ErrorOutput.WriteLine(message).
/// Thread-safe for concurrent calls.
/// </summary>
/// <param name="message">The error message to write</param>
void WriteError(string message);
}
7 changes: 0 additions & 7 deletions TUnit.Core/Interfaces/ITestParallelization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public interface ITestParallelization

/// <summary>
/// Gets the parallel limiter that controls how many tests can run concurrently.
/// Returns null if no limiter is configured.
/// </summary>
IParallelLimit? Limiter { get; }

Expand All @@ -33,10 +32,4 @@ public interface ITestParallelization
/// </summary>
/// <param name="constraint">The constraint to add</param>
void AddConstraint(IParallelConstraint constraint);

/// <summary>
/// Sets the parallel limiter for this test.
/// </summary>
/// <param name="parallelLimit">The parallel limit to apply</param>
void SetLimiter(IParallelLimit parallelLimit);
}
54 changes: 49 additions & 5 deletions TUnit.Core/Interfaces/ITestStateBag.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;

namespace TUnit.Core.Interfaces;

/// <summary>
/// Provides access to the runtime state storage for test-scoped values.
/// Provides a type-safe, thread-safe bag for storing and retrieving custom state during a test's execution.
/// Accessed via <see cref="TestContext.StateBag"/>.
/// This is a thread-safe key-value store for sharing data between hooks, data sources, and test methods.
/// </summary>
public interface ITestStateBag
{
/// <summary>
/// Gets the thread-safe dictionary for storing arbitrary test-scoped data.
/// Use this to share state between hooks, data sources, and test methods within a single test execution.
/// Thread-safe for concurrent access.
/// Gets the underlying concurrent dictionary for direct access.
/// </summary>
ConcurrentDictionary<string, object?> Items { get; }

/// <summary>
/// Gets or sets a value in the state bag.
/// </summary>
/// <param name="key">The key of the value to get or set.</param>
/// <returns>The value associated with the specified key.</returns>
object? this[string key] { get; set; }

/// <summary>
/// Gets the number of items in the state bag.
/// </summary>
int Count { get; }

/// <summary>
/// Gets a value indicating whether the specified key exists in the state bag.
/// </summary>
/// <param name="key">The key to check.</param>
/// <returns><c>true</c> if the key exists; otherwise, <c>false</c>.</returns>
bool ContainsKey(string key);

/// <summary>
/// Gets the value associated with the specified key, or adds it if it does not exist.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="key">The key of the value to get or add.</param>
/// <param name="valueFactory">The function used to generate a value for the key if it does not exist.</param>
/// <returns>The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary.</returns>
/// <exception cref="InvalidCastException">Thrown if a value already exists for the key but is not of type <typeparamref name="T"/>.</exception>
T GetOrAdd<T>(string key, Func<string, T> valueFactory);

/// <summary>
/// Attempts to get the value associated with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <param name="value">When this method returns, contains the value associated with the specified key, if the key is found and the value is of the correct type; otherwise, the default value for the type of the value parameter.</param>
/// <returns><c>true</c> if the key was found and the value is of the correct type; otherwise, <c>false</c>.</returns>
bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value);

/// <summary>
/// Attempts to remove a value with the specified key.
/// </summary>
/// <param name="key">The key of the element to remove.</param>
/// <param name="value">When this method returns, contains the object removed from the bag, or <c>null</c> if the key does not exist.</param>
/// <returns><c>true</c> if the object was removed successfully; otherwise, <c>false</c>.</returns>
bool TryRemove(string key, [MaybeNullWhen(false)] out object? value);
}
4 changes: 2 additions & 2 deletions TUnit.Core/TestBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static TestBuilderContext? Current
internal set => BuilderContexts.Value = value;
}

public Guid Id { get; } = Guid.NewGuid();
public string DefinitionId { get; } = Guid.NewGuid().ToString();
public ConcurrentDictionary<string, object?> ObjectBag { get; set; } = new();
public TestContextEvents Events { get; set; } = new();

Expand Down Expand Up @@ -49,7 +49,7 @@ internal static TestBuilderContext FromTestContext(TestContext testContext, IDat
{
return new TestBuilderContext
{
Events = testContext.Events, TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, DataSourceAttribute = dataSourceAttribute, ObjectBag = testContext.StateBag.Items,
Events = testContext.InternalEvents, TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, DataSourceAttribute = dataSourceAttribute, ObjectBag = testContext.StateBag.Items,
};
}
}
Expand Down
8 changes: 4 additions & 4 deletions TUnit.Core/TestContext.Dependencies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ public partial class TestContext
string? ITestDependencies.ParentTestId => ParentTestId;
TestRelationship ITestDependencies.Relationship => Relationship;

IEnumerable<TestContext> ITestDependencies.GetTests(Func<TestContext, bool> predicate) => GetTests(predicate);
List<TestContext> ITestDependencies.GetTests(string testName) => GetTests(testName);
List<TestContext> ITestDependencies.GetTests(string testName, Type classType) => GetTests(testName, classType);
IReadOnlyList<TestContext> ITestDependencies.GetTests(Func<TestContext, bool> predicate) => GetTests(predicate);
IReadOnlyList<TestContext> ITestDependencies.GetTests(string testName) => GetTests(testName);
IReadOnlyList<TestContext> ITestDependencies.GetTests(string testName, Type classType) => GetTests(testName, classType);

internal IEnumerable<TestContext> GetTests(Func<TestContext, bool> predicate)
internal List<TestContext> GetTests(Func<TestContext, bool> predicate)
{
var testFinder = ServiceProvider.GetService<ITestFinder>()!;

Expand Down
26 changes: 24 additions & 2 deletions TUnit.Core/TestContext.Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,29 @@ namespace TUnit.Core;

public partial class TestContext
{
internal TestContextEvents Events => _testBuilderContext.Events;
/// <summary>
/// For internal framework use only. External code should use the Events property which returns ITestEvents.
/// </summary>
internal TestContextEvents InternalEvents => _testBuilderContext.Events;

TestContextEvents ITestEvents.Events => Events;
/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnDispose => _testBuilderContext.Events.OnDispose;

/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnTestRegistered => _testBuilderContext.Events.OnTestRegistered;

/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnInitialize => _testBuilderContext.Events.OnInitialize;

/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnTestStart => _testBuilderContext.Events.OnTestStart;

/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnTestEnd => _testBuilderContext.Events.OnTestEnd;

/// <inheritdoc/>
AsyncEvent<TestContext>? ITestEvents.OnTestSkipped => _testBuilderContext.Events.OnTestSkipped;

/// <inheritdoc/>
AsyncEvent<(TestContext TestContext, int RetryAttempt)>? ITestEvents.OnTestRetry => _testBuilderContext.Events.OnTestRetry;
}
2 changes: 1 addition & 1 deletion TUnit.Core/TestContext.Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal void InvalidateDisplayNameCache()
_cachedDisplayName = null;
}

Guid ITestMetadata.Id => Id;
string ITestMetadata.DefinitionId => _testBuilderContext.DefinitionId;
TestDetails ITestMetadata.TestDetails
{
get => TestDetails;
Expand Down
5 changes: 2 additions & 3 deletions TUnit.Core/TestContext.Output.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,13 @@ void ITestOutput.AttachArtifact(Artifact artifact)
string ITestOutput.GetStandardOutput() => GetOutput();
string ITestOutput.GetErrorOutput() => GetErrorOutput();

// Internal methods for output capture (used by base Context class)
internal void WriteLine(string message)
void ITestOutput.WriteLine(string message)
{
_outputWriter ??= new StringWriter();
_outputWriter.WriteLine(message);
}

internal void WriteError(string message)
void ITestOutput.WriteError(string message)
{
_errorWriter ??= new StringWriter();
_errorWriter.WriteLine(message);
Expand Down
5 changes: 0 additions & 5 deletions TUnit.Core/TestContext.Parallelization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,4 @@ void ITestParallelization.AddConstraint(IParallelConstraint constraint)
_parallelConstraints.Add(constraint);
}
}

void ITestParallelization.SetLimiter(IParallelLimit parallelLimit)
{
ParallelLimiter = parallelLimit;
}
}
Loading
Loading