Skip to content

Commit 4eb2099

Browse files
authored
Improve latency testing (#97)
1 parent a020f0f commit 4eb2099

File tree

12 files changed

+415
-15
lines changed

12 files changed

+415
-15
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Built with .NET 8.0 — runs on Windows, Linux, and macOS.
2121
* Server discovery, latency measurement, download and upload speed testing.
2222
* Command-line application and C# Microsoft .Net [NuGet](https://www.nuget.org/packages/NetPace.Core/) library for developers.
2323
* User configurable output (eg. SI or IEC units, BitsPerSecond or BytesPerSecond, CSV and Json formats).
24-
* Highly configurable traffic profiles (see [DownloadTestSettings](https://github.com/FrankRay78/NetPace/blob/main/src/NetPace.Core/Clients/Ookla/Settings/DownloadTestSettings.cs) and [UploadTestSettings](https://github.com/FrankRay78/NetPace/blob/main/src/NetPace.Core/Clients/Ookla/Settings/UploadTestSettings.cs)).
24+
* Highly configurable traffic profiles (see [LatencyTestSettings](https://github.com/FrankRay78/NetPace/blob/main/src/NetPace.Core/Clients/Ookla/Settings/LatencyTestSettings.cs), [DownloadTestSettings](https://github.com/FrankRay78/NetPace/blob/main/src/NetPace.Core/Clients/Ookla/Settings/DownloadTestSettings.cs) and [UploadTestSettings](https://github.com/FrankRay78/NetPace/blob/main/src/NetPace.Core/Clients/Ookla/Settings/UploadTestSettings.cs)).
2525
* Highly reliable, utilises [Ookla's](https://www.speedtest.net/) Speedtest servers.
2626

2727
<br />

src/NetPace.Console.Tests/NetPaceConsoleTests.Servers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public async Task Should_Handle_No_Servers_Available()
7979
var mock = new SpeedTestMock
8080
{
8181
GetServersAsyncFunc = (cancellationToken) => Task.FromResult(Array.Empty<IServer>()),
82-
GetFastestServerByLatencyAsyncFunc = (servers, cancellationToken) => throw new Exception("No servers available"),
82+
GetFastestServerByLatencyAsyncFunc = (_, _, _) => throw new Exception("No servers available"),
8383
};
8484

8585
var registrar = new TypeRegistrar();

src/NetPace.Core.Tests/OoklaSpeedtestTests.cs

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System;
12
using System.Net;
23
using System.Text.RegularExpressions;
34
using NetPace.Core.Clients.Ookla;
5+
using NetPace.Core.Clients.Testing;
46
using RichardSzalay.MockHttp;
57
using Shouldly;
68

@@ -364,7 +366,8 @@ public async Task GetServerLatencyAsync_WithProgress_ShouldCancel_WhenTokenIsCan
364366
// Then
365367
exception.ShouldNotBeNull();
366368
exception.ShouldBeAssignableTo<OperationCanceledException>();
367-
cts.IsCancellationRequested.ShouldBeTrue();
369+
cts.IsCancellationRequested.ShouldBeTrue();
370+
progressReports.ShouldBeEmpty();
368371
}
369372

370373
[Fact]
@@ -399,6 +402,67 @@ public async Task GetServerLatencyAsync_WithProgress_ShouldPropagateException_Wh
399402
exception.Message.ShouldBe("Progress callback failed");
400403
}
401404

405+
[Fact]
406+
public async Task GetServerLatencyAsync_WithInterval_ShouldRequestDelayBetweenIterations()
407+
{
408+
// Given
409+
using var mockHttp = new MockHttpMessageHandler();
410+
mockHttp.When("http://testserver.com/latency.txt")
411+
.Respond("text/plain", "test=test");
412+
413+
var httpClient = mockHttp.ToHttpClient();
414+
var delayStub = new DelayProviderStub();
415+
var settings = new OoklaSpeedtestSettings
416+
{
417+
LatencyTest = new()
418+
{
419+
LatencyTestIterations = 3,
420+
LatencyTestIntervalMilliseconds = 50 // 50ms between iterations
421+
}
422+
};
423+
424+
var speedtest = new OoklaSpeedtest(settings, httpClient, delayStub);
425+
var server = new Server { Url = "http://testserver.com/", Sponsor = "Sponsor", Location = "Location" };
426+
427+
// When
428+
await speedtest.GetServerLatencyAsync(server);
429+
430+
// Then
431+
// With 3 iterations, we expect 2 delays (between iterations, not before first)
432+
delayStub.DelayCallCount.ShouldBe(2);
433+
delayStub.RequestedDelays.ShouldAllBe(d => d == 50);
434+
}
435+
436+
[Fact]
437+
public async Task GetServerLatencyAsync_WithZeroInterval_ShouldNotRequestDelay()
438+
{
439+
// Given
440+
using var mockHttp = new MockHttpMessageHandler();
441+
mockHttp.When("http://testserver.com/latency.txt")
442+
.Respond("text/plain", "test=test");
443+
444+
var httpClient = mockHttp.ToHttpClient();
445+
var delayStub = new DelayProviderStub();
446+
var settings = new OoklaSpeedtestSettings
447+
{
448+
LatencyTest = new()
449+
{
450+
LatencyTestIterations = 3,
451+
LatencyTestIntervalMilliseconds = 0 // No delay
452+
}
453+
};
454+
455+
var speedtest = new OoklaSpeedtest(settings, httpClient, delayStub);
456+
var server = new Server { Url = "http://testserver.com/", Sponsor = "Sponsor", Location = "Location" };
457+
458+
// When
459+
await speedtest.GetServerLatencyAsync(server);
460+
461+
// Then
462+
// With 0ms interval, no delays should be requested
463+
delayStub.DelayCallCount.ShouldBe(0);
464+
}
465+
402466
// --- GetFastestServerByLatencyAsync ---
403467

404468
[Fact]
@@ -515,6 +579,187 @@ public async Task GetFastestServerByLatencyAsync_ShouldCancel_WhenTokenIsCancell
515579
cts.IsCancellationRequested.ShouldBeTrue();
516580
}
517581

582+
// --- GetFastestServerByLatencyAsync with Progress ---
583+
584+
[Fact]
585+
public async Task GetFastestServerByLatencyAsync_WithProgress_ShouldReportProgress_ForThreeServers()
586+
{
587+
// Given
588+
using var mockHttp = new MockHttpMessageHandler();
589+
mockHttp.When("*/latency.txt")
590+
.Respond("text/plain", "test=test");
591+
592+
var httpClient = mockHttp.ToHttpClient();
593+
var settings = new OoklaSpeedtestSettings
594+
{
595+
LatencyTest = new()
596+
{
597+
LatencyTestIterations = 1,
598+
LatencyTestIntervalMilliseconds = 0,
599+
DefaultHttpTimeoutMilliseconds = 500
600+
}
601+
};
602+
603+
var speedtest = new OoklaSpeedtest(settings, httpClient);
604+
var servers = new[] {
605+
new Server { Url = "http://server1.com/", Sponsor = "Sponsor1", Location = "Location1" },
606+
new Server { Url = "http://server2.com/", Sponsor = "Sponsor2", Location = "Location2" },
607+
new Server { Url = "http://server3.com/", Sponsor = "Sponsor3", Location = "Location3" }
608+
};
609+
var progressReports = new List<int>();
610+
611+
// When
612+
var result = await speedtest.GetFastestServerByLatencyAsync(servers, progress => progressReports.Add(progress.PercentageComplete));
613+
614+
// Then
615+
result.ShouldNotBeNull();
616+
result.Server.ShouldBeOneOf(servers);
617+
progressReports.ShouldBe(new[] { 33, 66, 100 });
618+
}
619+
620+
[Fact]
621+
public async Task GetFastestServerByLatencyAsync_WithProgress_ShouldReport100Percent_WithSingleServer()
622+
{
623+
// Given
624+
using var mockHttp = new MockHttpMessageHandler();
625+
mockHttp.When("*/latency.txt")
626+
.Respond("text/plain", "test=test");
627+
628+
var httpClient = mockHttp.ToHttpClient();
629+
var settings = new OoklaSpeedtestSettings
630+
{
631+
LatencyTest = new()
632+
{
633+
LatencyTestIterations = 1,
634+
LatencyTestIntervalMilliseconds = 0,
635+
DefaultHttpTimeoutMilliseconds = 500
636+
}
637+
};
638+
639+
var speedtest = new OoklaSpeedtest(settings, httpClient);
640+
var server = new Server { Url = "http://server.com/", Sponsor = "Sponsor", Location = "Location" };
641+
var servers = new[] { server };
642+
var progressReports = new List<int>();
643+
644+
// When
645+
var result = await speedtest.GetFastestServerByLatencyAsync(servers, progress => progressReports.Add(progress.PercentageComplete));
646+
647+
// Then
648+
result.ShouldNotBeNull();
649+
result.Server.ShouldBe(server);
650+
progressReports.ShouldBe(new[] { 100 });
651+
}
652+
653+
[Fact]
654+
public async Task GetFastestServerByLatencyAsync_WithProgress_ShouldReportProgress_EvenWhenServersFail()
655+
{
656+
// Given
657+
using var mockHttp = new MockHttpMessageHandler();
658+
659+
// First two servers fail, third succeeds
660+
mockHttp.When("http://fail1.com/latency.txt")
661+
.Throw(new HttpRequestException("Server unreachable"));
662+
mockHttp.When("http://fail2.com/latency.txt")
663+
.Throw(new HttpRequestException("Server unreachable"));
664+
mockHttp.When("http://success.com/latency.txt")
665+
.Respond("text/plain", "test=test");
666+
667+
var httpClient = mockHttp.ToHttpClient();
668+
var settings = new OoklaSpeedtestSettings
669+
{
670+
LatencyTest = new()
671+
{
672+
LatencyTestIterations = 1,
673+
LatencyTestIntervalMilliseconds = 0,
674+
DefaultHttpTimeoutMilliseconds = 100
675+
}
676+
};
677+
678+
var speedtest = new OoklaSpeedtest(settings, httpClient);
679+
var failServer1 = new Server { Url = "http://fail1.com/", Sponsor = "Fail1", Location = "Location1" };
680+
var failServer2 = new Server { Url = "http://fail2.com/", Sponsor = "Fail2", Location = "Location2" };
681+
var successServer = new Server { Url = "http://success.com/", Sponsor = "Success", Location = "Location3" };
682+
var servers = new[] { failServer1, failServer2, successServer };
683+
var progressReports = new List<int>();
684+
685+
// When
686+
var result = await speedtest.GetFastestServerByLatencyAsync(servers, progress => progressReports.Add(progress.PercentageComplete));
687+
688+
// Then
689+
result.ShouldNotBeNull();
690+
result.Server.ShouldBe(successServer);
691+
progressReports.ShouldBe(new[] { 33, 66, 100 });
692+
}
693+
694+
[Fact]
695+
public async Task GetFastestServerByLatencyAsync_WithProgress_ShouldPropagateException_WhenProgressCallbackThrows()
696+
{
697+
// Given
698+
using var mockHttp = new MockHttpMessageHandler();
699+
mockHttp.When("*/latency.txt")
700+
.Respond("text/plain", "test=test");
701+
702+
var httpClient = mockHttp.ToHttpClient();
703+
var settings = new OoklaSpeedtestSettings
704+
{
705+
LatencyTest = new()
706+
{
707+
LatencyTestIterations = 1,
708+
LatencyTestIntervalMilliseconds = 0,
709+
DefaultHttpTimeoutMilliseconds = 500
710+
}
711+
};
712+
713+
var speedtest = new OoklaSpeedtest(settings, httpClient);
714+
var server = new Server { Url = "http://server.com/", Sponsor = "Sponsor", Location = "Location" };
715+
var servers = new[] { server };
716+
717+
// When
718+
var exception = await Record.ExceptionAsync(() => speedtest.GetFastestServerByLatencyAsync(servers, progress =>
719+
{
720+
throw new InvalidOperationException("Progress callback failed");
721+
}));
722+
723+
// Then
724+
exception.ShouldNotBeNull();
725+
exception.ShouldBeOfType<InvalidOperationException>();
726+
exception.Message.ShouldBe("Progress callback failed");
727+
}
728+
729+
[Fact]
730+
public async Task GetFastestServerByLatencyAsync_WithProgress_ShouldCancel_WhenTokenIsCancelled()
731+
{
732+
// Given
733+
using var mockHttp = new MockHttpMessageHandler();
734+
mockHttp.When("*").Respond(async _ =>
735+
{
736+
// Simulate slow response.
737+
await Task.Delay(1000);
738+
return new HttpResponseMessage(HttpStatusCode.OK);
739+
});
740+
741+
var httpClient = mockHttp.ToHttpClient();
742+
var speedtest = new OoklaSpeedtest(httpClientOverride: httpClient);
743+
var servers = new[] {
744+
new Server { Url = "http://server1.com/", Sponsor = "Sponsor1", Location = "Location1" },
745+
new Server { Url = "http://server2.com/", Sponsor = "Sponsor2", Location = "Location2" },
746+
new Server { Url = "http://server3.com/", Sponsor = "Sponsor3", Location = "Location3" }
747+
};
748+
var progressReports = new List<int>();
749+
750+
using var cts = new CancellationTokenSource();
751+
cts.CancelAfter(200);
752+
753+
// When
754+
var exception = await Record.ExceptionAsync(() => speedtest.GetFastestServerByLatencyAsync(servers, progress => progressReports.Add(progress.PercentageComplete), cts.Token));
755+
756+
// Then
757+
exception.ShouldNotBeNull();
758+
exception.ShouldBeAssignableTo<OperationCanceledException>();
759+
cts.IsCancellationRequested.ShouldBeTrue();
760+
progressReports.ShouldBeEmpty();
761+
}
762+
518763
// --- GetDownloadSpeedAsync ---
519764

520765
[Fact]

0 commit comments

Comments
 (0)