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.
100 lines
3.9 KiB
C#
100 lines
3.9 KiB
C#
using System.Diagnostics;
|
|
using System.Net;
|
|
using Jellyfin.Plugin.Multilang.Data;
|
|
using Jellyfin.Plugin.Multilang.Services.Providers;
|
|
|
|
namespace Jellyfin.Plugin.Multilang.Tests;
|
|
|
|
public sealed class MaintenanceAndProviderTests
|
|
{
|
|
[Fact]
|
|
public async Task ConcurrentRefreshesCanOverlapButMaintenanceWaitsForAllOfThem()
|
|
{
|
|
var root = TestPaths.CreateRoot();
|
|
try
|
|
{
|
|
var store = new TranslationStore(new TestApplicationPaths(root));
|
|
var first = await store.EnterRefreshAsync(CancellationToken.None);
|
|
var second = await store.EnterRefreshAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
|
|
using var cancel = new CancellationTokenSource();
|
|
var maintenance = store.EnterMaintenanceAsync(cancel.Token);
|
|
Assert.False(maintenance.IsCompleted);
|
|
first.Dispose();
|
|
Assert.False(maintenance.IsCompleted);
|
|
cancel.Cancel();
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => maintenance);
|
|
second.Dispose();
|
|
using var allSlots = await store.EnterMaintenanceAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
|
|
}
|
|
finally { TestPaths.DeleteRoot(root); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MaintenanceWaitsForWriterAndCanceledWaiterDoesNotLeakLease()
|
|
{
|
|
var root = TestPaths.CreateRoot();
|
|
try
|
|
{
|
|
var store = new TranslationStore(new TestApplicationPaths(root));
|
|
var writer = await store.EnterMaintenanceAsync(CancellationToken.None);
|
|
using var canceled = new CancellationTokenSource();
|
|
var waiting = store.EnterMaintenanceAsync(canceled.Token);
|
|
Assert.False(waiting.IsCompleted);
|
|
canceled.Cancel();
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => waiting);
|
|
var cleanup = store.EnterMaintenanceAsync(CancellationToken.None);
|
|
Assert.False(cleanup.IsCompleted);
|
|
writer.Dispose();
|
|
using var lease = await cleanup.WaitAsync(TimeSpan.FromSeconds(5));
|
|
}
|
|
finally { TestPaths.DeleteRoot(root); }
|
|
}
|
|
|
|
[Fact]
|
|
public void ResetKeepsExistingConnectionsOnTheSameDatabase()
|
|
{
|
|
var root = TestPaths.CreateRoot();
|
|
try
|
|
{
|
|
var store = new TranslationStore(new TestApplicationPaths(root));
|
|
store.SaveUserRules("user", new UserRulesDocument());
|
|
using var connection = store.Open();
|
|
connection.Open();
|
|
using var query = connection.CreateCommand();
|
|
query.CommandText = "SELECT COUNT(*) FROM user_rules;";
|
|
Assert.Equal(1L, query.ExecuteScalar());
|
|
store.ResetAll();
|
|
Assert.Equal(0L, query.ExecuteScalar());
|
|
}
|
|
finally { TestPaths.DeleteRoot(root); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProviderDoesNotRetryProgrammingErrors()
|
|
{
|
|
var attempts = 0;
|
|
using var http = new HttpClient(new TestHttpMessageHandler(_ =>
|
|
{
|
|
attempts++;
|
|
throw new InvalidOperationException("Programming error");
|
|
}));
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.test/");
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => ProviderHttp.SendWithRetryAsync(http, request, CancellationToken.None));
|
|
Assert.Equal(1, attempts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentProviderAdmissionsKeepTheirSpacing()
|
|
{
|
|
var limiter = new FetchRateLimiter(TimeSpan.FromMilliseconds(50));
|
|
var starts = await Task.WhenAll(Enumerable.Range(0, 4).Select(async _ =>
|
|
{
|
|
await limiter.WaitAsync(CancellationToken.None);
|
|
return Stopwatch.GetTimestamp();
|
|
}));
|
|
Array.Sort(starts);
|
|
Assert.All(starts.Zip(starts.Skip(1)), pair =>
|
|
Assert.True(Stopwatch.GetElapsedTime(pair.First, pair.Second) >= TimeSpan.FromMilliseconds(40)));
|
|
}
|
|
}
|