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/Helpers/TestDataRowUnwrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static bool TryUnwrap(object? value, out object? data, [NotNullWhen(true)
if (value is ITestDataRow testDataRow)
{
data = testDataRow.GetData();
metadata = new TestDataRowMetadata(testDataRow.DisplayName, testDataRow.Skip, testDataRow.Categories);
metadata = new TestDataRowMetadata(testDataRow.DisplayName, testDataRow.DataExpression, testDataRow.Skip, testDataRow.Categories);
return true;
}

Expand Down
7 changes: 7 additions & 0 deletions TUnit.Core/TestContext.Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ internal string GetDisplayName()
return _cachedDisplayName;
}

// Use expression-based display format when no explicit DisplayName was provided
if (!string.IsNullOrEmpty(DataSourceExpression))
{
_cachedDisplayName = $"{TestDetails.TestName}({DataSourceExpression})";
return _cachedDisplayName;
}

if (TestDetails.TestMethodArguments.Length == 0)
{
_cachedDisplayName = TestDetails.TestName;
Expand Down
10 changes: 10 additions & 0 deletions TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,16 @@ internal void SetDataSourceDisplayName(string displayName)
DataSourceDisplayName = displayName;
}

/// <summary>
/// Expression text from CallerArgumentExpression on TestDataRow.
/// Used as the argument representation in the default TestName(expression) format.
/// </summary>
internal string? DataSourceExpression { get; private set; }

internal void SetDataSourceExpression(string expression)
{
DataSourceExpression = expression;
}

internal TestDetails TestDetails { get; set; } = null!;

Expand Down
75 changes: 56 additions & 19 deletions TUnit.Core/TestDataRow.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Runtime.CompilerServices;

namespace TUnit.Core;

/// <summary>
Expand All @@ -8,6 +10,7 @@ internal interface ITestDataRow
{
object? GetData();
string? DisplayName { get; }
string? DataExpression { get; }
string? Skip { get; }
string[]? Categories { get; }
}
Expand All @@ -18,33 +21,67 @@ internal interface ITestDataRow
/// per-row display names, skip reasons, or categories.
/// </summary>
/// <typeparam name="T">The type of the test data.</typeparam>
/// <param name="Data">The actual test data to be passed to the test method.</param>
/// <param name="DisplayName">
/// Optional custom display name for the test case.
/// Supports parameter substitution using $paramName or $arg1, $arg2, etc.
/// </param>
/// <param name="Skip">
/// Optional skip reason. When set, the test case will be skipped with this message.
/// </param>
/// <param name="Categories">
/// Optional categories to apply to this specific test case.
/// </param>
/// <example>
/// <code>
/// public static IEnumerable&lt;TestDataRow&lt;(string Username, string Password)&gt;&gt; GetLoginData()
/// {
/// yield return new(("admin", "secret123"), DisplayName: "Admin login");
/// yield return new(("guest", "guest"), DisplayName: "Guest login");
/// yield return new(("admin", "secret123")); // DisplayName includes method name + expression
/// yield return new(("guest", "guest"), DisplayName: "Guest login"); // DisplayName fully overridden
/// yield return new(("", ""), DisplayName: "Empty credentials", Skip: "Not implemented yet");
/// }
/// </code>
/// </example>
public record TestDataRow<T>(
T Data,
string? DisplayName = null,
string? Skip = null,
string[]? Categories = null
) : ITestDataRow
public record TestDataRow<T> : ITestDataRow
{
/// <summary>
/// The actual test data to be passed to the test method.
/// </summary>
public T Data { get; init; }

/// <summary>
/// Optional custom display name for the test case. When set, replaces the entire display name.
/// Supports parameter substitution using $paramName or $arg1, $arg2, etc.
/// </summary>
public string? DisplayName { get; init; }

/// <summary>
/// Auto-captured expression text of the Data argument via CallerArgumentExpression.
/// Used as the argument representation in the default TestName(expression) format
/// when DisplayName is not explicitly set.
/// </summary>
public string? DataExpression { get; init; }

/// <summary>
/// Optional skip reason. When set, the test case will be skipped with this message.
/// </summary>
public string? Skip { get; init; }

/// <summary>
/// Optional categories to apply to this specific test case.
/// </summary>
public string[]? Categories { get; init; }

/// <summary>
/// Creates a new TestDataRow wrapping the specified data.
/// </summary>
/// <param name="Data">The test data.</param>
/// <param name="DisplayName">Optional custom display name. When set, replaces the entire display name.</param>
/// <param name="Skip">Optional skip reason.</param>
/// <param name="Categories">Optional categories.</param>
/// <param name="DataExpression">Auto-captured expression text. Leave unset to let the compiler fill it in.</param>
public TestDataRow(
T Data,
string? DisplayName = null,
string? Skip = null,
string[]? Categories = null,
[CallerArgumentExpression(nameof(Data))] string? DataExpression = null)
{
this.Data = Data;
this.DisplayName = DisplayName;
this.Skip = Skip;
this.Categories = Categories;
this.DataExpression = DataExpression;
}

object? ITestDataRow.GetData() => Data;
}
5 changes: 4 additions & 1 deletion TUnit.Core/TestDataRowMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ namespace TUnit.Core;
/// Metadata extracted from a <see cref="TestDataRow{T}"/> wrapper or data source attributes.
/// </summary>
/// <param name="DisplayName">Custom display name for the test case.</param>
/// <param name="DataExpression">Auto-captured expression text of the Data argument.</param>
/// <param name="Skip">Skip reason - test will be skipped if set.</param>
/// <param name="Categories">Categories to apply to the test case.</param>
internal record TestDataRowMetadata(
string? DisplayName,
string? DataExpression,
string? Skip,
string[]? Categories
)
{
/// <summary>
/// Returns true if any metadata property is set.
/// </summary>
public bool HasMetadata => DisplayName is not null || Skip is not null || Categories is { Length: > 0 };
public bool HasMetadata => DisplayName is not null || DataExpression is not null || Skip is not null || Categories is { Length: > 0 };

/// <summary>
/// Merges this metadata with another, preferring non-null values from this instance.
Expand All @@ -29,6 +31,7 @@ public TestDataRowMetadata MergeWith(TestDataRowMetadata? other)

return new TestDataRowMetadata(
DisplayName ?? other.DisplayName,
DataExpression ?? other.DataExpression,
Skip ?? other.Skip,
Categories ?? other.Categories
);
Expand Down
6 changes: 6 additions & 0 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,12 @@ public async Task<AbstractExecutableTest> BuildTestAsync(TestMetadata metadata,
context.SetDataSourceDisplayName(dataRowMetadata.DisplayName!);
}

// Apply data expression for default display name formatting (only when no explicit DisplayName)
if (string.IsNullOrEmpty(dataRowMetadata.DisplayName) && !string.IsNullOrEmpty(dataRowMetadata.DataExpression))
{
context.SetDataSourceExpression(dataRowMetadata.DataExpression!);
}

// Apply skip reason from data source
if (!string.IsNullOrEmpty(dataRowMetadata.Skip))
{
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Helpers/DataSourceMetadataExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal static class DataSourceMetadataExtractor
return null;
}

return new TestDataRowMetadata(displayName, skip, categories);
return new TestDataRowMetadata(displayName, null, skip, categories);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,9 +1405,10 @@ namespace
}
public class TestDataRow<T> : <.TestDataRow<T>>
{
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null) { }
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null, [.("Data")] string? DataExpression = null) { }
public string[]? Categories { get; init; }
public T Data { get; init; }
public string? DataExpression { get; init; }
public string? DisplayName { get; init; }
public string? Skip { get; init; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,9 +1405,10 @@ namespace
}
public class TestDataRow<T> : <.TestDataRow<T>>
{
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null) { }
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null, [.("Data")] string? DataExpression = null) { }
public string[]? Categories { get; init; }
public T Data { get; init; }
public string? DataExpression { get; init; }
public string? DisplayName { get; init; }
public string? Skip { get; init; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,9 +1405,10 @@ namespace
}
public class TestDataRow<T> : <.TestDataRow<T>>
{
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null) { }
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null, [.("Data")] string? DataExpression = null) { }
public string[]? Categories { get; init; }
public T Data { get; init; }
public string? DataExpression { get; init; }
public string? DisplayName { get; init; }
public string? Skip { get; init; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1355,9 +1355,10 @@ namespace
}
public class TestDataRow<T> : <.TestDataRow<T>>
{
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null) { }
public TestDataRow(T Data, string? DisplayName = null, string? Skip = null, string[]? Categories = null, [.("Data")] string? DataExpression = null) { }
public string[]? Categories { get; init; }
public T Data { get; init; }
public string? DataExpression { get; init; }
public string? DisplayName { get; init; }
public string? Skip { get; init; }
}
Expand Down
33 changes: 33 additions & 0 deletions TUnit.TestProject/TestDataRowCallerArgumentExpressionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject;

[EngineTest(ExpectedResult.Pass)]
public class TestDataRowCallerArgumentExpressionTests
{
[Test]
[MethodDataSource(nameof(InferredDisplayNameData))]
public async Task Inferred_DisplayName_Uses_CallerArgumentExpression(string username, string password)
{
await Assert.That(TestContext.Current!.Metadata.DisplayName)
.IsEqualTo("""Inferred_DisplayName_Uses_CallerArgumentExpression(("admin", "secret123"))""");
}

[Test]
[MethodDataSource(nameof(ExplicitDisplayNameData))]
public async Task Explicit_DisplayName_Overrides_CallerArgumentExpression(string username, string password)
{
await Assert.That(TestContext.Current!.Metadata.DisplayName)
.IsEqualTo("Admin login");
}

public static IEnumerable<TestDataRow<(string, string)>> InferredDisplayNameData()
{
yield return new(("admin", "secret123"));
}

public static IEnumerable<TestDataRow<(string, string)>> ExplicitDisplayNameData()
{
yield return new(("admin", "secret123"), DisplayName: "Admin login");
}
}
Loading