Files
OpenNest/OpenNest.WinForms.Tests/StaTestThread.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

49 lines
1.7 KiB
C#

using System.Runtime.ExceptionServices;
using System.Windows.Forms;
namespace OpenNest.WinForms.Tests;
/// <summary>
/// Runs a test body on its own STA thread and rethrows its failure on the
/// calling test thread.
/// </summary>
/// <remarks>
/// By default WinForms catches an exception raised while dispatching a window
/// message or a BeginInvoke/SynchronizationContext callback and hands it to the
/// thread's ThreadException handler. With none registered it opens a modal
/// ThreadExceptionDialog in an interactive session, which blocks the test until
/// its join timeout, and silently ignores the exception otherwise. Both the
/// unhandled-exception mode and the handler are per-thread, so they cannot be
/// configured once for the assembly: each STA thread selects
/// <see cref="UnhandledExceptionMode.ThrowException"/> before it creates a
/// window, and the exception then propagates to the code pumping messages
/// (Show, DoEvents, ShowDialog) and fails the test.
/// </remarks>
internal static class StaTestThread
{
public static void Run(Action body, TimeSpan timeout, string timeoutMessage)
{
Exception? failure = null;
var thread = new Thread(() =>
{
try
{
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
body();
}
catch (Exception ex)
{
failure = ex;
}
})
{
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Assert.True(thread.Join(timeout), timeoutMessage);
if (failure != null)
ExceptionDispatchInfo.Capture(failure).Throw();
}
}