Files
OpenNest/OpenNest.WinForms.Tests/StaTestThreadTests.cs
T
aj a15c54fbaf test(desktop): fail WinForms tests on UI-callback exceptions per STA thread
TestAssemblySetup set the unhandled-exception mode and a ThreadException
handler from a module initializer. WinForms stores both per thread
(ThreadContext/NativeWindow thread statics), so they applied only to the
runner thread that loaded the assembly, never to the STA threads the
tests run on. There, an exception escaping a window message or a
BeginInvoke/SynchronizationContext callback still opened a modal
ThreadExceptionDialog in an interactive session, or was silently ignored
otherwise; the handler would also have logged and swallowed it.

Replace the fifteen copied RunSta helpers with StaTestThread.Run, which
selects UnhandledExceptionMode.ThrowException on each STA thread before
it creates a window, so the exception propagates to the test body and
fails the test. Each class keeps its own timeout and message; all
threads are now background threads. Remove TestAssemblySetup.

StaTestThreadTests covers a throwing BeginInvoke callback through
StaTestThread (fails with the original exception) and, as a control,
the default mode on a plain STA thread (the exception goes to the
thread's handler and never reaches the test body). Cross-compiled on
Linux; Windows execution is the windows-desktop CI job.
2026-10-04 18:40:48 -04:00

65 lines
2.0 KiB
C#

using System.Runtime.ExceptionServices;
using System.Windows.Forms;
namespace OpenNest.WinForms.Tests;
public class StaTestThreadTests
{
private const string CallbackMessage = "thrown from a queued UI callback";
[Fact]
public void QueuedCallbackExceptionFailsTheTestInsteadOfOpeningADialog()
{
var error = Assert.Throws<InvalidOperationException>(() =>
StaTestThread.Run(
ThrowFromQueuedCallback,
TimeSpan.FromSeconds(30),
"The STA test did not complete."
)
);
Assert.Equal(CallbackMessage, error.Message);
}
[Fact]
public void WithoutThrowExceptionModeTheCallbackExceptionNeverReachesTheTestBody()
{
// Control: the default mode routes the same exception to the thread's
// ThreadException handler (or, with none, to a modal dialog), so the
// test body itself completes without seeing it.
Exception? routed = null;
Exception? bodyFailure = null;
var thread = new Thread(() =>
{
Application.ThreadException += (_, e) => routed = e.Exception;
try
{
ThrowFromQueuedCallback();
}
catch (Exception ex)
{
bodyFailure = ex;
}
})
{
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Assert.True(thread.Join(TimeSpan.FromSeconds(30)), "The control STA thread did not complete.");
if (bodyFailure != null)
ExceptionDispatchInfo.Capture(bodyFailure).Throw();
Assert.IsType<InvalidOperationException>(routed);
Assert.Equal(CallbackMessage, routed!.Message);
}
private static void ThrowFromQueuedCallback()
{
using var control = new Control();
_ = control.Handle;
control.BeginInvoke(new Action(() => throw new InvalidOperationException(CallbackMessage)));
Application.DoEvents();
}
}