diff --git a/.idea/.idea.Rucksack/.idea/indexLayout.xml b/.idea/.idea.Rucksack/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.Rucksack/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Rucksack/.idea/vcs.xml b/.idea/.idea.Rucksack/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.Rucksack/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 15be63e..bd8b6d5 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,9 @@ using Rucksack.LoadStrategies;
var options = new LoadTestOptions
{
LoadStrategy = new RepeatLoadStrategy(
- CountPerStep: 10,
- Interval: TimeSpan.FromSeconds(1),
- TotalDuration: TimeSpan.FromSeconds(10)),
+ countPerInterval: 10,
+ interval: TimeSpan.FromSeconds(1),
+ totalDuration: TimeSpan.FromSeconds(10)),
};
await LoadTestRunner.Run(async () =>
@@ -31,3 +31,4 @@ There are a couple built-in strategies for generating load, or you can implement
* `OneShotLoadStrategy`: Enqueue the given number of tasks all at once. Does not repeat.
* `RepeatLoadStrategy`: Repeat enqueueing the given count of tasks at each given interval until the given duration has passed.
+* `SteppedBurstLoadStrategy`: Enqueue an increasing (or decreasing) burst of tasks in a stepwise manner, regardless of how many are still running.
diff --git a/Rucksack.Tests/Rucksack.Tests.csproj b/Rucksack.Tests/Rucksack.Tests.csproj
index 74753f7..b7047ad 100644
--- a/Rucksack.Tests/Rucksack.Tests.csproj
+++ b/Rucksack.Tests/Rucksack.Tests.csproj
@@ -6,15 +6,22 @@
enable
false
true
+ true
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs b/Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs
index 1153477..d860806 100644
--- a/Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs
+++ b/Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs
@@ -11,19 +11,20 @@ public async Task Step_WithCountOf1_CallsActionOnce()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(1);
+ var context = new LoadStrategyContext(PreviousResult: null);
// Act
- var result = strategy.Step(() =>
+ var result = strategy.GenerateLoad(() =>
{
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
- }, null);
+ }, context);
await StrategyTestHelper.ExecuteStrategyResultAndWait(result);
// Assert
actionCalledCount.Should().Be(1);
- result.NextStepDelay.Should().BeNull();
+ result.RepeatDelay.Should().BeNull();
}
[Fact]
@@ -32,18 +33,19 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(3);
+ var context = new LoadStrategyContext(PreviousResult: null);
// Act
- var result = strategy.Step(() =>
+ var result = strategy.GenerateLoad(() =>
{
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
- }, null);
+ }, context);
await StrategyTestHelper.ExecuteStrategyResultAndWait(result);
// Assert
actionCalledCount.Should().Be(3);
- result.NextStepDelay.Should().BeNull();
+ result.RepeatDelay.Should().BeNull();
}
}
diff --git a/Rucksack.Tests/Strategies/RepeatLoadStrategyTests.cs b/Rucksack.Tests/Strategies/RepeatLoadStrategyTests.cs
index a8db49c..2e991c7 100644
--- a/Rucksack.Tests/Strategies/RepeatLoadStrategyTests.cs
+++ b/Rucksack.Tests/Strategies/RepeatLoadStrategyTests.cs
@@ -16,24 +16,25 @@ public async Task Step_WithCountOf1_CallsActionOnce()
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
+ var context = new LoadStrategyContext(PreviousResult: null);
// Act
- var result = strategy.Step(action, null);
+ var result = strategy.GenerateLoad(action, context);
var tasks = await StrategyTestHelper.ExecuteStrategyResult(result);
// Assert
- result.NextStepDelay.Should().Be(TimeSpan.FromSeconds(1));
+ result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));
// Call again
- result = strategy.Step(action, result);
+ result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));
tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await StrategyTestHelper.WhenAll(tasks);
// Assert
actionCalledCount.Should().Be(1);
- result.NextStepDelay.Should().BeNull();
+ result.RepeatDelay.Should().BeNull();
}
[Fact]
@@ -47,23 +48,24 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
+ var context = new LoadStrategyContext(PreviousResult: null);
// Act
- var result = strategy.Step(action, null);
+ var result = strategy.GenerateLoad(action, context);
var tasks = await StrategyTestHelper.ExecuteStrategyResult(result);
// Assert
- result.NextStepDelay.Should().Be(TimeSpan.FromSeconds(1));
+ result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));
// Call again
- result = strategy.Step(action, result);
+ result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));
tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await StrategyTestHelper.WhenAll(tasks);
// Assert
actionCalledCount.Should().Be(3);
- result.NextStepDelay.Should().BeNull();
+ result.RepeatDelay.Should().BeNull();
}
}
diff --git a/Rucksack.Tests/Strategies/SteppedBurstLoadStrategyTests.cs b/Rucksack.Tests/Strategies/SteppedBurstLoadStrategyTests.cs
new file mode 100644
index 0000000..27fd78a
--- /dev/null
+++ b/Rucksack.Tests/Strategies/SteppedBurstLoadStrategyTests.cs
@@ -0,0 +1,72 @@
+using FluentAssertions;
+using Rucksack.LoadStrategies;
+
+namespace Rucksack.Tests.Strategies;
+
+public class SteppedBurstLoadStrategyTests
+{
+ [InlineData(1, 1, 2, 3)] // generated load = [1, 2] for a total of 3
+ [InlineData(2, 2, 4, 6)] // generated load = [2, 4] for a total of 6
+ [InlineData(10, 10, 50, 150)] // generated load = [10, 20, 30, 40, 50] for a total of 150
+ [InlineData(-10, 50, 10, 150)] // generated load = [50, 40, 30, 20, 10] for a total of 150
+ [InlineData(-1, 2, 1, 3)] // generated load = [2, 1] for a total of 3
+ [Theory]
+ public async Task SteppedBurstLoadStrategy_FullTests(int step, int from, int to, int expectedCount)
+ {
+ // Arrange
+ var actionCalledCount = 0;
+ var strategy = new SteppedBurstLoadStrategy(step, from, to, interval: TimeSpan.FromSeconds(1));
+ var action = () =>
+ {
+ Interlocked.Increment(ref actionCalledCount);
+ return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
+ };
+
+ // Act
+ LoadStrategyResult? result = null;
+ List> tasks = [];
+
+ do
+ {
+ var context = new LoadStrategyContext(PreviousResult: result);
+ result = strategy.GenerateLoad(action, context);
+
+ if (result.Tasks is { } resultTasks)
+ {
+ tasks.AddRange(resultTasks);
+ }
+
+ if (result.RepeatDelay.HasValue)
+ {
+ await Task.Delay(result.RepeatDelay.Value);
+ }
+ }
+ while (result.RepeatDelay.HasValue);
+
+ await StrategyTestHelper.WhenAll(tasks);
+
+ // Assert
+ result.RepeatDelay.Should().BeNull();
+ actionCalledCount.Should().Be(expectedCount);
+ }
+
+ [InlineData(0, 1, 0, false)]
+ [InlineData(0, 1, 1, true)]
+ [InlineData(0, 1, 2, true)]
+ [InlineData(1, 0, 1, false)]
+ [InlineData(1, 0, 0, true)]
+ [InlineData(1, 0, -1, true)]
+ [InlineData(10, 100, 99, false)]
+ [InlineData(10, 100, 101, true)]
+ [InlineData(100, 10, 11, false)]
+ [InlineData(100, 10, 8, true)]
+ [Theory]
+ public void IsFinishedTests(int from, int to, int current, bool expected)
+ {
+ // Act
+ var result = SteppedBurstLoadStrategy.IsFinished(from, to, current);
+
+ // Assert
+ result.Should().Be(expected);
+ }
+}
diff --git a/Rucksack.Tests/Strategies/StrategyTestHelper.cs b/Rucksack.Tests/Strategies/StrategyTestHelper.cs
index f433c2d..ddbf3d7 100644
--- a/Rucksack.Tests/Strategies/StrategyTestHelper.cs
+++ b/Rucksack.Tests/Strategies/StrategyTestHelper.cs
@@ -19,9 +19,9 @@ public static async ValueTask WhenAll(IEnumerable> tas
public static async ValueTask>> ExecuteStrategyResult(LoadStrategyResult result)
{
- if (result.NextStepDelay.HasValue)
+ if (result.RepeatDelay.HasValue)
{
- await Task.Delay(result.NextStepDelay.Value);
+ await Task.Delay(result.RepeatDelay.Value);
}
return [..result.Tasks ?? []];
diff --git a/Rucksack/ILoadStrategy.cs b/Rucksack/ILoadStrategy.cs
index dd43e8f..5ca175c 100644
--- a/Rucksack/ILoadStrategy.cs
+++ b/Rucksack/ILoadStrategy.cs
@@ -2,5 +2,5 @@ namespace Rucksack;
public interface ILoadStrategy
{
- LoadStrategyResult Step(Func> action, LoadStrategyResult? previousResult);
+ LoadStrategyResult GenerateLoad(Func> action, LoadStrategyContext context);
}
diff --git a/Rucksack/LoadStrategies/OneShotLoadStrategy.cs b/Rucksack/LoadStrategies/OneShotLoadStrategy.cs
index 8553c21..11aabb8 100644
--- a/Rucksack/LoadStrategies/OneShotLoadStrategy.cs
+++ b/Rucksack/LoadStrategies/OneShotLoadStrategy.cs
@@ -2,19 +2,17 @@ namespace Rucksack.LoadStrategies;
public class OneShotLoadStrategy(int count) : ILoadStrategy
{
- public int Count { get; } = count;
-
- public LoadStrategyResult Step(Func> action, LoadStrategyResult? previousResult)
+ public LoadStrategyResult GenerateLoad(Func> action, LoadStrategyContext context)
{
- if (previousResult is not null)
+ if (context.PreviousResult is not null)
{
throw new InvalidOperationException("OneShotLoadStrategy does not support previous results.");
}
- var tasks = Enumerable.Range(0, Count)
+ var tasks = Enumerable.Range(0, count)
.Select(_ => action())
.ToArray();
- return new LoadStrategyResult(NextStepDelay: null, Tasks: tasks);
+ return new LoadStrategyResult(RepeatDelay: null, Tasks: tasks);
}
}
diff --git a/Rucksack/LoadStrategies/RepeatLoadStrategy.cs b/Rucksack/LoadStrategies/RepeatLoadStrategy.cs
index 2257ddf..de3f562 100644
--- a/Rucksack/LoadStrategies/RepeatLoadStrategy.cs
+++ b/Rucksack/LoadStrategies/RepeatLoadStrategy.cs
@@ -2,21 +2,21 @@
namespace Rucksack.LoadStrategies;
-public class RepeatLoadStrategy(int CountPerStep, TimeSpan Interval, TimeSpan TotalDuration)
+public class RepeatLoadStrategy(int countPerInterval, TimeSpan interval, TimeSpan totalDuration)
: ILoadStrategy
{
- public LoadStrategyResult Step(Func> action, LoadStrategyResult? previousResult)
+ public LoadStrategyResult GenerateLoad(Func> action, LoadStrategyContext context)
{
RepeatLoadStrategyResult result;
int iteration = 1;
- if (previousResult is null)
+ if (context.PreviousResult is null)
{
- result = new RepeatLoadStrategyResult(Interval, Stopwatch.StartNew(), iteration, null);
+ result = new RepeatLoadStrategyResult(interval, Stopwatch.StartNew(), iteration, null);
}
- else if (previousResult is not RepeatLoadStrategyResult previousRepeatResult)
+ else if (context.PreviousResult is not RepeatLoadStrategyResult previousRepeatResult)
{
- throw new ArgumentException($"Expected {nameof(RepeatLoadStrategyResult)} but got {previousResult.GetType().Name}", nameof(previousResult));
+ throw new ArgumentException($"Expected previous result type {nameof(RepeatLoadStrategyResult)} but got {context.PreviousResult.GetType().Name}", nameof(context));
}
else
{
@@ -24,12 +24,12 @@ public LoadStrategyResult Step(Func> action, LoadStrat
iteration = result.Iteration + 1;
}
- if (result.Stopwatch.Elapsed >= TotalDuration)
+ if (result.Stopwatch.Elapsed >= totalDuration)
{
return LoadStrategyResult.Finished;
}
- var tasks = Enumerable.Range(0, CountPerStep)
+ var tasks = Enumerable.Range(0, countPerInterval)
.Select(_ => action())
.ToArray();
@@ -40,6 +40,6 @@ public LoadStrategyResult Step(Func> action, LoadStrat
};
}
- private record RepeatLoadStrategyResult(TimeSpan? NextStepDelay, Stopwatch Stopwatch, int Iteration, IReadOnlyList>? Tasks)
- : LoadStrategyResult(NextStepDelay, Tasks);
+ private record RepeatLoadStrategyResult(TimeSpan? RepeatDelay, Stopwatch Stopwatch, int Iteration, IReadOnlyList>? Tasks)
+ : LoadStrategyResult(RepeatDelay, Tasks);
}
diff --git a/Rucksack/LoadStrategies/SteppedBurstLoadStrategy.cs b/Rucksack/LoadStrategies/SteppedBurstLoadStrategy.cs
new file mode 100644
index 0000000..ff6b838
--- /dev/null
+++ b/Rucksack/LoadStrategies/SteppedBurstLoadStrategy.cs
@@ -0,0 +1,73 @@
+namespace Rucksack.LoadStrategies;
+
+public class SteppedBurstLoadStrategy : ILoadStrategy
+{
+ private readonly int _step;
+ private readonly int _from;
+ private readonly int _to;
+ private readonly TimeSpan _interval;
+
+ public SteppedBurstLoadStrategy(int step, int from, int to, TimeSpan interval)
+ {
+ if (from == to)
+ {
+ throw new ArgumentException(
+ "From and to values must be different. " +
+ $"If you just need to fire a burst once, consider using {nameof(OneShotLoadStrategy)}.",
+ nameof(from));
+ }
+
+ if (from < to && step <= 0)
+ {
+ throw new ArgumentException("Step must be greater than 0 when from is less than to.", nameof(step));
+ }
+
+ if (from > to && step >= 0)
+ {
+ throw new ArgumentException("Step must be less than 0 when from is greater than to.", nameof(step));
+ }
+
+ _step = step;
+ _from = from;
+ _to = to;
+ _interval = interval;
+ }
+
+ public LoadStrategyResult GenerateLoad(Func> action, LoadStrategyContext context)
+ {
+ SteppedLoadStrategyResult result;
+ int currentCount = _from;
+
+ if (context.PreviousResult is null)
+ {
+ result = new SteppedLoadStrategyResult(_interval, currentCount, null);
+ }
+ else if (context.PreviousResult is not SteppedLoadStrategyResult previousSteppedResult)
+ {
+ throw new ArgumentException($"Expected previous result type {nameof(SteppedLoadStrategyResult)} but got {context.PreviousResult.GetType().Name}", nameof(context));
+ }
+ else
+ {
+ result = previousSteppedResult;
+ currentCount = result.CurrentCount + _step;
+ }
+
+ var tasks = Enumerable.Range(0, currentCount)
+ .Select(_ => action())
+ .ToArray();
+
+ return result with
+ {
+ RepeatDelay = IsFinished(_from, _to, currentCount) ? null : _interval,
+ CurrentCount = currentCount,
+ Tasks = tasks,
+ };
+ }
+
+ internal static bool IsFinished(int from, int to, int currentCount) =>
+ (from < to && currentCount >= to)
+ || (from > to && currentCount <= to);
+
+ private record SteppedLoadStrategyResult(TimeSpan? RepeatDelay, int CurrentCount, IReadOnlyList>? Tasks)
+ : LoadStrategyResult(RepeatDelay, Tasks);
+}
diff --git a/Rucksack/LoadStrategyContext.cs b/Rucksack/LoadStrategyContext.cs
new file mode 100644
index 0000000..66e7eca
--- /dev/null
+++ b/Rucksack/LoadStrategyContext.cs
@@ -0,0 +1,3 @@
+namespace Rucksack;
+
+public record LoadStrategyContext(LoadStrategyResult? PreviousResult);
diff --git a/Rucksack/LoadStrategyResult.cs b/Rucksack/LoadStrategyResult.cs
index e27e031..687e424 100644
--- a/Rucksack/LoadStrategyResult.cs
+++ b/Rucksack/LoadStrategyResult.cs
@@ -1,6 +1,6 @@
namespace Rucksack;
-public record LoadStrategyResult(TimeSpan? NextStepDelay, IReadOnlyList>? Tasks)
+public record LoadStrategyResult(TimeSpan? RepeatDelay, IReadOnlyList>? Tasks)
{
public static LoadStrategyResult Finished { get; } = new(null, null);
}
diff --git a/Rucksack/LoadTestRunner.cs b/Rucksack/LoadTestRunner.cs
index 4462a7f..7ab2895 100644
--- a/Rucksack/LoadTestRunner.cs
+++ b/Rucksack/LoadTestRunner.cs
@@ -28,7 +28,9 @@ public static async Task Run(Func action, LoadTestOptions options)
while (true)
{
- result = options.LoadStrategy.Step(LoadAction, result);
+ var context = new LoadStrategyContext(PreviousResult: result);
+
+ result = options.LoadStrategy.GenerateLoad(LoadAction, context);
if (result.Tasks is { } tasks)
{
@@ -37,12 +39,12 @@ public static async Task Run(Func action, LoadTestOptions options)
allTasks.AddRange(tasks);
}
- if (result.NextStepDelay == null)
+ if (result.RepeatDelay == null)
{
break;
}
- await Task.Delay(result.NextStepDelay.Value);
+ await Task.Delay(result.RepeatDelay.Value);
}
logger.LogInformation("Waiting for {Count} tasks to complete...", allTasks.Count);
diff --git a/Rucksack/Rucksack.csproj b/Rucksack/Rucksack.csproj
index 380ca0b..98d2e00 100644
--- a/Rucksack/Rucksack.csproj
+++ b/Rucksack/Rucksack.csproj
@@ -4,8 +4,13 @@
net8.0
enable
enable
+ true
+
+
+
+