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); } }