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 @@ -206,6 +206,41 @@ public void ComponentFactoryCreatesExpectedParallelLoopExecutionComponentsFromAn
Assert.IsTrue(confirmed);
}

[Test]
[TestCase("TEST-PROFILE-1-SEQUENTIAL.json")]
public void ComponentFactoryCreatesExpectedSequentialExecutionComponentsFromAnExecutionProfile(string profileName)
{
ExecutionProfile profile = File.ReadAllText(Path.Combine(MockFixture.TestAssemblyDirectory, "Resources", profileName))
.FromJson<ExecutionProfile>();

bool confirmed = false;
foreach (ExecutionProfileElement action in profile.Actions)
{
Assert.DoesNotThrow(() =>
{
VirtualClientComponent component = ComponentFactory.CreateComponent(action, this.mockFixture.Dependencies);
Assert.IsNotNull(component);
Assert.IsNotEmpty(component.Dependencies);
Assert.IsNotNull(component.Parameters);

SequentialExecution sequentialExecutionComponent = component as SequentialExecution;
if (sequentialExecutionComponent != null)
{
Assert.IsNotEmpty(sequentialExecutionComponent);
Assert.IsTrue(sequentialExecutionComponent.Count() == 2);
Assert.IsTrue(sequentialExecutionComponent.ElementAt(0) is TestExecutor);
Assert.IsTrue(sequentialExecutionComponent.ElementAt(1) is TestExecutor);
Assert.IsTrue(sequentialExecutionComponent.ElementAt(0).Parameters["Scenario"].ToString() == "ScenarioA");
Assert.IsTrue(sequentialExecutionComponent.ElementAt(1).Parameters["Scenario"].ToString() == "ScenarioB");
Assert.AreEqual(2, sequentialExecutionComponent.LoopCount);
confirmed = true;
}
});
}

Assert.IsTrue(confirmed);
}

[Test]
public void ComponentFactoryAddsExpectedComponentLevelMetadataToSubComponents_Deep_Nesting()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ public void ExecutionProfileElementIsJsonSerializableWithParallelLoopExecutionDe
SerializationAssert.IsJsonSerializable<ExecutionProfileElement>(element);
}

[Test]
public void ExecutionProfileElementIsJsonSerializableWithSequentialExecutionDefinitions()
{
// Add 2 child/subcomponents to the parent elements.
ExecutionProfileElement element = new ExecutionProfileElement(typeof(SequentialExecution).Name, null, null, new List<ExecutionProfileElement>
{
this.fixture.Create<ExecutionProfileElement>(),
this.fixture.Create<ExecutionProfileElement>()
});

SerializationAssert.IsJsonSerializable<ExecutionProfileElement>(element);
}

[Test]
public void ExecutionProfileElementImplementsHashCodeSemanticsCorrectly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ public void ExecutionProfileCanDeserializeProfileFilesWithParallelLoopExecutionC
Assert.IsTrue(profile.Actions[1].Components.Count() == 2);
}

[Test]
[TestCase("TEST-PROFILE-1-SEQUENTIAL.json")]
public void ExecutionProfileCanDeserializeProfileFilesWithSequentialExecutionComponents(string profileName)
{
ExecutionProfile profile = File.ReadAllText(Path.Combine(MockFixture.TestAssemblyDirectory, "Resources", profileName))
.FromJson<ExecutionProfile>();

Assert.IsNotEmpty(profile.Actions);
Assert.IsTrue(profile.Actions.Count == 2);

Assert.IsNotEmpty(profile.Actions[1].Components);
Assert.IsTrue(profile.Actions[1].Components.Count() == 2);
}

[Test]
public void ExecutionProfileImplementsHashCodeSemanticsCorrectly()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"Description": "Test Sequential Execution Profile",
"MinimumExecutionInterval": "00:01:00",
"Parameters": {
"Parameter1": "AnyValue",
"Parameter2": 12345
},
"Actions": [
{
"Type": "TestExecutor",
"Parameters": {
"Scenario": "Scenario1",
"PackageName": "anypackage",
"Parameter1": "$.Parameters.Parameter1",
"Parameter2": "$.Parameters.Parameter2"
}
},
{
"Type": "SequentialExecution",
"Parameters": {
"LoopCount": 2
},
"Components": [
{
"Type": "TestExecutor",
"Parameters": {
"Scenario": "ScenarioA",
"Parameter1": "$.Parameters.Parameter1"
}
},
{
"Type": "TestExecutor",
"Parameters": {
"Scenario": "ScenarioB",
"Parameter1": "$.Parameters.Parameter1"
}
}
]
}
],
"Dependencies": [
{
"Type": "TestDependency",
"Parameters": {}
}
],
"Monitors": [
{
"Type": "TestMonitor",
"Parameters": {}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace VirtualClient.Contracts
{
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using VirtualClient.Common.Telemetry;
using VirtualClient.Contracts;

[TestFixture]
[Category("Unit")]
public class SequentialExecutionTests
{
private MockFixture fixture;

[SetUp]
public void SetupDefaults()
{
this.fixture = new MockFixture();
this.fixture.Parameters = new Dictionary<string, IConvertible>
{
{ "LoopCount", 3 }
};
}

[Test]
public async Task SequentialExecution_ExecutesComponentsTheSpecifiedNumberOfTimes()
{
int loopCount = 5;
this.fixture.Parameters["LoopCount"] = loopCount;

var component1 = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
{
return Task.CompletedTask;
});

var component2 = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
{
return Task.CompletedTask;
});

var collection = new TestSequentialExecution(this.fixture);
collection.Add(component1);
collection.Add(component2);

await collection.ExecuteAsync(EventContext.None, CancellationToken.None);

Assert.AreEqual(loopCount, component1.ExecutionCount, "Component1 was not executed the expected number of times.");
Assert.AreEqual(loopCount, component2.ExecutionCount, "Component2 was not executed the expected number of times.");
}

[Test]
public async Task SequentialExecution_RespectsCancellationToken()
{
int loopCount = 100;
this.fixture.Parameters["LoopCount"] = loopCount;

var component = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, async token =>
{
await Task.Delay(100, token);
});

var collection = new TestSequentialExecution(this.fixture);
collection.Add(component);

using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
{
try
{
await collection.ExecuteAsync(EventContext.None, cts.Token);
}
catch (OperationCanceledException) { }
}

Assert.Less(component.ExecutionCount, loopCount, "Component should not have executed all iterations due to cancellation.");
}

[Test]
public void SequentialExecution_ThrowsWorkloadException_WhenComponentThrows()
{
this.fixture.Parameters["LoopCount"] = 2;

var component = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token =>
{
throw new InvalidOperationException("Test exception");
});

var collection = new TestSequentialExecution(this.fixture);
collection.Add(component);

var ex = Assert.ThrowsAsync<WorkloadException>(
() => collection.ExecuteAsync(EventContext.None, CancellationToken.None));
Assert.That(ex.Message, Does.Contain("task execution failed"));
Assert.IsInstanceOf<InvalidOperationException>(ex.InnerException);
}

[Test]
public async Task SequentialExecution_SkipsUnsupportedComponents()
{
int loopCount = 3;
this.fixture.Parameters["LoopCount"] = loopCount;

var supportedComponent = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token => Task.CompletedTask, isSupported: true);
var unsupportedComponent = new TestComponent(this.fixture.Dependencies, this.fixture.Parameters, token => Task.CompletedTask, isSupported: false);

var collection = new TestSequentialExecution(this.fixture);
collection.Add(supportedComponent);
collection.Add(unsupportedComponent);

await collection.ExecuteAsync(EventContext.None, CancellationToken.None);

Assert.AreEqual(loopCount, supportedComponent.ExecutionCount, "Supported component should be executed the expected number of times.");
Assert.AreEqual(0, unsupportedComponent.ExecutionCount, "Unsupported component should not be executed.");
}

private class TestComponent : VirtualClientComponent
{
private readonly Func<CancellationToken, Task> onExecuteAsync;
private readonly bool isSupported;

public int ExecutionCount { get; private set; }

public TestComponent(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters, Func<CancellationToken, Task> onExecuteAsync = null, bool isSupported = true)
: base(dependencies, parameters)
{
this.onExecuteAsync = onExecuteAsync ?? (_ => Task.CompletedTask);
this.isSupported = isSupported;
}

protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
this.ExecutionCount++;
await this.onExecuteAsync(cancellationToken);
}

protected override bool IsSupported()
{
return this.isSupported;
}
}

private class TestSequentialExecution : SequentialExecution
{
public TestSequentialExecution(MockFixture fixture)
: base(fixture.Dependencies, fixture.Parameters)
{
}

public new Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
return base.InitializeAsync(telemetryContext, cancellationToken);
}

public new Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
return base.ExecuteAsync(telemetryContext, cancellationToken);
}
}
}
}
82 changes: 82 additions & 0 deletions src/VirtualClient/VirtualClient.Contracts/SequentialExecution.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace VirtualClient.Contracts
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using VirtualClient.Common.Extensions;
using VirtualClient.Common.Telemetry;

/// <summary>
/// A component that executes a set of child components sequentially in a loop for a specified number of iterations.
/// </summary>
public class SequentialExecution : VirtualClientComponentCollection
{
/// <summary>
/// Initializes a new instance of the <see cref="SequentialExecution"/> class.
/// </summary>
/// <param name="dependencies">Provides all of the required dependencies to the Virtual Client component.</param>
/// <param name="parameters"> Parameters defined in the execution profile or supplied to the Virtual Client on the command line. </param>
public SequentialExecution(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters = null)
: base(dependencies, parameters)
{
}

/// <summary>
/// The number of times to execute the set of child components.
/// </summary>
public int LoopCount
{
get
{
return this.Parameters.GetValue<int>(nameof(this.LoopCount), 1);
}
}

/// <summary>
/// Executes all of the child components sequentially in a loop for the specified number of iterations.
/// </summary>
/// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
for (int i = 0; i < this.LoopCount && !cancellationToken.IsCancellationRequested; i++)
{
this.Logger.LogMessage(
$"{nameof(SequentialExecution)} Iteration '{i + 1}' of '{this.LoopCount}'",
LogLevel.Information,
telemetryContext);

foreach (VirtualClientComponent component in this)
{
if (!VirtualClientComponent.IsSupported(component))
{
this.Logger.LogMessage(
$"{nameof(SequentialExecution)} {component.TypeName} not supported on current platform: {this.PlatformArchitectureName}",
LogLevel.Information,
telemetryContext);
continue;
}

try
{
await component.ExecuteAsync(cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
throw new WorkloadException(
$"{component.TypeName} task execution failed.",
ex,
ErrorReason.WorkloadFailed);
}
}
}
}
}
}
Loading
Loading