Files
av-parser/tests/AvParser.Core.Tests/Proxies/ProxyPoolWarmUpTests.cs
T
Leonid Pershin eb5061ee23 Refactor media source handling and update collection options
- Updated `IMediaSourceCatalog` to support user-added media sources, allowing dynamic editing and management of sources.
- Removed the `UrlListSource` class as its functionality is now integrated into the new catalog structure.
- Enhanced `CollectOptions` to default `RequireProxy` to true, ensuring stricter handling of proxy requirements.
- Improved error handling in `ParseError` to include a `Subject` field for better context on failures.
- Adjusted dependency injection to reflect changes in media source management, removing old source registrations.
- Introduced background proxy checks to ensure a more robust proxy pool management during collection processes.

These changes streamline the media collection process and improve the overall user experience by providing clearer error reporting and more flexible source management.
2026-08-15 14:20:06 +03:00

159 lines
6.3 KiB
C#

using AvParser.Core.Proxies;
namespace AvParser.Core.Tests.Proxies;
public class ProxyPoolWarmUpTests
{
private static readonly ProxyOptions Options = new() { ProbeConcurrency = 1 };
private static ProxyPool Build(out FakeProxyProbe probe, out FakeTimeProvider clock, params string[] hosts)
{
var source = new FakeProxySource(ProxySourceKind.Feed);
source.Endpoints.AddRange(hosts.Select(host => ProxyFactory.Endpoint(host)));
probe = new FakeProxyProbe();
clock = new FakeTimeProvider();
return new ProxyPool([source], probe, Options, clock);
}
[Fact]
public async Task What_worked_last_time_is_tried_first()
{
// The whole point of remembering: a second launch should confirm the known-good ones
// rather than walking a list of a few thousand addresses from the top.
var pool = Build(out var probe, out var clock, "cold-a", "known-good", "cold-b");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
pool.Entries.Single(entry => entry.Endpoint.Host == "known-good")
.RestoreState(wasAlive: true, TimeSpan.FromMilliseconds(30), 12, 1, clock.GetUtcNow());
pool.WarmUpOrder()[0].Endpoint.Host.ShouldBe("known-good");
// Asking for two: one is already known live, so the warm-up has a reason to probe at all.
probe.DefaultAlive = true;
await pool.WarmUpAsync(2, cancellationToken: TestContext.Current.CancellationToken);
probe.Probed[0].ShouldContain("known-good");
}
[Fact]
public async Task The_faster_of_two_remembered_proxies_goes_first()
{
var pool = Build(out _, out var clock, "slow", "fast");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
pool.Entries.Single(entry => entry.Endpoint.Host == "slow")
.RestoreState(wasAlive: true, TimeSpan.FromMilliseconds(900), 3, 0, clock.GetUtcNow());
pool.Entries.Single(entry => entry.Endpoint.Host == "fast")
.RestoreState(wasAlive: true, TimeSpan.FromMilliseconds(40), 3, 0, clock.GetUtcNow());
pool.WarmUpOrder().Select(entry => entry.Endpoint.Host).ShouldBe(["fast", "slow"]);
}
[Fact]
public async Task Warming_up_stops_once_the_target_is_met()
{
// Probing all 200 when 2 were asked for would turn every launch into a full sweep.
var pool = Build(out var probe, out _, [.. Enumerable.Range(0, 200).Select(i => $"h{i}")]);
await pool.RefreshAsync(TestContext.Current.CancellationToken);
probe.DefaultAlive = true;
var live = await pool.WarmUpAsync(2, cancellationToken: TestContext.Current.CancellationToken);
live.ShouldBeGreaterThanOrEqualTo(2);
probe.ProbeCount.ShouldBeLessThan(200);
}
[Fact]
public async Task Warming_up_probes_nothing_when_enough_are_already_live()
{
var pool = Build(out var probe, out var clock, "a", "b");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
foreach (var entry in pool.Entries)
{
entry.RecordSuccess(clock.GetUtcNow(), TimeSpan.FromMilliseconds(10));
}
(await pool.WarmUpAsync(2, cancellationToken: TestContext.Current.CancellationToken)).ShouldBe(2);
probe.ProbeCount.ShouldBe(0);
}
[Fact]
public async Task A_quarantined_proxy_is_not_warmed_up()
{
var pool = Build(out _, out var clock, "a", "b");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
pool.Entries.Single(entry => entry.Endpoint.Host == "a")
.RecordFailure(clock.GetUtcNow(), TimeSpan.FromMinutes(5), TimeSpan.FromHours(1), 1);
pool.WarmUpOrder().Select(entry => entry.Endpoint.Host).ShouldBe(["b"]);
}
[Fact]
public async Task Nothing_live_reads_as_nothing_live()
{
var pool = Build(out var probe, out _, "a", "b");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
probe.DefaultAlive = false;
(await pool.WarmUpAsync(1, cancellationToken: TestContext.Current.CancellationToken)).ShouldBe(0);
pool.LiveCount.ShouldBe(0);
}
[Fact]
public async Task The_top_up_checks_what_the_warm_up_skipped()
{
// The warm-up leaves almost everything unknown by design; without this pass the pool looks
// — and behaves — as if it held two proxies rather than the fifty that answer.
var pool = Build(out var probe, out _, [.. Enumerable.Range(0, 50).Select(index => $"h{index}")]);
await pool.RefreshAsync(TestContext.Current.CancellationToken);
probe.DefaultAlive = true;
await pool.WarmUpAsync(2, cancellationToken: TestContext.Current.CancellationToken);
var afterWarmUp = probe.ProbeCount;
afterWarmUp.ShouldBeLessThan(50);
var found = await pool.TopUpAsync(4, cancellationToken: TestContext.Current.CancellationToken);
found.ShouldBe(50 - afterWarmUp);
pool.LiveCount.ShouldBe(50);
pool.Entries.ShouldAllBe(entry => entry.Health == ProxyHealthState.Alive);
}
[Fact]
public async Task The_top_up_does_not_re_probe_what_is_already_known()
{
var pool = Build(out var probe, out _, "a", "b");
await pool.RefreshAsync(TestContext.Current.CancellationToken);
probe.DefaultAlive = true;
await pool.TopUpAsync(2, cancellationToken: TestContext.Current.CancellationToken);
var first = probe.ProbeCount;
// Everything has a verdict now, so a second pass has nothing to do — re-probing would just
// be a sweep, and a sweep is something the user asks for.
(await pool.TopUpAsync(2, cancellationToken: TestContext.Current.CancellationToken)).ShouldBe(0);
probe.ProbeCount.ShouldBe(first);
}
[Fact]
public async Task A_stopped_top_up_keeps_what_it_learned()
{
var pool = Build(out var probe, out _, [.. Enumerable.Range(0, 20).Select(index => $"h{index}")]);
await pool.RefreshAsync(TestContext.Current.CancellationToken);
probe.DefaultAlive = true;
using var stop = new CancellationTokenSource();
await stop.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(() => pool.TopUpAsync(2, null, stop.Token));
// Cancelled before anything was probed, so nothing is claimed to be live either.
pool.LiveCount.ShouldBe(0);
}
}