// 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.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Headless.NUnit; using AwesomeAssertions; using ICSharpCode.Decompiler; using ICSharpCode.ILSpyX; using ILSpy; using ILSpy.AppEnv; using ILSpy.Commands; using ILSpy.Languages; using ILSpy.ViewModels; using NUnit.Framework; namespace ICSharpCode.ILSpy.Tests.Languages; /// /// End-to-end tests of , the headless engine behind the Export /// Project/Solution dialog: project mode (one assembly), solution mode (several), optional PDB /// emission, strong-name key copy, and the invariant that it never mutates persisted settings. /// [TestFixture] 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(); static ProjectExportOptions Options(string outputDir, bool generatePdb = false, bool embedSourceFilesInPdb = false, string? strongNameKeyFile = null) => new(outputDir, UseSdkStyleProjectFormat: true, UseNestedDirectoriesForNamespaces: false, RemoveDeadCode: false, RemoveDeadStores: false, UseDebugSymbols: false, StrongNameKeyFile: strongNameKeyFile, GeneratePdb: generatePdb, EmbedSourceFilesInPdb: embedSourceFilesInPdb); // PDB generation is exercised against a controlled assembly in // ICSharpCode.Decompiler.Tests.PdbGenerationTestRunner. It is deliberately NOT run here against // framework assemblies: the writer's DEBUG-only duplicate-sequence-point Debug.Assert (which also // [Ignore]s the Members PDB fixture) aborts the test host in a Debug build. The runner's project/ // solution structure and settings handling are what these tests cover. [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 tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProj_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var result = await ProjectExporter.ExportAsync( new[] { assembly }, solutionMode: false, Options(tempDir), new DecompilerSettings(), Language(), CancellationToken.None); result.Success.Should().BeTrue(result.StatusText); Directory.EnumerateFiles(tempDir, "*.csproj").Should().HaveCount(1); Directory.EnumerateFiles(tempDir, "*.cs", SearchOption.AllDirectories).Should().NotBeEmpty(); } finally { TryDelete(tempDir); } } [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 tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProjSln_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var result = await ProjectExporter.ExportAsync( assemblies, solutionMode: true, Options(tempDir), new DecompilerSettings(), Language(), CancellationToken.None); result.Success.Should().BeTrue(result.StatusText); Directory.EnumerateFiles(tempDir, "*.sln").Should().HaveCount(1); foreach (var a in assemblies) { var projectDir = Path.Combine(tempDir, a.ShortName); Directory.EnumerateFiles(projectDir, "*.csproj").Should().HaveCount(1, a.ShortName); } } finally { TryDelete(tempDir); } } [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 tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProjSnk_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); var keyFile = Path.Combine(Path.GetTempPath(), "ILSpyKey_" + System.Guid.NewGuid().ToString("N") + ".snk"); await File.WriteAllBytesAsync(keyFile, new byte[] { 1, 2, 3, 4 }); try { var result = await ProjectExporter.ExportAsync( new[] { assembly }, solutionMode: false, Options(tempDir, strongNameKeyFile: keyFile), new DecompilerSettings(), Language(), CancellationToken.None); result.Success.Should().BeTrue(result.StatusText); File.Exists(Path.Combine(tempDir, Path.GetFileName(keyFile))).Should().BeTrue( "the strong-name key must be copied next to the exported project"); } finally { TryDelete(tempDir); TryDeleteFile(keyFile); } } [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 settingsService = AppComposition.Current.GetExport(); bool originalSdk = settingsService.DecompilerSettings.UseSdkStyleProjectFormat; var tempDir = Path.Combine(Path.GetTempPath(), "ILSpyProjNoMutate_" + System.Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { // Flip every settings-backed toggle relative to the persisted value, on a CLONE. var options = Options(tempDir) with { UseSdkStyleProjectFormat = !originalSdk }; await ProjectExporter.ExportAsync( new[] { assembly }, solutionMode: false, options, settingsService.DecompilerSettings.Clone(), Language(), CancellationToken.None); settingsService.DecompilerSettings.UseSdkStyleProjectFormat.Should().Be(originalSdk, "exporting must apply overrides to a clone, never the persisted settings"); } finally { TryDelete(tempDir); } } static void TryDelete(string dir) { try { Directory.Delete(dir, recursive: true); } catch { /* best-effort */ } } static void TryDeleteFile(string file) { try { File.Delete(file); } catch { /* best-effort */ } } }