diff --git a/VERSION b/VERSION index e3baf30b73..0aa25e148f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.35 \ No newline at end of file +2.1.36 \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs b/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs new file mode 100644 index 0000000000..7d64084f05 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Actions.UnitTests/Network/NetworkPingExecutorTests.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Actions +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using NUnit.Framework; + using VirtualClient.Actions.NetworkPerformance; + using VirtualClient.Contracts; + + [TestFixture] + [Category("Unit")] + public class NetworkPingExecutorTests + { + private MockFixture mockFixture; + + [SetUp] + public void SetupTest() + { + this.mockFixture = new MockFixture(); + } + + [Test] + public void NetworkPingExecutorThrowsIfIPAddressIsNotDefined() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "NotDefined" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + WorkloadException exception = Assert.ThrowsAsync( + () => executor.ExecuteAsync(CancellationToken.None)); + + Assert.AreEqual(ErrorReason.InstructionsNotProvided, exception.Reason); + Assert.IsTrue(exception.Message.Contains("IP address")); + } + } + + [Test] + [TestCase("")] + [TestCase("invalid-ip")] + [TestCase("999.999.999.999")] + [TestCase("not.an.ip.address")] + public void NetworkPingExecutorThrowsIfIPAddressIsInvalid(string invalidIpAddress) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), invalidIpAddress } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + WorkloadException exception = Assert.ThrowsAsync( + () => executor.ExecuteAsync(CancellationToken.None)); + + Assert.AreEqual(ErrorReason.InstructionsNotValid, exception.Reason); + Assert.IsTrue(exception.Message.Contains("Invalid IP address format")); + } + } + + [Test] + public void NetworkPingExecutorParsesIPAddressParameter() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual("192.168.1.1", executor.IPAddress); + } + } + + [Test] + [TestCase("192.168.1.1")] + [TestCase("10.0.0.1")] + [TestCase("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] + [TestCase("::1")] + public void NetworkPingExecutorAcceptsValidIPAddresses(string ipAddress) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), ipAddress } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(ipAddress, executor.IPAddress); + } + } + + [Test] + public void NetworkPingExecutorUsesDefaultPingIterationsWhenNotSpecified() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(50, executor.PingIterations); + } + } + + [Test] + [TestCase(100)] + [TestCase(200)] + [TestCase(500)] + public void NetworkPingExecutorParsesPingIterationsParameter(int iterations) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.PingIterations), iterations } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual(iterations, executor.PingIterations); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenNotSpecified() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenSetToEmptyString() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), string.Empty } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + public void NetworkPingExecutorDurationIsNullWhenSetToNull() + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), null } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNull(executor.Duration); + } + } + + [Test] + [TestCase("00:05:00", 300)] // 5 minutes = 300 seconds + [TestCase("00:10:00", 600)] // 10 minutes = 600 seconds + [TestCase("01:00:00", 3600)] // 1 hour = 3600 seconds + [TestCase("00:00:30", 30)] // 30 seconds + public void NetworkPingExecutorParsesTimeSpanFormatForDuration(string durationString, int expectedSeconds) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), durationString } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(expectedSeconds, executor.Duration.Value.TotalSeconds); + } + } + + [Test] + [TestCase(300)] // 300 seconds + [TestCase(600)] // 600 seconds + [TestCase(3600)] // 3600 seconds + public void NetworkPingExecutorParsesNumericFormatForDuration(int durationSeconds) + { + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), durationSeconds } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(durationSeconds, executor.Duration.Value.TotalSeconds); + } + } + + [Test] + public void NetworkPingExecutorSupportsSettingBothDurationAndPingIterations() + { + // When Duration is set, it takes precedence over PingIterations + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), "00:05:00" }, + { nameof(NetworkPingExecutor.PingIterations), 100 } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.IsNotNull(executor.Duration); + Assert.AreEqual(300, executor.Duration.Value.TotalSeconds); + Assert.AreEqual(100, executor.PingIterations); + } + } + + [Test] + public void NetworkPingExecutorParametersAreConsistentWithProfile() + { + // Verify that the parameters match what's expected in the PERF-NETWORK-PING.json profile + this.mockFixture.Parameters = new Dictionary + { + { nameof(NetworkPingExecutor.IPAddress), "192.168.1.1" }, + { nameof(NetworkPingExecutor.Duration), null }, + { nameof(NetworkPingExecutor.PingIterations), 300 } + }; + + using (NetworkPingExecutor executor = new NetworkPingExecutor(this.mockFixture.Dependencies, this.mockFixture.Parameters)) + { + Assert.AreEqual("192.168.1.1", executor.IPAddress); + Assert.IsNull(executor.Duration); + Assert.AreEqual(300, executor.PingIterations); + } + } + } +} diff --git a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs index 27a6c5a39b..248fe205ea 100644 --- a/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs +++ b/src/VirtualClient/VirtualClient.Actions/Network/NetworkPingExecutor.cs @@ -64,6 +64,28 @@ public int PingIterations } } + /// + /// Parameter. Defines the duration for which the network ping test will run. + /// This can be a valid timespan (e.g. 00:10:00) or a simple numeric value representing total seconds (e.g. 600). + /// + public TimeSpan? Duration + { + get + { + // Check if the parameter exists and is not empty + if (this.Parameters.ContainsKey(nameof(NetworkPingExecutor.Duration))) + { + string durationValue = this.Parameters[nameof(NetworkPingExecutor.Duration)]?.ToString(); + if (!string.IsNullOrWhiteSpace(durationValue)) + { + return this.Parameters.GetTimeSpanValue(nameof(NetworkPingExecutor.Duration)); + } + } + + return null; + } + } + /// /// The retry policy to apply to ping operations for handling transient /// issues/errors. @@ -116,8 +138,18 @@ private async Task ExecutePingServerAsync(IPAddress ipAddress, EventContext tele DateTime startTime = DateTime.UtcNow; Stopwatch blipTimer = Stopwatch.StartNew(); - while (iterations < this.PingIterations && !cancellationToken.IsCancellationRequested) + + // Use Duration if specified, otherwise use PingIterations + DateTime stopTime = startTime.Add(this.Duration ?? TimeSpan.Zero); + + while (!cancellationToken.IsCancellationRequested) { + if ((this.Duration != null && DateTime.UtcNow >= stopTime) || + (this.Duration == null && iterations >= this.PingIterations)) + { + break; + } + try { await this.PingRetryPolicy.ExecuteAsync(async () => diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json index a3a5ede5cd..060ebaf797 100644 --- a/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json +++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-NETWORK-PING.json @@ -6,7 +6,9 @@ "SupportedOperatingSystems": "CBL-Mariner,CentOS,Debian,RedHat,Suse,Ubuntu,Windows" }, "Parameters": { - "IPAddress": "NotDefined" + "IPAddress": "NotDefined", + "Duration": null, + "PingIterations": 300 }, "Actions": [ { @@ -14,7 +16,8 @@ "Parameters": { "Scenario": "ICMP", "IPAddress": "$.Parameters.IPAddress", - "PingIterations": 300, + "Duration": "$.Parameters.Duration", + "PingIterations": "$.Parameters.PingIterations", "Tags": "Performance,Networking,Ping" } } diff --git a/website/docs/workloads/network-ping/network-ping-profiles.md b/website/docs/workloads/network-ping/network-ping-profiles.md index b3328bc2b7..26391c0a36 100644 --- a/website/docs/workloads/network-ping/network-ping-profiles.md +++ b/website/docs/workloads/network-ping/network-ping-profiles.md @@ -30,9 +30,11 @@ respond to network ping requests. * **Profile Parameters** The following parameters can be optionally supplied on the command line to modify the behaviors of the workload. - | Parameter | Purpose | - |-------------|---------| - | IPAddress | Required. The IP address of the target endpoint to which to send network pings and measure round trip response times. Loopback address can be used: 127.0.0.1. | + | Parameter | Purpose | Default Value | + |----------------|---------|---------------| + | IPAddress | Required. The IP address of the target endpoint to which to send network pings and measure round trip response times. Loopback address can be used: 127.0.0.1. | NotDefined | + | Duration | Optional. The duration for which the network ping test will run. This can be a valid timespan (e.g. 00:10:00 for 10 minutes) or a simple numeric value representing total seconds (e.g. 600). When specified, it overrides PingIterations. | null (uses PingIterations) | + | PingIterations | Optional. The number of individual network pings that will be conducted. This is used when Duration is not specified. | 300 | * **Profile Runtimes** See the 'Metadata' section of the profile for estimated runtimes. These timings represent the length of time required to run a single round of profile @@ -43,6 +45,12 @@ respond to network ping requests. The following section provides a few basic examples of how to use the workload profile. ``` bash - # Execute the workload profile + # Execute the workload profile with default behavior (300 ping iterations) VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4 + + # Execute the workload profile with custom duration (10 minutes) + VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4,,,Duration=00:10:00 + + # Execute the workload profile with custom ping iterations + VirtualClient.exe --profile=PERF-NETWORK-PING.json --system=Demo --timeout=1440 --parameters=IPAddress=1.2.3.4,,,PingIterations=500 ``` \ No newline at end of file