Files
jellyfin-ha-src/tests/Jellyfin.Server.Implementations.Tests/ScheduledTasks/ScanLeaderOptionsTests.cs
T
unkin-agent 1965c68a76
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix(ha): gate the remaining timer-driven scheduled tasks
Add the eight provider, live TV and plugin-update task keys to
ScanLeaderOptions.GatedTaskKeys and widen the test's task-key discovery to
every assembly that declares an IScheduledTask.
2026-09-20 23:36:34 +10:00

104 lines
3.8 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using MediaBrowser.Controller.ScheduledTasks;
using MediaBrowser.Model.Tasks;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.ScheduledTasks;
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>
/// 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.
/// </summary>
[Fact]
public void DefaultGatedTaskKeys_Should_MatchRegisteredScheduledTasks()
{
var registeredKeys = DiscoverScheduledTaskKeys(_taskAssemblies);
Assert.NotEmpty(registeredKeys);
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))}");
}
/// <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"))
{
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;
}
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 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
// them without standing up each task's dependency graph.
var task = (IScheduledTask)RuntimeHelpers.GetUninitializedObject(type);
keys.Add(task.Key);
}
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!);
}
}