Files
jellyfin-multilang/tests/Jellyfin.Plugin.Multilang.Tests/RefreshCoordinatorTests.cs
T
ajp_anton e36775f1a3 Release 0.2.5: harden proxy and streamline metadata fetching
Remove obsolete rule formats, batch SQLite work, preserve artwork source URLs, and coordinate refresh workers with maintenance. Add regression coverage and document backup format changes.
2026-09-09 01:18:43 +00:00

152 lines
7.0 KiB
C#

using Jellyfin.Plugin.Multilang.Services;
using Jellyfin.Plugin.Multilang.Services.Refresh;
namespace Jellyfin.Plugin.Multilang.Tests;
public sealed class RefreshCoordinatorTests
{
[Fact]
public async Task IndependentItemsOverlapWithinWorkerLimitAndDuplicateWorkIsShared()
{
using var coordinator = new RefreshCoordinator(concurrency: 2);
var twoStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var count = 0;
Task<bool> Queue(string id) => coordinator.EnqueueAsync(id, RefreshSourceTier.Manual, RefreshWorkClass.Movies,
RefreshJobType.Full, async (_, _) =>
{
if (Interlocked.Increment(ref count) == 2) twoStarted.SetResult();
await release.Task;
return true;
}, CancellationToken.None);
var first = Queue("first");
var second = Queue("second");
await twoStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Same(first, Queue("first"));
var third = Queue("third");
Assert.Equal(2, count);
Assert.Equal(2, coordinator.GetDiagnostics().ActiveItems.Length);
release.SetResult();
await Task.WhenAll(first, second, third).WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(3, count);
}
[Fact]
public async Task ActiveMissingRefreshRunsFullUpgradeBeforeCompleting()
{
using var coordinator = new RefreshCoordinator(concurrency: 1);
var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var fullRan = false;
var first = coordinator.EnqueueAsync("item", RefreshSourceTier.Background, RefreshWorkClass.Movies, RefreshJobType.Missing, async (_, _) =>
{
started.SetResult();
await release.Task;
return false;
}, CancellationToken.None);
await started.Task.WaitAsync(TimeSpan.FromSeconds(5));
var full = coordinator.EnqueueAsync("item", RefreshSourceTier.Manual, RefreshWorkClass.Movies, RefreshJobType.Full, (_, _) =>
{
fullRan = true;
return Task.FromResult(true);
}, CancellationToken.None);
release.SetResult();
Assert.True(await full.WaitAsync(TimeSpan.FromSeconds(5)));
Assert.True(fullRan);
Assert.True(await first);
}
[Fact]
public async Task CanceledWaiterDoesNotCancelAnotherWaiter()
{
using var coordinator = new RefreshCoordinator(concurrency: 1);
using var cancel = new CancellationTokenSource();
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var canceled = coordinator.EnqueueAsync("item", RefreshSourceTier.Background, RefreshWorkClass.Movies, RefreshJobType.Full,
async (_, _) => { await release.Task; return true; }, cancel.Token);
var survivor = coordinator.EnqueueAsync("item", RefreshSourceTier.Background, RefreshWorkClass.Movies, RefreshJobType.Full,
(_, _) => Task.FromResult(false), CancellationToken.None);
cancel.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => canceled);
release.SetResult();
Assert.True(await survivor.WaitAsync(TimeSpan.FromSeconds(5)));
}
[Fact]
public async Task QueuedItemsUseSourceTierThenWorkClassPriority()
{
using var coordinator = new RefreshCoordinator(concurrency: 1);
var activeStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseActive = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var order = new List<string>();
var active = coordinator.EnqueueAsync("active", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, async (_, _) =>
{
activeStarted.SetResult();
await releaseActive.Task;
return true;
}, CancellationToken.None);
await activeStarted.Task;
var series = coordinator.EnqueueAsync("series", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, (_, _) =>
{
order.Add("series");
return Task.FromResult(true);
}, CancellationToken.None);
var movie = coordinator.EnqueueAsync("movie", RefreshSourceTier.Background, RefreshWorkClass.Movies, RefreshJobType.Full, (_, _) =>
{
order.Add("movie");
return Task.FromResult(true);
}, CancellationToken.None);
var onTheFlyCollection = coordinator.EnqueueAsync("collection", RefreshSourceTier.OnTheFly, RefreshWorkClass.Collections, RefreshJobType.Full, (_, _) =>
{
order.Add("collection");
return Task.FromResult(true);
}, CancellationToken.None);
releaseActive.SetResult();
await Task.WhenAll(active, series, movie, onTheFlyCollection);
Assert.Equal(["collection", "movie", "series"], order);
}
[Fact]
public async Task DuplicateQueuedItemUsesHighestJobAndTierWithoutInvalidatingUpstreamData()
{
var cache = new ItemsProxyCache();
Assert.True(cache.Store("target", "user", "/Items", 1, "body", "application/json", 1, 1024, TimeSpan.FromMinutes(1)));
using var coordinator = new RefreshCoordinator(concurrency: 1);
var activeStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseActive = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var runs = new List<RefreshJobType>();
var active = coordinator.EnqueueAsync("active", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Full, async (_, _) =>
{
activeStarted.SetResult();
await releaseActive.Task;
return true;
}, CancellationToken.None);
await activeStarted.Task;
var missing = coordinator.EnqueueAsync("target", RefreshSourceTier.Background, RefreshWorkClass.Series, RefreshJobType.Missing, (job, _) =>
{
runs.Add(job);
return Task.FromResult(true);
}, CancellationToken.None);
var full = coordinator.EnqueueAsync("target", RefreshSourceTier.Manual, RefreshWorkClass.Series, RefreshJobType.Full, (job, _) =>
{
runs.Add(job);
return Task.FromResult(true);
}, CancellationToken.None);
Assert.Same(missing, full);
releaseActive.SetResult();
await Task.WhenAll(active, full);
Assert.Equal([RefreshJobType.Full], runs);
Assert.True(cache.TryGet("target", TimeSpan.FromMinutes(1), out _));
var recent = coordinator.GetDiagnostics().Recent.Single(item => item.ItemId == "target");
Assert.Equal("Manual", recent.SourceTier);
Assert.Equal("Full", recent.JobType);
}
}