1- using System . Collections . Concurrent ;
21using TUnit . Core ;
32using TUnit . Core . Logging ;
43using TUnit . Engine . Logging ;
@@ -41,14 +40,14 @@ public async ValueTask ExecuteTestsWithConstraintsAsync(
4140 var lockedKeys = new HashSet < string > ( ) ;
4241 var lockObject = new object ( ) ;
4342
44- // Queue for tests waiting for their constraint keys to become available
45- var waitingTests = new ConcurrentQueue < ( AbstractExecutableTest Test , IReadOnlyList < string > ConstraintKeys , TaskCompletionSource < bool > StartSignal ) > ( ) ;
43+ // Indexed structure for tests waiting for their constraint keys to become available
44+ var waitingTestIndex = new WaitingTestIndex ( ) ;
4645
4746 // Active test tasks
4847 var activeTasks = new List < Task > ( ) ;
4948
5049 // Process each test
51- foreach ( var ( test , constraintKeys , _ ) in sortedTests )
50+ foreach ( var ( test , constraintKeys , priority ) in sortedTests )
5251 {
5352 var startSignal = new TaskCompletionSource < bool > ( ) ;
5453
@@ -75,6 +74,17 @@ public async ValueTask ExecuteTestsWithConstraintsAsync(
7574 lockedKeys . Add ( constraintKeys [ i ] ) ;
7675 }
7776 }
77+ else
78+ {
79+ // Add to the indexed waiting structure while still under lock
80+ waitingTestIndex . Add ( new WaitingTest
81+ {
82+ TestId = test . TestId ,
83+ ConstraintKeys = constraintKeys ,
84+ StartSignal = startSignal ,
85+ Priority = priority
86+ } ) ;
87+ }
7888 }
7989
8090 if ( canStart )
@@ -84,18 +94,17 @@ public async ValueTask ExecuteTestsWithConstraintsAsync(
8494 await _logger . LogDebugAsync ( $ "Starting test { test . TestId } with constraint keys: { string . Join ( ", " , constraintKeys ) } ") . ConfigureAwait ( false ) ;
8595 startSignal . SetResult ( true ) ;
8696
87- var testTask = ExecuteTestAndReleaseKeysAsync ( test , constraintKeys , lockedKeys , lockObject , waitingTests , cancellationToken ) ;
97+ var testTask = ExecuteTestAndReleaseKeysAsync ( test , constraintKeys , lockedKeys , lockObject , waitingTestIndex , cancellationToken ) ;
8898 test . ExecutionTask = testTask ;
8999 activeTasks . Add ( testTask ) ;
90100 }
91101 else
92102 {
93- // Queue the test to wait for its keys
103+ // Test was already added to the waiting index inside the lock above
94104 if ( _logger . IsDebugEnabled )
95105 await _logger . LogDebugAsync ( $ "Queueing test { test . TestId } waiting for constraint keys: { string . Join ( ", " , constraintKeys ) } ") . ConfigureAwait ( false ) ;
96- waitingTests . Enqueue ( ( test , constraintKeys , startSignal ) ) ;
97106
98- var testTask = WaitAndExecuteTestAsync ( test , constraintKeys , startSignal , lockedKeys , lockObject , waitingTests , cancellationToken ) ;
107+ var testTask = WaitAndExecuteTestAsync ( test , constraintKeys , startSignal , lockedKeys , lockObject , waitingTestIndex , cancellationToken ) ;
99108 test . ExecutionTask = testTask ;
100109 activeTasks . Add ( testTask ) ;
101110 }
@@ -114,7 +123,7 @@ private async Task WaitAndExecuteTestAsync(
114123 TaskCompletionSource < bool > startSignal ,
115124 HashSet < string > lockedKeys ,
116125 object lockObject ,
117- ConcurrentQueue < ( AbstractExecutableTest Test , IReadOnlyList < string > ConstraintKeys , TaskCompletionSource < bool > StartSignal ) > waitingTests ,
126+ WaitingTestIndex waitingTestIndex ,
118127 CancellationToken cancellationToken )
119128 {
120129 // Wait for signal to start
@@ -123,7 +132,7 @@ private async Task WaitAndExecuteTestAsync(
123132 if ( _logger . IsDebugEnabled )
124133 await _logger . LogDebugAsync ( $ "Starting previously queued test { test . TestId } with constraint keys: { string . Join ( ", " , constraintKeys ) } ") . ConfigureAwait ( false ) ;
125134
126- await ExecuteTestAndReleaseKeysAsync ( test , constraintKeys , lockedKeys , lockObject , waitingTests , cancellationToken ) . ConfigureAwait ( false ) ;
135+ await ExecuteTestAndReleaseKeysAsync ( test , constraintKeys , lockedKeys , lockObject , waitingTestIndex , cancellationToken ) . ConfigureAwait ( false ) ;
127136 }
128137
129138 #if NET6_0_OR_GREATER
@@ -134,7 +143,7 @@ private async Task ExecuteTestAndReleaseKeysAsync(
134143 IReadOnlyList < string > constraintKeys ,
135144 HashSet < string > lockedKeys ,
136145 object lockObject ,
137- ConcurrentQueue < ( AbstractExecutableTest Test , IReadOnlyList < string > ConstraintKeys , TaskCompletionSource < bool > StartSignal ) > waitingTests ,
146+ WaitingTestIndex waitingTestIndex ,
138147 CancellationToken cancellationToken )
139148 {
140149 SemaphoreSlim ? parallelLimiterSemaphore = null ;
@@ -158,9 +167,7 @@ private async Task ExecuteTestAndReleaseKeysAsync(
158167 parallelLimiterSemaphore ? . Release ( ) ;
159168
160169 // Release the constraint keys and check if any waiting tests can now run
161- // Pre-allocate lists outside the lock to minimize lock duration
162- var testsToStart = new List < ( AbstractExecutableTest Test , IReadOnlyList < string > ConstraintKeys , TaskCompletionSource < bool > StartSignal ) > ( ) ;
163- var testsToRequeue = new List < ( AbstractExecutableTest Test , IReadOnlyList < string > ConstraintKeys , TaskCompletionSource < bool > StartSignal ) > ( ) ;
170+ var testsToStart = new List < WaitingTest > ( ) ;
164171
165172 lock ( lockObject )
166173 {
@@ -170,16 +177,23 @@ private async Task ExecuteTestAndReleaseKeysAsync(
170177 lockedKeys . Remove ( key ) ;
171178 }
172179
173- // Check waiting tests to see if any can now run
180+ // Only examine tests that are waiting on the keys we just released (O(k) lookup)
181+ var candidates = waitingTestIndex . GetCandidatesForReleasedKeys ( constraintKeys ) ;
174182
175- while ( waitingTests . TryDequeue ( out var waitingTest ) )
183+ // Sort candidates by priority to respect ordering
184+ // Use a simple list + sort rather than a SortedSet to avoid per-element allocation
185+ var sortedCandidates = new List < WaitingTest > ( candidates . Count ) ;
186+ sortedCandidates . AddRange ( candidates ) ;
187+ sortedCandidates . Sort ( static ( a , b ) => a . Priority . CompareTo ( b . Priority ) ) ;
188+
189+ foreach ( var candidate in sortedCandidates )
176190 {
177- // Check if all constraint keys are available for this waiting test - manual loop avoids LINQ allocation
191+ // Check if all constraint keys are available for this candidate
178192 var canStart = true ;
179- var waitingKeyCount = waitingTest . ConstraintKeys . Count ;
193+ var waitingKeyCount = candidate . ConstraintKeys . Count ;
180194 for ( var i = 0 ; i < waitingKeyCount ; i ++ )
181195 {
182- if ( lockedKeys . Contains ( waitingTest . ConstraintKeys [ i ] ) )
196+ if ( lockedKeys . Contains ( candidate . ConstraintKeys [ i ] ) )
183197 {
184198 canStart = false ;
185199 break ;
@@ -191,23 +205,14 @@ private async Task ExecuteTestAndReleaseKeysAsync(
191205 // Lock the keys for this test
192206 for ( var i = 0 ; i < waitingKeyCount ; i ++ )
193207 {
194- lockedKeys . Add ( waitingTest . ConstraintKeys [ i ] ) ;
208+ lockedKeys . Add ( candidate . ConstraintKeys [ i ] ) ;
195209 }
196210
197- // Mark test to start after we exit the lock
198- testsToStart . Add ( waitingTest ) ;
211+ // Remove from the index and mark for starting
212+ waitingTestIndex . Remove ( candidate ) ;
213+ testsToStart . Add ( candidate ) ;
199214 }
200- else
201- {
202- // Still can't run, keep it in the queue
203- testsToRequeue . Add ( waitingTest ) ;
204- }
205- }
206-
207- // Re-add tests that still can't run
208- foreach ( var waitingTestItem in testsToRequeue )
209- {
210- waitingTests . Enqueue ( waitingTestItem ) ;
215+ // If can't start, leave it in the index for future key releases
211216 }
212217 }
213218
@@ -218,7 +223,7 @@ private async Task ExecuteTestAndReleaseKeysAsync(
218223 foreach ( var testToStart in testsToStart )
219224 {
220225 if ( _logger . IsDebugEnabled )
221- await _logger . LogDebugAsync ( $ "Unblocking waiting test { testToStart . Test . TestId } with constraint keys: { string . Join ( ", " , testToStart . ConstraintKeys ) } ") . ConfigureAwait ( false ) ;
226+ await _logger . LogDebugAsync ( $ "Unblocking waiting test { testToStart . TestId } with constraint keys: { string . Join ( ", " , testToStart . ConstraintKeys ) } ") . ConfigureAwait ( false ) ;
222227 testToStart . StartSignal . SetResult ( true ) ;
223228 }
224229 }
0 commit comments