.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

203 lines
7.3 KiB

// 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;
/// <summary>
/// End-to-end tests of <see cref="ProjectExporter"/>, 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.
/// </summary>
[TestFixture]
public class ProjectExportRunnerTests
{
static CSharpLanguage Language()
=> AppComposition.Current.GetExport<LanguageService>().Languages.OfType<CSharpLanguage>().First();
static List<LoadedAssembly> 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<SettingsService>();
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 */ }
}
}