Skip to content
Open
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
29 changes: 15 additions & 14 deletions examples/k6/CommunityToolkit.Aspire.Hosting.k6.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
using Projects;

var builder = DistributedApplication.CreateBuilder(args);

var apiservice = builder
.AddProject<CommunityToolkit_Aspire_Hosting_k6_ApiService>("apiservice")
.WithHttpHealthCheck("/health");

var k6 = builder.AddK6("k6")
.WithBindMount("scripts", "/scripts", true)
.WithScript("/scripts/main.js")
.WithReference(apiservice)
.WaitFor(apiservice);

using CommunityToolkit.Aspire.Hosting.k6;
using Projects;

var builder = DistributedApplication.CreateBuilder(args);

var apiservice = builder
.AddProject<CommunityToolkit_Aspire_Hosting_k6_ApiService>("apiservice")
.WithHttpHealthCheck("/health");

var k6 = builder.AddK6("k6")
.WithBindMount("scripts", "/scripts", true)
.WithScript("/scripts/main.js", summaryMode: K6SummaryMode.Full)
.WithReference(apiservice)
.WaitFor(apiservice);

builder.Build().Run();
53 changes: 35 additions & 18 deletions src/CommunityToolkit.Aspire.Hosting.k6/K6BuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ public static IResourceBuilder<K6Resource> AddK6(
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="scriptPath">The path to the JS script to run.</param>
/// <param name="virtualUsers">The number of virtual users for the test. Defaults to `10`.</param>
/// <param name="duration">The duration of the test. Defaults to `30s`.</param>
/// <param name="virtualUsers">The number of virtual users for the test..</param>
/// <param name="duration">The duration of the test, e.g. <c>30s</c>.</param>
/// <param name="summaryMode">The summary mode of the test execution.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <c>--vus</c> and <c>--duration</c> are the k6 CLI shorthand for a single constant-VU scenario and take precedence over the script's <c>options.scenarios</c>.
/// Leave them <see langword="null"/> when the script defines its own scenarios or executors.
/// <example>
/// Add a Grafana k6 container to the application model and reference it in a .NET project. Additionally, in this
/// example a script runs when the container starts.
/// Add a Grafana k6 container to the application model and reference it in a .NET project.
/// Additionally, in this example a script runs when the container starts.
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
Expand All @@ -95,23 +98,37 @@ public static IResourceBuilder<K6Resource> AddK6(
public static IResourceBuilder<K6Resource> WithScript(
this IResourceBuilder<K6Resource> builder,
string scriptPath,
int virtualUsers = 10,
string duration = "30s")
int? virtualUsers = null,
string? duration = null,
K6SummaryMode? summaryMode = null)

@afscrome afscrome Sep 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an additional parameter & changing types (int --> int?) is a breaking API change.

Not entirely sure on this repo's approach to breaking changes, but I suggest adding a new overload, rather than changing the existing signature.

{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(scriptPath);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(virtualUsers);
ArgumentNullException.ThrowIfNull(duration);

return builder.WithArgs(
"run",
"--address",
$"0.0.0.0:{K6Port}",
"--vus",
virtualUsers.ToString(CultureInfo.InvariantCulture),
"--duration",
duration,
scriptPath);

var args = new List<string> { "run", "--address", $"0.0.0.0:{K6Port}" };

if (virtualUsers is not null)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(virtualUsers.Value, nameof(virtualUsers));
args.Add("--vus");
args.Add(virtualUsers.Value.ToString(CultureInfo.InvariantCulture));
}

if (duration is not null)
{
args.Add("--duration");
args.Add(duration);
}

if (summaryMode is not null)
{
args.Add("--summary-mode");
args.Add(summaryMode.Value.ToString().ToLowerInvariant());
}

args.Add(scriptPath);

return builder.WithArgs([.. args]);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ internal static class K6ContainerImageTags
public const string Registry = "docker.io";
/// <summary>grafana/k6</summary>
public const string Image = "grafana/k6";
/// <summary>1.2.3</summary>
public const string Tag = "1.2.3";
/// <summary>2.3.0</summary>
public const string Tag = "2.3.0";
}
20 changes: 20 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.k6/K6SummaryMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace CommunityToolkit.Aspire.Hosting.k6;

/// <summary>
/// Define how detailed the end-of-test summary should be.
/// </summary>
public enum K6SummaryMode
{
/// <summary>
/// Default mode. Shows the most relevant test results in a concise format.
/// </summary>
Compact,
/// <summary>
/// Shows all available information.
/// </summary>
Full,
/// <summary>
/// Completely disables the summary generation.
/// </summary>
Disabled
}
43 changes: 36 additions & 7 deletions tests/CommunityToolkit.Aspire.Hosting.k6.Tests/K6PublicApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.k6.Tests;

Expand Down Expand Up @@ -75,19 +76,47 @@ public void WithScriptShouldThrowWhenVirtualUsersIsNegativeOrZero(int virtualUse
}

[Fact]
public void WithScriptShouldThrowWhenDurationIsNull()
public async Task WithScriptShouldOmitVusAndDurationFlagsWhenNull()
{
var builder = new DistributedApplicationBuilder([]);
var resourceBuilder = builder.AddK6("k6");

const string scriptPath = "scripts/main.js";
int vus = 10;
string duration = null!;
resourceBuilder.WithScript("scripts/main.js");

var action = () => resourceBuilder.WithScript(scriptPath, vus, duration);
Assert.Equal(["run", "--address", "0.0.0.0:6565", "scripts/main.js"], await GetArgsAsync(resourceBuilder));
}

var exception = Assert.Throws<ArgumentNullException>(action);
Assert.Equal(nameof(duration), exception.ParamName);
[Fact]
public async Task WithScriptShouldEmitEachFlagIndependently()
{
var builder = new DistributedApplicationBuilder([]);
var resourceBuilder = builder.AddK6("k6");

resourceBuilder.WithScript("scripts/main.js", duration: "1m");

Assert.Equal(["run", "--address", "0.0.0.0:6565", "--duration", "1m", "scripts/main.js"], await GetArgsAsync(resourceBuilder));
}

[Fact]
public async Task WithScriptShouldEmitVusAndDurationWhenSet()
{
var builder = new DistributedApplicationBuilder([]);
var resourceBuilder = builder.AddK6("k6");

resourceBuilder.WithScript("scripts/main.js", 5, "30s");

Assert.Equal(["run", "--address", "0.0.0.0:6565", "--vus", "5", "--duration", "30s", "scripts/main.js"], await GetArgsAsync(resourceBuilder));
}

private static async Task<List<string>> GetArgsAsync(IResourceBuilder<K6Resource> resourceBuilder)
{
var args = new List<object>();
var context = new CommandLineArgsCallbackContext(args, CancellationToken.None);
foreach (var annotation in resourceBuilder.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>())
{
await annotation.Callback(context);
}
return [.. args.Cast<string>()];
}

[Fact]
Expand Down
Loading