From 372b286b1c01af6f9274bfac56d6b1ffa2aadf4f Mon Sep 17 00:00:00 2001 From: Frank Ray <52075808+FrankRay78@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:10:28 +0200 Subject: [PATCH 1/2] Surface transfer failures instead of silently reporting 0 bps (#206) Core reports the truth via per-request counts; the CLI applies policy. - NetPace.Core: aggregate per-request failures into RequestsAttempted/ Succeeded/Failed on SpeedTestResult instead of swallowing them; validate upload HTTP status (a rejected upload is a failed request, not throughput); never throw for network outcomes (only cancellation and operational faults); stream each failure reason live via SpeedTestProgress.FailedRequestReason; reverse the ISpeedTestService exception-propagation contract. - NetPace.Console: route human/operational output to a stderr console; make the exit code reflect only NetPace's health (network conditions exit 0); add --fail-on total|partial|none (fail-fast); surface counts in every format (token annotation, CSV columns, JSON fields); stream failure reasons at Debug. - Docs: README --help snapshot and a USER_GUIDE section on detecting failures. Public NetPace.Core API change ships as a MINOR bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 + USER_GUIDE.md | 59 +++++ .../CommandLineTestHost.cs | 10 + ...d_Perform_Speed_Test_With_CSV.verified.txt | 4 +- ...ed_Test_With_CSV_Continuously.verified.txt | 22 +- ...ith_CSV_Delimiter_delimiter=,.verified.txt | 4 +- ...ith_CSV_Delimiter_delimiter=-.verified.txt | 4 +- ...ith_CSV_Delimiter_delimiter=;.verified.txt | 4 +- ...h_CSV_Header_Units_No_Latency.verified.txt | 4 +- ...Delay_count=10_delay=00-10-00.verified.txt | 22 +- ...ltiple_Times_With_Fixed_Scale.verified.txt | 8 +- ...ed_Scale_In_Header_scale=Base.verified.txt | 8 +- ...ed_Scale_In_Header_scale=Kilo.verified.txt | 8 +- ...ed_Scale_In_Header_scale=Mega.verified.txt | 8 +- ...th_CSV_Multiple_Times_count=5.verified.txt | 12 +- ...eed_Test_With_CSV_No_Download.verified.txt | 4 +- ...peed_Test_With_CSV_No_Latency.verified.txt | 4 +- ...Speed_Test_With_CSV_No_Upload.verified.txt | 4 +- ...With_CSV_With_Scale_In_Header.verified.txt | 4 +- ...In_File_Output_And_Quiet_Mode.verified.txt | 1 - ...ould_Write_CSV_Output_To_File.verified.txt | 4 +- ...uld_Write_Json_Output_To_File.verified.txt | 2 +- ...usly_jsonSwitch=--json-pretty.verified.txt | 40 +++ ...ontinuously_jsonSwitch=--json.verified.txt | 20 +- ...retty_count=10_delay=00-10-00.verified.txt | 40 +++ ...-json_count=10_delay=00-10-00.verified.txt | 20 +- ...itch=--json-pretty_scale=Base.verified.txt | 12 + ...itch=--json-pretty_scale=Kilo.verified.txt | 12 + ...itch=--json-pretty_scale=Mega.verified.txt | 12 + ..._jsonSwitch=--json_scale=Base.verified.txt | 6 +- ..._jsonSwitch=--json_scale=Kilo.verified.txt | 6 +- ..._jsonSwitch=--json_scale=Mega.verified.txt | 6 +- ...nSwitch=--json-pretty_count=5.verified.txt | 20 ++ ...mes_jsonSwitch=--json_count=5.verified.txt | 10 +- ...load_jsonSwitch=--json-pretty.verified.txt | 2 + ...No_Download_jsonSwitch=--json.verified.txt | 2 +- ...ency_jsonSwitch=--json-pretty.verified.txt | 4 + ..._No_Latency_jsonSwitch=--json.verified.txt | 2 +- ...load_jsonSwitch=--json-pretty.verified.txt | 2 + ...n_No_Upload_jsonSwitch=--json.verified.txt | 2 +- ...Json_jsonSwitch=--json-pretty.verified.txt | 4 + ...t_With_Json_jsonSwitch=--json.verified.txt | 2 +- ..._When_A_Measurement_All_Fails.verified.txt | 10 + ...soleTests.Should_Display_Help.verified.txt | 68 +++--- ...uld_Handle_Network_Exceptions.verified.txt | 2 +- ...d_Handle_No_Servers_Available.verified.txt | 2 +- ...vers_Available_With_NoLatency.verified.txt | 2 +- ...--random-speedtest-server.com.verified.txt | 8 +- ...h_Server_url=http---test1.com.verified.txt | 8 +- ...h_Server_url=http---test2.com.verified.txt | 8 +- ...h_Server_url=http---test3.com.verified.txt | 8 +- .../NetPaceConsoleTests.CSV.cs | 5 +- .../NetPaceConsoleTests.Failures.cs | 231 ++++++++++++++++++ .../NetPaceConsoleTests.File.cs | 15 +- .../NetPaceConsoleTests.cs | 44 ++-- .../ScriptedSpeedTester.cs | 90 +++++++ .../Commands/SpeedTestCommand.cs | 107 +++++--- .../Commands/SpeedTestCommandSettings.cs | 5 + .../ConsoleWriters/CSVConsoleWriter.cs | 103 ++++---- .../ConsoleWriters/DefaultConsoleWriter.cs | 47 +++- .../ConsoleWriters/JsonConsoleWriter.cs | 26 +- .../ConsoleWriters/MinimalConsoleWriter.cs | 17 +- .../ConsoleWriters/ServerSelector.cs | 66 ++--- src/NetPace.Console/ErrorConsole.cs | 25 ++ src/NetPace.Console/FailOn.cs | 28 +++ src/NetPace.Console/IConsoleWriter.cs | 9 +- src/NetPace.Console/JsonResult.cs | 30 ++- src/NetPace.Console/Program.cs | 25 +- src/NetPace.Console/SpeedTestOutcome.cs | 36 +++ .../SpeedTestResultReporting.cs | 31 +++ .../OoklaSpeedtestTests.Failures.cs | 188 ++++++++++++++ src/NetPace.Core.Tests/OoklaSpeedtestTests.cs | 10 +- .../Clients/Ookla/OoklaSpeedtest.cs | 52 +++- .../Clients/Testing/SpeedTestStub.cs | 4 +- src/NetPace.Core/ISpeedTestService.cs | 36 ++- src/NetPace.Core/SpeedTestProgress.cs | 11 + src/NetPace.Core/SpeedTestResult.cs | 22 ++ 77 files changed, 1453 insertions(+), 352 deletions(-) delete mode 100644 src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Handle_Network_Exceptions_In_File_Output_And_Quiet_Mode.verified.txt create mode 100644 src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails.verified.txt create mode 100644 src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs create mode 100644 src/NetPace.Console.Tests/ScriptedSpeedTester.cs create mode 100644 src/NetPace.Console/ErrorConsole.cs create mode 100644 src/NetPace.Console/FailOn.cs create mode 100644 src/NetPace.Console/SpeedTestOutcome.cs create mode 100644 src/NetPace.Console/SpeedTestResultReporting.cs create mode 100644 src/NetPace.Core.Tests/OoklaSpeedtestTests.Failures.cs diff --git a/README.md b/README.md index 5f9a9554..37fde428 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ OPTIONS: -f, --file Write output to file. --file-mode Append Determines file output behavior. -q, --quiet Suppress all normal console output (file output still works). + --fail-on None Exit with a non-zero code on a failed measurement. + None (default) never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request. Fail-fast across --count and --loop. COMMANDS: servers Show the nearest speed test servers. diff --git a/USER_GUIDE.md b/USER_GUIDE.md index a4e43ea3..56624c49 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -100,6 +100,65 @@ NetPace --profile large --downloadsize 200 --- +## Detecting failed measurements + +A speed test runs many small requests in parallel and reports the aggregate throughput. If some +of those requests fail (a dropped connection, a TLS error, a timeout, or a server that rejects the +transfer), NetPace does **not** silently treat them as zero-speed data — it counts them, so you can +tell a genuinely slow link from a server that isn't transferring at all. When *every* request to a +dimension fails, the speed reads `0 bps`, and the counts tell you it was a total failure rather than +a 0 bps link. + +Every output format carries the counts: + +- **Normal / Minimal** — the result token is annotated only when requests failed: + ``` + Latency: 24 ms, Download: 512.6 Mbps, Upload: 0 bps (32 of 32 requests failed) + ``` + In normal output, an all-failed dimension also prints a short notice on **standard error**: + ``` + Upload failed: all 32 requests to http://…/upload.php failed. + ``` +- **CSV** — a `Succeeded` and `Failed` column sits next to each speed column: + ``` + Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname + ``` +- **JSON** — each active dimension gains integer `…Succeeded` / `…Failed` fields; on a total + failure the speed field is omitted (there is no valid measurement) while the counts remain: + ```json + { "UploadSucceeded": 0, "UploadFailed": 32, … } + ``` + +Machine formats (CSV, JSON) self-describe through the counts and never duplicate the notice on +standard error. At `--verbosity Debug`, the raw reason for each failed request is streamed live to +standard error. + +### Exit codes + +The exit code reports only whether **NetPace itself** functioned. Network conditions — a total +outage, 100% request failure, or no servers found — are *data*, not errors, and exit `0`. Only an +operational failure (for example, being unable to write the `--file` output) exits non-zero. So a +`0 bps` measurement still exits `0` by default: inspect the counts (or use `--fail-on`) to detect it. + +If you want a failed measurement to fail the process — for scripting or CI — opt in with +`--fail-on`: + +| Value | Exits non-zero when… | +|---|---| +| `None` (default) | never — measurement outcomes don't affect the exit code | +| `Total` | a requested dimension is all-failed (no request succeeded) | +| `Partial` | any request in a requested dimension failed (strict; intended for pristine-run checks) | + +`--fail-on` is fail-fast and uniform across single runs, `--count`, and `--loop`: the process exits +`1` at the first measurement that meets the threshold. + +```bash +# In CI: treat a totally-failed dimension as a build failure +NetPace --fail-on Total +``` + +--- + For more options and details, run: ```bash NetPace --help diff --git a/src/NetPace.Console.Tests/CommandLineTestHost.cs b/src/NetPace.Console.Tests/CommandLineTestHost.cs index 24ef32fb..105639a1 100644 --- a/src/NetPace.Console.Tests/CommandLineTestHost.cs +++ b/src/NetPace.Console.Tests/CommandLineTestHost.cs @@ -34,6 +34,10 @@ public async Task RunAsync(string[] args, CancellationToken cancella using var testConsole = new TestConsole().Width(int.MaxValue); serviceCollection.TryAddSingleton(testConsole); + // Register a separate standard-error console so tests can assert on stderr independently. + using var testErrorConsole = new TestConsole().Width(int.MaxValue); + serviceCollection.TryAddSingleton(new ErrorConsole(testErrorConsole)); + // Default IClientInfoProvider stub unless a test already registered one serviceCollection.TryAddSingleton(); @@ -48,6 +52,7 @@ public async Task RunAsync(string[] args, CancellationToken cancella { ExitCode = exitCode, Output = testConsole.Output, + Error = testErrorConsole.Output, }; } } @@ -66,4 +71,9 @@ public sealed record TestResult /// Gets the output written to stdout. /// public required string Output { get; init; } + + /// + /// Gets the output written to stderr. + /// + public required string Error { get; init; } } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV.verified.txt index be2d98e6..139df2b3 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Continuously.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Continuously.verified.txt index 2e70745a..9d31ffcd 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Continuously.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Continuously.verified.txt @@ -1,11 +1,11 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:05,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:10,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:15,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:20,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:25,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:30,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:35,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:40,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:45,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:05,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:10,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:15,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:20,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:25,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:30,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:35,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:40,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:45,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=,.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=,.verified.txt index be2d98e6..139df2b3 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=,.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=,.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=-.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=-.verified.txt index fd3eb8ad..4d85fbce 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=-.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=-.verified.txt @@ -1,2 +1,2 @@ -Timestamp Latency Download Upload IPAddress Hostname -1980-01-01 10:05:00 100 ms 8 Kbps 18.67 Kbps 192.168.1.1 test-host +Timestamp Latency Download DownloadSucceeded DownloadFailed Upload UploadSucceeded UploadFailed IPAddress Hostname +1980-01-01 10:05:00 100 ms 8 Kbps 150 0 18.67 Kbps 32 0 192.168.1.1 test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=;.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=;.verified.txt index bb0bf518..2fefb214 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=;.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Delimiter_delimiter=;.verified.txt @@ -1,2 +1,2 @@ -Timestamp;Latency;Download;Upload;IPAddress;Hostname -1980-01-01 10:05:00;100 ms;8 Kbps;18.67 Kbps;192.168.1.1;test-host +Timestamp;Latency;Download;DownloadSucceeded;DownloadFailed;Upload;UploadSucceeded;UploadFailed;IPAddress;Hostname +1980-01-01 10:05:00;100 ms;8 Kbps;150;0;18.67 Kbps;32;0;192.168.1.1;test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Header_Units_No_Latency.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Header_Units_No_Latency.verified.txt index 215ed780..e0a332a5 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Header_Units_No_Latency.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Header_Units_No_Latency.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Download (Kbps),Upload (Kbps),IPAddress,Hostname -1980-01-01 10:05:00,8,18.67,192.168.1.1,test-host +Timestamp,Download (Kbps),DownloadSucceeded,DownloadFailed,Upload (Kbps),UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,8,150,0,18.67,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Delay_count=10_delay=00-10-00.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Delay_count=10_delay=00-10-00.verified.txt index 2e70745a..9d31ffcd 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Delay_count=10_delay=00-10-00.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Delay_count=10_delay=00-10-00.verified.txt @@ -1,11 +1,11 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:05,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:10,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:15,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:20,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:25,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:30,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:35,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:40,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:45,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:05,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:10,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:15,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:20,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:25,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:30,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:35,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:40,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:45,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale.verified.txt index af42ff9f..68fc7ac2 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,75 ms,0.25 Mbps,0.5 Mbps,192.168.1.1,test-host -1980-01-01 10:05:05,100 ms,1 Mbps,3 Mbps,192.168.1.1,test-host -1980-01-01 10:05:10,150 ms,2.75 Mbps,1.33 Mbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,75 ms,0.25 Mbps,0,0,0.5 Mbps,0,0,192.168.1.1,test-host +1980-01-01 10:05:05,100 ms,1 Mbps,0,0,3 Mbps,0,0,192.168.1.1,test-host +1980-01-01 10:05:10,150 ms,2.75 Mbps,0,0,1.33 Mbps,0,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Base.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Base.verified.txt index ec448484..6e40e6c2 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Base.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Base.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency (ms),Download (bps),Upload (bps),IPAddress,Hostname -1980-01-01 10:05:00,75,250000,500000,192.168.1.1,test-host -1980-01-01 10:05:05,100,1000000,3000000,192.168.1.1,test-host -1980-01-01 10:05:10,150,2750000,1330000,192.168.1.1,test-host +Timestamp,Latency (ms),Download (bps),DownloadSucceeded,DownloadFailed,Upload (bps),UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,75,250000,0,0,500000,0,0,192.168.1.1,test-host +1980-01-01 10:05:05,100,1000000,0,0,3000000,0,0,192.168.1.1,test-host +1980-01-01 10:05:10,150,2750000,0,0,1330000,0,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Kilo.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Kilo.verified.txt index 087a934a..940e6189 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Kilo.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Kilo.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency (ms),Download (Kbps),Upload (Kbps),IPAddress,Hostname -1980-01-01 10:05:00,75,250,500,192.168.1.1,test-host -1980-01-01 10:05:05,100,1000,3000,192.168.1.1,test-host -1980-01-01 10:05:10,150,2750,1330,192.168.1.1,test-host +Timestamp,Latency (ms),Download (Kbps),DownloadSucceeded,DownloadFailed,Upload (Kbps),UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,75,250,0,0,500,0,0,192.168.1.1,test-host +1980-01-01 10:05:05,100,1000,0,0,3000,0,0,192.168.1.1,test-host +1980-01-01 10:05:10,150,2750,0,0,1330,0,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Mega.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Mega.verified.txt index aaf8a60c..72ba1c47 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Mega.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_With_Fixed_Scale_In_Header_scale=Mega.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency (ms),Download (Mbps),Upload (Mbps),IPAddress,Hostname -1980-01-01 10:05:00,75,0.25,0.5,192.168.1.1,test-host -1980-01-01 10:05:05,100,1,3,192.168.1.1,test-host -1980-01-01 10:05:10,150,2.75,1.33,192.168.1.1,test-host +Timestamp,Latency (ms),Download (Mbps),DownloadSucceeded,DownloadFailed,Upload (Mbps),UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,75,0.25,0,0,0.5,0,0,192.168.1.1,test-host +1980-01-01 10:05:05,100,1,0,0,3,0,0,192.168.1.1,test-host +1980-01-01 10:05:10,150,2.75,0,0,1.33,0,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_count=5.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_count=5.verified.txt index d15cceb3..f552b829 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_count=5.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_Multiple_Times_count=5.verified.txt @@ -1,6 +1,6 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:05,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:10,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:15,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host -1980-01-01 10:05:20,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:05,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:10,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:15,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:20,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Download.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Download.verified.txt index aa6e2420..b81dfcab 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Download.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Download.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Latency.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Latency.verified.txt index 0a1bb292..c9e69a11 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Latency.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Latency.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Upload.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Upload.verified.txt index d6cf60e2..ea75b339 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Upload.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_No_Upload.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency,Download,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_With_Scale_In_Header.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_With_Scale_In_Header.verified.txt index 6c5f26fe..cbb870c0 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_With_Scale_In_Header.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.CSV.Should_Perform_Speed_Test_With_CSV_With_Scale_In_Header.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency (ms),Download (Kbps),Upload (Kbps),IPAddress,Hostname -1980-01-01 10:05:00,75,250,500,192.168.1.1,test-host +Timestamp,Latency (ms),Download (Kbps),DownloadSucceeded,DownloadFailed,Upload (Kbps),UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,75,250,0,0,500,0,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Handle_Network_Exceptions_In_File_Output_And_Quiet_Mode.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Handle_Network_Exceptions_In_File_Output_And_Quiet_Mode.verified.txt deleted file mode 100644 index 77ee21df..00000000 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Handle_Network_Exceptions_In_File_Output_And_Quiet_Mode.verified.txt +++ /dev/null @@ -1 +0,0 @@ -Error: Could not connect to server diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_CSV_Output_To_File.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_CSV_Output_To_File.verified.txt index be2d98e6..139df2b3 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_CSV_Output_To_File.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_CSV_Output_To_File.verified.txt @@ -1,2 +1,2 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,8 Kbps,18.67 Kbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,8 Kbps,150,0,18.67 Kbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_Json_Output_To_File.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_Json_Output_To_File.verified.txt index 0a5c9967..a07854eb 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_Json_Output_To_File.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.File.Should_Write_Json_Output_To_File.verified.txt @@ -1 +1 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json-pretty.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json-pretty.verified.txt index 326d8926..c679086b 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json-pretty.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json-pretty.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -38,7 +50,11 @@ "Timestamp": "1980-01-01 10:05:15", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -49,7 +65,11 @@ "Timestamp": "1980-01-01 10:05:20", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -60,7 +80,11 @@ "Timestamp": "1980-01-01 10:05:25", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -71,7 +95,11 @@ "Timestamp": "1980-01-01 10:05:30", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -82,7 +110,11 @@ "Timestamp": "1980-01-01 10:05:35", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -93,7 +125,11 @@ "Timestamp": "1980-01-01 10:05:40", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -104,7 +140,11 @@ "Timestamp": "1980-01-01 10:05:45", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json.verified.txt index 45ed4d61..b1cc4687 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Continuously_jsonSwitch=--json.verified.txt @@ -1,10 +1,10 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:25","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:30","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:35","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:40","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:45","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:25","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:30","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:35","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:40","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:45","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json-pretty_count=10_delay=00-10-00.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json-pretty_count=10_delay=00-10-00.verified.txt index 326d8926..c679086b 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json-pretty_count=10_delay=00-10-00.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json-pretty_count=10_delay=00-10-00.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -38,7 +50,11 @@ "Timestamp": "1980-01-01 10:05:15", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -49,7 +65,11 @@ "Timestamp": "1980-01-01 10:05:20", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -60,7 +80,11 @@ "Timestamp": "1980-01-01 10:05:25", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -71,7 +95,11 @@ "Timestamp": "1980-01-01 10:05:30", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -82,7 +110,11 @@ "Timestamp": "1980-01-01 10:05:35", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -93,7 +125,11 @@ "Timestamp": "1980-01-01 10:05:40", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -104,7 +140,11 @@ "Timestamp": "1980-01-01 10:05:45", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json_count=10_delay=00-10-00.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json_count=10_delay=00-10-00.verified.txt index 45ed4d61..b1cc4687 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json_count=10_delay=00-10-00.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Delay_jsonSwitch=--json_count=10_delay=00-10-00.verified.txt @@ -1,10 +1,10 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:25","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:30","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:35","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:40","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:45","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:25","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:30","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:35","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:40","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:45","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Base.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Base.verified.txt index 1e958602..68032d3c 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Base.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Base.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "75 ms", "DownloadSpeed": "250000 bps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "500000 bps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "1000000 bps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "3000000 bps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "150 ms", "DownloadSpeed": "2750000 bps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "1330000 bps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Kilo.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Kilo.verified.txt index a9ce407b..90837686 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Kilo.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Kilo.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "75 ms", "DownloadSpeed": "250 Kbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "500 Kbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "1000 Kbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "3000 Kbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "150 ms", "DownloadSpeed": "2750 Kbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "1330 Kbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Mega.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Mega.verified.txt index bc45b67c..f08ce0ca 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Mega.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json-pretty_scale=Mega.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "75 ms", "DownloadSpeed": "0.25 Mbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "0.5 Mbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "1 Mbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "3 Mbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "150 ms", "DownloadSpeed": "2.75 Mbps", + "DownloadSucceeded": 0, + "DownloadFailed": 0, "UploadSpeed": "1.33 Mbps", + "UploadSucceeded": 0, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Base.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Base.verified.txt index 44649d15..dcc6eeb4 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Base.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Base.verified.txt @@ -1,3 +1,3 @@ -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"250000 bps","UploadSpeed":"500000 bps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1000000 bps","UploadSpeed":"3000000 bps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2750000 bps","UploadSpeed":"1330000 bps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"250000 bps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"500000 bps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1000000 bps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"3000000 bps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2750000 bps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"1330000 bps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Kilo.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Kilo.verified.txt index 57effed9..ae3b5a0b 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Kilo.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Kilo.verified.txt @@ -1,3 +1,3 @@ -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"250 Kbps","UploadSpeed":"500 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1000 Kbps","UploadSpeed":"3000 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2750 Kbps","UploadSpeed":"1330 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"250 Kbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"500 Kbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1000 Kbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"3000 Kbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2750 Kbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"1330 Kbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Mega.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Mega.verified.txt index 445df829..90bd0105 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Mega.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_With_Fixed_Scale_jsonSwitch=--json_scale=Mega.verified.txt @@ -1,3 +1,3 @@ -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"0.25 Mbps","UploadSpeed":"0.5 Mbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1 Mbps","UploadSpeed":"3 Mbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2.75 Mbps","UploadSpeed":"1.33 Mbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:00","Latency":"75 ms","DownloadSpeed":"0.25 Mbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"0.5 Mbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"1 Mbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"3 Mbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Test Location","ServerSponsor":"Test Sponsor","ServerUrl":"http://test.com","Timestamp":"1980-01-01 10:05:10","Latency":"150 ms","DownloadSpeed":"2.75 Mbps","DownloadSucceeded":0,"DownloadFailed":0,"UploadSpeed":"1.33 Mbps","UploadSucceeded":0,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json-pretty_count=5.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json-pretty_count=5.verified.txt index ef7b3969..51e317a3 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json-pretty_count=5.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json-pretty_count=5.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -16,7 +20,11 @@ "Timestamp": "1980-01-01 10:05:05", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -27,7 +35,11 @@ "Timestamp": "1980-01-01 10:05:10", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -38,7 +50,11 @@ "Timestamp": "1980-01-01 10:05:15", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } @@ -49,7 +65,11 @@ "Timestamp": "1980-01-01 10:05:20", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json_count=5.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json_count=5.verified.txt index c17cb8e1..958642dc 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json_count=5.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_Multiple_Times_jsonSwitch=--json_count=5.verified.txt @@ -1,5 +1,5 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:05","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:10","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:15","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:20","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json-pretty.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json-pretty.verified.txt index 7e05c749..8ef062fc 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json-pretty.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json-pretty.verified.txt @@ -5,6 +5,8 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json.verified.txt index 3626be7c..2b8e4660 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Download_jsonSwitch=--json.verified.txt @@ -1 +1 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json-pretty.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json-pretty.verified.txt index 95ab3f2d..5be0ac7c 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json-pretty.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json-pretty.verified.txt @@ -4,7 +4,11 @@ "ServerUrl": "http://test1.com", "Timestamp": "1980-01-01 10:05:00", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json.verified.txt index f838b523..c29028ff 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Latency_jsonSwitch=--json.verified.txt @@ -1 +1 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json-pretty.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json-pretty.verified.txt index b0bed730..7f3447b2 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json-pretty.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json-pretty.verified.txt @@ -5,6 +5,8 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json.verified.txt index a0ac684b..5cfea3d2 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_No_Upload_jsonSwitch=--json.verified.txt @@ -1 +1 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json-pretty.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json-pretty.verified.txt index 11f02ffe..da8088c6 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json-pretty.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json-pretty.verified.txt @@ -5,7 +5,11 @@ "Timestamp": "1980-01-01 10:05:00", "Latency": "100 ms", "DownloadSpeed": "8 Kbps", + "DownloadSucceeded": 150, + "DownloadFailed": 0, "UploadSpeed": "18.67 Kbps", + "UploadSucceeded": 32, + "UploadFailed": 0, "IPAddress": "192.168.1.1", "Hostname": "test-host" } diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json.verified.txt index 0a5c9967..a07854eb 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Json.Should_Perform_Speed_Test_With_Json_jsonSwitch=--json.verified.txt @@ -1 +1 @@ -{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","UploadSpeed":"18.67 Kbps","IPAddress":"192.168.1.1","Hostname":"test-host"} +{"ServerLocation":"Location 1","ServerSponsor":"Test Sponsor 1","ServerUrl":"http://test1.com","Timestamp":"1980-01-01 10:05:00","Latency":"100 ms","DownloadSpeed":"8 Kbps","DownloadSucceeded":150,"DownloadFailed":0,"UploadSpeed":"18.67 Kbps","UploadSucceeded":32,"UploadFailed":0,"IPAddress":"192.168.1.1","Hostname":"test-host"} diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails.verified.txt new file mode 100644 index 00000000..d2196f2d --- /dev/null +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails.verified.txt @@ -0,0 +1,10 @@ +1980-01-01 10:05:00, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:05, Latency: 24 ms, Download: 0 bps (150 of 150 requests failed), Upload: 8 Kbps +1980-01-01 10:05:10, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:15, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:20, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:25, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:30, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:35, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:40, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps +1980-01-01 10:05:45, Latency: 24 ms, Download: 8 Kbps, Upload: 8 Kbps diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt index 84145d65..683e77a4 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Display_Help.verified.txt @@ -1,4 +1,4 @@ - + _ __ __ ____ / | / / ___ / /_ / __ \ ____ _ _____ ___ / |/ / / _ \ / __/ / /_/ / / __ `/ / ___/ / _ \ @@ -13,38 +13,40 @@ USAGE: NetPace [OPTIONS] [COMMAND] OPTIONS: - DEFAULT - -h, --help Prints help information. - -v, --version Prints version information. - --loop Performs the speed test on continuous loop. - --count Stop speed testing after this many times. - --delay Time between multiple speed tests (HH:MM:SS). - --csv Display minimal output in CSV format (always includes timestamp). - --csv-delimiter , Single character delimiter to use in CSV output. - --csv-header-units Display speed test units (eg. Mbps) in the CSV header row, not the data rows. - --unit-scale must not be for multiple speed tests (eg. --loop or --count). - --json Display output in Json format. - --json-pretty Display output in Json format (pretty print). - --no-latency Do not perform latency test. - When used without --server, the first available server is selected. - --no-download Do not perform download test. - --no-upload Do not perform upload test. - --server The url of a specific speed test sever. - 'NetPace servers -l' will return your nearest servers. - -t, --timestamp Include a timestamp in the output. - --datetimeformat yyyy-MM-dd HH:mm:ss The datetime format string, as defined by Microsoft.Net. - --profile Medium Profile bundle of payload settings (Tiny | Small | Medium | Large | Mega). - --downloadsize Stop the download test after this many megabytes (IEC MiB). - --uploadsize Stop the upload test after this many megabytes (IEC MiB). - -u, --unit BitsPerSecond The speed unit. - --unit-scale Auto The speed unit scale. - --unit-system SI The speed unit system. - SI steps up in powers of 1000 (KB, MB, GB), common in networking, while IEC uses powers of 1024 (KiB, MiB, GiB), standard in computing and storage. - --verbosity Normal The verbosity level. - Minimal is ideal for batch scripts and redirected output. - -f, --file Write output to file. - --file-mode Append Determines file output behavior. - -q, --quiet Suppress all normal console output (file output still works). + DEFAULT + -h, --help Prints help information. + -v, --version Prints version information. + --loop Performs the speed test on continuous loop. + --count Stop speed testing after this many times. + --delay Time between multiple speed tests (HH:MM:SS). + --csv Display minimal output in CSV format (always includes timestamp). + --csv-delimiter , Single character delimiter to use in CSV output. + --csv-header-units Display speed test units (eg. Mbps) in the CSV header row, not the data rows. + --unit-scale must not be for multiple speed tests (eg. --loop or --count). + --json Display output in Json format. + --json-pretty Display output in Json format (pretty print). + --no-latency Do not perform latency test. + When used without --server, the first available server is selected. + --no-download Do not perform download test. + --no-upload Do not perform upload test. + --server The url of a specific speed test sever. + 'NetPace servers -l' will return your nearest servers. + -t, --timestamp Include a timestamp in the output. + --datetimeformat yyyy-MM-dd HH:mm:ss The datetime format string, as defined by Microsoft.Net. + --profile Medium Profile bundle of payload settings (Tiny | Small | Medium | Large | Mega). + --downloadsize Stop the download test after this many megabytes (IEC MiB). + --uploadsize Stop the upload test after this many megabytes (IEC MiB). + -u, --unit BitsPerSecond The speed unit. + --unit-scale Auto The speed unit scale. + --unit-system SI The speed unit system. + SI steps up in powers of 1000 (KB, MB, GB), common in networking, while IEC uses powers of 1024 (KiB, MiB, GiB), standard in computing and storage. + --verbosity Normal The verbosity level. + Minimal is ideal for batch scripts and redirected output. + -f, --file Write output to file. + --file-mode Append Determines file output behavior. + -q, --quiet Suppress all normal console output (file output still works). + --fail-on None Exit with a non-zero code on a failed measurement. + None (default) never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request. Fail-fast across --count and --loop. COMMANDS: servers Show the nearest speed test servers. diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_Network_Exceptions.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_Network_Exceptions.verified.txt index 2b4bcf83..92a05136 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_Network_Exceptions.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_Network_Exceptions.verified.txt @@ -1 +1 @@ -Error: Could not open socket +No speed test servers were found. diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available.verified.txt index 067f9351..92a05136 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available.verified.txt @@ -1 +1 @@ -Error: No servers available +No speed test servers were found. diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available_With_NoLatency.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available_With_NoLatency.verified.txt index 067f9351..92a05136 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available_With_NoLatency.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Handle_No_Servers_Available_With_NoLatency.verified.txt @@ -1 +1 @@ -Error: No servers available +No speed test servers were found. diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---random-speedtest-server.com.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---random-speedtest-server.com.verified.txt index da14a544..0504bf13 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---random-speedtest-server.com.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---random-speedtest-server.com.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,1000 ms,0 Mbps,0 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,1000 ms,0 Mbps,0 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,1000 ms,0 Mbps,0 Mbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,1000 ms,0 Mbps,150,0,0 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,1000 ms,0 Mbps,150,0,0 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,1000 ms,0 Mbps,150,0,0 Mbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test1.com.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test1.com.verified.txt index 9bb0cb56..ccd9979a 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test1.com.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test1.com.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,100 ms,0.01 Mbps,0.02 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,100 ms,0.01 Mbps,0.02 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,100 ms,0.01 Mbps,0.02 Mbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,100 ms,0.01 Mbps,150,0,0.02 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,100 ms,0.01 Mbps,150,0,0.02 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,100 ms,0.01 Mbps,150,0,0.02 Mbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test2.com.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test2.com.verified.txt index f989c0c7..ed6039bc 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test2.com.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test2.com.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,200 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,200 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,200 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,200 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,200 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,200 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test3.com.verified.txt b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test3.com.verified.txt index 6d9dea23..cdef60a0 100644 --- a/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test3.com.verified.txt +++ b/src/NetPace.Console.Tests/Expectations/NetPaceConsoleTests.Should_Perform_Speed_Test_Multiple_Times_With_Server_url=http---test3.com.verified.txt @@ -1,4 +1,4 @@ -Timestamp,Latency,Download,Upload,IPAddress,Hostname -1980-01-01 10:05:00,300 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,300 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host -1980-01-01 10:05:00,300 ms,0 Mbps,0.01 Mbps,192.168.1.1,test-host +Timestamp,Latency,Download,DownloadSucceeded,DownloadFailed,Upload,UploadSucceeded,UploadFailed,IPAddress,Hostname +1980-01-01 10:05:00,300 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,300 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host +1980-01-01 10:05:00,300 ms,0 Mbps,150,0,0.01 Mbps,32,0,192.168.1.1,test-host diff --git a/src/NetPace.Console.Tests/NetPaceConsoleTests.CSV.cs b/src/NetPace.Console.Tests/NetPaceConsoleTests.CSV.cs index 0d2dd651..3ca23d51 100644 --- a/src/NetPace.Console.Tests/NetPaceConsoleTests.CSV.cs +++ b/src/NetPace.Console.Tests/NetPaceConsoleTests.CSV.cs @@ -154,9 +154,10 @@ public async Task Should_Return_Validation_Error_For_Speed_Test_With_CSV_Multipl // When var result = await host.RunAsync([ "--csv", "--csv-header-units", "--count", "3", "--unit-scale", "Auto" ]); - // Then + // Then the validation error is reported on standard error. Assert.Equal(1, result.ExitCode); - await Verify(result.Output); + Assert.Empty(result.Output); + await Verify(result.Error); } [Fact] diff --git a/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs b/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs new file mode 100644 index 00000000..fb4bc4d5 --- /dev/null +++ b/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs @@ -0,0 +1,231 @@ +namespace NetPace.Console.Tests; + +/// +/// CLI behaviour for surfacing transfer failures (issue #206): counts appear in every output +/// format, the exit code reflects only NetPace's health by default, standard error carries the +/// human notice, and --fail-on opts in to a failure exit code. +/// +public sealed partial class NetPaceConsoleTests +{ + public sealed class Failures + { + private static CommandLineTestHost HostWith(ISpeedTestService service) + { + var services = new ServiceCollection(); + services.AddSingleton(service); + services.AddSingleton(); + services.AddSingleton(); + return new CommandLineTestHost(services); + } + + [Fact] + public async Task Total_Network_Failure_Exits_Zero_By_Default() + { + // SCENARIO: Total network failure exits 0 by default (AC5) + + // Given every upload request fails. + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When (default settings) + var result = await host.RunAsync([]); + + // Then the process succeeds, the failure is visible in stdout, and stderr carries guidance. + Assert.Equal(0, result.ExitCode); + Assert.Contains("32 of 32 requests failed", result.Output); + Assert.Contains("Upload failed: all 32 requests to", result.Error); + } + + [Fact] + public async Task Partial_Failure_Is_Annotated_On_Stdout_Without_Stderr_Notice() + { + // SCENARIO: Normal + verbosity gradation - partial failure (AC9) + + // Given some download requests fail but the dimension still measured throughput. + var service = new ScriptedSpeedTester { DownloadFactory = _ => ScriptedSpeedTester.Partial(150, 5) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([]); + + // Then the token is annotated on stdout, but partial failure gets no stderr notice. + Assert.Equal(0, result.ExitCode); + Assert.Contains("5 of 150 requests failed", result.Output); + Assert.Empty(result.Error); + } + + [Fact] + public async Task Json_All_Failed_Self_Describes_On_Stdout_With_No_Stderr() + { + // SCENARIO: Machine formats self-describe on stdout - JSON (AC8a) + + // Given every upload request fails. + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--json" ]); + + // Then the JSON carries the counts and no upload speed value; nothing is written to stderr. + Assert.Equal(0, result.ExitCode); + Assert.Contains("\"UploadSucceeded\":0", result.Output); + Assert.Contains("\"UploadFailed\":32", result.Output); + Assert.DoesNotContain("\"UploadSpeed\"", result.Output); + Assert.Empty(result.Error); + } + + [Fact] + public async Task Csv_All_Failed_Row_Distinguishes_Total_Failure_With_No_Stderr() + { + // SCENARIO: Machine formats self-describe on stdout - CSV (AC8b) + + // Given every upload request fails. + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--csv" ]); + + // Then the data row shows UploadSucceeded=0; nothing is written to stderr. + Assert.Equal(0, result.ExitCode); + var dataRow = result.Output.Split('\n')[1]; + Assert.Contains(",0,32,", dataRow); // UploadSucceeded=0, UploadFailed=32 + Assert.Empty(result.Error); + } + + [Fact] + public async Task Minimal_All_Failed_Annotates_Token_Without_Stderr_Notice() + { + // SCENARIO: Normal + verbosity gradation - Minimal (AC9) + + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--verbosity", "Minimal" ]); + + // Then the token annotation carries the failure; Minimal emits no separate stderr notice. + Assert.Equal(0, result.ExitCode); + Assert.Contains("32 of 32 requests failed", result.Output); + Assert.Empty(result.Error); + } + + [Fact] + public async Task Debug_Streams_Each_Failure_Reason_To_Stderr() + { + // SCENARIO: Normal + verbosity gradation - Debug (AC9) + + var service = new ScriptedSpeedTester + { + UploadFactory = _ => ScriptedSpeedTester.AllFailed(32), + StreamedFailureReason = "Connection reset by peer" + }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--verbosity", "Debug" ]); + + // Then the raw reason is emitted live on stderr, in addition to the total-failure notice. + Assert.Equal(0, result.ExitCode); + Assert.Contains("Upload request failed: Connection reset by peer", result.Error); + Assert.Contains("Upload failed: all 32 requests to", result.Error); + } + + [Fact] + public async Task FailOn_Total_Exits_One_On_All_Failed_Dimension() + { + // SCENARIO: --fail-on total opt-in (AC10) + + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--fail-on", "Total" ]); + + // Then + Assert.Equal(1, result.ExitCode); + } + + [Fact] + public async Task FailOn_None_Exits_Zero_On_All_Failed_Dimension() + { + // SCENARIO: --fail-on total opt-in, default is none (AC10) + + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When (explicit default) + var result = await host.RunAsync([ "--fail-on", "None" ]); + + // Then + Assert.Equal(0, result.ExitCode); + } + + [Fact] + public async Task FailOn_Partial_Exits_One_When_Any_Request_Failed() + { + // SCENARIO: --fail-on partial opt-in (AC11) + + var service = new ScriptedSpeedTester { DownloadFactory = _ => ScriptedSpeedTester.Partial(150, 5) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--fail-on", "Partial" ]); + + // Then + Assert.Equal(1, result.ExitCode); + } + + [Fact] + public async Task FailOn_Total_Exits_Zero_On_Partial_Failure() + { + // SCENARIO: --fail-on partial opt-in, total does not trip on partial (AC11) + + var service = new ScriptedSpeedTester { DownloadFactory = _ => ScriptedSpeedTester.Partial(150, 5) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--fail-on", "Total" ]); + + // Then + Assert.Equal(0, result.ExitCode); + } + + [Fact] + public async Task Unreachable_User_Specified_Server_Exits_Zero_Regardless_Of_Latency() + { + // An unreachable --server is a network condition, not a NetPace fault: it must exit 0 + // whether or not the latency probe runs (the latency probe is in the selection region). + + // Given a user-specified server whose latency probe throws (host down). + var mock = new SpeedTestMock + { + GetServerLatencyByServerUrlAsyncFunc = (_, _, _) => throw new HttpRequestException("Could not open socket"), + }; + var host = HostWith(mock); + + // When run with the default latency probe enabled. + var result = await host.RunAsync([ "--server", "http://unreachable.example/upload.php" ]); + + // Then the condition is reported on stderr and the process exits 0 (not 1). + Assert.Equal(0, result.ExitCode); + Assert.Contains("No speed test servers were found.", result.Error); + } + + [Fact] + public async Task FailOn_Is_FailFast_Under_Count() + { + // SCENARIO: --fail-on is uniform and fail-fast (AC12) + + // Given the first iteration already all-fails. + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When run with --count under --fail-on total. + var result = await host.RunAsync([ "--count", "5", "--fail-on", "Total", "--verbosity", "Minimal" ]); + + // Then the process exits 1 at the first triggering measurement. + Assert.Equal(1, result.ExitCode); + } + } +} diff --git a/src/NetPace.Console.Tests/NetPaceConsoleTests.File.cs b/src/NetPace.Console.Tests/NetPaceConsoleTests.File.cs index 5979d74b..8ba90c2d 100644 --- a/src/NetPace.Console.Tests/NetPaceConsoleTests.File.cs +++ b/src/NetPace.Console.Tests/NetPaceConsoleTests.File.cs @@ -295,12 +295,14 @@ public async Task Should_Handle_File_Creation_Exception() // When var result = await host.RunAsync([ "--file", invalidPath ]); - // Then + // Then an operational failure (cannot write the output file) exits non-zero and is + // reported on standard error. Assert.Equal(1, result.ExitCode); + Assert.Empty(result.Output); // Normalize directory separators to Windows-style backslashes so the Windows snapshots match across platforms. - var normalizedOutput = (result.Output ?? string.Empty).Replace('/', '\\'); - await Verify(normalizedOutput); + var normalizedError = (result.Error ?? string.Empty).Replace('/', '\\'); + await Verify(normalizedError); } [InlineData("-q")] @@ -365,12 +367,11 @@ public async Task Should_Handle_Network_Exceptions_In_File_Output_And_Quiet_Mode // When var result = await host.RunAsync([ quiet, "--file", testFile ]); - // Then + // Then an unreachable discovery endpoint is data (exit 0): no data row is written to + // the file, and the notice appears on standard error (not the file/stdout channel). Assert.Equal(0, result.ExitCode); Assert.Empty(result.Output); - - var fileContent = await System.IO.File.ReadAllTextAsync(testFile); - await Verify(fileContent).DisableRequireUniquePrefix(); + Assert.Contains("No speed test servers were found.", result.Error); } finally { diff --git a/src/NetPace.Console.Tests/NetPaceConsoleTests.cs b/src/NetPace.Console.Tests/NetPaceConsoleTests.cs index 804e42ee..276cfcb9 100644 --- a/src/NetPace.Console.Tests/NetPaceConsoleTests.cs +++ b/src/NetPace.Console.Tests/NetPaceConsoleTests.cs @@ -330,9 +330,10 @@ public async Task Should_Return_Validation_Error_When_No_Tests_Selected() // When var result = await host.RunAsync([ "--no-latency", "--no-download", "--no-upload" ]); - // Then + // Then the validation error is reported on standard error. Assert.Equal(1, result.ExitCode); - await Verify(result.Output); + Assert.Empty(result.Output); + await Verify(result.Error); } [Fact] @@ -443,35 +444,28 @@ public async Task Should_Handle_Network_Exceptions() // When var result = await host.RunAsync(Array.Empty()); - // Then + // Then an unreachable discovery endpoint is a reported data outcome (exit 0), surfaced on stderr. Assert.Equal(0, result.ExitCode); - await Verify(result.Output); + await Verify(result.Error); } [Fact] - public async Task Should_Continue_Multiple_Speed_Tests_On_Exception() + public async Task Should_Continue_Multiple_Speed_Tests_When_A_Measurement_All_Fails() { - // Given + // A measurement that all-fails is data, not an error: the loop keeps running and the exit + // code stays 0 (network conditions never set it by default). + + // Given the second iteration's download all-fails, the rest measure cleanly. var cancellationTokenSource = new CancellationTokenSource(); var waiter = new SelfCancellingWaiter(10, cancellationTokenSource); - // Create a stateful fault function that tracks calls - var downloadCallCount = 0; - var faultyTester = new FaultySpeedTester( - inner: new SpeedTestStub(), - isFaulted: (sponsor, methodName) => - { - if (methodName == nameof(ISpeedTestService.GetDownloadSpeedAsync)) - { - downloadCallCount++; - return downloadCallCount == 2; // Fail only on the second call - } - return false; // Don't fail other methods - } - ); + var service = new ScriptedSpeedTester + { + DownloadFactory = i => i == 1 ? ScriptedSpeedTester.AllFailed(150) : ScriptedSpeedTester.Clean(150) + }; var services = new ServiceCollection(); - services.AddSingleton(faultyTester); + services.AddSingleton(service); services.AddSingleton(); services.AddSingleton(waiter); var host = GetCommandLineTestHost(services); @@ -502,9 +496,9 @@ public async Task Should_Handle_No_Servers_Available() // When var result = await host.RunAsync(Array.Empty()); - // Then + // Then the no-servers condition is reported on stderr and exits 0. Assert.Equal(0, result.ExitCode); - await Verify(result.Output); + await Verify(result.Error); } [Fact] @@ -525,9 +519,9 @@ public async Task Should_Handle_No_Servers_Available_With_NoLatency() // When var result = await host.RunAsync([ "--no-latency" ]); - // Then + // Then the no-servers condition is reported on stderr and exits 0. Assert.Equal(0, result.ExitCode); - await Verify(result.Output); + await Verify(result.Error); } [InlineData("-h")] diff --git a/src/NetPace.Console.Tests/ScriptedSpeedTester.cs b/src/NetPace.Console.Tests/ScriptedSpeedTester.cs new file mode 100644 index 00000000..8cab2e9a --- /dev/null +++ b/src/NetPace.Console.Tests/ScriptedSpeedTester.cs @@ -0,0 +1,90 @@ +using NetPace.Core; +using NetPace.Core.Clients.Ookla; + +namespace NetPace.Console.Tests; + +/// +/// A configurable for CLI failure scenarios: it always finds a +/// server (Deutsche Telekom-style URL, matching the issue #206 example), and returns download/upload +/// results supplied per call index so a test can script clean, partial, and all-failed iterations. +/// Optionally streams a per-request failure reason on the progress channel for debug-verbosity tests. +/// +public sealed class ScriptedSpeedTester : ISpeedTestService +{ + public const string Url = "http://ffm.wsqm.telekom-dienste.de:8080/speedtest/upload.php"; + + private readonly IServer server = new Server { Location = "Frankfurt", Sponsor = "Deutsche Telekom", Url = Url }; + + private int downloadCall; + private int uploadCall; + + /// Supplies the download result for a given zero-based call index. + public Func DownloadFactory { get; set; } = _ => Clean(150); + + /// Supplies the upload result for a given zero-based call index. + public Func UploadFactory { get; set; } = _ => Clean(32); + + /// When set, a request failure reason streamed on the progress channel. + public string? StreamedFailureReason { get; set; } + + /// A clean result: every request succeeded. + public static SpeedTestResult Clean(int attempts) => + new() { BytesProcessed = 1000, ElapsedMilliseconds = 1000, RequestsAttempted = attempts, RequestsSucceeded = attempts, RequestsFailed = 0 }; + + /// An all-failed result: zero bytes, every request failed. + public static SpeedTestResult AllFailed(int attempts) => + new() { BytesProcessed = 0, ElapsedMilliseconds = 1000, RequestsAttempted = attempts, RequestsSucceeded = 0, RequestsFailed = attempts }; + + /// A partial result: some requests failed, the rest contributed bytes. + public static SpeedTestResult Partial(int attempts, int failed) => + new() { BytesProcessed = 1000, ElapsedMilliseconds = 1000, RequestsAttempted = attempts, RequestsSucceeded = attempts - failed, RequestsFailed = failed }; + + public Task GetServersAsync(CancellationToken cancellationToken = default) => + Task.FromResult(new[] { server }); + + public Task GetServerLatencyAsync(IServer server, CancellationToken cancellationToken = default) => + Task.FromResult(new LatencyTestResult { Server = server, LatencyMilliseconds = 24 }); + + public Task GetServerLatencyAsync(IServer server, IProgress progress, CancellationToken cancellationToken = default) => + GetServerLatencyAsync(server, cancellationToken); + + public Task GetServerLatencyAsync(string serverUrl, CancellationToken cancellationToken = default) => + Task.FromResult(new LatencyTestResult { Server = new Server { Location = "Frankfurt", Sponsor = "Deutsche Telekom", Url = serverUrl }, LatencyMilliseconds = 24 }); + + public Task GetServerLatencyAsync(string serverUrl, IProgress progress, CancellationToken cancellationToken = default) => + GetServerLatencyAsync(serverUrl, cancellationToken); + + public Task GetFastestServerByLatencyAsync(IServer[] servers, CancellationToken cancellationToken = default) => + Task.FromResult(new LatencyTestResult { Server = servers[0], LatencyMilliseconds = 24 }); + + public Task GetFastestServerByLatencyAsync(IServer[] servers, IProgress progress, CancellationToken cancellationToken = default) => + GetFastestServerByLatencyAsync(servers, cancellationToken); + + public Task GetDownloadSpeedAsync(IServer server, CancellationToken cancellationToken = default) => + GetDownloadSpeedAsync(server, new NullProgress(), cancellationToken); + + public Task GetDownloadSpeedAsync(IServer server, IProgress progress, CancellationToken cancellationToken = default) + { + var result = DownloadFactory(downloadCall++); + StreamReasonIfFailed(result, progress); + return Task.FromResult(result); + } + + public Task GetUploadSpeedAsync(IServer server, CancellationToken cancellationToken = default) => + GetUploadSpeedAsync(server, new NullProgress(), cancellationToken); + + public Task GetUploadSpeedAsync(IServer server, IProgress progress, CancellationToken cancellationToken = default) + { + var result = UploadFactory(uploadCall++); + StreamReasonIfFailed(result, progress); + return Task.FromResult(result); + } + + private void StreamReasonIfFailed(SpeedTestResult result, IProgress progress) + { + if (StreamedFailureReason is not null && result.RequestsFailed > 0) + { + progress.Report(new SpeedTestProgress { PercentageComplete = 100, FailedRequestReason = StreamedFailureReason }); + } + } +} diff --git a/src/NetPace.Console/Commands/SpeedTestCommand.cs b/src/NetPace.Console/Commands/SpeedTestCommand.cs index 8318a04e..0a972ae6 100644 --- a/src/NetPace.Console/Commands/SpeedTestCommand.cs +++ b/src/NetPace.Console/Commands/SpeedTestCommand.cs @@ -3,19 +3,17 @@ namespace NetPace.Console.Commands; -public sealed class SpeedTestCommand(IAnsiConsole console, ISpeedTestService speedTestClient, IClock clock, IClientInfoProvider clientInfoProvider, IWaiter waiter) +public sealed class SpeedTestCommand(IAnsiConsole console, IAnsiConsole errorConsole, ISpeedTestService speedTestClient, IClock clock, IClientInfoProvider clientInfoProvider, IWaiter waiter) { - /// - /// Writes an error message to the console. - /// - private static void WriteError(IAnsiConsole console, string message) - { - console.Markup($"[red]Error:[/] {message.EscapeMarkup()}\n"); - } - /// /// Executes the speed test command using the provided settings. /// + /// + /// Network and discovery outcomes are data, not errors: they are reported through the output + /// (counts) and standard-error notices, and leave the exit code at 0 unless the consumer + /// opts in via --fail-on. Only operational failures (which propagate out of this method to + /// the top-level handler) produce a non-zero exit code. + /// public async Task ExecuteAsync(SpeedTestCommandSettings settings, CancellationToken cancellationToken) { if (settings.Quiet || !string.IsNullOrWhiteSpace(settings.OutputFile)) @@ -56,18 +54,14 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat { try { - // Run the speed test. - await writer.PerformSpeedTestAsync(initialSpeedTest: firstLoop, console, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: firstLoop, console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + if (ProcessOutcome(outcome, settings)) return 1; } - catch (TaskCanceledException) + catch (OperationCanceledException) { // User requested cancellation. return 0; } - catch (Exception e) - { - WriteError(console, e.Message); - } finally { firstLoop = false; @@ -78,7 +72,7 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat // Pause before the next speed test. await waiter.Delay(settings.Delay, cancellationToken); } - catch (TaskCanceledException) + catch (OperationCanceledException) { // User requested cancellation. return 0; @@ -93,18 +87,14 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat { try { - // Run the speed test. - await writer.PerformSpeedTestAsync(initialSpeedTest: (i == 0), console, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: (i == 0), console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + if (ProcessOutcome(outcome, settings)) return 1; } - catch (TaskCanceledException) + catch (OperationCanceledException) { // User requested cancellation. return 0; } - catch (Exception e) - { - WriteError(console, e.Message); - } if ((i + 1) < settings.Count) { @@ -113,7 +103,7 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat // Pause before the next speed test. await waiter.Delay(settings.Delay, cancellationToken); } - catch (TaskCanceledException) + catch (OperationCanceledException) { // User requested cancellation. return 0; @@ -126,18 +116,14 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat // Run once. try { - // Run the speed test. - await writer.PerformSpeedTestAsync(initialSpeedTest: true, console, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: true, console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + if (ProcessOutcome(outcome, settings)) return 1; } - catch (TaskCanceledException) + catch (OperationCanceledException) { // User requested cancellation. return 0; } - catch (Exception e) - { - WriteError(console, e.Message); - } } return 0; @@ -150,4 +136,61 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat } } } + + /// + /// Emits standard-error notices for the outcome and evaluates the --fail-on threshold. + /// + /// when --fail-on is met and the process should exit with a non-zero code. + private bool ProcessOutcome(SpeedTestOutcome outcome, SpeedTestCommandSettings settings) + { + if (!outcome.ServersFound) + { + // No usable server is a reported data outcome, not an error (exit code stays 0). + errorConsole.WriteLine("No speed test servers were found."); + return false; + } + + // Standard error is the human channel for interactive output. Machine formats (JSON, CSV) + // self-describe via the counts, and Minimal keeps the token annotation only, so neither + // gets a duplicate notice. + if (ShouldEmitFailureNotice(settings)) + { + EmitAllFailedNotice("Download", settings.NoDownload ? null : outcome.Download, outcome.ServerUrl); + EmitAllFailedNotice("Upload", settings.NoUpload ? null : outcome.Upload, outcome.ServerUrl); + } + + return FailOnTriggered(outcome, settings); + } + + /// + /// Whether an all-failed dimension should produce a standard-error notice for the active output mode. + /// + private static bool ShouldEmitFailureNotice(SpeedTestCommandSettings settings) => + !settings.CSV && !settings.Json && !settings.JsonPretty && settings.Verbosity != Verbosity.Minimal; + + private void EmitAllFailedNotice(string dimension, SpeedTestResult? result, string? serverUrl) + { + if (result is not null && result.IsAllFailed()) + { + errorConsole.WriteLine($"{dimension} failed: all {result.RequestsAttempted} requests to {serverUrl} failed."); + } + } + + /// + /// Evaluates the --fail-on threshold against a single measurement (fail-fast). + /// + private static bool FailOnTriggered(SpeedTestOutcome outcome, SpeedTestCommandSettings settings) + { + if (settings.FailOn == FailOn.None) return false; + + foreach (var dimension in new[] { outcome.Download, outcome.Upload }) + { + if (dimension is null) continue; + + if (settings.FailOn == FailOn.Total && dimension.IsAllFailed()) return true; + if (settings.FailOn == FailOn.Partial && dimension.HasFailures()) return true; + } + + return false; + } } diff --git a/src/NetPace.Console/Commands/SpeedTestCommandSettings.cs b/src/NetPace.Console/Commands/SpeedTestCommandSettings.cs index eecb898a..afe50114 100644 --- a/src/NetPace.Console/Commands/SpeedTestCommandSettings.cs +++ b/src/NetPace.Console/Commands/SpeedTestCommandSettings.cs @@ -129,6 +129,11 @@ public sealed class SpeedTestCommandSettings /// Suppress all normal console output. /// public required bool Quiet { get; init; } + + /// + /// Whether a measurement outcome causes a non-zero exit code (opt-in; default ). + /// + public required FailOn FailOn { get; init; } } public static class SpeedTestCommandSettingsExtensions diff --git a/src/NetPace.Console/ConsoleWriters/CSVConsoleWriter.cs b/src/NetPace.Console/ConsoleWriters/CSVConsoleWriter.cs index c664402e..e5252512 100644 --- a/src/NetPace.Console/ConsoleWriters/CSVConsoleWriter.cs +++ b/src/NetPace.Console/ConsoleWriters/CSVConsoleWriter.cs @@ -4,10 +4,11 @@ namespace NetPace.Console.ConsoleWriters; public sealed class CSVConsoleWriter : IConsoleWriter { - public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) + public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IAnsiConsole errorConsole, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) { // Get the server to use for speed testing. var fastest = await ServerSelector.GetServerAsync(speedTestClient, settings, cancellationToken); + if (fastest is null) return SpeedTestOutcome.NoServers; var downloadResult = new SpeedTestResult(); @@ -18,63 +19,63 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons if (!settings.NoUpload) uploadResult = await speedTestClient.GetUploadSpeedAsync(fastest.Server, cancellationToken); - // Display speed test result. - if (settings.CSVHeaderUnits) - { - var downloadFormattedParts = downloadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); - var uploadFormattedParts = uploadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); + // Display speed test result. Count columns (which carry no units) sit adjacent to each + // speed column so a single row distinguishes total from partial failure. + var downloadSpeed = settings.CSVHeaderUnits + ? downloadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale).speed + : downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); + var uploadSpeed = settings.CSVHeaderUnits + ? uploadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale).speed + : uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); - // Header row. - if (initialSpeedTest) - { - console.WriteLine(string.Join(settings.CSVDelimiter, new[] - { - "Timestamp", - !settings.NoLatency ? "Latency (ms)" : null, - !settings.NoDownload ? $"Download ({downloadFormattedParts.unit})" : null, - !settings.NoUpload ? $"Upload ({uploadFormattedParts.unit})" : null, - "IPAddress", - "Hostname" - }.Where(s => s is not null))); - } + var downloadHeader = settings.CSVHeaderUnits + ? $"Download ({downloadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale).unit})" + : "Download"; + var uploadHeader = settings.CSVHeaderUnits + ? $"Upload ({uploadResult.GetSpeedStringParts(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale).unit})" + : "Upload"; + var latencyValue = settings.CSVHeaderUnits ? $"{fastest.LatencyMilliseconds}" : $"{fastest.LatencyMilliseconds} ms"; + var latencyHeader = settings.CSVHeaderUnits ? "Latency (ms)" : "Latency"; - // Data row. + // Header row. + if (initialSpeedTest) + { console.WriteLine(string.Join(settings.CSVDelimiter, new[] { - clock.Now.ToString(settings.DateTimeFormat), - !settings.NoLatency ? $"{fastest.LatencyMilliseconds}" : null, - !settings.NoDownload ? downloadFormattedParts.speed : null, - !settings.NoUpload ? uploadFormattedParts.speed : null, - clientInfoProvider.GetIPAddress(), - clientInfoProvider.GetHostname() + "Timestamp", + !settings.NoLatency ? latencyHeader : null, + !settings.NoDownload ? downloadHeader : null, + !settings.NoDownload ? "DownloadSucceeded" : null, + !settings.NoDownload ? "DownloadFailed" : null, + !settings.NoUpload ? uploadHeader : null, + !settings.NoUpload ? "UploadSucceeded" : null, + !settings.NoUpload ? "UploadFailed" : null, + "IPAddress", + "Hostname" }.Where(s => s is not null))); } - else + + // Data row. + console.WriteLine(string.Join(settings.CSVDelimiter, new[] { - // Header row. - if (initialSpeedTest) - { - console.WriteLine(string.Join(settings.CSVDelimiter, new[] - { - "Timestamp", - !settings.NoLatency ? "Latency" : null, - !settings.NoDownload ? "Download" : null, - !settings.NoUpload ? "Upload" : null, - "IPAddress", - "Hostname" - }.Where(s => s is not null))); - } + clock.Now.ToString(settings.DateTimeFormat), + !settings.NoLatency ? latencyValue : null, + !settings.NoDownload ? downloadSpeed : null, + !settings.NoDownload ? $"{downloadResult.RequestsSucceeded}" : null, + !settings.NoDownload ? $"{downloadResult.RequestsFailed}" : null, + !settings.NoUpload ? uploadSpeed : null, + !settings.NoUpload ? $"{uploadResult.RequestsSucceeded}" : null, + !settings.NoUpload ? $"{uploadResult.RequestsFailed}" : null, + clientInfoProvider.GetIPAddress(), + clientInfoProvider.GetHostname() + }.Where(s => s is not null))); - // Data row. - console.WriteLine(string.Join(settings.CSVDelimiter, new[] - { - clock.Now.ToString(settings.DateTimeFormat), - !settings.NoLatency ? $"{fastest.LatencyMilliseconds} ms" : null, - !settings.NoDownload ? downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale) : null, - !settings.NoUpload ? uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale) : null, - clientInfoProvider.GetIPAddress(), - clientInfoProvider.GetHostname() - }.Where(s => s is not null))); - } + return new SpeedTestOutcome + { + ServersFound = true, + ServerUrl = fastest.Server.Url, + Download = settings.NoDownload ? null : downloadResult, + Upload = settings.NoUpload ? null : uploadResult + }; } } diff --git a/src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs b/src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs index 4268d184..42c5046e 100644 --- a/src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs +++ b/src/NetPace.Console/ConsoleWriters/DefaultConsoleWriter.cs @@ -6,8 +6,10 @@ namespace NetPace.Console.ConsoleWriters; public sealed class DefaultConsoleWriter : IConsoleWriter { - public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) + public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IAnsiConsole errorConsole, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) { + var debug = (settings.Verbosity & Verbosity.Debug) != 0; + // Get the server to use for speed testing. var fastest = await console.Progress() .AutoClear(true) @@ -21,15 +23,17 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons var fastestServerProgress = progress.AddTask("Choosing server", autoStart: true, maxValue: 100); try - { + { return await ServerSelector.GetServerAsync(speedTestClient, settings, cancellationToken); } finally - { + { fastestServerProgress.StopTask(); } }); + if (fastest is null) return SpeedTestOutcome.NoServers; + // Display server latency. console.WriteLine(""); @@ -76,19 +80,36 @@ await console.Progress() // SyncContext/thread pool, which races Spectre's renderer and drops updates. if (!settings.NoDownload) { - var downloadProgressReporter = new SyncProgress(p => downloadProgress!.Value = p.PercentageComplete); + var downloadProgressReporter = new SyncProgress(p => + { + downloadProgress!.Value = p.PercentageComplete; + + // At debug verbosity, stream each per-request failure reason live to stderr. + if (debug && p.FailedRequestReason is not null) + { + errorConsole.WriteLine($"Download request failed: {p.FailedRequestReason}"); + } + }); downloadResult = await speedTestClient.GetDownloadSpeedAsync(fastest.Server, downloadProgressReporter, cancellationToken); } if (!settings.NoUpload) { - var uploadProgressReporter = new SyncProgress(p => uploadProgress!.Value = p.PercentageComplete); + var uploadProgressReporter = new SyncProgress(p => + { + uploadProgress!.Value = p.PercentageComplete; + + if (debug && p.FailedRequestReason is not null) + { + errorConsole.WriteLine($"Upload request failed: {p.FailedRequestReason}"); + } + }); uploadResult = await speedTestClient.GetUploadSpeedAsync(fastest.Server, uploadProgressReporter, cancellationToken); } }); } - if ((settings.Verbosity & Verbosity.Debug) != 0) + if (debug) { // Display detailed diagnostics ByteSize size; TimeSpan elapsed; @@ -120,17 +141,25 @@ await console.Progress() } - // Display speed test result. + // Display speed test result. The token carries the count annotation when requests failed. console.WriteLine(string.Join(", ", new[] { settings.IncludeTimestamp ? clock.Now.ToString(settings.DateTimeFormat) : null, !settings.NoLatency ? $"Latency: {fastest.LatencyMilliseconds} ms" : null, - !settings.NoDownload ? $"Download: {downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}" : null, - !settings.NoUpload ? $"Upload: {uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}" : null + !settings.NoDownload ? $"Download: {downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}{downloadResult.GetFailureAnnotation()}" : null, + !settings.NoUpload ? $"Upload: {uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}{uploadResult.GetFailureAnnotation()}" : null }.Where(s => !string.IsNullOrEmpty(s)))); console.WriteLine("\nTry 'NetPace --help' for more information."); + + return new SpeedTestOutcome + { + ServersFound = true, + ServerUrl = fastest.Server.Url, + Download = settings.NoDownload ? null : downloadResult, + Upload = settings.NoUpload ? null : uploadResult + }; } private sealed class SyncProgress(Action handler) : IProgress diff --git a/src/NetPace.Console/ConsoleWriters/JsonConsoleWriter.cs b/src/NetPace.Console/ConsoleWriters/JsonConsoleWriter.cs index 3219d003..0f07d6dd 100644 --- a/src/NetPace.Console/ConsoleWriters/JsonConsoleWriter.cs +++ b/src/NetPace.Console/ConsoleWriters/JsonConsoleWriter.cs @@ -5,10 +5,11 @@ namespace NetPace.Console.ConsoleWriters; public sealed class JsonConsoleWriter : IConsoleWriter { - public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) + public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IAnsiConsole errorConsole, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) { // Get the server to use for speed testing. var fastest = await ServerSelector.GetServerAsync(speedTestClient, settings, cancellationToken); + if (fastest is null) return SpeedTestOutcome.NoServers; var downloadResult = new SpeedTestResult(); @@ -19,10 +20,15 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons if (!settings.NoUpload) uploadResult = await speedTestClient.GetUploadSpeedAsync(fastest.Server, cancellationToken); - // Display speed test result. + // Display speed test result. On an all-failed dimension the speed is null (no valid + // measurement); the counts always accompany an active dimension so the JSON self-describes. var latencyFormatted = !settings.NoLatency ? $"{fastest.LatencyMilliseconds} ms" : null; - var downloadFormatted = !settings.NoDownload ? downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale) : null; - var uploadFormatted = !settings.NoUpload ? uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale) : null; + var downloadFormatted = settings.NoDownload || downloadResult.IsAllFailed() + ? null + : downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); + var uploadFormatted = settings.NoUpload || uploadResult.IsAllFailed() + ? null + : uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale); var jsonResult = new JsonResult { @@ -32,7 +38,11 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons Timestamp = clock.Now.ToString(settings.DateTimeFormat), Latency = latencyFormatted!, DownloadSpeed = downloadFormatted!, + DownloadSucceeded = settings.NoDownload ? null : downloadResult.RequestsSucceeded, + DownloadFailed = settings.NoDownload ? null : downloadResult.RequestsFailed, UploadSpeed = uploadFormatted!, + UploadSucceeded = settings.NoUpload ? null : uploadResult.RequestsSucceeded, + UploadFailed = settings.NoUpload ? null : uploadResult.RequestsFailed, IPAddress = clientInfoProvider.GetIPAddress(), Hostname = clientInfoProvider.GetHostname() }; @@ -43,5 +53,13 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons string jsonString = JsonSerializer.Serialize(jsonResult, typeInfo); console.WriteLine(jsonString); + + return new SpeedTestOutcome + { + ServersFound = true, + ServerUrl = fastest.Server.Url, + Download = settings.NoDownload ? null : downloadResult, + Upload = settings.NoUpload ? null : uploadResult + }; } } diff --git a/src/NetPace.Console/ConsoleWriters/MinimalConsoleWriter.cs b/src/NetPace.Console/ConsoleWriters/MinimalConsoleWriter.cs index 02cc5b36..e537191a 100644 --- a/src/NetPace.Console/ConsoleWriters/MinimalConsoleWriter.cs +++ b/src/NetPace.Console/ConsoleWriters/MinimalConsoleWriter.cs @@ -5,10 +5,11 @@ namespace NetPace.Console.ConsoleWriters; public sealed class MinimalConsoleWriter : IConsoleWriter { - public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) + public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IAnsiConsole errorConsole, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken) { // Get the server to use for speed testing. var fastest = await ServerSelector.GetServerAsync(speedTestClient, settings, cancellationToken); + if (fastest is null) return SpeedTestOutcome.NoServers; var downloadResult = new SpeedTestResult(); @@ -19,13 +20,21 @@ public async Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole cons if (!settings.NoUpload) uploadResult = await speedTestClient.GetUploadSpeedAsync(fastest.Server, cancellationToken); - // Display speed test result. + // Display speed test result. The token carries the count annotation when requests failed. console.WriteLine(string.Join(", ", new[] { settings.IncludeTimestamp ? clock.Now.ToString(settings.DateTimeFormat) : null, !settings.NoLatency ? $"Latency: {fastest.LatencyMilliseconds} ms" : null, - !settings.NoDownload ? $"Download: {downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}" : null, - !settings.NoUpload ? $"Upload: {uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}" : null + !settings.NoDownload ? $"Download: {downloadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}{downloadResult.GetFailureAnnotation()}" : null, + !settings.NoUpload ? $"Upload: {uploadResult.GetSpeedString(settings.SpeedUnit, settings.SpeedUnitSystem, settings.SpeedScale)}{uploadResult.GetFailureAnnotation()}" : null }.Where(s => !string.IsNullOrEmpty(s)))); + + return new SpeedTestOutcome + { + ServersFound = true, + ServerUrl = fastest.Server.Url, + Download = settings.NoDownload ? null : downloadResult, + Upload = settings.NoUpload ? null : uploadResult + }; } } diff --git a/src/NetPace.Console/ConsoleWriters/ServerSelector.cs b/src/NetPace.Console/ConsoleWriters/ServerSelector.cs index 04d5717a..7442fda9 100644 --- a/src/NetPace.Console/ConsoleWriters/ServerSelector.cs +++ b/src/NetPace.Console/ConsoleWriters/ServerSelector.cs @@ -10,48 +10,56 @@ internal static class ServerSelector /// /// Gets the server to use for speed testing based on settings. /// - public static async Task GetServerAsync(ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken = default) + /// + /// The selected server and its latency, or when discovery returns no + /// usable server or the discovery endpoint cannot be reached. A missing server is a reported + /// data outcome, not an error — only caller-requested cancellation propagates. + /// + public static async Task GetServerAsync(ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(speedTestClient); ArgumentNullException.ThrowIfNull(settings); - if (settings.NoLatency) + // Obtaining a server is part of the discovery/selection region, so a network failure here + // (an unreachable discovery endpoint, a user-specified host that does not respond to the + // latency probe, or all latency probes failing) is data, not an error: it is reported as + // "no usable server" rather than thrown. Only caller-requested cancellation propagates. + try { - if (string.IsNullOrEmpty(settings.ServerUrl)) + if (!string.IsNullOrEmpty(settings.ServerUrl)) { - // Get the first speed test server. - var servers = await speedTestClient.GetServersAsync(cancellationToken); - if (servers.Length == 0) + if (settings.NoLatency) { - throw new Exception("No servers available"); + // Create a minimal speed test server without testing latency. + var server = new Server() { Sponsor = "(Unknown)", Url = settings.ServerUrl }; + return new LatencyTestResult { Server = server, LatencyMilliseconds = 0 }; } - var firstServer = servers.First(); - return new LatencyTestResult { Server = firstServer, LatencyMilliseconds = 0 }; - } - else - { - // Create a minimal speed test server without testing latency. - var server = new Server() { Sponsor = "(Unknown)", Url = settings.ServerUrl }; - return new LatencyTestResult { Server = server, LatencyMilliseconds = 0 }; + + // User specified speed test server. + return await speedTestClient.GetServerLatencyAsync(settings.ServerUrl, cancellationToken); } - } - else - { - if (string.IsNullOrEmpty(settings.ServerUrl)) + + var servers = await speedTestClient.GetServersAsync(cancellationToken); + if (servers.Length == 0) { - // Get the fastest speed test server. - var servers = await speedTestClient.GetServersAsync(cancellationToken); - if (servers.Length == 0) - { - throw new Exception("No servers available"); - } - return await speedTestClient.GetFastestServerByLatencyAsync(servers, cancellationToken); + return null; } - else + + if (settings.NoLatency) { - // User specified speed test server. - return await speedTestClient.GetServerLatencyAsync(settings.ServerUrl, cancellationToken); + return new LatencyTestResult { Server = servers.First(), LatencyMilliseconds = 0 }; } + + return await speedTestClient.GetFastestServerByLatencyAsync(servers, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Caller-requested cancellation always propagates. + throw; + } + catch (Exception) + { + return null; } } } diff --git a/src/NetPace.Console/ErrorConsole.cs b/src/NetPace.Console/ErrorConsole.cs new file mode 100644 index 00000000..ee102873 --- /dev/null +++ b/src/NetPace.Console/ErrorConsole.cs @@ -0,0 +1,25 @@ +namespace NetPace.Console; + +/// +/// Wraps the that targets standard error. +/// +/// +/// Standard error is the human/operational channel, kept separate from the machine-readable +/// standard-output stream. It carries measurement-failure notices (in interactive output modes), +/// the "no servers found" notice, live per-request failure reasons at debug verbosity, and +/// operational-failure errors. Machine formats (JSON, CSV) self-describe measurement validity via +/// the request counts, so they are never duplicated on standard error. +/// +public sealed class ErrorConsole +{ + /// + /// Initializes a new instance of the class. + /// + /// The console that writes to standard error. + public ErrorConsole(IAnsiConsole console) => Console = console; + + /// + /// Gets the console that writes to standard error. + /// + public IAnsiConsole Console { get; } +} diff --git a/src/NetPace.Console/FailOn.cs b/src/NetPace.Console/FailOn.cs new file mode 100644 index 00000000..fff19028 --- /dev/null +++ b/src/NetPace.Console/FailOn.cs @@ -0,0 +1,28 @@ +namespace NetPace.Console; + +/// +/// Controls whether a measurement outcome causes a non-zero exit code. +/// +/// +/// By default () network conditions never affect the exit code — they are data, +/// reported via the request counts in the output. The other values let a consumer opt in to +/// treating a failed measurement as a process failure (for example, in CI). Evaluation is +/// fail-fast and uniform across single, --count, and --loop runs. +/// +public enum FailOn +{ + /// + /// Measurement outcomes never affect the exit code (default). + /// + None, + + /// + /// Exit with a non-zero code when a requested dimension is all-failed (no request succeeded). + /// + Total, + + /// + /// Exit with a non-zero code when any request in a requested dimension failed. + /// + Partial +} diff --git a/src/NetPace.Console/IConsoleWriter.cs b/src/NetPace.Console/IConsoleWriter.cs index cd0dfde9..5833131d 100644 --- a/src/NetPace.Console/IConsoleWriter.cs +++ b/src/NetPace.Console/IConsoleWriter.cs @@ -14,11 +14,16 @@ public interface IConsoleWriter /// if this is the first test in a sequence; used by writers that emit a /// header row (e.g. CSV) to decide whether to include it. /// - /// The Spectre.Console instance used for output. + /// The Spectre.Console instance used for standard output. + /// The console used for standard-error output (live per-request failure reasons at debug verbosity). /// Clock used to obtain the current timestamp for each result. /// Provider for device identity values (IP address and hostname). /// Speed test service that performs latency, download and upload measurements. /// Parsed command-line settings controlling which measurements to run and how to format output. /// Token that can be used to cancel the operation. - Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken); + /// + /// The outcome of the run (whether a server was found and the per-dimension measurements), + /// used by the command to apply exit-code policy and emit standard-error notices. + /// + Task PerformSpeedTestAsync(bool initialSpeedTest, IAnsiConsole console, IAnsiConsole errorConsole, IClock clock, IClientInfoProvider clientInfoProvider, ISpeedTestService speedTestClient, SpeedTestCommandSettings settings, CancellationToken cancellationToken); } diff --git a/src/NetPace.Console/JsonResult.cs b/src/NetPace.Console/JsonResult.cs index a6c609e0..04dedfa9 100644 --- a/src/NetPace.Console/JsonResult.cs +++ b/src/NetPace.Console/JsonResult.cs @@ -35,15 +35,41 @@ public sealed record JsonResult public required string Latency { get; init; } /// - /// Gets the measured download speed. + /// Gets the measured download speed, or when every download request + /// failed (no valid measurement) or the download dimension was not run. /// public required string DownloadSpeed { get; init; } /// - /// Gets the measured upload speed. + /// Gets the number of download requests that succeeded, or when the + /// download dimension was not run. + /// + public int? DownloadSucceeded { get; init; } + + /// + /// Gets the number of download requests that failed, or when the + /// download dimension was not run. + /// + public int? DownloadFailed { get; init; } + + /// + /// Gets the measured upload speed, or when every upload request failed + /// (no valid measurement) or the upload dimension was not run. /// public required string UploadSpeed { get; init; } + /// + /// Gets the number of upload requests that succeeded, or when the upload + /// dimension was not run. + /// + public int? UploadSucceeded { get; init; } + + /// + /// Gets the number of upload requests that failed, or when the upload + /// dimension was not run. + /// + public int? UploadFailed { get; init; } + // Device identity /// diff --git a/src/NetPace.Console/Program.cs b/src/NetPace.Console/Program.cs index 83b40b64..0a8e86ed 100644 --- a/src/NetPace.Console/Program.cs +++ b/src/NetPace.Console/Program.cs @@ -188,6 +188,12 @@ internal static RootCommand CreateRootCommand(IServiceProvider serviceProvider) }; quietOption.Aliases.Add("-q"); + var failOnOption = new Option("--fail-on") + { + Description = "Exit with a non-zero code on a failed measurement. \nNone (default) never affects the exit code; Total triggers when a dimension is all-failed; Partial triggers on any failed request. Fail-fast across --count and --loop.", + DefaultValueFactory = _ => FailOn.None + }; + // Add options command.Options.Add(versionOption); command.Options.Add(loopOption); @@ -214,6 +220,7 @@ internal static RootCommand CreateRootCommand(IServiceProvider serviceProvider) command.Options.Add(fileOption); command.Options.Add(fileModeOption); command.Options.Add(quietOption); + command.Options.Add(failOnOption); // Set command action command.SetAction((Func>)(async (parseResult, cancellationToken) => @@ -246,7 +253,8 @@ internal static RootCommand CreateRootCommand(IServiceProvider serviceProvider) Verbosity = parseResult.GetValue(verbosityOption), OutputFile = parseResult.GetValue(fileOption) ?? string.Empty, FileModeValue = parseResult.GetValue(fileModeOption), - Quiet = parseResult.GetValue(quietOption) + Quiet = parseResult.GetValue(quietOption), + FailOn = parseResult.GetValue(failOnOption) }; // Validate settings @@ -260,19 +268,21 @@ internal static RootCommand CreateRootCommand(IServiceProvider serviceProvider) // Get services from DI var ansiConsole = serviceProvider.GetRequiredService(); + var errorConsole = serviceProvider.GetRequiredService().Console; var speedTestService = serviceProvider.GetRequiredService(); var clock = serviceProvider.GetRequiredService(); var clientInfoProvider = serviceProvider.GetRequiredService(); var waiter = serviceProvider.GetRequiredService(); // Create and execute command - var command = new SpeedTestCommand(ansiConsole, speedTestService, clock, clientInfoProvider, waiter); + var command = new SpeedTestCommand(ansiConsole, errorConsole, speedTestService, clock, clientInfoProvider, waiter); return await command.ExecuteAsync(settings, cancellationToken); } catch (Exception ex) { - var ansiConsole = serviceProvider.GetRequiredService(); - ansiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); + // Usage/configuration errors and operational failures are reported on standard error. + var errorConsole = serviceProvider.GetRequiredService().Console; + errorConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); return 1; } })); @@ -375,8 +385,13 @@ public async static Task Main(string[] args) // Setup DI var services = new ServiceCollection(); - // Register AnsiConsole + // Register AnsiConsole (standard output) and a standard-error console for the + // human/operational channel. services.AddSingleton(AnsiConsole.Console); + services.AddSingleton(new ErrorConsole(AnsiConsole.Create(new AnsiConsoleSettings + { + Out = new AnsiConsoleOutput(System.Console.Error) + }))); if (args != null && args.Contains("--test")) { diff --git a/src/NetPace.Console/SpeedTestOutcome.cs b/src/NetPace.Console/SpeedTestOutcome.cs new file mode 100644 index 00000000..79425a51 --- /dev/null +++ b/src/NetPace.Console/SpeedTestOutcome.cs @@ -0,0 +1,36 @@ +using NetPace.Core; + +namespace NetPace.Console; + +/// +/// The outcome of a single speed-test run, returned by an so the +/// command can apply exit-code policy and emit human-facing notices without re-running the test. +/// +public sealed record SpeedTestOutcome +{ + /// + /// Gets a value indicating whether a usable server was found. When , no + /// measurement ran and no data row was written to standard output. + /// + public required bool ServersFound { get; init; } + + /// + /// Gets the URL of the server the test ran against, or when no server was found. + /// + public string? ServerUrl { get; init; } + + /// + /// Gets the download measurement, or when the download dimension was not requested. + /// + public SpeedTestResult? Download { get; init; } + + /// + /// Gets the upload measurement, or when the upload dimension was not requested. + /// + public SpeedTestResult? Upload { get; init; } + + /// + /// An outcome representing that no usable server was found. + /// + public static readonly SpeedTestOutcome NoServers = new() { ServersFound = false }; +} diff --git a/src/NetPace.Console/SpeedTestResultReporting.cs b/src/NetPace.Console/SpeedTestResultReporting.cs new file mode 100644 index 00000000..f6736b7e --- /dev/null +++ b/src/NetPace.Console/SpeedTestResultReporting.cs @@ -0,0 +1,31 @@ +using NetPace.Core; + +namespace NetPace.Console; + +/// +/// CLI-side helpers that derive measurement validity from a 's request +/// counts. The counts are the universal currency of validity; these helpers apply the consumer +/// policy the core library deliberately does not. +/// +internal static class SpeedTestResultReporting +{ + /// + /// Whether every request in the dimension failed (zero valid measurement). + /// + public static bool IsAllFailed(this SpeedTestResult result) => + result.RequestsAttempted > 0 && result.RequestsSucceeded == 0; + + /// + /// Whether at least one request in the dimension failed. + /// + public static bool HasFailures(this SpeedTestResult result) => result.RequestsFailed > 0; + + /// + /// The parenthetical annotation appended to a result token when any request failed + /// (for example, " (5 of 150 requests failed)"); an empty string when none failed. + /// + public static string GetFailureAnnotation(this SpeedTestResult result) => + result.RequestsFailed > 0 + ? $" ({result.RequestsFailed} of {result.RequestsAttempted} requests failed)" + : string.Empty; +} diff --git a/src/NetPace.Core.Tests/OoklaSpeedtestTests.Failures.cs b/src/NetPace.Core.Tests/OoklaSpeedtestTests.Failures.cs new file mode 100644 index 00000000..3cc6e465 --- /dev/null +++ b/src/NetPace.Core.Tests/OoklaSpeedtestTests.Failures.cs @@ -0,0 +1,188 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using NetPace.Core.Clients.Ookla; +using RichardSzalay.MockHttp; +using Shouldly; + +namespace NetPace.Core.Tests; + +/// +/// Per-request failure aggregation for download and upload tests (issue #206): failed requests are +/// counted, not swallowed; the call does not throw for network outcomes; user cancellation still +/// propagates. +/// +public sealed partial class OoklaSpeedtestTests +{ + [Fact] + public async Task GetUploadSpeedAsync_ShouldReportAllRequestsFailed_WhenEveryRequestThrows() + { + // SCENARIO: All requests fail, no exception + + // Given every upload POST fails at the transport level. + using var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("*").Throw(new HttpRequestException("Connection reset by peer")); + + var httpClient = mockHttp.ToHttpClient(); + var settings = new OoklaSpeedtestSettings + { + UploadTest = new() + { + UploadIncrements = 1, + UploadSizeIterations = 4, + UploadParallelTasks = 2 + } + }; + var speedtest = new OoklaSpeedtest(settings, httpClient); + var server = new Server { Url = "http://example.com/", Sponsor = "Test", Location = "Test" }; + + // When + var result = await speedtest.GetUploadSpeedAsync(server); + + // Then the call returns (does not throw), zero succeeded, all attempts failed, zero bytes. + result.ShouldNotBeNull(); + result.BytesProcessed.ShouldBe(0); + result.RequestsAttempted.ShouldBeGreaterThan(0); + result.RequestsSucceeded.ShouldBe(0); + result.RequestsFailed.ShouldBe(result.RequestsAttempted); + } + + [Fact] + public async Task GetDownloadSpeedAsync_ShouldReportAllRequestsFailed_WhenEveryRequestThrows() + { + // SCENARIO: All requests fail, no exception (download facet) + + // Given every download GET fails at the transport level. + using var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("*").Throw(new HttpRequestException("Name or service not known")); + + var httpClient = mockHttp.ToHttpClient(); + var settings = new OoklaSpeedtestSettings + { + DownloadTest = new() + { + DownloadSizes = new[] { 100 }, + DownloadSizeIterations = 4, + DownloadParallelTasks = 2 + } + }; + var speedtest = new OoklaSpeedtest(settings, httpClient); + var server = new Server { Url = "http://example.com/", Sponsor = "Test", Location = "Test" }; + + // When + var result = await speedtest.GetDownloadSpeedAsync(server); + + // Then + result.ShouldNotBeNull(); + result.BytesProcessed.ShouldBe(0); + result.RequestsAttempted.ShouldBeGreaterThan(0); + result.RequestsSucceeded.ShouldBe(0); + result.RequestsFailed.ShouldBe(result.RequestsAttempted); + } + + [Fact] + public async Task GetUploadSpeedAsync_ShouldCountOnlySuccessfulBytes_WhenSomeRequestsFail() + { + // SCENARIO: Partial failure is measured, not hidden + + // Given a server that rejects the middle request but accepts the others (serialised so the + // alternation is deterministic). + var requestNumber = 0; + using var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("*").Respond(_ => + { + var n = Interlocked.Increment(ref requestNumber); + return new HttpResponseMessage(n == 2 ? HttpStatusCode.InternalServerError : HttpStatusCode.OK); + }); + + var httpClient = mockHttp.ToHttpClient(); + var settings = new OoklaSpeedtestSettings + { + UploadTest = new() + { + UploadIncrements = 1, + UploadSizeIterations = 3, + UploadParallelTasks = 1 + } + }; + var speedtest = new OoklaSpeedtest(settings, httpClient); + var server = new Server { Url = "http://example.com/", Sponsor = "Test", Location = "Test" }; + + // When + var result = await speedtest.GetUploadSpeedAsync(server); + + // Then only the successful requests contribute bytes; the failure is counted. + result.ShouldNotBeNull(); + result.RequestsFailed.ShouldBe(1); + result.RequestsSucceeded.ShouldBe(result.RequestsAttempted - 1); + result.RequestsSucceeded.ShouldBeGreaterThan(0); + result.BytesProcessed.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task GetUploadSpeedAsync_ShouldStreamFailureReason_WhenRequestFails() + { + // SCENARIO: The raw failure reason is streamed live for diagnosis (R4b) + + // Given every upload fails with a recognisable message. + using var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("*").Throw(new HttpRequestException("boom-telekom")); + + var httpClient = mockHttp.ToHttpClient(); + var settings = new OoklaSpeedtestSettings + { + UploadTest = new() + { + UploadIncrements = 1, + UploadSizeIterations = 2, + UploadParallelTasks = 1 + } + }; + var speedtest = new OoklaSpeedtest(settings, httpClient); + var server = new Server { Url = "http://example.com/", Sponsor = "Test", Location = "Test" }; + + var reasons = new List(); + var progress = new SynchronousProgress(p => + { + if (p.FailedRequestReason is not null) reasons.Add(p.FailedRequestReason); + }); + + // When + await speedtest.GetUploadSpeedAsync(server, progress); + + // Then the failure reason is surfaced on the progress channel. + reasons.ShouldNotBeEmpty(); + reasons.ShouldAllBe(r => r.Contains("boom-telekom")); + } + + [Fact] + public async Task GetUploadSpeedAsync_ShouldPropagateCancellation_WhenCallerCancels() + { + // SCENARIO: Cancellation still propagates + + // Given a caller token that is already cancelled. + using var mockHttp = new MockHttpMessageHandler(); + mockHttp.When("*").Respond(HttpStatusCode.OK); + + var httpClient = mockHttp.ToHttpClient(); + var settings = new OoklaSpeedtestSettings + { + UploadTest = new() + { + UploadIncrements = 1, + UploadSizeIterations = 3, + UploadParallelTasks = 1 + } + }; + var speedtest = new OoklaSpeedtest(settings, httpClient); + var server = new Server { Url = "http://example.com/", Sponsor = "Test", Location = "Test" }; + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // When / Then cancellation surfaces to the caller (it is not swallowed as a failed request). + await Should.ThrowAsync(async () => + await speedtest.GetUploadSpeedAsync(server, cts.Token)); + } +} diff --git a/src/NetPace.Core.Tests/OoklaSpeedtestTests.cs b/src/NetPace.Core.Tests/OoklaSpeedtestTests.cs index f5bb9a69..9a7c98d5 100644 --- a/src/NetPace.Core.Tests/OoklaSpeedtestTests.cs +++ b/src/NetPace.Core.Tests/OoklaSpeedtestTests.cs @@ -1451,7 +1451,7 @@ public async Task GetUploadSpeedAsync_ShouldCompleteWithZeroBytes_WhenAllUploads [InlineData(HttpStatusCode.InternalServerError)] [InlineData(HttpStatusCode.ServiceUnavailable)] [InlineData(HttpStatusCode.BadGateway)] - public async Task GetUploadSpeedAsync_ShouldCompleteSuccessfully_EvenWithErrorResponses(HttpStatusCode errorCode) + public async Task GetUploadSpeedAsync_ShouldTreatErrorResponsesAsFailures(HttpStatusCode errorCode) { // Given using var mockHttp = new MockHttpMessageHandler(); @@ -1476,9 +1476,11 @@ public async Task GetUploadSpeedAsync_ShouldCompleteSuccessfully_EvenWithErrorRe // Then result.ShouldNotBeNull(); - // Unlike downloads, uploads don't check response status codes - // Bytes are counted as uploaded even if server returns error status - result.BytesProcessed.ShouldBeGreaterThan(0); + // A non-success status is a rejected upload, not throughput: zero bytes, all requests failed. + result.BytesProcessed.ShouldBe(0); + result.RequestsSucceeded.ShouldBe(0); + result.RequestsFailed.ShouldBe(result.RequestsAttempted); + result.RequestsAttempted.ShouldBeGreaterThan(0); result.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(0); } diff --git a/src/NetPace.Core/Clients/Ookla/OoklaSpeedtest.cs b/src/NetPace.Core/Clients/Ookla/OoklaSpeedtest.cs index 5c41995a..4bded8f3 100644 --- a/src/NetPace.Core/Clients/Ookla/OoklaSpeedtest.cs +++ b/src/NetPace.Core/Clients/Ookla/OoklaSpeedtest.cs @@ -275,7 +275,11 @@ public async Task GetUploadSpeedAsync(IServer server, IProgress { // Use RandomStreamContent to stream generated random bytes in small chunks to avoid LOH allocations. using var content = new RandomStreamContent(length); - await client.PostAsync(server.Url, content, cancellationToken).ConfigureAwait(false); + using var response = await client.PostAsync(server.Url, content, cancellationToken).ConfigureAwait(false); + + // A rejected upload (non-success status) is a failed request, not throughput - + // mirror the download path so error statuses are aggregated into the failure count. + response.EnsureSuccessStatusCode(); return length; }; @@ -304,6 +308,8 @@ private async Task GenericTestSpeedAsync( long totalBytesReturned = 0; var completedCount = 0; + var succeededCount = 0; + var failedCount = 0; var totalCount = testData.Count(); var timer = new Stopwatch(); @@ -317,6 +323,9 @@ private async Task GenericTestSpeedAsync( { var bytesReturned = 0; + // Null unless this request failed; carries the reason for live (Debug) reporting. + string? failureReason = null; + try { // Limit concurrent executions by waiting for a permit from the semaphore. @@ -327,15 +336,22 @@ private async Task GenericTestSpeedAsync( } catch (Exception e) { - // An exception was thrown when performing the work - // - Progress will be reported as if no failure - // - Bytes returned will be treated as zero - - if (e is OperationCanceledException && !wasCancelledLocally) + // Genuine user cancellation (the caller's token) must propagate; it is not a + // per-request failure. + if (e is OperationCanceledException && cancellationToken.IsCancellationRequested) { - // Propagate user cancelled exceptions throw; } + + // A cancellation raised locally because the byte budget was reached is not a + // failure - the request is simply excluded (see the cts gate below). Any other + // exception (transport error, TLS, timeout, or a non-success HTTP status surfaced + // by EnsureSuccessStatusCode) is a per-request failure, aggregated into the counts + // rather than swallowed. Its bytes remain zero. + if (!(e is OperationCanceledException && wasCancelledLocally)) + { + failureReason = e.Message; + } } finally { @@ -346,7 +362,16 @@ private async Task GenericTestSpeedAsync( if (!cts.IsCancellationRequested) { completedCount++; - totalBytesReturned += bytesReturned; + + if (failureReason is not null) + { + failedCount++; + } + else + { + succeededCount++; + totalBytesReturned += bytesReturned; + } if (totalBytesReturned >= maxBytes) { @@ -357,7 +382,8 @@ private async Task GenericTestSpeedAsync( { PercentageComplete = 100, BytesProcessed = totalBytesReturned, - ElapsedMilliseconds = timer.ElapsedMilliseconds + ElapsedMilliseconds = timer.ElapsedMilliseconds, + FailedRequestReason = failureReason }); } else @@ -382,7 +408,8 @@ private async Task GenericTestSpeedAsync( { PercentageComplete = percentageComplete, BytesProcessed = totalBytesReturned, - ElapsedMilliseconds = timer.ElapsedMilliseconds + ElapsedMilliseconds = timer.ElapsedMilliseconds, + FailedRequestReason = failureReason }); } } @@ -406,7 +433,10 @@ private async Task GenericTestSpeedAsync( return new SpeedTestResult { BytesProcessed = totalBytesReturned, - ElapsedMilliseconds = timer.ElapsedMilliseconds + ElapsedMilliseconds = timer.ElapsedMilliseconds, + RequestsAttempted = completedCount, + RequestsSucceeded = succeededCount, + RequestsFailed = failedCount }; } diff --git a/src/NetPace.Core/Clients/Testing/SpeedTestStub.cs b/src/NetPace.Core/Clients/Testing/SpeedTestStub.cs index d7d269a6..082c66ea 100644 --- a/src/NetPace.Core/Clients/Testing/SpeedTestStub.cs +++ b/src/NetPace.Core/Clients/Testing/SpeedTestStub.cs @@ -144,7 +144,7 @@ public Task GetDownloadSpeedAsync(IServer server, IProgress @@ -170,6 +170,6 @@ public Task GetUploadSpeedAsync(IServer server, IProgress /// -/// Implementations of this interface should favor allowing network-related exceptions (e.g., timeouts, connection failures) -/// to propagate to the caller rather than catching and suppressing them. This approach enables consumers of the library -/// to implement their own error handling strategies that align with their application's needs. +/// For download and upload tests, per-request network outcomes are data, not errors: +/// individual request failures (transport errors, timeouts, and non-success HTTP statuses) are +/// aggregated into the returned 's request counts +/// (, , +/// and ) rather than propagating to the caller — even +/// when every request fails. Callers detect an unusable measurement by inspecting the counts +/// (for example, is zero). Exceptions are reserved +/// for caller-requested cancellation and for genuine operational failures; they do not signal +/// network conditions. /// public interface ISpeedTestService { @@ -73,7 +79,11 @@ public interface ISpeedTestService /// /// The server to measure download speed from. /// The token to allow the operation to be cancelled. - /// The result including bytes processed and elapsed time in milliseconds. + /// + /// The result including bytes processed, elapsed time in milliseconds, and the per-request + /// counts (attempted, succeeded, failed). Per-request network failures are reflected in the + /// counts rather than thrown. + /// public Task GetDownloadSpeedAsync(IServer server, CancellationToken cancellationToken = default); /// @@ -82,7 +92,11 @@ public interface ISpeedTestService /// The server to measure download speed from. /// A progress reporter that receives download progress updates. /// The token to allow the operation to be cancelled. - /// The result including bytes processed and elapsed time in milliseconds. + /// + /// The result including bytes processed, elapsed time in milliseconds, and the per-request + /// counts (attempted, succeeded, failed). Per-request network failures are reflected in the + /// counts rather than thrown. + /// public Task GetDownloadSpeedAsync(IServer server, IProgress progress, CancellationToken cancellationToken = default); /// @@ -90,7 +104,11 @@ public interface ISpeedTestService /// /// The server to measure upload speed from. /// The token to allow the operation to be cancelled. - /// The result including bytes processed and elapsed time in milliseconds. + /// + /// The result including bytes processed, elapsed time in milliseconds, and the per-request + /// counts (attempted, succeeded, failed). Per-request network failures are reflected in the + /// counts rather than thrown. + /// public Task GetUploadSpeedAsync(IServer server, CancellationToken cancellationToken = default); /// @@ -99,6 +117,10 @@ public interface ISpeedTestService /// The server to measure upload speed from. /// A progress reporter that receives upload progress updates. /// The token to allow the operation to be cancelled. - /// The result including bytes processed and elapsed time in milliseconds. + /// + /// The result including bytes processed, elapsed time in milliseconds, and the per-request + /// counts (attempted, succeeded, failed). Per-request network failures are reflected in the + /// counts rather than thrown. + /// public Task GetUploadSpeedAsync(IServer server, IProgress progress, CancellationToken cancellationToken = default); } diff --git a/src/NetPace.Core/SpeedTestProgress.cs b/src/NetPace.Core/SpeedTestProgress.cs index 501b767b..ef819e18 100644 --- a/src/NetPace.Core/SpeedTestProgress.cs +++ b/src/NetPace.Core/SpeedTestProgress.cs @@ -19,4 +19,15 @@ public sealed record SpeedTestProgress /// Gets the total elapsed time, in milliseconds. /// public long ElapsedMilliseconds { get; init; } + + /// + /// Gets the reason a single request failed, when this update announces a per-request failure; + /// otherwise . + /// + /// + /// Per-request failures are streamed live on the same progress channel that drives the progress + /// bar so consumers can surface them (for example, at a diagnostic verbosity level) as they + /// happen. The reason is not retained on ; only the request counts are. + /// + public string? FailedRequestReason { get; init; } } diff --git a/src/NetPace.Core/SpeedTestResult.cs b/src/NetPace.Core/SpeedTestResult.cs index 874a36e9..ffb61861 100644 --- a/src/NetPace.Core/SpeedTestResult.cs +++ b/src/NetPace.Core/SpeedTestResult.cs @@ -14,4 +14,26 @@ public sealed record SpeedTestResult /// Gets the total elapsed time, in milliseconds. /// public long ElapsedMilliseconds { get; init; } + + /// + /// Gets the number of individual requests attempted during the test (successes plus failures). + /// + /// + /// Requests skipped because the configured byte budget was reached are not counted. A consumer + /// determines whether a dimension produced any valid measurement via + /// ; a value of zero with greater + /// than zero means every request failed and the reported speed is not a valid measurement. + /// + public int RequestsAttempted { get; init; } + + /// + /// Gets the number of requests that completed successfully and contributed to . + /// + public int RequestsSucceeded { get; init; } + + /// + /// Gets the number of requests that failed (a transport error, timeout, or non-success HTTP status). + /// Failed requests contribute no bytes to . + /// + public int RequestsFailed { get; init; } } From 3d8eff68df92c987e83b05f4f59c51ea8488b876 Mon Sep 17 00:00:00 2001 From: Frank Ray <52075808+FrankRay78@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:44:53 +0200 Subject: [PATCH 2/2] Address PR #222 review: CSV header edge case, doc scope, CodeQL - CSV header no longer dropped when a leading --loop/--count iteration finds no server: key the header on the first actual data row, not the iteration index. - Scope the USER_GUIDE Debug live-streaming claim to normal/interactive output (machine formats never write failure reasons to stderr, including at Debug). - Rename the shadowing 'server' field in ScriptedSpeedTester (clears CodeQL). - Add tests: CSV header survives a leading no-server iteration; JSON+Debug writes nothing to stderr; --quiet still emits the all-failed stderr notice. Co-Authored-By: Claude Opus 4.8 (1M context) --- USER_GUIDE.md | 5 +- .../NetPaceConsoleTests.Failures.cs | 74 +++++++++++++++++++ .../ScriptedSpeedTester.cs | 4 +- .../Commands/SpeedTestCommand.cs | 20 ++--- 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 56624c49..6142410a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -130,8 +130,9 @@ Every output format carries the counts: ``` Machine formats (CSV, JSON) self-describe through the counts and never duplicate the notice on -standard error. At `--verbosity Debug`, the raw reason for each failed request is streamed live to -standard error. +standard error — that includes `--verbosity Debug`. In the normal (interactive) output mode, +`--verbosity Debug` additionally streams the raw reason for each failed request live to standard +error. ### Exit codes diff --git a/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs b/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs index fb4bc4d5..ae30329a 100644 --- a/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs +++ b/src/NetPace.Console.Tests/NetPaceConsoleTests.Failures.cs @@ -1,3 +1,5 @@ +using NetPace.Core.Clients.Ookla; + namespace NetPace.Console.Tests; /// @@ -191,6 +193,78 @@ public async Task FailOn_Total_Exits_Zero_On_Partial_Failure() Assert.Equal(0, result.ExitCode); } + [Fact] + public async Task Json_Debug_Streams_Nothing_To_Stderr() + { + // Machine formats self-describe via the counts and never duplicate on stderr - that + // holds at Debug too (the live reason stream is a normal/interactive concern only). + + // Given every upload request fails and a reason would be streamed on the progress channel. + var service = new ScriptedSpeedTester + { + UploadFactory = _ => ScriptedSpeedTester.AllFailed(32), + StreamedFailureReason = "Connection reset by peer" + }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--json", "--verbosity", "Debug" ]); + + // Then the JSON carries the counts and stderr stays empty. + Assert.Equal(0, result.ExitCode); + Assert.Contains("\"UploadFailed\":32", result.Output); + Assert.Empty(result.Error); + } + + [Fact] + public async Task Quiet_All_Failed_Still_Emits_Stderr_Notice() + { + // --quiet suppresses standard output, but the all-failed notice is an operational/human + // signal and still reaches standard error. + + var service = new ScriptedSpeedTester { UploadFactory = _ => ScriptedSpeedTester.AllFailed(32) }; + var host = HostWith(service); + + // When + var result = await host.RunAsync([ "--quiet" ]); + + // Then + Assert.Equal(0, result.ExitCode); + Assert.Empty(result.Output); + Assert.Contains("Upload failed: all 32 requests to", result.Error); + } + + [Fact] + public async Task Csv_Header_Survives_A_Leading_No_Server_Iteration() + { + // The CSV header must print on the first actual data row, even when earlier iterations + // found no server (a routine outcome now that discovery failures don't throw). + + // Given the first discovery finds no server, the second succeeds. + var server = new Server { Location = "Frankfurt", Sponsor = "Deutsche Telekom", Url = "http://ffm.example/upload.php" }; + var discoveryCall = 0; + var mock = new SpeedTestMock + { + GetServersAsyncFunc = _ => Task.FromResult(discoveryCall++ == 0 ? Array.Empty() : new IServer[] { server }), + GetFastestServerByLatencyAsyncFunc = (servers, _, _) => Task.FromResult(new LatencyTestResult { Server = servers[0], LatencyMilliseconds = 24 }), + GetDownloadSpeedAsyncFunc = (_, _, _) => Task.FromResult(ScriptedSpeedTester.Clean(150)), + GetUploadSpeedAsyncFunc = (_, _, _) => Task.FromResult(ScriptedSpeedTester.Clean(32)), + }; + var host = HostWith(mock); + + // When + var result = await host.RunAsync([ "--csv", "--count", "2" ]); + + // Then the output begins with the CSV header (not a bare data row), and exactly one data + // row is written (the single server-found iteration). + Assert.Equal(0, result.ExitCode); + Assert.StartsWith("Timestamp,", result.Output.TrimStart()); + Assert.Contains("DownloadSucceeded", result.Output); + var dataRows = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray(); + Assert.Single(dataRows); + Assert.Contains("No speed test servers were found.", result.Error); + } + [Fact] public async Task Unreachable_User_Specified_Server_Exits_Zero_Regardless_Of_Latency() { diff --git a/src/NetPace.Console.Tests/ScriptedSpeedTester.cs b/src/NetPace.Console.Tests/ScriptedSpeedTester.cs index 8cab2e9a..90e829d5 100644 --- a/src/NetPace.Console.Tests/ScriptedSpeedTester.cs +++ b/src/NetPace.Console.Tests/ScriptedSpeedTester.cs @@ -13,7 +13,7 @@ public sealed class ScriptedSpeedTester : ISpeedTestService { public const string Url = "http://ffm.wsqm.telekom-dienste.de:8080/speedtest/upload.php"; - private readonly IServer server = new Server { Location = "Frankfurt", Sponsor = "Deutsche Telekom", Url = Url }; + private readonly IServer defaultServer = new Server { Location = "Frankfurt", Sponsor = "Deutsche Telekom", Url = Url }; private int downloadCall; private int uploadCall; @@ -40,7 +40,7 @@ public static SpeedTestResult Partial(int attempts, int failed) => new() { BytesProcessed = 1000, ElapsedMilliseconds = 1000, RequestsAttempted = attempts, RequestsSucceeded = attempts - failed, RequestsFailed = failed }; public Task GetServersAsync(CancellationToken cancellationToken = default) => - Task.FromResult(new[] { server }); + Task.FromResult(new[] { defaultServer }); public Task GetServerLatencyAsync(IServer server, CancellationToken cancellationToken = default) => Task.FromResult(new LatencyTestResult { Server = server, LatencyMilliseconds = 24 }); diff --git a/src/NetPace.Console/Commands/SpeedTestCommand.cs b/src/NetPace.Console/Commands/SpeedTestCommand.cs index 0a972ae6..2b4fcb84 100644 --- a/src/NetPace.Console/Commands/SpeedTestCommand.cs +++ b/src/NetPace.Console/Commands/SpeedTestCommand.cs @@ -48,13 +48,16 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat if (settings.Loop) { - // Run continuously. - var firstLoop = true; + // Run continuously. `firstWrite` tracks whether a data row has actually been written + // (not merely the iteration index), so a header-emitting writer (CSV) still prints + // its header on the first successful row when earlier iterations found no server. + var firstWrite = true; do { try { - var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: firstLoop, console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: firstWrite, console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + if (outcome.ServersFound) firstWrite = false; if (ProcessOutcome(outcome, settings)) return 1; } catch (OperationCanceledException) @@ -62,10 +65,6 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat // User requested cancellation. return 0; } - finally - { - firstLoop = false; - } try { @@ -82,12 +81,15 @@ public async Task ExecuteAsync(SpeedTestCommandSettings settings, Cancellat } else if (settings.Count > 1) { - // Run multiple times. + // Run multiple times. `firstWrite` tracks the first actual data row (see the loop + // branch) so the CSV header survives leading iterations that found no server. + var firstWrite = true; for (int i = 0; i < settings.Count; i++) { try { - var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: (i == 0), console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + var outcome = await writer.PerformSpeedTestAsync(initialSpeedTest: firstWrite, console, errorConsole, clock, clientInfoProvider, speedTestClient, settings, cancellationToken); + if (outcome.ServersFound) firstWrite = false; if (ProcessOutcome(outcome, settings)) return 1; } catch (OperationCanceledException)