aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-09-01 20:47:39 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-09-01 21:42:06 +0200
commitc56e14d8fb559abcc73fc7c2c83533cd32cb5320 (patch)
tree680fb051a4b51fef6725dba72abbf0fffe6a3433 /tests
parentc5f8a93513c48501c24b192a987c7467d8a98608 (diff)
Keep a media process and its exit state usable by the caller that started it
Diffstat (limited to 'tests')
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Encoder/ProcessWrapperTests.cs103
1 files changed, 103 insertions, 0 deletions
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Encoder/ProcessWrapperTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ProcessWrapperTests.cs
new file mode 100644
index 0000000000..141164815c
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Encoder/ProcessWrapperTests.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.MediaEncoding.Encoder;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests.Encoder;
+
+public class ProcessWrapperTests
+{
+ [Fact]
+ public async Task ExitedProcess_StaysUsableForTheCallerThatStartedIt()
+ {
+ using var process = CreateProcess();
+ using var exitHandled = new ManualResetEventSlim(false);
+
+ using (var wrapper = new MediaEncoder.ProcessWrapper(process, CreateEncoder()))
+ {
+ // Subscribed after the wrapper, so by the time this is set the wrapper's own handler has
+ // already run: whatever it does to the process has happened.
+ process.Exited += (_, _) => exitHandled.Set();
+
+ process.Start();
+ await process.WaitForExitAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ Assert.True(exitHandled.Wait(TimeSpan.FromSeconds(15), TestContext.Current.CancellationToken), "The process never raised Exited.");
+
+ // The caller still owns the process here. Disposing it from the exit handler handed
+ // whoever exited quickest an ObjectDisposedException out of these three lines.
+ var output = await process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+ Assert.Equal("jellyfin", output.Trim());
+
+ Assert.True(wrapper.HasExited);
+ Assert.Equal(3, wrapper.ExitCode);
+ }
+ }
+
+ [Fact]
+ public async Task ExitState_IsReadableBeforeTheExitEventArrives()
+ {
+ using var process = CreateProcess();
+
+ using (var wrapper = new MediaEncoder.ProcessWrapper(process, CreateEncoder()))
+ {
+ process.Start();
+ await process.WaitForExitAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ // The exit event is raised on the thread pool and can lag behind the wait that just
+ // returned, so neither of these may depend on it having arrived.
+ Assert.True(wrapper.HasExited);
+ Assert.Equal(3, wrapper.ExitCode);
+ }
+ }
+
+ [Fact]
+ public async Task ExitCode_SurvivesDisposal()
+ {
+ using var process = CreateProcess();
+ var wrapper = new MediaEncoder.ProcessWrapper(process, CreateEncoder());
+
+ process.Start();
+ await process.WaitForExitAsync(TestContext.Current.CancellationToken).ConfigureAwait(true);
+
+ var exitCode = wrapper.ExitCode;
+ wrapper.Dispose();
+
+ Assert.Equal(exitCode, wrapper.ExitCode);
+ Assert.True(wrapper.HasExited);
+ }
+
+ private static MediaEncoder CreateEncoder()
+ => new(
+ Mock.Of<ILogger<MediaEncoder>>(),
+ Mock.Of<IServerConfigurationManager>(),
+ Mock.Of<IFileSystem>(),
+ Mock.Of<IBlurayExaminer>(),
+ Mock.Of<ILocalizationManager>(),
+ new ConfigurationBuilder().Build(),
+ Mock.Of<IServerConfigurationManager>());
+
+ // Writes to stdout and exits immediately with a non-zero code, standing in for the ffprobe that
+ // rejects a file outright - the process that used to win the race against its own caller.
+ private static Process CreateProcess()
+ {
+ var startInfo = OperatingSystem.IsWindows()
+ ? new ProcessStartInfo("cmd.exe", "/c echo jellyfin & exit 3")
+ : new ProcessStartInfo("/bin/sh", "-c \"printf 'jellyfin\\n'; exit 3\"");
+
+ startInfo.CreateNoWindow = true;
+ startInfo.UseShellExecute = false;
+ startInfo.RedirectStandardOutput = true;
+
+ return new Process { StartInfo = startInfo, EnableRaisingEvents = true };
+ }
+}