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
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ internal sealed class AsyncMethodDataSourceDrivenTests_AsyncMethodDataSource_Wit
[
new global::TUnit.Core.TestAttribute(),
new global::TUnit.Core.MethodDataSourceAttribute("AsyncDataMethodWithArgs")
{Arguments = [5],},
{Arguments = new object[]{5},},
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ internal sealed class MethodDataSourceDrivenTests_DataSource_Method3_TestSource_
[
new global::TUnit.Core.TestAttribute(),
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = [5],},
{Arguments = new object[]{5},},
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = new object[] { 5 },},
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
Expand Down Expand Up @@ -545,11 +545,11 @@ internal sealed class MethodDataSourceDrivenTests_DataSource_Method4_TestSource_
[
new global::TUnit.Core.TestAttribute(),
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = ["Hello World!",5,true],},
{Arguments = new object[]{"Hello World!",5,true},},
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = new object[] { "Hello World!", 6, true },},
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = ["Hello World!",7,true],},
{Arguments = new object[]{"Hello World!",7,true},},
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
{Arguments = new object[] { "Hello World!", 8, true },},
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class MultipleClassDataSourceDrivenTests_Test1_TestSource_GUID :
new global::TUnit.Core.TestAttribute(),
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass),
new global::TUnit.Core.ClassDataSourceAttribute<global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject1, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject2, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject3, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject4, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject5>()
{Shared = [global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None],}
{Shared = new global::TUnit.Core.SharedType[]{global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None},}
],
DataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[]
Expand Down Expand Up @@ -154,7 +154,7 @@ internal sealed class MultipleClassDataSourceDrivenTests_Test2_TestSource_GUID :
new global::TUnit.Core.TestAttribute(),
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass),
new global::TUnit.Core.ClassDataSourceAttribute<global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject1, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject2, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject3, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject4, global::TUnit.TestProject.MultipleClassDataSourceDrivenTests.Inject5>()
{Shared = [global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None],}
{Shared = new global::TUnit.Core.SharedType[]{global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None,global::TUnit.Core.SharedType.None},}
],
DataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,20 +176,58 @@ public override SyntaxNode VisitTypeOfExpression(TypeOfExpressionSyntax node)
#if ROSLYN4_7_OR_GREATER
public override SyntaxNode? VisitCollectionExpression(CollectionExpressionSyntax node)
{
// For collection expressions, visit each element and ensure proper type conversion
var rewrittenElements = node.Elements.Select(element =>
// Convert collection expressions to array initializers for property assignments
// Collection expressions like [1, 2, 3] need to be converted to new object[] { 1, 2, 3 }
// when used in property initializers to avoid compilation errors

// Get the type info from the semantic model if available
var typeInfo = semanticModel.GetTypeInfo(node);
var elementType = "object";

if (typeInfo.ConvertedType is IArrayTypeSymbol arrayTypeSymbol)
{
elementType = arrayTypeSymbol.ElementType.GloballyQualified();
}
else if (typeInfo.Type is IArrayTypeSymbol arrayTypeSymbol2)
{
elementType = arrayTypeSymbol2.ElementType.GloballyQualified();
}

// Visit and rewrite each element
var rewrittenElements = new List<ExpressionSyntax>();
foreach (var element in node.Elements)
{
if (element is ExpressionElementSyntax expressionElement)
{
var rewrittenExpression = Visit(expressionElement.Expression);
return SyntaxFactory.ExpressionElement((ExpressionSyntax)rewrittenExpression);
rewrittenElements.Add((ExpressionSyntax)rewrittenExpression);
}
return element;
}).ToList();

return SyntaxFactory.CollectionExpression(
}

// Create an array creation expression instead of a collection expression
// This ensures compatibility with property initializers
var arrayTypeSyntax = SyntaxFactory.ArrayType(
SyntaxFactory.ParseTypeName(elementType),
SyntaxFactory.SingletonList(
SyntaxFactory.ArrayRankSpecifier(
SyntaxFactory.SingletonSeparatedList<ExpressionSyntax>(
SyntaxFactory.OmittedArraySizeExpression()
)
)
)
);

var initializer = SyntaxFactory.InitializerExpression(
SyntaxKind.ArrayInitializerExpression,
SyntaxFactory.SeparatedList(rewrittenElements)
);

// Create the array creation expression with proper spacing
return SyntaxFactory.ArrayCreationExpression(
SyntaxFactory.Token(SyntaxKind.NewKeyword).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")),
arrayTypeSyntax,
initializer
);
}
#endif
}
122 changes: 122 additions & 0 deletions TUnit.Engine/ConcurrentHashSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
namespace TUnit.Engine;

internal class ConcurrentHashSet<T>
{
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
private readonly HashSet<T> _hashSet = [];

#region Implementation of ICollection<T> ...ish

public bool Add(T item)
{
_lock.EnterWriteLock();

try
{
return _hashSet.Add(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}

public void Clear()
{
_lock.EnterWriteLock();

try
{
_hashSet.Clear();
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}

public bool Contains(T item)
{
_lock.EnterReadLock();

try
{
return _hashSet.Contains(item);
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}

public bool Remove(T item)
{
_lock.EnterWriteLock();

try
{
return _hashSet.Remove(item);
}
finally
{
if (_lock.IsWriteLockHeld)
{
_lock.ExitWriteLock();
}
}
}

public int Count
{
get
{
_lock.EnterReadLock();

try
{
return _hashSet.Count;
}
finally
{
if (_lock.IsReadLockHeld)
{
_lock.ExitReadLock();
}
}
}
}

#endregion

#region Dispose

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_lock.Dispose();
}
}

~ConcurrentHashSet()
{
Dispose(false);
}

#endregion
}
16 changes: 8 additions & 8 deletions TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ internal sealed class EventReceiverOrchestrator : IDisposable
private readonly ThreadSafeDictionary<string, Counter> _assemblyTestCounts = new();
private readonly ThreadSafeDictionary<Type, Counter> _classTestCounts = new();
private int _sessionTestCount;

// Track which objects have already been initialized to avoid duplicates
private readonly HashSet<object> _initializedObjects = new();
private readonly ConcurrentHashSet<object> _initializedObjects = new();

// Track registered First event receiver types to avoid duplicate registrations
private readonly HashSet<Type> _registeredFirstEventReceiverTypes = new();
private readonly ConcurrentHashSet<Type> _registeredFirstEventReceiverTypes = new();

public EventReceiverOrchestrator(TUnitFrameworkLogger logger)
{
Expand All @@ -45,19 +45,19 @@ public async ValueTask InitializeAllEligibleObjectsAsync(TestContext context, Ca
// Only initialize and register objects that haven't been processed yet
var newObjects = new List<object>();
var objectsToRegister = new List<object>();

foreach (var obj in eligibleObjects)
{
if (_initializedObjects.Add(obj)) // Add returns false if already present
{
newObjects.Add(obj);

// For First event receivers, only register one instance per type
var objType = obj.GetType();
bool isFirstEventReceiver = obj is IFirstTestInTestSessionEventReceiver ||
obj is IFirstTestInAssemblyEventReceiver ||
obj is IFirstTestInClassEventReceiver;

if (isFirstEventReceiver)
{
if (_registeredFirstEventReceiverTypes.Add(objType))
Expand All @@ -80,7 +80,7 @@ obj is IFirstTestInAssemblyEventReceiver ||
// Register only the objects that should be registered
_registry.RegisterReceivers(objectsToRegister);
}

if (newObjects.Count > 0)
{
// Initialize all new objects (even if not registered)
Expand Down
97 changes: 97 additions & 0 deletions TUnit.TestProject/Bugs/Issue2504CollectionExpressionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Collections.Concurrent;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.Bugs;

/// <summary>
/// Test for issue #2504 - Collection expression syntax in MethodDataSource Arguments
/// </summary>
[EngineTest(ExpectedResult.Pass)]
public class Issue2504CollectionExpressionTest
{
private static readonly ConcurrentBag<string> ExecutedTests = [];

[Test]
[MethodDataSource(nameof(GetDataWithSingleIntParam), Arguments = [5])] // Collection expression syntax
[MethodDataSource(nameof(GetDataWithSingleIntParam), Arguments = new object[] { 10 })] // Traditional array syntax
public async Task TestWithSingleArgument(int value)
{
ExecutedTests.Add($"SingleParam:{value}");
await Assert.That(value).IsIn(10, 20); // 5*2=10, 10*2=20
}

[Test]
[MethodDataSource(nameof(GetDataWithMultipleParams), Arguments = [10, "test"])] // Collection expression syntax
public async Task TestWithMultipleArguments(int number, string text)
{
ExecutedTests.Add($"MultiParam:{number}-{text}");
await Assert.That(number).IsEqualTo(15);
await Assert.That(text).IsEqualTo("test_modified");
}

[Test]
[MethodDataSource(nameof(GetDataWithArrayParam), Arguments = [new int[] { 4, 5 }])] // Collection expression with array element
public async Task TestWithArrayArgument(int value)
{
ExecutedTests.Add($"ArrayParam:{value}");
await Assert.That(value).IsIn(4, 5);
}

public static IEnumerable<int> GetDataWithSingleIntParam(int multiplier)
{
// Return test data based on the multiplier
yield return multiplier * 2;
}

public static IEnumerable<object[]> GetDataWithMultipleParams(int baseNumber, string baseText)
{
// Return modified values
yield return [baseNumber + 5, baseText + "_modified"];
}

public static IEnumerable<int> GetDataWithArrayParam(int[] values)
{
// Return each value from the array as test data
foreach (var value in values)
{
yield return value;
}
}

[After(Assembly)]
public static async Task VerifyTestsExecuted()
{
var executedTests = ExecutedTests.ToList();

// Skip verification if no tests were executed (e.g., filtered run)
if (executedTests.Count == 0)
{
return;
}

// Should have 5 test instances total:
// - 1 from first MethodDataSource with [5]
// - 1 from second MethodDataSource with { 10 }
// - 1 from TestWithMultipleArguments
// - 2 from TestWithArrayArgument (array has 2 elements)
await Assert.That(executedTests.Count).IsEqualTo(5);

// Verify we have the expected values
var expected = new[]
{
"SingleParam:10", // 5 * 2
"SingleParam:20", // 10 * 2 (this should be 20, not 15!)
"MultiParam:15-test_modified",
"ArrayParam:4",
"ArrayParam:5"
};

foreach (var expectedTest in expected)
{
await Assert.That(executedTests).Contains(expectedTest);
}

// Clear for next run
ExecutedTests.Clear();
}
}
Loading
Loading