From 81f2e4b5e35c491451fba5fd2068e1eb33b7c895 Mon Sep 17 00:00:00 2001 From: Brian Robbins Date: Wed, 27 May 2026 11:08:40 -0700 Subject: [PATCH] Stabilize XamlMessageBox UI-thread dispatch test Use xUnit's WPF dispatcher for the XamlMessageBox background-thread dispatch regression test instead of creating and shutting down a private dispatcher thread. This avoids intermittent hangs from dispatcher/Application state while preserving the regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Dialogs/XamlMessageBoxTests.cs | 112 ++++-------------- 1 file changed, 23 insertions(+), 89 deletions(-) diff --git a/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs b/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs index 131582c51..50a59c926 100644 --- a/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs +++ b/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs @@ -1,15 +1,10 @@ using System; -using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; -using System.Windows.Threading; using PerfView.Dialogs; using Xunit; -#pragma warning disable VSTHRD001 // Use JoinableTaskFactory — we're explicitly testing WPF Dispatcher threading -#pragma warning disable VSTHRD110 // Observe awaitable — fire-and-forget Task.Run is intentional in these tests - namespace PerfViewTests.Dialogs { /// @@ -25,97 +20,34 @@ public class XamlMessageBoxTests /// Also verifies that calling from the UI thread directly still works (no-op dispatch). /// This is the core regression test for issue #2300. /// - [Fact] - public void Show_AutoDispatchesToUIThreadFromBackgroundThread() +#pragma warning disable VSTHRD200 // Keep the original regression test name stable. + [WpfFact] + public async Task Show_AutoDispatchesToUIThreadFromBackgroundThread() +#pragma warning restore VSTHRD200 { - Exception exception = null; - MessageBoxResult uiResult = MessageBoxResult.None; - MessageBoxResult bgResult = MessageBoxResult.None; - - var staThread = new Thread(() => - { - try - { - var app = Application.Current ?? new Application(); - app.ShutdownMode = ShutdownMode.OnExplicitShutdown; - RegisterMinimalThemeResources(app); - - // Catch unhandled dispatcher exceptions so they don't silently hang. - app.DispatcherUnhandledException += (s, args) => - { - exception = args.Exception; - args.Handled = true; - Dispatcher.CurrentDispatcher.InvokeShutdown(); - }; + Application app = Application.Current ?? new Application(); + app.ShutdownMode = ShutdownMode.OnExplicitShutdown; + RegisterMinimalThemeResources(app); - // Auto-close any XamlMessageBox dialogs as soon as they load. - RegisterAutoCloseHandler(); + // Auto-close any XamlMessageBox dialogs as soon as they load. + RegisterAutoCloseHandler(); - // Safety timeout: force shutdown if the test hangs. - var safetyTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(8) }; - safetyTimer.Tick += (s, e) => - { - safetyTimer.Stop(); - if (exception == null) - { - exception = new TimeoutException("Safety timer fired — dialog was not auto-closed"); - } - Dispatcher.CurrentDispatcher.InvokeShutdown(); - }; - safetyTimer.Start(); + // Part 1: Call directly from the UI thread (dispatch is a no-op). + MessageBoxResult uiResult = XamlMessageBox.Show("Test message", "XamlMBTest_UI", MessageBoxButton.OK); - // Use Dispatcher.CurrentDispatcher (the STA thread's dispatcher being - // pumped by Dispatcher.Run) rather than app.Dispatcher, because if - // Application.Current was reused from a prior test, app.Dispatcher may - // belong to a different thread. - var currentDispatcher = Dispatcher.CurrentDispatcher; - currentDispatcher.BeginInvoke((Action)(() => - { - try - { - // Part 1: Call directly from the UI thread (dispatch is a no-op). - uiResult = XamlMessageBox.Show("Test message", "XamlMBTest_UI", MessageBoxButton.OK); + // Part 2: Call from a background thread — before the fix for issue #2300, + // this would throw InvalidOperationException ("The calling thread must be STA") + // because XamlMessageBox creates a WPF Window requiring the UI thread. + Task backgroundShowTask = Task.Run(() => + XamlMessageBox.Show("Test message", "XamlMBTest_BG", MessageBoxButton.YesNo)); - // Part 2: Call from a background thread — before the fix for issue #2300, - // this would throw InvalidOperationException ("The calling thread must be STA") - // because XamlMessageBox creates a WPF Window requiring the UI thread. - Task.Run(() => - { - try - { - bgResult = XamlMessageBox.Show("Test message", "XamlMBTest_BG", MessageBoxButton.YesNo); - } - catch (Exception ex) - { - exception = ex; - } - finally - { - currentDispatcher.BeginInvoke( - (Action)(() => currentDispatcher.InvokeShutdown())); - } - }); - } - catch (Exception ex) - { - exception = ex; - Dispatcher.CurrentDispatcher.InvokeShutdown(); - } - })); - - Dispatcher.Run(); - } - catch (Exception ex) - { - exception = ex; - } - }); + Task completedTask = await Task.WhenAny(backgroundShowTask, Task.Delay(TimeSpan.FromSeconds(10))); + Assert.True( + ReferenceEquals(backgroundShowTask, completedTask), + "Timed out waiting for XamlMessageBox.Show to dispatch to the WPF test thread."); - staThread.SetApartmentState(ApartmentState.STA); - staThread.Start(); - Assert.True(staThread.Join(TimeSpan.FromSeconds(10)), "Test timed out — dialog may not have been auto-closed"); + MessageBoxResult bgResult = await backgroundShowTask; - Assert.Null(exception); // Both dialogs were auto-closed without clicking a button, so Result is None. Assert.Equal(MessageBoxResult.None, uiResult); Assert.Equal(MessageBoxResult.None, bgResult); @@ -144,7 +76,9 @@ private static void RegisterAutoCloseHandler() Window w = sender as Window; if (w != null && w.Title != null && w.Title.StartsWith("XamlMBTest_")) { +#pragma warning disable VSTHRD001, VSTHRD110 // Loaded is already on the WPF thread; defer Close until loading completes. w.Dispatcher.BeginInvoke((Action)(() => w.Close())); +#pragma warning restore VSTHRD001, VSTHRD110 } })); }