Files
Htmx/Testing/stress-test-01/Program.cs
T

194 lines
6.1 KiB
C#

using Microsoft.Playwright;
const string defaultSelector = "button:has-text('Click to')";
var targetUrl = PromptForTargetUrl();
var options = StressOptions.Parse(args, defaultSelector, targetUrl);
Console.WriteLine($"URL: {options.TargetUrl}");
Console.WriteLine($"Instances: {options.InstanceCount}");
Console.WriteLine($"Interval: {options.IntervalMs}ms");
Console.WriteLine($"Selector: {options.ButtonSelector}");
Console.WriteLine($"Headless: {options.Headless}");
Console.WriteLine("Press Ctrl+C to stop.");
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cts.Cancel();
};
var workers = Enumerable.Range(1, options.InstanceCount)
.Select(instanceId => RunWorkerAsync(instanceId, options, cts.Token))
.ToArray();
await Task.WhenAll(workers);
static string PromptForTargetUrl()
{
while (true)
{
Console.Write("Enter target URL: ");
var input = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(input)
&& Uri.TryCreate(input, UriKind.Absolute, out var uri)
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
return uri.ToString();
}
Console.WriteLine("Invalid URL. Please enter a full http/https URL.");
}
}
static async Task RunWorkerAsync(int instanceId, StressOptions options, CancellationToken cancellationToken)
{
try
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = options.Headless
});
var page = await browser.NewPageAsync();
await page.GotoAsync(options.TargetUrl, new PageGotoOptions
{
WaitUntil = WaitUntilState.DOMContentLoaded,
Timeout = 30000
});
Console.WriteLine($"[{instanceId}] connected");
var clickCount = 0;
while (!cancellationToken.IsCancellationRequested)
{
try
{
// HTMX can replace the button after each request, so resolve it fresh every loop.
var button = page.Locator(options.ButtonSelector).First;
await button.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 5000
});
await button.ClickAsync(new LocatorClickOptions
{
Timeout = 5000,
Force = true
});
clickCount += 1;
if (clickCount % 25 == 0)
{
Console.WriteLine($"[{instanceId}] clicks sent: {clickCount}");
}
}
catch (TimeoutException)
{
Console.WriteLine($"[{instanceId}] click timeout");
}
catch (PlaywrightException ex)
{
Console.WriteLine($"[{instanceId}] click error: {ex.Message}");
}
try
{
await Task.Delay(options.IntervalMs, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
await page.CloseAsync();
Console.WriteLine($"[{instanceId}] stopped");
}
catch (OperationCanceledException)
{
Console.WriteLine($"[{instanceId}] canceled");
}
catch (Exception ex)
{
Console.WriteLine($"[{instanceId}] fatal: {ex.Message}");
if (ex.Message.Contains("Executable doesn't exist", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Install browser binaries first:");
Console.WriteLine("./bin/Debug/net10.0/.playwright/node/linux-x64/node ./bin/Debug/net10.0/.playwright/package/cli.js install chromium");
}
}
}
internal sealed record StressOptions(
string TargetUrl,
int InstanceCount,
int IntervalMs,
string ButtonSelector,
bool Headless)
{
public static StressOptions Parse(string[] args, string defaultSelector, string targetUrl)
{
var instanceCount = ParsePositiveInt(Environment.GetEnvironmentVariable("INSTANCE_COUNT"), 20);
var intervalMs = ParsePositiveInt(Environment.GetEnvironmentVariable("CLICK_INTERVAL_MS"), 200);
var buttonSelector = Environment.GetEnvironmentVariable("BUTTON_SELECTOR") ?? defaultSelector;
var headless = ParseBool(Environment.GetEnvironmentVariable("HEADLESS"), true);
foreach (var arg in args)
{
var split = arg.Split('=', 2, StringSplitOptions.TrimEntries);
if (split.Length != 2)
{
continue;
}
var key = split[0].TrimStart('-', '/').ToLowerInvariant();
var value = split[1];
switch (key)
{
case "instances":
case "instancecount":
instanceCount = ParsePositiveInt(value, instanceCount);
break;
case "interval":
case "intervalms":
intervalMs = ParsePositiveInt(value, intervalMs);
break;
case "selector":
case "buttonselector":
buttonSelector = value;
break;
case "headless":
headless = ParseBool(value, headless);
break;
}
}
return new StressOptions(targetUrl, instanceCount, intervalMs, buttonSelector, headless);
}
private static int ParsePositiveInt(string? rawValue, int fallback)
{
if (!string.IsNullOrWhiteSpace(rawValue) && int.TryParse(rawValue, out var parsed) && parsed > 0)
{
return parsed;
}
return fallback;
}
private static bool ParseBool(string? rawValue, bool fallback)
{
if (!string.IsNullOrWhiteSpace(rawValue) && bool.TryParse(rawValue, out var parsed))
{
return parsed;
}
return fallback;
}
}