Skip to content

Commit 3b318df

Browse files
authored
+semver:minor - Convert collection expressions to array initializers for property assignments in FullyQualifiedWithGlobalPrefixRewriter (#3151)
* Convert collection expressions to array initializers for property assignments in FullyQualifiedWithGlobalPrefixRewriter * Update MethodDataSource attributes to use object initializers for Arguments and Shared properties * Add ConcurrentHashSet implementation and update EventReceiverOrchestrator to use it for tracking initialized objects
1 parent fd1150a commit 3b318df

File tree

8 files changed

+316
-21
lines changed

8 files changed

+316
-21
lines changed

TUnit.Core.SourceGenerator.Tests/AsyncMethodDataSourceDrivenTests.Test.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ internal sealed class AsyncMethodDataSourceDrivenTests_AsyncMethodDataSource_Wit
566566
[
567567
new global::TUnit.Core.TestAttribute(),
568568
new global::TUnit.Core.MethodDataSourceAttribute("AsyncDataMethodWithArgs")
569-
{Arguments = [5],},
569+
{Arguments = new object[]{5},},
570570
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
571571
],
572572
DataSources = new global::TUnit.Core.IDataSourceAttribute[]

TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ internal sealed class MethodDataSourceDrivenTests_DataSource_Method3_TestSource_
400400
[
401401
new global::TUnit.Core.TestAttribute(),
402402
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
403-
{Arguments = [5],},
403+
{Arguments = new object[]{5},},
404404
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
405405
{Arguments = new object[] { 5 },},
406406
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)
@@ -545,11 +545,11 @@ internal sealed class MethodDataSourceDrivenTests_DataSource_Method4_TestSource_
545545
[
546546
new global::TUnit.Core.TestAttribute(),
547547
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
548-
{Arguments = ["Hello World!",5,true],},
548+
{Arguments = new object[]{"Hello World!",5,true},},
549549
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
550550
{Arguments = new object[] { "Hello World!", 6, true },},
551551
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
552-
{Arguments = ["Hello World!",7,true],},
552+
{Arguments = new object[]{"Hello World!",7,true},},
553553
new global::TUnit.Core.MethodDataSourceAttribute("SomeMethod")
554554
{Arguments = new object[] { "Hello World!", 8, true },},
555555
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass)

TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.Test.verified.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal sealed class MultipleClassDataSourceDrivenTests_Test1_TestSource_GUID :
2020
new global::TUnit.Core.TestAttribute(),
2121
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass),
2222
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>()
23-
{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],}
23+
{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},}
2424
],
2525
DataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
2626
ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[]
@@ -154,7 +154,7 @@ internal sealed class MultipleClassDataSourceDrivenTests_Test2_TestSource_GUID :
154154
new global::TUnit.Core.TestAttribute(),
155155
new global::TUnit.TestProject.Attributes.EngineTest(global::TUnit.TestProject.Attributes.ExpectedResult.Pass),
156156
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>()
157-
{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],}
157+
{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},}
158158
],
159159
DataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
160160
ClassDataSources = new global::TUnit.Core.IDataSourceAttribute[]

TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,20 +176,58 @@ public override SyntaxNode VisitTypeOfExpression(TypeOfExpressionSyntax node)
176176
#if ROSLYN4_7_OR_GREATER
177177
public override SyntaxNode? VisitCollectionExpression(CollectionExpressionSyntax node)
178178
{
179-
// For collection expressions, visit each element and ensure proper type conversion
180-
var rewrittenElements = node.Elements.Select(element =>
179+
// Convert collection expressions to array initializers for property assignments
180+
// Collection expressions like [1, 2, 3] need to be converted to new object[] { 1, 2, 3 }
181+
// when used in property initializers to avoid compilation errors
182+
183+
// Get the type info from the semantic model if available
184+
var typeInfo = semanticModel.GetTypeInfo(node);
185+
var elementType = "object";
186+
187+
if (typeInfo.ConvertedType is IArrayTypeSymbol arrayTypeSymbol)
188+
{
189+
elementType = arrayTypeSymbol.ElementType.GloballyQualified();
190+
}
191+
else if (typeInfo.Type is IArrayTypeSymbol arrayTypeSymbol2)
192+
{
193+
elementType = arrayTypeSymbol2.ElementType.GloballyQualified();
194+
}
195+
196+
// Visit and rewrite each element
197+
var rewrittenElements = new List<ExpressionSyntax>();
198+
foreach (var element in node.Elements)
181199
{
182200
if (element is ExpressionElementSyntax expressionElement)
183201
{
184202
var rewrittenExpression = Visit(expressionElement.Expression);
185-
return SyntaxFactory.ExpressionElement((ExpressionSyntax)rewrittenExpression);
203+
rewrittenElements.Add((ExpressionSyntax)rewrittenExpression);
186204
}
187-
return element;
188-
}).ToList();
189-
190-
return SyntaxFactory.CollectionExpression(
205+
}
206+
207+
// Create an array creation expression instead of a collection expression
208+
// This ensures compatibility with property initializers
209+
var arrayTypeSyntax = SyntaxFactory.ArrayType(
210+
SyntaxFactory.ParseTypeName(elementType),
211+
SyntaxFactory.SingletonList(
212+
SyntaxFactory.ArrayRankSpecifier(
213+
SyntaxFactory.SingletonSeparatedList<ExpressionSyntax>(
214+
SyntaxFactory.OmittedArraySizeExpression()
215+
)
216+
)
217+
)
218+
);
219+
220+
var initializer = SyntaxFactory.InitializerExpression(
221+
SyntaxKind.ArrayInitializerExpression,
191222
SyntaxFactory.SeparatedList(rewrittenElements)
192223
);
224+
225+
// Create the array creation expression with proper spacing
226+
return SyntaxFactory.ArrayCreationExpression(
227+
SyntaxFactory.Token(SyntaxKind.NewKeyword).WithTrailingTrivia(SyntaxFactory.Whitespace(" ")),
228+
arrayTypeSyntax,
229+
initializer
230+
);
193231
}
194232
#endif
195233
}

TUnit.Engine/ConcurrentHashSet.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
namespace TUnit.Engine;
2+
3+
internal class ConcurrentHashSet<T>
4+
{
5+
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
6+
private readonly HashSet<T> _hashSet = [];
7+
8+
#region Implementation of ICollection<T> ...ish
9+
10+
public bool Add(T item)
11+
{
12+
_lock.EnterWriteLock();
13+
14+
try
15+
{
16+
return _hashSet.Add(item);
17+
}
18+
finally
19+
{
20+
if (_lock.IsWriteLockHeld)
21+
{
22+
_lock.ExitWriteLock();
23+
}
24+
}
25+
}
26+
27+
public void Clear()
28+
{
29+
_lock.EnterWriteLock();
30+
31+
try
32+
{
33+
_hashSet.Clear();
34+
}
35+
finally
36+
{
37+
if (_lock.IsWriteLockHeld)
38+
{
39+
_lock.ExitWriteLock();
40+
}
41+
}
42+
}
43+
44+
public bool Contains(T item)
45+
{
46+
_lock.EnterReadLock();
47+
48+
try
49+
{
50+
return _hashSet.Contains(item);
51+
}
52+
finally
53+
{
54+
if (_lock.IsReadLockHeld)
55+
{
56+
_lock.ExitReadLock();
57+
}
58+
}
59+
}
60+
61+
public bool Remove(T item)
62+
{
63+
_lock.EnterWriteLock();
64+
65+
try
66+
{
67+
return _hashSet.Remove(item);
68+
}
69+
finally
70+
{
71+
if (_lock.IsWriteLockHeld)
72+
{
73+
_lock.ExitWriteLock();
74+
}
75+
}
76+
}
77+
78+
public int Count
79+
{
80+
get
81+
{
82+
_lock.EnterReadLock();
83+
84+
try
85+
{
86+
return _hashSet.Count;
87+
}
88+
finally
89+
{
90+
if (_lock.IsReadLockHeld)
91+
{
92+
_lock.ExitReadLock();
93+
}
94+
}
95+
}
96+
}
97+
98+
#endregion
99+
100+
#region Dispose
101+
102+
public void Dispose()
103+
{
104+
Dispose(true);
105+
GC.SuppressFinalize(this);
106+
}
107+
108+
protected virtual void Dispose(bool disposing)
109+
{
110+
if (disposing)
111+
{
112+
_lock.Dispose();
113+
}
114+
}
115+
116+
~ConcurrentHashSet()
117+
{
118+
Dispose(false);
119+
}
120+
121+
#endregion
122+
}

TUnit.Engine/Services/EventReceiverOrchestrator.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ internal sealed class EventReceiverOrchestrator : IDisposable
2626
private readonly ThreadSafeDictionary<string, Counter> _assemblyTestCounts = new();
2727
private readonly ThreadSafeDictionary<Type, Counter> _classTestCounts = new();
2828
private int _sessionTestCount;
29-
29+
3030
// Track which objects have already been initialized to avoid duplicates
31-
private readonly HashSet<object> _initializedObjects = new();
32-
31+
private readonly ConcurrentHashSet<object> _initializedObjects = new();
32+
3333
// Track registered First event receiver types to avoid duplicate registrations
34-
private readonly HashSet<Type> _registeredFirstEventReceiverTypes = new();
34+
private readonly ConcurrentHashSet<Type> _registeredFirstEventReceiverTypes = new();
3535

3636
public EventReceiverOrchestrator(TUnitFrameworkLogger logger)
3737
{
@@ -45,19 +45,19 @@ public async ValueTask InitializeAllEligibleObjectsAsync(TestContext context, Ca
4545
// Only initialize and register objects that haven't been processed yet
4646
var newObjects = new List<object>();
4747
var objectsToRegister = new List<object>();
48-
48+
4949
foreach (var obj in eligibleObjects)
5050
{
5151
if (_initializedObjects.Add(obj)) // Add returns false if already present
5252
{
5353
newObjects.Add(obj);
54-
54+
5555
// For First event receivers, only register one instance per type
5656
var objType = obj.GetType();
5757
bool isFirstEventReceiver = obj is IFirstTestInTestSessionEventReceiver ||
5858
obj is IFirstTestInAssemblyEventReceiver ||
5959
obj is IFirstTestInClassEventReceiver;
60-
60+
6161
if (isFirstEventReceiver)
6262
{
6363
if (_registeredFirstEventReceiverTypes.Add(objType))
@@ -80,7 +80,7 @@ obj is IFirstTestInAssemblyEventReceiver ||
8080
// Register only the objects that should be registered
8181
_registry.RegisterReceivers(objectsToRegister);
8282
}
83-
83+
8484
if (newObjects.Count > 0)
8585
{
8686
// Initialize all new objects (even if not registered)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Collections.Concurrent;
2+
using TUnit.TestProject.Attributes;
3+
4+
namespace TUnit.TestProject.Bugs;
5+
6+
/// <summary>
7+
/// Test for issue #2504 - Collection expression syntax in MethodDataSource Arguments
8+
/// </summary>
9+
[EngineTest(ExpectedResult.Pass)]
10+
public class Issue2504CollectionExpressionTest
11+
{
12+
private static readonly ConcurrentBag<string> ExecutedTests = [];
13+
14+
[Test]
15+
[MethodDataSource(nameof(GetDataWithSingleIntParam), Arguments = [5])] // Collection expression syntax
16+
[MethodDataSource(nameof(GetDataWithSingleIntParam), Arguments = new object[] { 10 })] // Traditional array syntax
17+
public async Task TestWithSingleArgument(int value)
18+
{
19+
ExecutedTests.Add($"SingleParam:{value}");
20+
await Assert.That(value).IsIn(10, 20); // 5*2=10, 10*2=20
21+
}
22+
23+
[Test]
24+
[MethodDataSource(nameof(GetDataWithMultipleParams), Arguments = [10, "test"])] // Collection expression syntax
25+
public async Task TestWithMultipleArguments(int number, string text)
26+
{
27+
ExecutedTests.Add($"MultiParam:{number}-{text}");
28+
await Assert.That(number).IsEqualTo(15);
29+
await Assert.That(text).IsEqualTo("test_modified");
30+
}
31+
32+
[Test]
33+
[MethodDataSource(nameof(GetDataWithArrayParam), Arguments = [new int[] { 4, 5 }])] // Collection expression with array element
34+
public async Task TestWithArrayArgument(int value)
35+
{
36+
ExecutedTests.Add($"ArrayParam:{value}");
37+
await Assert.That(value).IsIn(4, 5);
38+
}
39+
40+
public static IEnumerable<int> GetDataWithSingleIntParam(int multiplier)
41+
{
42+
// Return test data based on the multiplier
43+
yield return multiplier * 2;
44+
}
45+
46+
public static IEnumerable<object[]> GetDataWithMultipleParams(int baseNumber, string baseText)
47+
{
48+
// Return modified values
49+
yield return [baseNumber + 5, baseText + "_modified"];
50+
}
51+
52+
public static IEnumerable<int> GetDataWithArrayParam(int[] values)
53+
{
54+
// Return each value from the array as test data
55+
foreach (var value in values)
56+
{
57+
yield return value;
58+
}
59+
}
60+
61+
[After(Assembly)]
62+
public static async Task VerifyTestsExecuted()
63+
{
64+
var executedTests = ExecutedTests.ToList();
65+
66+
// Skip verification if no tests were executed (e.g., filtered run)
67+
if (executedTests.Count == 0)
68+
{
69+
return;
70+
}
71+
72+
// Should have 5 test instances total:
73+
// - 1 from first MethodDataSource with [5]
74+
// - 1 from second MethodDataSource with { 10 }
75+
// - 1 from TestWithMultipleArguments
76+
// - 2 from TestWithArrayArgument (array has 2 elements)
77+
await Assert.That(executedTests.Count).IsEqualTo(5);
78+
79+
// Verify we have the expected values
80+
var expected = new[]
81+
{
82+
"SingleParam:10", // 5 * 2
83+
"SingleParam:20", // 10 * 2 (this should be 20, not 15!)
84+
"MultiParam:15-test_modified",
85+
"ArrayParam:4",
86+
"ArrayParam:5"
87+
};
88+
89+
foreach (var expectedTest in expected)
90+
{
91+
await Assert.That(executedTests).Contains(expectedTest);
92+
}
93+
94+
// Clear for next run
95+
ExecutedTests.Clear();
96+
}
97+
}

0 commit comments

Comments
 (0)