diff --git a/Testing/stress-test-01/Program.cs b/Testing/stress-test-01/Program.cs new file mode 100644 index 0000000..4d96e87 --- /dev/null +++ b/Testing/stress-test-01/Program.cs @@ -0,0 +1,193 @@ +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; + } +} diff --git a/Testing/stress-test-01/README.md b/Testing/stress-test-01/README.md new file mode 100644 index 0000000..6ae962a --- /dev/null +++ b/Testing/stress-test-01/README.md @@ -0,0 +1,48 @@ +# stress-test-01 + +C# .NET console app that launches 20 Playwright Chromium instances, navigates to the deployed URL, and clicks a button every 200ms. + +## Defaults + +- URL: prompted from user input every run +- Instances: `20` +- Interval: `200` ms +- Button selector: `button:visible` (first match) +- Headless: `true` + +## Run + +```bash +cd Testing/stress-test-01 +dotnet restore +dotnet build +./bin/Debug/net10.0/.playwright/node/linux-x64/node ./bin/Debug/net10.0/.playwright/package/cli.js install chromium +dotnet run +``` + +PowerShell alternative: + +```bash +pwsh bin/Debug/net10.0/playwright.ps1 install chromium +``` + +The app will prompt: + +```text +Enter target URL: +``` + +## Optional overrides + +Use either environment variables or CLI args: + +- `INSTANCE_COUNT` or `--instances=` +- `CLICK_INTERVAL_MS` or `--intervalms=` +- `BUTTON_SELECTOR` or `--selector=` +- `HEADLESS` or `--headless=` + +Example: + +```bash +INSTANCE_COUNT=20 CLICK_INTERVAL_MS=200 dotnet run +``` diff --git a/Testing/stress-test-01/stress-test-01.csproj b/Testing/stress-test-01/stress-test-01.csproj new file mode 100644 index 0000000..7eef001 --- /dev/null +++ b/Testing/stress-test-01/stress-test-01.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + enable + latest + + + + + +