Skip to content

Commit e382676

Browse files
imadityaaAditya Abhishek
andauthored
Add a loop executor to selectively run components in a loop in a profile (#544)
* Add a loop executor * add documentation and rename component * one more conflict * fix conflict issues --------- Co-authored-by: Aditya Abhishek <[email protected]>
1 parent 84af897 commit e382676

File tree

7 files changed

+483
-0
lines changed

7 files changed

+483
-0
lines changed

src/VirtualClient/VirtualClient.Contracts.UnitTests/ComponentFactoryTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,41 @@ public void ComponentFactoryCreatesExpectedParallelLoopExecutionComponentsFromAn
206206
Assert.IsTrue(confirmed);
207207
}
208208

209+
[Test]
210+
[TestCase("TEST-PROFILE-1-SEQUENTIAL.json")]
211+
public void ComponentFactoryCreatesExpectedSequentialExecutionComponentsFromAnExecutionProfile(string profileName)
212+
{
213+
ExecutionProfile profile = File.ReadAllText(Path.Combine(MockFixture.TestAssemblyDirectory, "Resources", profileName))
214+
.FromJson<ExecutionProfile>();
215+
216+
bool confirmed = false;
217+
foreach (ExecutionProfileElement action in profile.Actions)
218+
{
219+
Assert.DoesNotThrow(() =>
220+
{
221+
VirtualClientComponent component = ComponentFactory.CreateComponent(action, this.mockFixture.Dependencies);
222+
Assert.IsNotNull(component);
223+
Assert.IsNotEmpty(component.Dependencies);
224+
Assert.IsNotNull(component.Parameters);
225+
226+
SequentialExecution sequentialExecutionComponent = component as SequentialExecution;
227+
if (sequentialExecutionComponent != null)
228+
{
229+
Assert.IsNotEmpty(sequentialExecutionComponent);
230+
Assert.IsTrue(sequentialExecutionComponent.Count() == 2);
231+
Assert.IsTrue(sequentialExecutionComponent.ElementAt(0) is TestExecutor);
232+
Assert.IsTrue(sequentialExecutionComponent.ElementAt(1) is TestExecutor);
233+
Assert.IsTrue(sequentialExecutionComponent.ElementAt(0).Parameters["Scenario"].ToString() == "ScenarioA");
234+
Assert.IsTrue(sequentialExecutionComponent.ElementAt(1).Parameters["Scenario"].ToString() == "ScenarioB");
235+
Assert.AreEqual(2, sequentialExecutionComponent.LoopCount);
236+
confirmed = true;
237+
}
238+
});
239+
}
240+
241+
Assert.IsTrue(confirmed);
242+
}
243+
209244
[Test]
210245
public void ComponentFactoryAddsExpectedComponentLevelMetadataToSubComponents_Deep_Nesting()
211246
{

src/VirtualClient/VirtualClient.Contracts.UnitTests/ExecutionProfileElementTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ public void ExecutionProfileElementIsJsonSerializableWithParallelLoopExecutionDe
8383
SerializationAssert.IsJsonSerializable<ExecutionProfileElement>(element);
8484
}
8585

86+
[Test]
87+
public void ExecutionProfileElementIsJsonSerializableWithSequentialExecutionDefinitions()
88+
{
89+
// Add 2 child/subcomponents to the parent elements.
90+
ExecutionProfileElement element = new ExecutionProfileElement(typeof(SequentialExecution).Name, null, null, new List<ExecutionProfileElement>
91+
{
92+
this.fixture.Create<ExecutionProfileElement>(),
93+
this.fixture.Create<ExecutionProfileElement>()
94+
});
95+
96+
SerializationAssert.IsJsonSerializable<ExecutionProfileElement>(element);
97+
}
98+
8699
[Test]
87100
public void ExecutionProfileElementImplementsHashCodeSemanticsCorrectly()
88101
{

src/VirtualClient/VirtualClient.Contracts.UnitTests/ExecutionProfileTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ public void ExecutionProfileCanDeserializeProfileFilesWithParallelLoopExecutionC
9393
Assert.IsTrue(profile.Actions[1].Components.Count() == 2);
9494
}
9595

96+
[Test]
97+
[TestCase("TEST-PROFILE-1-SEQUENTIAL.json")]
98+
public void ExecutionProfileCanDeserializeProfileFilesWithSequentialExecutionComponents(string profileName)
99+
{
100+
ExecutionProfile profile = File.ReadAllText(Path.Combine(MockFixture.TestAssemblyDirectory, "Resources", profileName))
101+
.FromJson<ExecutionProfile>();
102+
103+
Assert.IsNotEmpty(profile.Actions);
104+
Assert.IsTrue(profile.Actions.Count == 2);
105+
106+
Assert.IsNotEmpty(profile.Actions[1].Components);
107+
Assert.IsTrue(profile.Actions[1].Components.Count() == 2);
108+
}
109+
96110
[Test]
97111
public void ExecutionProfileImplementsHashCodeSemanticsCorrectly()
98112
{
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"Description": "Test Sequential Execution Profile",
3+
"MinimumExecutionInterval": "00:01:00",
4+
"Parameters": {
5+
"Parameter1": "AnyValue",
6+
"Parameter2": 12345
7+
},
8+
"Actions": [
9+
{
10+
"Type": "TestExecutor",
11+
"Parameters": {
12+
"Scenario": "Scenario1",
13+
"PackageName": "anypackage",
14+
"Parameter1": "$.Parameters.Parameter1",
15+
"Parameter2": "$.Parameters.Parameter2"
16+
}
17+
},
18+
{
19+
"Type": "SequentialExecution",
20+
"Parameters": {
21+
"LoopCount": 2
22+
},
23+
"Components": [
24+
{
25+
"Type": "TestExecutor",
26+
"Parameters": {
27+
"Scenario": "ScenarioA",
28+
"Parameter1": "$.Parameters.Parameter1"
29+
}
30+
},
31+
{
32+
"Type": "TestExecutor",
33+
"Parameters": {
34+
"Scenario": "ScenarioB",
35+
"Parameter1": "$.Parameters.Parameter1"
36+
}
37+
}
38+
]
39+
}
40+
],
41+
"Dependencies": [
42+
{
43+
"Type": "TestDependency",
44+
"Parameters": {}
45+
}
46+
],
47+
"Monitors": [
48+
{
49+
"Type": "TestMonitor",
50+
"Parameters": {}
51+
}
52+
]
53+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace VirtualClient.Contracts
5+
{
6+
using Microsoft.Extensions.DependencyInjection;
7+
using NUnit.Framework;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using VirtualClient.Common.Telemetry;
13+
using VirtualClient.Contracts;
14+
15+
[TestFixture]
16+
[Category("Unit")]
17+
public class SequentialExecutionTests
18+
{
19+
private MockFixture fixture;
20+
21+
[SetUp]
22+
public void SetupDefaults()
23+
{
24+
this.fixture = new MockFixture();
25+
this.fixture.Parameters = new Dictionary<string, IConvertible>
26+
{
27+
{ "LoopCount", 3 }
28+
};
29+
}
30+
31+
[Test]
32+
public async Task SequentialExecution_ExecutesComponentsTheSpecifiedNumberOfTimes()
33+
{
34+
int loopCount = 5;
35+
this.fixture.Parameters["LoopCount"] = loopCount;
36+
37+
var component1 = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
38+
{
39+
return Task.CompletedTask;
40+
});
41+
42+
var component2 = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
43+
{
44+
return Task.CompletedTask;
45+
});
46+
47+
var collection = new TestSequentialExecution(this.fixture);
48+
collection.Add(component1);
49+
collection.Add(component2);
50+
51+
await collection.ExecuteAsync(EventContext.None, CancellationToken.None);
52+
53+
Assert.AreEqual(loopCount, component1.ExecutionCount, "Component1 was not executed the expected number of times.");
54+
Assert.AreEqual(loopCount, component2.ExecutionCount, "Component2 was not executed the expected number of times.");
55+
}
56+
57+
[Test]
58+
public async Task SequentialExecution_RespectsCancellationToken()
59+
{
60+
int loopCount = 100;
61+
this.fixture.Parameters["LoopCount"] = loopCount;
62+
63+
var component = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, async token =>
64+
{
65+
await Task.Delay(100, token);
66+
});
67+
68+
var collection = new TestSequentialExecution(this.fixture);
69+
collection.Add(component);
70+
71+
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
72+
{
73+
try
74+
{
75+
await collection.ExecuteAsync(EventContext.None, cts.Token);
76+
}
77+
catch (OperationCanceledException) { }
78+
}
79+
80+
Assert.Less(component.ExecutionCount, loopCount, "Component should not have executed all iterations due to cancellation.");
81+
}
82+
83+
[Test]
84+
public void SequentialExecution_ThrowsWorkloadException_WhenComponentThrows()
85+
{
86+
this.fixture.Parameters["LoopCount"] = 2;
87+
88+
var component = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
89+
{
90+
throw new InvalidOperationException("Test exception");
91+
});
92+
93+
var collection = new TestSequentialExecution(this.fixture);
94+
collection.Add(component);
95+
96+
var ex = Assert.ThrowsAsync<WorkloadException>(
97+
() => collection.ExecuteAsync(EventContext.None, CancellationToken.None));
98+
Assert.That(ex.Message, Does.Contain("task execution failed"));
99+
Assert.IsInstanceOf<InvalidOperationException>(ex.InnerException);
100+
}
101+
102+
[Test]
103+
public async Task SequentialExecution_SkipsUnsupportedComponents()
104+
{
105+
int loopCount = 3;
106+
this.fixture.Parameters["LoopCount"] = loopCount;
107+
108+
var supportedComponent = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token => Task.CompletedTask, isSupported: true);
109+
var unsupportedComponent = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token => Task.CompletedTask, isSupported: false);
110+
111+
var collection = new TestSequentialExecution(this.fixture);
112+
collection.Add(supportedComponent);
113+
collection.Add(unsupportedComponent);
114+
115+
await collection.ExecuteAsync(EventContext.None, CancellationToken.None);
116+
117+
Assert.AreEqual(loopCount, supportedComponent.ExecutionCount, "Supported component should be executed the expected number of times.");
118+
Assert.AreEqual(0, unsupportedComponent.ExecutionCount, "Unsupported component should not be executed.");
119+
}
120+
121+
private class TestComponent : VirtualClientComponent
122+
{
123+
private readonly Func<CancellationToken, Task> onExecuteAsync;
124+
private readonly bool isSupported;
125+
126+
public int ExecutionCount { get; private set; }
127+
128+
public TestComponent(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters, Func<CancellationToken, Task> onExecuteAsync = null, bool isSupported = true)
129+
: base(dependencies, parameters)
130+
{
131+
this.onExecuteAsync = onExecuteAsync ?? (_ => Task.CompletedTask);
132+
this.isSupported = isSupported;
133+
}
134+
135+
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
136+
{
137+
this.ExecutionCount++;
138+
await this.onExecuteAsync(cancellationToken);
139+
}
140+
141+
protected override bool IsSupported()
142+
{
143+
return this.isSupported;
144+
}
145+
}
146+
147+
private class TestSequentialExecution : SequentialExecution
148+
{
149+
public TestSequentialExecution(MockFixture fixture)
150+
: base(fixture.Dependencies, fixture.Parameters)
151+
{
152+
}
153+
154+
public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
155+
{
156+
return base.InitializeAsync(telemetryContext, cancellationToken);
157+
}
158+
159+
public new Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
160+
{
161+
return base.ExecuteAsync(telemetryContext, cancellationToken);
162+
}
163+
}
164+
}
165+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace VirtualClient.Contracts
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
12+
using VirtualClient.Common.Extensions;
13+
using VirtualClient.Common.Telemetry;
14+
15+
/// <summary>
16+
/// A component that executes a set of child components sequentially in a loop for a specified number of iterations.
17+
/// </summary>
18+
public class SequentialExecution : VirtualClientComponentCollection
19+
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="SequentialExecution"/> class.
22+
/// </summary>
23+
/// <param name="dependencies">Provides all of the required dependencies to the Virtual Client component.</param>
24+
/// <param name="parameters"> Parameters defined in the execution profile or supplied to the Virtual Client on the command line. </param>
25+
public SequentialExecution(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters = null)
26+
: base(dependencies, parameters)
27+
{
28+
}
29+
30+
/// <summary>
31+
/// The number of times to execute the set of child components.
32+
/// </summary>
33+
public int LoopCount
34+
{
35+
get
36+
{
37+
return this.Parameters.GetValue<int>(nameof(this.LoopCount), 1);
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Executes all of the child components sequentially in a loop for the specified number of iterations.
43+
/// </summary>
44+
/// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
45+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
46+
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
47+
{
48+
for (int i = 0; i < this.LoopCount && !cancellationToken.IsCancellationRequested; i++)
49+
{
50+
this.Logger.LogMessage(
51+
$"{nameof(SequentialExecution)} Iteration '{i + 1}' of '{this.LoopCount}'",
52+
LogLevel.Information,
53+
telemetryContext);
54+
55+
foreach (VirtualClientComponent component in this)
56+
{
57+
if (!VirtualClientComponent.IsSupported(component))
58+
{
59+
this.Logger.LogMessage(
60+
$"{nameof(SequentialExecution)} {component.TypeName} not supported on current platform: {this.PlatformArchitectureName}",
61+
LogLevel.Information,
62+
telemetryContext);
63+
continue;
64+
}
65+
66+
try
67+
{
68+
await component.ExecuteAsync(cancellationToken)
69+
.ConfigureAwait(false);
70+
}
71+
catch (Exception ex)
72+
{
73+
throw new WorkloadException(
74+
$"{component.TypeName} task execution failed.",
75+
ex,
76+
ErrorReason.WorkloadFailed);
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)