From be4625fbf6866d141a5f471d038d15a8b1ea93c7 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 2 Jun 2026 20:21:50 +0200 Subject: [PATCH] Adapt DecompilerViewTests to the shared test helpers This fixture was missed by both the helper-extraction and visual-breakpoint sweeps, so it still carried 16 copies of the boot prologue and no captures. Route it through TestHarness.BootAsync / TreeNavigation and add a Step after each decompile, matching every other headless fixture. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Editor/DecompilerViewTests.cs | 225 ++++++++++------------ 1 file changed, 98 insertions(+), 127 deletions(-) diff --git a/ILSpy.Tests/Editor/DecompilerViewTests.cs b/ILSpy.Tests/Editor/DecompilerViewTests.cs index d6426952c..7009fe83e 100644 --- a/ILSpy.Tests/Editor/DecompilerViewTests.cs +++ b/ILSpy.Tests/Editor/DecompilerViewTests.cs @@ -50,22 +50,19 @@ public class DecompilerViewTests // resulting C# (signature + body) must land in the document tab. Also verifies the // row scrolls into view (CenteredInView assertion) so the user sees the selection. - // Arrange — boot the window, wait for assemblies, expand Enumerable, locate AsEnumerable. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand Enumerable, locate AsEnumerable. + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.IsExpanded = true; - var methodNode = typeNode.Children.OfType() - .Single(m => m.MethodDefinition.Name == "AsEnumerable"); + "System.Linq", "System.Linq", "System.Linq.Enumerable").Expand(); + var methodNode = typeNode.GetChild(m => m.MethodDefinition.Name == "AsEnumerable"); methodNode.MethodDefinition.IsExtensionMethod.Should().BeTrue(); // Act — select the method node and wait for the decompile. vm.AssemblyTreeModel.SelectNode(methodNode); + TestCapture.Step("selected-asenumerable"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("asenumerable-decompiled"); // Assert — signature + body fragments are present and the row centred in the tree. tab.Text.Should().Contain("AsEnumerable"); @@ -82,19 +79,18 @@ public class DecompilerViewTests // summary listing (target framework, runtime pack, referenced assemblies, load log) // rather than decompiled C#. - // Arrange — boot, wait for assemblies, expand the System.Linq assembly node. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand the System.Linq assembly node, grab its References folder. + var (_, vm) = await TestHarness.BootAsync(3); - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.IsExpanded = true; - var refFolder = assemblyNode.Children.OfType().Single(); + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq").Expand(); + TestCapture.Step("expanded-system-linq"); + var refFolder = assemblyNode.GetChild(); // Act — select the References folder and wait for its decompile output. vm.AssemblyTreeModel.SelectNode(refFolder); + TestCapture.Step("selected-references"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("references-listing"); // Assert — every header and a representative referenced assembly land in the listing. tab.Text.Should().Contain("Detected TargetFramework-Id:"); @@ -110,19 +106,17 @@ public class DecompilerViewTests // Selecting a TypeTreeNode (System.Version) must decompile the full class declaration // including representative members. - // Arrange — boot, wait for assemblies, find System.Version. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, find System.Version. + var (_, vm) = await TestHarness.BootAsync(3); - var coreLib = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Version"); + TreeNavigation.CoreLibName, "System", "System.Version"); // Act — select the type node, wait for the decompile. vm.AssemblyTreeModel.SelectNode(typeNode); + TestCapture.Step("selected-version"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("version-class"); // Assert — class declaration + representative property names land in the output. tab.Text.Should().Contain("class Version"); @@ -136,22 +130,18 @@ public class DecompilerViewTests // PropertyTreeNode → just the single property's declaration (with type prefix), not // the full enclosing class. - // Arrange — boot, wait for assemblies, expand System.Version, grab Major. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand System.Version, grab Major. + var (_, vm) = await TestHarness.BootAsync(3); - var coreLib = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Version"); - typeNode.IsExpanded = true; - var propertyNode = typeNode.Children.OfType() - .Single(p => p.PropertyDefinition.Name == "Major"); + TreeNavigation.CoreLibName, "System", "System.Version").Expand(); + var propertyNode = typeNode.GetChild(p => p.PropertyDefinition.Name == "Major"); // Act — select the property, wait for the decompile. vm.AssemblyTreeModel.SelectNode(propertyNode); + TestCapture.Step("selected-major"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("major-property"); // Assert — property declaration lands in the output. tab.Text.Should().Contain("public int Major"); @@ -163,22 +153,18 @@ public class DecompilerViewTests // FieldTreeNode (System.Math.PI) → decompiled field declaration with its constant // initializer. - // Arrange — boot, wait for assemblies, expand System.Math, grab PI. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand System.Math, grab PI. + var (_, vm) = await TestHarness.BootAsync(3); - var coreLib = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Math"); - typeNode.IsExpanded = true; - var fieldNode = typeNode.Children.OfType() - .Single(f => f.FieldDefinition.Name == "PI"); + TreeNavigation.CoreLibName, "System", "System.Math").Expand(); + var fieldNode = typeNode.GetChild(f => f.FieldDefinition.Name == "PI"); // Act — select the field, wait for the decompile. vm.AssemblyTreeModel.SelectNode(fieldNode); + TestCapture.Step("selected-pi"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("pi-field"); // Assert — declaration and constant value both land. tab.Text.Should().Contain("public const double PI"); @@ -190,22 +176,18 @@ public class DecompilerViewTests { // EventTreeNode (AppDomain.ProcessExit) → decompiled event declaration. - // Arrange — boot, wait for assemblies, expand AppDomain, grab ProcessExit. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand AppDomain, grab ProcessExit. + var (_, vm) = await TestHarness.BootAsync(3); - var coreLib = typeof(object).Assembly.GetName().Name!; var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.AppDomain"); - typeNode.IsExpanded = true; - var eventNode = typeNode.Children.OfType() - .Single(e => e.EventDefinition.Name == "ProcessExit"); + TreeNavigation.CoreLibName, "System", "System.AppDomain").Expand(); + var eventNode = typeNode.GetChild(e => e.EventDefinition.Name == "ProcessExit"); // Act — select the event, wait for the decompile. vm.AssemblyTreeModel.SelectNode(eventNode); + TestCapture.Step("selected-processexit"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("processexit-event"); // Assert — event keyword + name land in the output. tab.Text.Should().Contain("event EventHandler"); @@ -217,26 +199,27 @@ public class DecompilerViewTests { // Under the IL disassembler, selecting a NamespaceTreeNode lists the namespace's // types — ILLanguage overrides DecompileNamespace to disassemble them, matching the - // WPF app where IL mode disassembles the namespace. + // app's IL mode that disassembles the whole namespace. - // Arrange — boot, wait for assemblies, switch to IL, locate System.Runtime.Versioning. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, switch to IL, locate System.Runtime.Versioning. + var (_, vm) = await TestHarness.BootAsync(3); var languageService = AppComposition.Current.GetExport(); languageService.CurrentLanguage = languageService.GetLanguage("IL"); + TestCapture.Step("select-il-language"); - var coreLib = typeof(object).Assembly.GetName().Name!; var namespaceNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System.Runtime.Versioning"); + TreeNavigation.CoreLibName, "System.Runtime.Versioning"); // Act — select the namespace, wait for the decompile. vm.AssemblyTreeModel.SelectNode(namespaceNode); + TestCapture.Step("selected-namespace"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("namespace-il-listing"); - // Assert — representative type names land in the IL output. + // Assert — the document was produced by the IL language, and representative type + // names land in the IL output. + tab.Language.Name.Should().Be("IL"); tab.Text.Should().Contain("TargetFrameworkAttribute"); tab.Text.Should().Contain("SupportedOSPlatformAttribute"); } @@ -246,25 +229,24 @@ public class DecompilerViewTests { // The C# language does NOT override DecompileNamespace, so selecting a namespace node // falls through to the base Language.DecompileNamespace, which writes just a - // "// " comment — not the contained types. Matches WPF, and keeps a single - // click from triggering a multi-second decompile of the whole namespace. + // "// " comment — not the contained types. Keeps a single click from + // triggering a multi-second decompile of the whole namespace. - // Arrange — boot, wait for assemblies, ensure C#, locate System.Runtime.Versioning. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, ensure C#, locate System.Runtime.Versioning. + var (_, vm) = await TestHarness.BootAsync(3); var languageService = AppComposition.Current.GetExport(); languageService.CurrentLanguage = languageService.GetLanguage("C#"); + TestCapture.Step("select-csharp-language"); - var coreLib = typeof(object).Assembly.GetName().Name!; var namespaceNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System.Runtime.Versioning"); + TreeNavigation.CoreLibName, "System.Runtime.Versioning"); // Act — select the namespace, wait for the decompile. vm.AssemblyTreeModel.SelectNode(namespaceNode); + TestCapture.Step("selected-namespace"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("namespace-csharp-comment"); // Assert — only the namespace comment, none of the contained types. tab.Text.Should().Contain("System.Runtime.Versioning"); @@ -279,29 +261,27 @@ public class DecompilerViewTests // the full "(version, tfm)" suffix wired up. The tab title used to lock in the bare // ShortName at that moment; it must instead update once Text becomes available. - // Arrange — boot, wait for assemblies. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot. + var (_, vm) = await TestHarness.BootAsync(3); // Act 1 — fire OpenCommand on a new path. OpenCommand selects the new node before its - // rich Text (with version + tfm) settles, exercising the late-update path. + // rich Text (with version + tfm) settles, exercising the late-update path. We can't use + // OpenAssemblyAsync here: it awaits the load, which would skip the pre-settle window. var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; + var openCommand = AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Open)); openCommand.Execute(newAsmPath); await Waiters.WaitForAsync(() => vm.AssemblyTreeModel.SelectedItem is AssemblyTreeNode n && string.Equals(n.LoadedAssembly.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); + TestCapture.Step("assembly-selected"); // Act 2 — wait for the rich form of LoadedAssembly.Text to materialise. var node = (AssemblyTreeNode)vm.AssemblyTreeModel.SelectedItem!; await node.LoadedAssembly.GetLoadResultAsync(); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("opened-assembly"); // Assert — once Text is rich, the tab title catches up. Sanity-check the form is rich // (otherwise the test isn't exercising the late-update path). @@ -318,28 +298,29 @@ public class DecompilerViewTests // spinner glyphs so the user sees activity. The editor's Text must NOT receive the // glyph (only the title bar). Once decompilation finishes, the glyph clears. - // Arrange — boot, wait for assemblies, run an initial decompile so the second selection - // produces a real "in flight" period (not the cold-start one). - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, run an initial decompile so the second selection produces a real + // "in flight" period (not the cold-start one). + var (_, vm) = await TestHarness.BootAsync(3); var firstNode = vm.AssemblyTreeModel.FindNode("System.Linq"); vm.AssemblyTreeModel.SelectNode(firstNode); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("first-decompile"); var titles = new List(); var editorTexts = new List(); tab.PropertyChanged += OnTabPropertyChanged; - // Act — kick off a second decompile and observe Title/Text changes throughout. - var coreLib = typeof(object).Assembly.GetName().Name!; - var second = vm.AssemblyTreeModel.FindNode(coreLib, "System", "System.Version"); + // Act — kick off a second decompile and observe Title/Text changes throughout. No + // breakpoint inside the observation window: a capture's RunJobs could perturb the + // in-flight title/text transitions this test is asserting on. + var second = vm.AssemblyTreeModel.FindNode( + TreeNavigation.CoreLibName, "System", "System.Version"); vm.AssemblyTreeModel.SelectNode(second); await vm.DockWorkspace.WaitForDecompiledTextAsync(); tab.PropertyChanged -= OnTabPropertyChanged; + TestCapture.Step("after-second-decompile"); // Assert — at least one captured title contained a spinner glyph; no editor Text ever // did; the final title is glyph-free. @@ -366,15 +347,11 @@ public class DecompilerViewTests // Selecting several nodes via Ctrl+Click feeds them all into one decompile. The tab // title must list every selected node's display text comma-joined. - // Arrange — boot, wait for assemblies, expand Enumerable, grab two methods. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand Enumerable, grab two methods. + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.IsExpanded = true; + "System.Linq", "System.Linq", "System.Linq.Enumerable").Expand(); var asEnumerable = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "AsEnumerable"); var empty = typeNode.Children.OfType() @@ -384,8 +361,10 @@ public class DecompilerViewTests vm.AssemblyTreeModel.SelectedItems.Clear(); vm.AssemblyTreeModel.SelectedItems.Add(asEnumerable); vm.AssemblyTreeModel.SelectedItems.Add(empty); + TestCapture.Step("multi-selected"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("multi-selection-title"); // Assert — title is the two display texts joined with ", ". tab.Title.Should().Be($"{asEnumerable.Text}, {empty.Text}"); @@ -397,15 +376,11 @@ public class DecompilerViewTests // Multi-selection isn't just a title concern — both methods' bodies must actually be // decompiled into one combined document. - // Arrange — boot, wait for assemblies, expand Enumerable, grab two methods. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, expand Enumerable, grab two methods. + var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.IsExpanded = true; + "System.Linq", "System.Linq", "System.Linq.Enumerable").Expand(); var asEnumerable = typeNode.Children.OfType() .First(m => m.MethodDefinition.Name == "AsEnumerable"); var empty = typeNode.Children.OfType() @@ -415,8 +390,10 @@ public class DecompilerViewTests vm.AssemblyTreeModel.SelectedItems.Clear(); vm.AssemblyTreeModel.SelectedItems.Add(asEnumerable); vm.AssemblyTreeModel.SelectedItems.Add(empty); + TestCapture.Step("multi-selected"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("multi-selection-decompiled"); // Assert — both names AND a body-fragment from one of them confirm both bodies // actually rendered (not just signatures). @@ -431,18 +408,16 @@ public class DecompilerViewTests // AssemblyTreeNode → emit a "manifest"-style summary header (file, identity, runtime, // architecture, global type) plus the assembly-level attributes block. - // Arrange — boot, wait for assemblies, find System.Linq. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, find System.Linq. + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); // Act — select the assembly node, wait for the decompile. vm.AssemblyTreeModel.SelectNode(assemblyNode); - + TestCapture.Step("selected-assembly"); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("assembly-manifest"); // Assert — every documented header line and at least one assembly attribute land. tab.Text.Should().Contain("// " + assemblyNode.LoadedAssembly.FileName); @@ -460,12 +435,9 @@ public class DecompilerViewTests // DecompilerTextView.ApplyDocument must wire AvaloniaEdit's XmlFoldingStrategy so // multi-line elements become collapsible. - // Arrange — boot, wait for assemblies, seed the decompiler tab with an assembly view - // so the document/editor wiring is realised. - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + // Arrange — boot, seed the decompiler tab with an assembly view so the document/editor + // wiring is realised. + var (_, vm) = await TestHarness.BootAsync(3); var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); vm.AssemblyTreeModel.SelectNode(assemblyNode); @@ -477,6 +449,7 @@ public class DecompilerViewTests var view = new DecompilerTextView { DataContext = tab }; var host = new global::Avalonia.Controls.Window { Content = view, Width = 600, Height = 400 }; host.Show(); + host.Capture("assembly-in-host"); // Act — drop a synthetic XML resource into the tab. XmlResourceEntryNode triggers // SyntaxExtension=".xml" and the XmlFoldingStrategy install. @@ -491,6 +464,7 @@ public class DecompilerViewTests global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); await Task.Delay(20); } + host.Capture("xml-folding"); // Assert — FoldingManager is installed (private field, reflected) and produced fold // ranges for the multi-line elements. @@ -524,20 +498,16 @@ public class DecompilerViewTests System.Threading.Tasks.TaskScheduler.UnobservedTaskException += handler; try { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.IsExpanded = true; - var method = typeNode.Children.OfType() - .First(m => m.MethodDefinition.Name == "AsEnumerable"); + var (_, vm) = await TestHarness.BootAsync(3); + + var method = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable") + .Expand().GetChild(m => m.MethodDefinition.Name == "AsEnumerable"); vm.AssemblyTreeModel.SelectNode(method); var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); tab.Foldings.Should().NotBeNull( "C# decompile of a method body should produce at least one fold"); + TestCapture.Step("csharp-method"); // Switch the language — the buggy path was here. var languageService = AppComposition.Current.GetExport(); @@ -546,6 +516,7 @@ public class DecompilerViewTests languageService.CurrentLanguage = blockIL; await vm.DockWorkspace.WaitForDecompiledTextAsync(); + TestCapture.Step("ilast-method"); // Force GC to flush any unobserved Task faults that escaped via the dispatcher. System.GC.Collect();