diff --git a/ILSpy.Tests/Commands/SaveCodeFallbackTests.cs b/ILSpy.Tests/Commands/SaveCodeFallbackTests.cs index 2fd45e0aa..2274af9c8 100644 --- a/ILSpy.Tests/Commands/SaveCodeFallbackTests.cs +++ b/ILSpy.Tests/Commands/SaveCodeFallbackTests.cs @@ -57,9 +57,10 @@ public class SaveCodeFallbackTests [AvaloniaTest] public async Task SaveCode_Writes_A_Clicked_Type_Node_To_A_File() { - var (_, vm) = await TestHarness.BootAsync(3); + var (_, vm) = await TestHarness.BootAsync(); + var fixture = await vm.OpenFixtureAsync(); var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); + fixture.ShortName, fixture.ShortName, $"{fixture.ShortName}.{FixtureAssembly.TypeName}"); Assert.That(typeNode, Is.Not.Null); var language = AppComposition.Current.GetExport() @@ -72,8 +73,8 @@ public class SaveCodeFallbackTests File.Exists(path).Should().BeTrue(); var contents = await File.ReadAllTextAsync(path); - contents.Should().Contain("Enumerable", "the decompiled type's name must be present"); - contents.Should().Contain("AsEnumerable", "the type's members must be decompiled (FullDecompilation)"); + contents.Should().Contain(FixtureAssembly.TypeName, "the decompiled type's name must be present"); + contents.Should().Contain(FixtureAssembly.MemberName, "the type's members must be decompiled (FullDecompilation)"); } finally { diff --git a/ILSpy.Tests/Compare/CompareViewRenderTests.cs b/ILSpy.Tests/Compare/CompareViewRenderTests.cs index 0d6db9c6c..884264eb5 100644 --- a/ILSpy.Tests/Compare/CompareViewRenderTests.cs +++ b/ILSpy.Tests/Compare/CompareViewRenderTests.cs @@ -58,8 +58,10 @@ public class CompareViewRenderTests var entry = AppComposition.Current.GetExport() .Entries.Single(e => e.Metadata.Header == "Compare...").Value; - var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); + var assemblies = new[] { + await vm.OpenFixtureAsync("FixtureA"), + await vm.OpenFixtureAsync("FixtureB"), + }; var nodes = assemblies.Select(a => (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray(); @@ -84,8 +86,10 @@ public class CompareViewRenderTests var entry = AppComposition.Current.GetExport() .Entries.Single(e => e.Metadata.Header == "Compare...").Value; - var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); + var assemblies = new[] { + await vm.OpenFixtureAsync("FixtureA"), + await vm.OpenFixtureAsync("FixtureB"), + }; var nodes = assemblies.Select(a => (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray(); @@ -120,8 +124,7 @@ public class CompareViewRenderTests var vm = (MainWindowViewModel)window.DataContext!; await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - var assembly = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => a.IsLoadedAsValidAssembly); + var assembly = await vm.OpenFixtureAsync(); // Construct the compare model directly with the same assembly on both sides so every // entry collapses to DiffKind.None. The View renders via the standard OpenNewTab path. diff --git a/ILSpy.Tests/FixtureAssembly.cs b/ILSpy.Tests/FixtureAssembly.cs new file mode 100644 index 000000000..3c47c2403 --- /dev/null +++ b/ILSpy.Tests/FixtureAssembly.cs @@ -0,0 +1,96 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading.Tasks; + +using ICSharpCode.ILSpyX; + +using global::ILSpy.ViewModels; + +namespace ICSharpCode.ILSpy.Tests; + +/// +/// Emits a tiny throwaway assembly to a temp file so the project-export / save-code / compare +/// tests can exercise the full pipeline without decompiling a multi-hundred-type framework +/// assembly. The default assembly list loads System.Linq, CoreLib and the Uri assembly; targeting +/// those made each of these tests run for ~10s purely on the type count. A handful of public +/// members is enough to produce a non-empty .cs, a real namespace subtree and a resolvable +/// .csproj, while keeping each test well under a second. +/// +public static class FixtureAssembly +{ + /// Short name of the single public type a caller can assert on / navigate to. + public const string TypeName = "Greeter"; + + /// Name of a member of a caller can assert is decompiled. + public const string MemberName = "Hello"; + + /// + /// Emits a fresh fixture assembly named to a temp file and returns its + /// path. Each call produces a distinct file so callers can build solutions / comparisons out of + /// several fixtures. + /// + public static string Emit(string name) + { + var ab = new PersistedAssemblyBuilder(new AssemblyName(name), typeof(object).Assembly); + var module = ab.DefineDynamicModule(name); + + var greeter = module.DefineType($"{name}.{TypeName}", TypeAttributes.Public | TypeAttributes.Class); + var hello = greeter.DefineMethod( + MemberName, MethodAttributes.Public | MethodAttributes.Static, typeof(string), Type.EmptyTypes); + var il = hello.GetILGenerator(); + il.Emit(OpCodes.Ldstr, $"hello from {name}"); + il.Emit(OpCodes.Ret); + greeter.CreateType(); + + // A second type in a nested namespace so the namespace subtree and the project layout are + // not entirely trivial. + var widget = module.DefineType($"{name}.Widgets.Widget", TypeAttributes.Public | TypeAttributes.Class); + var add = widget.DefineMethod( + "Add", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int), typeof(int)]); + var il2 = add.GetILGenerator(); + il2.Emit(OpCodes.Ldarg_0); + il2.Emit(OpCodes.Ldarg_1); + il2.Emit(OpCodes.Add); + il2.Emit(OpCodes.Ret); + widget.CreateType(); + + // Unique directory (not filename) so the file is ".dll": the assembly's ShortName then + // equals and matches the "" namespace prefix the types use, keeping FindNode + // paths simple. + var dir = Path.Combine(Path.GetTempPath(), $"ILSpyFixture_{Guid.NewGuid():N}"); + Directory.CreateDirectory(dir); + var path = Path.Combine(dir, $"{name}.dll"); + ab.Save(path); + return path; + } + + /// + /// Emits a fixture assembly and opens it through the production Open command, returning the + /// loaded assembly once its load result is available. + /// + public static Task OpenFixtureAsync(this MainWindowViewModel vm, string name = "Fixture") + { + ArgumentNullException.ThrowIfNull(vm); + return vm.OpenAssemblyAsync(Emit(name)); + } +} diff --git a/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs b/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs index 9f0a1afbe..1767cd775 100644 --- a/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs +++ b/ILSpy.Tests/Languages/ProjectExportRunnerTests.cs @@ -50,11 +50,16 @@ public class ProjectExportRunnerTests static CSharpLanguage Language() => AppComposition.Current.GetExport().Languages.OfType().First(); - static List NonCoreLibAssemblies(MainWindowViewModel vm, int count) - => vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Where(a => a.IsLoadedAsValidAssembly && a.ShortName != TreeNavigation.CoreLibName) - .GroupBy(a => a.ShortName).Select(g => g.First()) - .Take(count).ToList(); + // Emit and open `count` distinct tiny fixture assemblies. Exporting these instead of the + // multi-hundred-type framework assemblies the default list carries keeps each test near ~1s + // while still exercising the full project/solution writer end to end. + static async Task> OpenFixtures(MainWindowViewModel vm, int count) + { + var result = new List(); + for (int i = 0; i < count; i++) + result.Add(await vm.OpenFixtureAsync($"Fixture{(char)('A' + i)}")); + return result; + } static ProjectExportOptions Options(string outputDir, bool generatePdb = false, bool embedSourceFilesInPdb = false, string? strongNameKeyFile = null) @@ -77,9 +82,8 @@ public class ProjectExportRunnerTests [AvaloniaTest] public async Task Project_Mode_Writes_Csproj_And_Cs() { - var (_, vm) = await TestHarness.BootAsync(3); - var assembly = NonCoreLibAssemblies(vm, 1).Single(); - await assembly.GetLoadResultAsync(); + var (_, vm) = await TestHarness.BootAsync(); + var assembly = await vm.OpenFixtureAsync(); var tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProj_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -102,11 +106,8 @@ public class ProjectExportRunnerTests [AvaloniaTest] public async Task Solution_Mode_Writes_Sln_And_Projects() { - var (_, vm) = await TestHarness.BootAsync(3); - var assemblies = NonCoreLibAssemblies(vm, 2); - assemblies.Should().HaveCountGreaterThanOrEqualTo(2); - foreach (var a in assemblies) - await a.GetLoadResultAsync(); + var (_, vm) = await TestHarness.BootAsync(); + var assemblies = await OpenFixtures(vm, 2); var tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProjSln_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -133,9 +134,8 @@ public class ProjectExportRunnerTests [AvaloniaTest] public async Task StrongNameKeyFile_Is_Copied_Into_Project() { - var (_, vm) = await TestHarness.BootAsync(3); - var assembly = NonCoreLibAssemblies(vm, 1).Single(); - await assembly.GetLoadResultAsync(); + var (_, vm) = await TestHarness.BootAsync(); + var assembly = await vm.OpenFixtureAsync(); var tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProjSnk_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -161,9 +161,8 @@ public class ProjectExportRunnerTests [AvaloniaTest] public async Task Runner_Does_Not_Mutate_Persisted_Settings() { - var (_, vm) = await TestHarness.BootAsync(3); - var assembly = NonCoreLibAssemblies(vm, 1).Single(); - await assembly.GetLoadResultAsync(); + var (_, vm) = await TestHarness.BootAsync(); + var assembly = await vm.OpenFixtureAsync(); var settingsService = AppComposition.Current.GetExport(); bool originalSdk = settingsService.DecompilerSettings.UseSdkStyleProjectFormat; diff --git a/ILSpy.Tests/Languages/ProjectExportTests.cs b/ILSpy.Tests/Languages/ProjectExportTests.cs index ea0e61425..c5fd3a2fd 100644 --- a/ILSpy.Tests/Languages/ProjectExportTests.cs +++ b/ILSpy.Tests/Languages/ProjectExportTests.cs @@ -66,19 +66,14 @@ public class ProjectExportTests [AvaloniaTest] public async Task DecompileAssembly_With_SaveAsProjectDirectory_Writes_Csproj_And_Cs_Files() { - // Drives the project-export path end-to-end against an already-loaded assembly in - // the headless composition. CoreLib is too big for a smoke test (multi-second - // decompile); System.Linq is a manageable subset and is loaded by the existing - // test fixture. + // Drives the project-export path end-to-end against a tiny emitted fixture assembly, so the + // smoke test isn't dominated by decompiling a multi-hundred-type framework assembly. var window = AppComposition.Current.GetExport(); window.Show(); var vm = (MainWindowViewModel)window.DataContext!; await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); - var node = vm.AssemblyTreeModel.FindNode("System.Linq"); - Assert.That(node, Is.Not.Null, "System.Linq must be discoverable in the headless test fixture"); - var loaded = node!.LoadedAssembly; - await loaded.GetLoadResultAsync(); + var loaded = await vm.OpenFixtureAsync(); var tempDir = Path.Combine(Path.GetTempPath(), "ILSpyExport_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); diff --git a/ILSpy.Tests/Languages/SolutionExportTests.cs b/ILSpy.Tests/Languages/SolutionExportTests.cs index 102b58c48..cdb6bb8ed 100644 --- a/ILSpy.Tests/Languages/SolutionExportTests.cs +++ b/ILSpy.Tests/Languages/SolutionExportTests.cs @@ -43,18 +43,14 @@ public class SolutionExportTests [AvaloniaTest] public async Task CreateSolution_Writes_Sln_And_Per_Assembly_Projects() { - var (_, vm) = await TestHarness.BootAsync(3); - - // CoreLib is too big for a smoke test; the Uri and System.Linq reference assemblies - // the headless fixture seeds are both manageable full-project decompiles. - var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Where(a => a.IsLoadedAsValidAssembly && a.ShortName != TreeNavigation.CoreLibName) - .GroupBy(a => a.ShortName).Select(g => g.First()) - .Take(2).ToList(); - assemblies.Should().HaveCountGreaterThanOrEqualTo(2, "a solution needs at least two distinct assemblies"); - - foreach (var a in assemblies) - await a.GetLoadResultAsync(); + var (_, vm) = await TestHarness.BootAsync(); + + // Two tiny emitted fixtures keep the full-solution decompile fast; what this exercises is the + // writer's per-assembly project layout, not decompile breadth. + var assemblies = new[] { + await vm.OpenFixtureAsync("FixtureA"), + await vm.OpenFixtureAsync("FixtureB"), + }; var language = AppComposition.Current.GetExport() .Languages.OfType().First();