Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44b62dcc64 | |||
| 1965c68a76 |
@@ -43,6 +43,14 @@ public sealed class ScanLeaderOptions
|
|||||||
"TaskExtractMediaSegments",
|
"TaskExtractMediaSegments",
|
||||||
"KeyframeExtraction",
|
"KeyframeExtraction",
|
||||||
"CleanupUserDataTask",
|
"CleanupUserDataTask",
|
||||||
"OptimizeDatabaseTask"
|
"OptimizeDatabaseTask",
|
||||||
|
"DownloadLyrics",
|
||||||
|
"DownloadSubtitles",
|
||||||
|
"TmdbRefreshUpcomingEpisodes",
|
||||||
|
"RefreshTrickplayImages",
|
||||||
|
"MoveTrickplayImages",
|
||||||
|
"RefreshInternetChannels",
|
||||||
|
"RefreshGuide",
|
||||||
|
"PluginUpdates"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -31,6 +31,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
||||||
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||||
|
<ProjectReference Include="..\..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
|
<ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
|
<ProjectReference Include="..\..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+100
-9
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Emby.Server.Implementations.ScheduledTasks.Tasks;
|
using Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||||
using MediaBrowser.Controller.ScheduledTasks;
|
using MediaBrowser.Controller.ScheduledTasks;
|
||||||
@@ -11,6 +13,14 @@ namespace Jellyfin.Server.Implementations.Tests.ScheduledTasks;
|
|||||||
|
|
||||||
public class ScanLeaderOptionsTests
|
public class ScanLeaderOptionsTests
|
||||||
{
|
{
|
||||||
|
private static readonly Assembly[] _taskAssemblies =
|
||||||
|
{
|
||||||
|
typeof(DeleteTranscodeFileTask).Assembly,
|
||||||
|
typeof(MediaBrowser.Providers.Lyric.LyricScheduledTask).Assembly,
|
||||||
|
typeof(Jellyfin.LiveTv.Guide.RefreshGuideScheduledTask).Assembly,
|
||||||
|
typeof(Jellyfin.MediaEncoding.Hls.ScheduledTasks.KeyframeExtractionScheduledTask).Assembly
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A gated key that matches no registered task silently stops gating anything, so the default
|
/// A gated key that matches no registered task silently stops gating anything, so the default
|
||||||
/// set is pinned to the task keys that actually exist in the build.
|
/// set is pinned to the task keys that actually exist in the build.
|
||||||
@@ -18,28 +28,92 @@ public class ScanLeaderOptionsTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultGatedTaskKeys_Should_MatchRegisteredScheduledTasks()
|
public void DefaultGatedTaskKeys_Should_MatchRegisteredScheduledTasks()
|
||||||
{
|
{
|
||||||
var registeredKeys = DiscoverScheduledTaskKeys();
|
var registeredKeys = DiscoverScheduledTaskKeys(_taskAssemblies);
|
||||||
|
|
||||||
Assert.NotEmpty(registeredKeys);
|
Assert.NotEmpty(registeredKeys);
|
||||||
Assert.Empty(new ScanLeaderOptions().GatedTaskKeys.Except(registeredKeys, StringComparer.Ordinal));
|
|
||||||
|
var unmatched = new ScanLeaderOptions().GatedTaskKeys.Except(registeredKeys, StringComparer.Ordinal).ToList();
|
||||||
|
Assert.True(
|
||||||
|
unmatched.Count == 0,
|
||||||
|
$"Gated keys match no scheduled task: {string.Join(", ", unmatched)}. Known keys: {string.Join(", ", registeredKeys.Order(StringComparer.Ordinal))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<string> DiscoverScheduledTaskKeys()
|
/// <summary>
|
||||||
|
/// A key dropped from the default set silently un-gates that task on every replica, so the whole
|
||||||
|
/// set is pinned against a hand-maintained expectation rather than read back from the options.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DefaultGatedTaskKeys_Should_BeTheExpectedSet()
|
||||||
{
|
{
|
||||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
string[] expected =
|
||||||
var assemblies = new[]
|
|
||||||
{
|
{
|
||||||
typeof(DeleteTranscodeFileTask).Assembly,
|
"AudioNormalization",
|
||||||
typeof(Jellyfin.MediaEncoding.Hls.ScheduledTasks.KeyframeExtractionScheduledTask).Assembly
|
"CleanupUserDataTask",
|
||||||
|
"DownloadLyrics",
|
||||||
|
"DownloadSubtitles",
|
||||||
|
"KeyframeExtraction",
|
||||||
|
"MoveTrickplayImages",
|
||||||
|
"OptimizeDatabaseTask",
|
||||||
|
"PluginUpdates",
|
||||||
|
"RefreshChapterImages",
|
||||||
|
"RefreshGuide",
|
||||||
|
"RefreshInternetChannels",
|
||||||
|
"RefreshLibrary",
|
||||||
|
"RefreshPeople",
|
||||||
|
"RefreshTrickplayImages",
|
||||||
|
"TaskExtractMediaSegments",
|
||||||
|
"TmdbRefreshUpcomingEpisodes"
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var type in assemblies.SelectMany(a => a.GetTypes()))
|
var actual = new ScanLeaderOptions().GatedTaskKeys;
|
||||||
|
var missing = expected.Except(actual, StringComparer.Ordinal).ToList();
|
||||||
|
var unexpected = actual.Except(expected, StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
missing.Count == 0 && unexpected.Count == 0,
|
||||||
|
$"Default gated task keys drifted. Missing: {Describe(missing)}. Unexpected: {Describe(unexpected)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The key universe is only as complete as the assemblies it is read from, so a task added to an
|
||||||
|
/// unscanned assembly must fail here rather than narrow what the previous test can catch.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TaskAssemblies_Should_CoverEveryAssemblyDeclaringScheduledTasks()
|
||||||
|
{
|
||||||
|
var scanned = _taskAssemblies.Select(a => a.GetName().Name).ToHashSet(StringComparer.Ordinal);
|
||||||
|
var missing = new List<string>();
|
||||||
|
|
||||||
|
foreach (var path in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.dll"))
|
||||||
{
|
{
|
||||||
if (type.IsAbstract || type.IsInterface || !typeof(IScheduledTask).IsAssignableFrom(type))
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
if (scanned.Contains(name)
|
||||||
|
|| name.EndsWith(".Tests", StringComparison.Ordinal)
|
||||||
|
|| !(name.StartsWith("Jellyfin.", StringComparison.Ordinal)
|
||||||
|
|| name.StartsWith("Emby.", StringComparison.Ordinal)
|
||||||
|
|| name.StartsWith("MediaBrowser.", StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GetScheduledTaskTypes(Assembly.LoadFrom(path)).Any())
|
||||||
|
{
|
||||||
|
missing.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(missing.Count == 0, $"Assemblies declaring scheduled tasks but not scanned: {string.Join(", ", missing)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Describe(IReadOnlyCollection<string> keys)
|
||||||
|
=> keys.Count == 0 ? "none" : string.Join(", ", keys.Order(StringComparer.Ordinal));
|
||||||
|
|
||||||
|
private static HashSet<string> DiscoverScheduledTaskKeys(IEnumerable<Assembly> assemblies)
|
||||||
|
{
|
||||||
|
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var type in assemblies.SelectMany(GetScheduledTaskTypes))
|
||||||
|
{
|
||||||
// Task keys are constant expressions, so an uninitialised instance is enough to read
|
// Task keys are constant expressions, so an uninitialised instance is enough to read
|
||||||
// them without standing up each task's dependency graph.
|
// them without standing up each task's dependency graph.
|
||||||
var task = (IScheduledTask)RuntimeHelpers.GetUninitializedObject(type);
|
var task = (IScheduledTask)RuntimeHelpers.GetUninitializedObject(type);
|
||||||
@@ -48,4 +122,21 @@ public class ScanLeaderOptionsTests
|
|||||||
|
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Type> GetScheduledTaskTypes(Assembly assembly)
|
||||||
|
{
|
||||||
|
Type?[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types
|
||||||
|
.Where(t => t is not null && !t.IsAbstract && !t.IsInterface && typeof(IScheduledTask).IsAssignableFrom(t))
|
||||||
|
.Select(t => t!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user