mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 11:12:11 -04:00
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.
65 lines
2.0 KiB
C#
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();
|
|
}
|
|
}
|