1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ namespace VirtualClient . Contracts
5+ {
6+ using Microsoft . Extensions . DependencyInjection ;
7+ using Moq ;
8+ using NUnit . Framework ;
9+ using System ;
10+ using System . Collections . Generic ;
11+ using System . Threading ;
12+ using System . Threading . Tasks ;
13+ using VirtualClient . Common . Telemetry ;
14+ using VirtualClient . Contracts ;
15+
16+ [ TestFixture ]
17+ [ Category ( "Unit" ) ]
18+ public class ParallelLoopExecutionTests
19+ {
20+ private MockFixture fixture ;
21+
22+ [ SetUp ]
23+ public void SetupDefaults ( )
24+ {
25+ this . fixture = new MockFixture ( ) ;
26+ this . fixture . Parameters = new Dictionary < string , IConvertible >
27+ {
28+ { "Duration" , "00:00:01" } , // Default duration for tests
29+ { "MinimumIteration" , 0 } // Default minimum iterations
30+ } ;
31+ }
32+
33+ [ Test ]
34+ public async Task ParallelLoopExecution_RespectsDurationParameter ( )
35+ {
36+ var component = new TestComponent ( this . fixture . Dependencies , this . fixture . Parameters , async token =>
37+ {
38+ await Task . Delay ( 5000 , token ) ; // Simulate long-running task
39+ } ) ;
40+
41+ var collection = new TestParallelLoopExecution ( this . fixture ) ;
42+ collection . Add ( component ) ;
43+
44+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
45+ await collection . ExecuteAsync ( EventContext . None , CancellationToken . None ) ;
46+ sw . Stop ( ) ;
47+
48+ // Assert: Should not run for more than ~2 seconds (buffer for scheduling)
49+ Assert . LessOrEqual ( sw . Elapsed . TotalSeconds , 2.5 , "Execution did not respect the Duration parameter." ) ;
50+ }
51+
52+ [ Test ]
53+ public async Task ParallelLoopExecution_RespectsMinimumIterationParameterAndTimeout ( )
54+ {
55+ this . fixture . Parameters [ "MinimumIteration" ] = 2 ;
56+ this . fixture . Parameters [ "Duration" ] = "00:00:01" ;
57+
58+ var component = new TestComponent ( this . fixture . Dependencies , this . fixture . Parameters , async token =>
59+ {
60+ await Task . Delay ( 600 , token ) ; // Simulate long-running task
61+ } ) ;
62+
63+ var collection = new TestParallelLoopExecution ( this . fixture ) ;
64+ collection . Add ( component ) ;
65+
66+ using ( var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 2 ) ) )
67+ {
68+ try
69+ {
70+ await collection . ExecuteAsync ( EventContext . None , cts . Token ) ;
71+ }
72+ catch { /* ignore */ }
73+ }
74+
75+ // Assert: Should run at exactly 2 times, as each iteration takes 600ms,
76+ // Timeout is 1 second and Cancellation Token comes at 2 seconds
77+ Assert . AreEqual ( component . ExecutionCount , 2 , "Did not execute the minimum number of iterations." ) ;
78+ }
79+
80+ [ Test ]
81+ public async Task ParallelLoopExecution_RespectsMinimumIterationParameter ( )
82+ {
83+ this . fixture . Parameters [ "MinimumIteration" ] = 7 ;
84+
85+ var component = new TestComponent ( this . fixture . Dependencies , this . fixture . Parameters , token =>
86+ {
87+ return Task . CompletedTask ;
88+ } ) ;
89+
90+ var collection = new TestParallelLoopExecution ( this . fixture ) ;
91+ collection . Add ( component ) ;
92+
93+ using ( var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 2 ) ) )
94+ {
95+ try
96+ {
97+ await collection . ExecuteAsync ( EventContext . None , cts . Token ) ;
98+ }
99+ catch { /* ignore */ }
100+ }
101+
102+ // Assert: Should run at least MinimumIteration times
103+ Assert . GreaterOrEqual ( component . ExecutionCount , 7 , "Did not execute the minimum number of iterations." ) ;
104+ }
105+
106+ [ Test ]
107+ public void ParallelLoopExecution_ThrowsWorkloadException_WhenComponentThrows ( )
108+ {
109+ var component = new TestComponent ( this . fixture . Dependencies , this . fixture . Parameters , token =>
110+ {
111+ throw new InvalidOperationException ( "Test exception" ) ;
112+ } ) ;
113+
114+ var collection = new TestParallelLoopExecution ( this . fixture ) ;
115+ collection . Add ( component ) ;
116+
117+ var ex = Assert . ThrowsAsync < WorkloadException > (
118+ ( ) => collection . ExecuteAsync ( EventContext . None , CancellationToken . None ) ) ;
119+ Assert . That ( ex . Message , Does . Contain ( "task execution failed" ) ) ;
120+ Assert . IsInstanceOf < InvalidOperationException > ( ex . InnerException ) ;
121+ }
122+
123+ private class TestComponent : VirtualClientComponent
124+ {
125+ private readonly Func < CancellationToken , Task > onExecuteAsync ;
126+
127+ public int ExecutionCount { get ; private set ; }
128+
129+ public TestComponent ( IServiceCollection dependencies , IDictionary < string , IConvertible > parameters , Func < CancellationToken , Task > onExecuteAsync = null )
130+ : base ( dependencies , parameters )
131+ {
132+ this . onExecuteAsync = onExecuteAsync ?? ( _ => Task . CompletedTask ) ;
133+ }
134+
135+ protected override async Task ExecuteAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
136+ {
137+ this . ExecutionCount ++ ;
138+ await this . onExecuteAsync ( cancellationToken ) ;
139+ }
140+ }
141+
142+ private class TestParallelLoopExecution : ParallelLoopExecution
143+ {
144+ public TestParallelLoopExecution ( MockFixture fixture )
145+ : base ( fixture . Dependencies , fixture . Parameters )
146+ {
147+ }
148+
149+ public new Task InitializeAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
150+ {
151+ return base . InitializeAsync ( telemetryContext , cancellationToken ) ;
152+ }
153+
154+ public new Task ExecuteAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
155+ {
156+ return base . ExecuteAsync ( telemetryContext , cancellationToken ) ;
157+ }
158+ }
159+ }
160+ }
0 commit comments