From 0c287f5cde1668f108e2a139942e8f0fc9418302 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Fri, 7 Aug 2026 11:03:15 +0200 Subject: [PATCH] Stabilize the bookmark-highlight tests against dispatcher stalls The bookmark navigation tests asserted the one-shot line highlight by polling the text view's renderer collection, but the adorner self-dismisses after an ~800 ms lifetime driven by a DispatcherTimer. On a loaded CI runner (the desktop job runs the UI and decompiler test suites concurrently) the dispatcher can stall long enough that the adorner registers and is dismissed again before the test's next predicate check, so the wait misses the entire play and burns its full 60 s timeout; raising the timeout cannot help with that. Record the last played line on DecompilerTextView as persistent evidence of the one-shot highlight and assert that instead - it also pins the highlight to the expected line, which the presence check never did. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Bookmarks/BookmarkNavigationViewTests.cs | 32 +++++++++---------- ILSpy/TextView/DecompilerTextView.axaml.cs | 8 +++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs b/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs index 4685b2d01..52e147f18 100644 --- a/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs +++ b/ILSpy.Tests/Bookmarks/BookmarkNavigationViewTests.cs @@ -23,8 +23,6 @@ using Avalonia.Headless.NUnit; using Avalonia.Threading; using Avalonia.VisualTree; -using AvaloniaEdit.Rendering; - using AwesomeAssertions; using ICSharpCode.ILSpy.AppEnv; @@ -97,11 +95,11 @@ public class BookmarkNavigationViewTests await vm.DockWorkspace.WaitForDecompiledTextAsync(); view = await window.WaitForComponent(); - // Wait until the one-shot highlight has registered, then assert without any further delay: the - // adorner self-dismisses after an ~800 ms lifetime, so a fixed-length pump on a loaded CI runner - // can outlast it and observe an empty collection. The same deferred apply also lands the caret. - await Waiters.WaitForAsync(() => view.Editor.TextArea.TextView.BackgroundRenderers - .OfType().Any()); + // Wait until the one-shot highlight has played. The adorner itself self-dismisses after an + // ~800 ms lifetime and a stalled CI dispatcher can register and dismiss it inside a single + // pump, so polling the renderer collection can miss the entire play; LastHighlightPlayedLine + // is the view's persistent record of it. The same deferred apply also lands the caret. + await Waiters.WaitForAsync(() => view.LastHighlightPlayedLine != null); int targetLine = view.GetLineForBookmark(bookmark) ?? -1; targetLine.Should().BeGreaterThan(1, "the bookmark resolves to a line below the top in the freshly shown document"); @@ -110,9 +108,9 @@ public class BookmarkNavigationViewTests view.Editor.TextArea.Caret.Line.Should().Be(targetLine, "bookmark navigation from non-decompiler content must scroll to the saved line"); - // P2: the one-shot line highlight is playing on the freshly shown view. - view.Editor.TextArea.TextView.BackgroundRenderers.OfType() - .Should().ContainSingle("the destination line must be highlighted after the content switch"); + // P2: the one-shot line highlight played on the freshly shown view, on the right line. + view.LastHighlightPlayedLine.Should().Be(targetLine, + "the destination line must be highlighted after the content switch"); } // Regression: when the active tab is frozen, navigating to a bookmark in a different node must @@ -160,21 +158,21 @@ public class BookmarkNavigationViewTests var activeModel = vm.DockWorkspace.ActiveDecompilerTab; activeModel.Should().NotBeNull("navigation must surface a decompiler tab"); - // Wait until the fresh preview's view exists and its one-shot highlight has registered, then - // assert without any further delay: the adorner self-dismisses after an ~800 ms lifetime, so a - // fixed-length pump on a loaded CI runner can outlast it and observe an empty collection. + // Wait until the fresh preview's view exists and its one-shot highlight has played. The + // adorner itself self-dismisses after an ~800 ms lifetime and a stalled CI dispatcher can + // register and dismiss it inside a single pump, so polling the renderer collection can miss + // the entire play; LastHighlightPlayedLine is the view's persistent record of it. DecompilerTextView? ActiveView() => window.GetVisualDescendants().OfType() .FirstOrDefault(v => ReferenceEquals(v.DataContext, activeModel)); - await Waiters.WaitForAsync(() => ActiveView()?.Editor.TextArea.TextView.BackgroundRenderers - .OfType().Any() == true); + await Waiters.WaitForAsync(() => ActiveView()?.LastHighlightPlayedLine != null); var activeView = ActiveView()!; int targetLine = activeView.GetLineForBookmark(bookmark) ?? -1; targetLine.Should().BeGreaterThan(1, "the fresh preview shows System.String with the bookmarked line below the top"); activeView.Editor.TextArea.Caret.Line.Should().Be(targetLine, "opening a fresh preview for a frozen-tab navigation must still scroll to the bookmark"); - activeView.Editor.TextArea.TextView.BackgroundRenderers.OfType() - .Should().ContainSingle("the destination line must be highlighted in the fresh preview"); + activeView.LastHighlightPlayedLine.Should().Be(targetLine, + "the destination line must be highlighted in the fresh preview"); } // Regression: a bookmark re-anchors by token/IL offset, so a decompiler-setting change that diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 4bac40bff..cd5f39ab8 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -843,6 +843,13 @@ namespace ICSharpCode.ILSpy.TextView return false; } + // The last line the one-shot navigation highlight was played on in this view, or null when + // none has played yet. The adorner itself self-dismisses after its ~800 ms lifetime, so an + // observer polling the renderer collection can miss the entire play when the dispatcher + // stalls (a headless test on a loaded CI runner); this record is the persistent evidence + // that the highlight ran, and where. + internal int? LastHighlightPlayedLine { get; private set; } + void ScrollToLine(int line, Bookmarks.BookmarkViewState? viewState = null) { var document = Editor.Document; @@ -862,6 +869,7 @@ namespace ICSharpCode.ILSpy.TextView RestoreBookmarkFoldings(viewState); CenterLineInView(document, line); LineHighlightAdorner.DisplayLineHighlight(Editor.TextArea, line); + LastHighlightPlayedLine = line; bookmarkMargin?.PulseLine(line); }, DispatcherPriority.Background); }