Files
ILSpy/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
T
Siegfried Pammer 8faa1848d5 Close the test fixtures that hold an assembly open
A PEFile keeps its assembly mapped until it is disposed, and three fixtures
opened one and never did: DuplicateAssemblyReferenceTests both directly and
for every reference its resolver hands back, TypeForwarderResolutionTests for
everything the resolver probes, and LocationsInAstTests for the module behind
each decompiler. Their teardowns then deleted the directory those files live
in, which cannot work while the mapping is open, so a Windows run reported a
failed teardown for a run that otherwise passed. They now dispose what they
opened, the way WpfProjectExportTests already did.

Deleting is cleanup either way, so it goes through RepeatOnIOError rather than
straight at the file system, and that helper no longer rethrows: its last of
five attempts was deliberately unguarded, and four retries ten milliseconds
apart is a short window to wait for a handle that a virus scanner or the
indexer still holds. It now backs off to about a second and a half and, failing
that, reports and leaves the file behind, which costs nothing.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 08:43:07 +02:00

1277 lines
48 KiB
C#

// Copyright (c) 2015 Daniel Grunwald
//
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
using CliWrap;
using CliWrap.Buffered;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.PdbProvider;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.Helpers
{
[Flags]
public enum CompilerOptions
{
None,
Optimize = 0x1,
UseDebug = 0x2,
Force32Bit = 0x4,
Library = 0x8,
UseRoslyn1_3_2 = 0x10,
UseMcs2_6_4 = 0x20,
ReferenceVisualBasic = 0x40,
TargetNet40 = 0x80,
GeneratePdb = 0x100,
Preview = 0x200,
UseRoslyn2_10_0 = 0x400,
UseRoslyn3_11_0 = 0x800,
UseRoslynLatest = 0x1000,
UseMcs5_23 = 0x2000,
UseTestRunner = 0x4000,
NullableEnable = 0x8000,
ReferenceUnsafe = 0x10000,
CheckForOverflowUnderflow = 0x20000,
ProcessXmlDoc = 0x40000,
UseRoslyn4_14_0 = 0x80000,
EnableRuntimeAsync = 0x100000,
UseMcsMask = UseMcs2_6_4 | UseMcs5_23,
UseRoslynMask = UseRoslyn1_3_2 | UseRoslyn2_10_0 | UseRoslyn3_11_0 | UseRoslyn4_14_0 | UseRoslynLatest
}
[Flags]
public enum AssemblerOptions
{
None,
UseDebug = 0x1,
Force32Bit = 0x2,
Library = 0x4,
// Testing our own disassembler, or working around a bug in ildasm.
UseOwnDisassembler = 0x8,
// Work around bug in .NET 5 ilasm (https://github.com/dotnet/runtime/issues/32400)
UseLegacyAssembler = 0x10,
// UseSortByNameFilter, implies UseOwnDisassembler
SortedOutput = 0x20,
}
public static partial class Tester
{
public static readonly string TesterPath;
public static readonly string TestCasePath;
static readonly string testRunnerBasePath;
static readonly string packagesPropsFile;
static readonly string roslynLatestVersion;
static readonly RoslynToolset roslynToolset;
static readonly VsWhereToolset vswhereToolset;
internal static readonly RefAssembliesToolset RefAssembliesToolset;
// ilasm/ildasm ship as `ilasm[.exe]` inside the Microsoft.NETCore.IL{,D}Asm
// per-RID runtime packages, picked up by NuGet's RID graph from the project's
// RuntimeIdentifier. The umbrella package drops the binary alongside the test
// assembly, but only Windows uses the .exe suffix.
static readonly string ExecutableExtension = OperatingSystem.IsWindows() ? ".exe" : "";
static string ResolveToolPath(string toolName) =>
Path.Combine(TesterPath, toolName + ExecutableExtension);
static Tester()
{
TesterPath = Path.GetDirectoryName(typeof(Tester).Assembly.Location);
TestCasePath = Path.Combine(TesterPath, "../../../../TestCases");
#if DEBUG
testRunnerBasePath = Path.Combine(TesterPath, $"../../../../../ICSharpCode.Decompiler.TestRunner/bin/Debug/net{CurrentNetCoreVersion.Version}");
#else
testRunnerBasePath = Path.Combine(TesterPath, $"../../../../../ICSharpCode.Decompiler.TestRunner/bin/Release/net{CurrentNetCoreVersion.Version}");
#endif
// To parse: <Project><ItemGroup><PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0-3.final" />
packagesPropsFile = Path.Combine(TesterPath, "../../../../../Directory.Packages.props");
roslynLatestVersion = ((IEnumerable<object>)(XDocument
.Load(packagesPropsFile)
.XPathEvaluate("//Project//PropertyGroup//RoslynVersion")))
.OfType<XElement>()
.Single()
.Value;
roslynToolset = new RoslynToolset();
vswhereToolset = new VsWhereToolset();
RefAssembliesToolset = new RefAssembliesToolset();
}
internal static async Task Initialize()
{
await roslynToolset.Fetch("1.3.2", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false);
if (OperatingSystem.IsWindows())
{
await roslynToolset.Fetch("2.10.0", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false);
}
else
{
// Microsoft.Net.Compilers only ships .NET Framework executables. The sibling
// Microsoft.NETCore.Compilers package contains the dotnet-hosted build of the
// same compiler version (tools/bincore/csc.dll), usable on any platform.
await roslynToolset.Fetch("2.10.0", "Microsoft.NETCore.Compilers", "tools/bincore").ConfigureAwait(false);
}
// On non-Windows hosts the net472 compiler binaries cannot be executed; use the
// .NET build of each toolset instead. Its tasks folder is named "netcoreapp3.1"
// up to Roslyn 3.x and "netcore" from Roslyn 4.x on.
await roslynToolset.Fetch("3.11.0", sourcePath: OperatingSystem.IsWindows() ? "tasks/net472" : "tasks/netcoreapp3.1").ConfigureAwait(false);
await roslynToolset.Fetch("4.14.0", sourcePath: OperatingSystem.IsWindows() ? "tasks/net472" : "tasks/netcore").ConfigureAwait(false);
await roslynToolset.Fetch(roslynLatestVersion, sourcePath: OperatingSystem.IsWindows() ? "tasks/net472" : "tasks/netcore").ConfigureAwait(false);
await vswhereToolset.Fetch().ConfigureAwait(false);
await RefAssembliesToolset.Fetch("5.0.0", sourcePath: "ref/net5.0").ConfigureAwait(false);
await RefAssembliesToolset.Fetch("9.0.0", sourcePath: "ref/net9.0").ConfigureAwait(false);
await RefAssembliesToolset.Fetch(CurrentNetCoreVersion.RefAsmVersion, sourcePath: $"ref/net{CurrentNetCoreVersion.Version}").ConfigureAwait(false);
#if DEBUG
const string testRunnerConfig = "Debug";
#else
const string testRunnerConfig = "Release";
#endif
if (OperatingSystem.IsWindows())
{
await BuildTestRunner("win-x86", testRunnerConfig).ConfigureAwait(false);
await BuildTestRunner("win-x64", testRunnerConfig).ConfigureAwait(false);
}
else
{
// Self-contained build for the host platform. The RID must be one of the
// RuntimeIdentifiers pinned in ICSharpCode.Decompiler.TestRunner.csproj.
await BuildTestRunner(OperatingSystem.IsMacOS() ? "osx-arm64" : "linux-x64", testRunnerConfig).ConfigureAwait(false);
}
}
/// <summary>
/// True when a Mono runtime is on the PATH. Mono hosts the .NET Framework builds of
/// compilers that have no .NET build (Roslyn 1.x csc.exe from Microsoft.Net.Compilers);
/// Roslyn 2.x+ configurations use dotnet-hosted builds and do not depend on Mono.
/// </summary>
public static readonly bool MonoRuntimeAvailable =
!OperatingSystem.IsWindows()
&& (Environment.GetEnvironmentVariable("PATH") ?? "")
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
.Any(dir => File.Exists(Path.Combine(dir, "mono")));
/// <summary>
/// Reduces a compiler-configuration matrix to the entries usable on the current platform.
/// On Windows every configuration is supported. On other platforms, configurations that
/// depend on Windows-only tools or runtimes are removed: the legacy (pre-Roslyn) csc/vbc
/// and Force32Bit (requires a 32-bit runtime) always; Roslyn 1.x/2.x (their packages only
/// ship .NET Framework binaries) and mcs unless a Mono runtime is available to host them.
/// When <paramref name="executesCompiledOutput"/> is set (correctness-style fixtures),
/// configurations targeting .NET Framework 4.0 are removed as well, because their output
/// executables only run on Windows, and the Mono-hosted compilers are not included either
/// (their output would likewise need a Mono host to execute).
/// </summary>
public static CompilerOptions[] SupportedOnCurrentPlatform(CompilerOptions[] configurations, bool executesCompiledOutput = false)
{
if (OperatingSystem.IsWindows())
return configurations;
const CompilerOptions dotnetHostedCompilers = CompilerOptions.UseRoslyn3_11_0 | CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.UseRoslynLatest;
CompilerOptions supportedCompilers = dotnetHostedCompilers;
if (!executesCompiledOutput)
{
// The dotnet-hosted Roslyn 2.x build (Microsoft.NETCore.Compilers) can compile
// on any platform, but its output targets .NET Core 2.2 or .NET Framework,
// which the correctness runners cannot execute here.
supportedCompilers |= CompilerOptions.UseRoslyn2_10_0;
// Roslyn 1.3.2 has only a .NET Framework build and needs Mono as its host.
// The configuration stays in the matrix even without Mono so that a missing
// runtime shows up as skipped tests (see WrapCompiler) instead of a silently
// smaller matrix. mcs 2.6.4 stays excluded: it needs the Reflection.Emit
// COMPILER_ACCESS mode that current Mono runtimes no longer implement.
supportedCompilers |= CompilerOptions.UseRoslyn1_3_2;
}
return configurations.Where(c => (c & supportedCompilers) != 0
&& !c.HasFlag(CompilerOptions.Force32Bit)
&& !(executesCompiledOutput && c.HasFlag(CompilerOptions.TargetNet40))).ToArray();
}
/// <summary>
/// Whether to pass the undocumented /shared switch, which makes the compiler use the
/// VBCSCompiler compiler server and so avoids paying compiler startup per invocation.
/// The dotnet-hosted Roslyn 2.x build cannot start its compiler server under a current
/// 'dotnet' host (the roll-forward in WrapCompiler applies to csc/vbc itself, not to
/// the server process the client tries to spawn), so with /shared every invocation
/// first waits out the client's full 20-second new-server connection timeout before
/// falling back to a sub-second in-process compile. Only the dotnet-hosted Roslyn 3.0+
/// builds get the switch on non-Windows platforms; the Mono-hosted 1.x compilers skip
/// it too rather than rely on the server protocol working under Mono.
/// </summary>
static bool UseCompilerServer(string roslynVersion)
{
return OperatingSystem.IsWindows()
|| Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion)).Major > 2;
}
/// <summary>
/// Wraps an external compiler invocation. The .NET Framework builds of the Roslyn
/// compilers (csc.exe/vbc.exe) are directly executable on Windows and are hosted by
/// Mono elsewhere; the .NET builds ship as a .dll that must be launched through the
/// 'dotnet' host.
/// </summary>
static Command WrapCompiler(string compilerPath, string arguments)
{
// Old compiler builds pin an out-of-support runtime in their runtimeconfig
// (e.g. Roslyn 2.x pins Microsoft.NETCore.App 2.0), so allow the host to
// roll forward across major versions to whatever runtime is installed.
if (compilerPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
return Cli.Wrap("dotnet").WithArguments($"--roll-forward LatestMajor \"{compilerPath}\" {arguments}");
if (!OperatingSystem.IsWindows() && compilerPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
if (!MonoRuntimeAvailable)
{
Assert.Ignore($"{Path.GetFileName(compilerPath)} requires a Mono runtime on the PATH to run on this platform.");
}
return Cli.Wrap("mono").WithArguments($"\"{compilerPath}\" {arguments}");
}
return Cli.Wrap(compilerPath).WithArguments(arguments);
}
static async Task BuildTestRunner(string runtime, string config)
{
await Cli.Wrap("dotnet")
.WithArguments(new[] { "build", Path.Combine(TesterPath, "../../../../../ICSharpCode.Decompiler.TestRunner/ICSharpCode.Decompiler.TestRunner.csproj"), "-r", runtime, "-c", config, "--self-contained" })
.ExecuteAsync();
}
public static async Task<string> AssembleIL(string sourceFileName, AssemblerOptions options = AssemblerOptions.UseDebug)
{
string ilasmPath;
if (options.HasFlag(AssemblerOptions.UseLegacyAssembler))
{
if (!OperatingSystem.IsWindows())
Assert.Ignore("UseLegacyAssembler requires the .NET Framework ilasm.exe, which only exists on Windows.");
ilasmPath = Path.Combine(Environment.GetEnvironmentVariable("windir"), @"Microsoft.NET\Framework\v4.0.30319\ilasm.exe");
}
else
{
ilasmPath = ResolveToolPath("ilasm");
}
string outputFile = Path.Combine(Path.GetDirectoryName(sourceFileName), Path.GetFileNameWithoutExtension(sourceFileName));
// Options use the '-' prefix because on Unix the ilasm option parser treats
// '/option' as a file path; the Windows builds accept both prefixes.
string otherOptions = " ";
if (options.HasFlag(AssemblerOptions.Force32Bit))
{
outputFile += ".32";
otherOptions += "-32BitPreferred ";
}
if (options.HasFlag(AssemblerOptions.Library))
{
outputFile += ".dll";
otherOptions += "-dll ";
}
else
{
outputFile += ".exe";
otherOptions += "-exe ";
}
if (options.HasFlag(AssemblerOptions.UseDebug))
{
otherOptions += "-debug ";
}
var command = Cli.Wrap(ilasmPath)
.WithArguments($"-quiet {otherOptions}-output=\"{outputFile}\" \"{sourceFileName}\"")
.WithValidation(CommandResultValidation.None);
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
}
Assert.That(result.ExitCode, Is.EqualTo(0), "ilasm failed");
return outputFile;
}
public static async Task<string> Disassemble(string sourceFileName, string outputFile, AssemblerOptions asmOptions)
{
if (asmOptions.HasFlag(AssemblerOptions.UseOwnDisassembler) || asmOptions.HasFlag(AssemblerOptions.SortedOutput))
{
using (var peFileStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read))
using (var peFile = new PEFile(sourceFileName, peFileStream))
using (var writer = new StringWriter())
{
var metadata = peFile.Metadata;
var output = new PlainTextOutput(writer);
ReflectionDisassembler rd = new ReflectionDisassembler(output, CancellationToken.None);
if (asmOptions.HasFlag(AssemblerOptions.SortedOutput))
{
rd.EntityProcessor = new SortByNameProcessor();
}
rd.AssemblyResolver = new UniversalAssemblyResolver(sourceFileName, throwOnError: true, null);
rd.DetectControlStructure = false;
rd.WriteAssemblyReferences(metadata);
if (metadata.IsAssembly)
rd.WriteAssemblyHeader(peFile);
output.WriteLine();
rd.WriteModuleHeader(peFile, skipMVID: true);
output.WriteLine();
rd.WriteModuleContents(peFile);
File.WriteAllText(outputFile, ReplacePrivImplDetails(writer.ToString()));
}
return outputFile;
}
string ildasmPath = ResolveToolPath("ildasm");
// '-' option prefix: see AssembleIL.
var command = Cli.Wrap(ildasmPath)
.WithArguments($"-utf8 -out=\"{outputFile}\" \"{sourceFileName}\"")
.WithValidation(CommandResultValidation.None);
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
}
Assert.That(result.ExitCode, Is.EqualTo(0), "ildasm failed");
// Unlike the .imagebase directive (which is a fixed value when compiling with /deterministic),
// the image base comment still varies... ildasm putting a random number here?
string il = File.ReadAllText(outputFile);
il = Regex.Replace(il, @"^// Image base: 0x[0-9A-F]+\r?\n", "", RegexOptions.Multiline);
// and while we're at it, also remove the MVID
il = Regex.Replace(il, @"^// MVID: \{[0-9A-F-]+\}\r?\n", "", RegexOptions.Multiline);
// and the ildasm version info (varies from system to system)
il = Regex.Replace(il, @"^// +Microsoft .* Disassembler\. +Version.*\r?\n", "", RegexOptions.Multiline);
// copyright header "All rights reserved" is dependent on system language
il = Regex.Replace(il, @"^// +Copyright .* Microsoft.*\r?\n", "", RegexOptions.Multiline);
// filename may contain full path
il = Regex.Replace(il, @"^// WARNING: Created Win32 resource file.*\r?\n", "", RegexOptions.Multiline);
il = ReplacePrivImplDetails(il);
File.WriteAllText(outputFile, il);
return outputFile;
}
private static string ReplacePrivImplDetails(string il)
{
return Regex.Replace(il, @"'<PrivateImplementationDetails>\{[0-9A-F-]+\}'", "'<PrivateImplementationDetails>'");
}
static readonly string[] defaultReferences = new[] {
"System.dll",
"System.Core.dll",
"System.Runtime.dll",
"System.Xml.dll",
"Microsoft.CSharp.dll"
};
static readonly string[] core220DefaultReferences = new[]
{
"netstandard.dll",
"mscorlib.dll",
"System.dll",
"System.Collections.dll",
"System.Console.dll",
"System.Core.dll",
"System.Linq.dll",
"System.Linq.Expressions.dll",
"System.Linq.Queryable.dll",
"System.IO.FileSystem.Watcher.dll",
"System.Memory.dll",
"System.Private.CoreLib.dll",
"System.Private.Xml.dll",
"System.Threading.dll",
"System.Threading.Thread.dll",
"System.Runtime.dll",
"System.Runtime.Extensions.dll",
"System.Runtime.InteropServices.dll",
"System.Xml.dll",
"System.Xml.ReaderWriter.dll",
"System.ValueTuple.dll",
"System.Collections.NonGeneric.dll",
"Microsoft.CSharp.dll",
"Microsoft.VisualBasic.dll",
};
static readonly string[] coreDefaultReferences = new[]
{
"netstandard.dll",
"mscorlib.dll",
"System.dll",
"System.Collections.dll",
"System.Console.dll",
"System.Core.dll",
"System.Linq.dll",
"System.Linq.Expressions.dll",
"System.Linq.Queryable.dll",
"System.IO.FileSystem.Watcher.dll",
"System.Memory.dll",
"System.ObjectModel.dll",
"System.Threading.dll",
"System.Threading.Thread.dll",
"System.Runtime.dll",
"System.Runtime.InteropServices.dll",
"System.Xml.dll",
"System.Xml.ReaderWriter.dll",
"System.ValueTuple.dll",
"Microsoft.CSharp.dll",
"Microsoft.VisualBasic.dll",
};
public static IReadOnlyList<string> CoreDefaultReferences => coreDefaultReferences;
static readonly Dictionary<string, Lazy<string>> targetFrameworkAttributeSnippetFiles = new() {
{ CurrentNetCoreVersion.AppVersion, new Lazy<string>(() => GetTargetFrameworkAttributeSnippetFile(CurrentNetCoreVersion.AppVersion)) },
{ ".NETCoreApp,Version=v9.0", new Lazy<string>(() => GetTargetFrameworkAttributeSnippetFile(".NETCoreApp,Version=v9.0")) },
{ ".NETCoreApp,Version=v5.0", new Lazy<string>(() => GetTargetFrameworkAttributeSnippetFile(".NETCoreApp,Version=v5.0")) },
{ ".NETCoreApp,Version=v2.2", new Lazy<string>(() => GetTargetFrameworkAttributeSnippetFile(".NETCoreApp,Version=v2.2")) },
};
static string GetTargetFrameworkAttributeSnippetFile(string targetFrameworkMoniker)
{
// Note: this leaks a temporary file, we're not attempting to delete it, because it is only one.
var tempFile = Path.GetTempFileName();
File.WriteAllText(tempFile, $@"
[assembly: System.Runtime.Versioning.TargetFramework(""{targetFrameworkMoniker}"")]
");
return tempFile;
}
const string nonEmbeddedAttributesSnippet = @"
using System;
#if !NET60
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
internal sealed class CompilerFeatureRequiredAttribute : Attribute
{
public CompilerFeatureRequiredAttribute(string featureName)
{
}
}
internal class IsExternalInit
{
}
#endif
#if !NET70
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal sealed class RequiredMemberAttribute : Attribute
{
}
#endif
#if !NET60
}
#endif
";
static readonly Lazy<string> nonEmbeddedAttributesSnippetFile = new Lazy<string>(GetNonEmbeddedAttributesSnippetFile);
static string GetNonEmbeddedAttributesSnippetFile()
{
// Note: this leaks a temporary file, we're not attempting to delete it, because it is only one.
var tempFile = Path.GetTempFileName();
File.WriteAllText(tempFile, nonEmbeddedAttributesSnippet);
return tempFile;
}
public static List<string> GetPreprocessorSymbols(CompilerOptions flags)
{
var preprocessorSymbols = new List<string>();
if (flags.HasFlag(CompilerOptions.UseDebug))
{
preprocessorSymbols.Add("DEBUG");
}
if (flags.HasFlag(CompilerOptions.Optimize))
{
preprocessorSymbols.Add("OPT");
}
if (flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NET40");
}
if ((flags & CompilerOptions.UseRoslynMask) != 0)
{
if (!flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NETCORE");
}
preprocessorSymbols.Add("ROSLYN");
preprocessorSymbols.Add("CS60");
preprocessorSymbols.Add("VB11");
preprocessorSymbols.Add("VB14");
if (flags.HasFlag(CompilerOptions.UseRoslyn2_10_0)
|| flags.HasFlag(CompilerOptions.UseRoslyn3_11_0)
|| flags.HasFlag(CompilerOptions.UseRoslyn4_14_0)
|| flags.HasFlag(CompilerOptions.UseRoslynLatest))
{
preprocessorSymbols.Add("ROSLYN2");
preprocessorSymbols.Add("CS70");
preprocessorSymbols.Add("CS71");
preprocessorSymbols.Add("CS72");
preprocessorSymbols.Add("CS73");
preprocessorSymbols.Add("VB15");
}
if (flags.HasFlag(CompilerOptions.UseRoslyn3_11_0)
|| flags.HasFlag(CompilerOptions.UseRoslyn4_14_0)
|| flags.HasFlag(CompilerOptions.UseRoslynLatest))
{
if (!flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NET50");
}
preprocessorSymbols.Add("ROSLYN3");
preprocessorSymbols.Add("CS80");
preprocessorSymbols.Add("CS90");
preprocessorSymbols.Add("VB16");
}
if (flags.HasFlag(CompilerOptions.UseRoslyn4_14_0)
|| flags.HasFlag(CompilerOptions.UseRoslynLatest))
{
if (!flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NET60");
preprocessorSymbols.Add("NET70");
preprocessorSymbols.Add("NET80");
preprocessorSymbols.Add("NET90");
}
preprocessorSymbols.Add("ROSLYN4");
preprocessorSymbols.Add("CS100");
preprocessorSymbols.Add("CS110");
preprocessorSymbols.Add("CS120");
preprocessorSymbols.Add("CS130");
}
if (flags.HasFlag(CompilerOptions.UseRoslynLatest))
{
if (!flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NET100");
preprocessorSymbols.Add("NET110");
}
preprocessorSymbols.Add("ROSLYN5");
preprocessorSymbols.Add("CS140");
preprocessorSymbols.Add("CS150");
}
}
else if ((flags & CompilerOptions.UseMcsMask) != 0)
{
preprocessorSymbols.Add("MCS");
if (flags.HasFlag(CompilerOptions.UseMcs2_6_4))
{
preprocessorSymbols.Add("MCS2");
}
if (flags.HasFlag(CompilerOptions.UseMcs5_23))
{
preprocessorSymbols.Add("MCS5");
}
}
else
{
preprocessorSymbols.Add("LEGACY_CSC");
preprocessorSymbols.Add("LEGACY_VBC");
}
if (flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
preprocessorSymbols.Add("RUNTIMEASYNC");
}
return preprocessorSymbols;
}
public static async Task<CompilerResults> CompileCSharp(string sourceFileName, CompilerOptions flags = CompilerOptions.UseDebug, string outputFileName = null)
{
List<string> sourceFileNames = new List<string> { sourceFileName };
foreach (Match match in Regex.Matches(File.ReadAllText(sourceFileName), @"#include ""([\w\d./]+)"""))
{
sourceFileNames.Add(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFileName), match.Groups[1].Value)));
}
List<string> dependencyAssemblies = new List<string>();
string sourceText = File.ReadAllText(sourceFileName);
foreach (Match match in Regex.Matches(sourceText, @"//\s*#dependency\s+([\w\d./\\]+)"))
{
string depSourcePath = Path.GetFullPath(Path.Combine(
Path.GetDirectoryName(sourceFileName), match.Groups[1].Value));
var depResults = await CompileCSharp(depSourcePath, flags | CompilerOptions.Library).ConfigureAwait(false);
dependencyAssemblies.Add(Path.GetFullPath(depResults.PathToAssembly));
}
bool targetNet40 = (flags & CompilerOptions.TargetNet40) != 0;
bool useRoslyn = (flags & CompilerOptions.UseRoslynMask) != 0;
if (targetNet40)
{
sourceFileNames.Add(nonEmbeddedAttributesSnippetFile.Value);
}
var preprocessorSymbols = GetPreprocessorSymbols(flags);
if ((flags & CompilerOptions.UseMcsMask) == 0)
{
CompilerResults results = new CompilerResults();
results.PathToAssembly = outputFileName;
var (roslynVersion, languageVersion, targetFramework) = (flags & CompilerOptions.UseRoslynMask) switch {
0 => ("legacy", "5", null),
CompilerOptions.UseRoslyn1_3_2 => ("1.3.2", "6", null),
CompilerOptions.UseRoslyn2_10_0 => ("2.10.0", "latest", targetNet40 ? null : ".NETCoreApp,Version=v2.2"),
CompilerOptions.UseRoslyn3_11_0 => ("3.11.0", "latest", targetNet40 ? null : ".NETCoreApp,Version=v5.0"),
CompilerOptions.UseRoslyn4_14_0 => ("4.14.0", "latest", targetNet40 ? null : ".NETCoreApp,Version=v9.0"),
_ => (roslynLatestVersion, flags.HasFlag(CompilerOptions.Preview) ? "preview" : "latest", targetNet40 ? null : CurrentNetCoreVersion.AppVersion)
};
var cscPath = roslynToolset.GetCSharpCompiler(roslynVersion);
string libPath, refAsmPath;
IEnumerable<string> references;
if (useRoslyn && targetFramework != null)
{
refAsmPath = RefAssembliesToolset.GetPath(targetFramework);
if (targetFramework == ".NETCoreApp,Version=v2.2")
{
references = core220DefaultReferences;
if (flags.HasFlag(CompilerOptions.ReferenceVisualBasic))
{
references = references.Append("Microsoft.VisualBasic.dll");
}
}
else
{
references = coreDefaultReferences;
if (flags.HasFlag(CompilerOptions.ReferenceVisualBasic))
{
references = references.Append("Microsoft.VisualBasic.dll");
references = references.Append("Microsoft.VisualBasic.Core.dll");
}
}
libPath = "\"" + refAsmPath + "\"";
sourceFileNames.Add(targetFrameworkAttributeSnippetFiles[targetFramework].Value);
}
else
{
refAsmPath = RefAssembliesToolset.GetPath("legacy");
libPath = "\"" + refAsmPath + "\"";
references = defaultReferences;
if (!OperatingSystem.IsWindows())
{
// The .NET Framework csc.exe implicitly references the mscorlib of its
// runtime directory; the dotnet-hosted compiler has no implicit
// references, so the target framework's mscorlib must be passed explicitly.
references = references.Prepend("mscorlib.dll");
}
if (flags.HasFlag(CompilerOptions.ReferenceVisualBasic))
{
references = references.Append("Microsoft.VisualBasic.dll");
}
}
if (useRoslyn && !targetNet40 && flags.HasFlag(CompilerOptions.ReferenceUnsafe))
{
references = references.Append("System.Runtime.CompilerServices.Unsafe.dll");
}
references = references.Select(r => "-r:\"" + Path.Combine(refAsmPath, r) + "\"");
foreach (var dependency in dependencyAssemblies)
{
references = references.Append($"-r:\"{dependency}\"");
}
string otherOptions = $"-nologo -noconfig " +
$"-langversion:{languageVersion} " +
$"-unsafe -o{(flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ")}";
HashSet<string> noWarn = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (roslynVersion != "legacy")
{
if (UseCompilerServer(roslynVersion))
{
otherOptions += "/shared ";
}
var version = Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion));
if (!targetNet40 && version.Major > 2)
{
if (flags.HasFlag(CompilerOptions.NullableEnable))
otherOptions += "/nullable+ ";
else
otherOptions += "/nullable- ";
}
if (!targetNet40 && roslynVersion == roslynLatestVersion && flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
otherOptions += "/features:runtime-async=on ";
}
}
if (flags.HasFlag(CompilerOptions.Library))
{
otherOptions += "-t:library ";
}
else
{
otherOptions += "-t:exe ";
}
if (flags.HasFlag(CompilerOptions.GeneratePdb))
{
// The Windows PDB writer needs the native DiaSymReader, so only portable
// PDBs work on other platforms. Roslyn 2.x+ falls back to portable on its
// own; Roslyn 1.x needs the explicit request.
if (!OperatingSystem.IsWindows() && (flags & CompilerOptions.UseRoslynMask) != 0)
{
otherOptions += "-debug:portable ";
}
else
{
otherOptions += "-debug:full ";
}
}
else
{
otherOptions += "-debug- ";
}
if (flags.HasFlag(CompilerOptions.Force32Bit))
{
otherOptions += "-platform:x86 ";
}
else
{
otherOptions += "-platform:anycpu ";
}
if (flags.HasFlag(CompilerOptions.ProcessXmlDoc))
{
otherOptions += $"-doc:\"{Path.ChangeExtension(results.PathToAssembly, ".xml")}\" ";
noWarn.Add("CS1591"); // Missing XML comment for publicly visible type or member 'Type_or_Member'
}
if (flags.HasFlag(CompilerOptions.CheckForOverflowUnderflow))
{
otherOptions += "-checked+ ";
}
else
{
otherOptions += "-checked- ";
}
if (preprocessorSymbols.Count > 0)
{
otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
}
if (noWarn.Count > 0)
{
otherOptions += " -nowarn:" + string.Join(",", noWarn);
}
var command = WrapCompiler(cscPath, $"{otherOptions} -lib:{libPath} {string.Join(" ", references)} -out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}")
.WithValidation(CommandResultValidation.None);
//Console.WriteLine($"\"{command.TargetFilePath}\" {command.Arguments}");
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
}
Assert.That(result.ExitCode, Is.EqualTo(0), "csc failed");
return results;
}
else
{
CompilerResults results = new CompilerResults();
results.PathToAssembly = outputFileName;
string testBasePath = Roundtrip.RoundtripAssembly.TestDir;
if (!Directory.Exists(testBasePath))
{
Assert.Ignore($"Compilation with mcs ignored: test directory '{testBasePath}' needs to be checked out separately." + Environment.NewLine +
$"git clone https://github.com/icsharpcode/ILSpy-tests \"{testBasePath}\"");
}
// On Windows the submodule's bat launchers run the bundled Windows-native Mono;
// elsewhere the bundled compilers (managed .NET Framework assemblies) are hosted
// by the system Mono runtime via WrapCompiler.
string mcsPath = (flags & CompilerOptions.UseMcsMask, OperatingSystem.IsWindows()) switch {
(CompilerOptions.UseMcs5_23, true) => Path.Combine(testBasePath, @"mcs\5.23\bin\mcs.bat"),
(CompilerOptions.UseMcs5_23, false) => Path.Combine(testBasePath, "mcs", "5.23", "lib", "mono", "4.5", "mcs.exe"),
(_, true) => Path.Combine(testBasePath, @"mcs\2.6.4\bin\gmcs.bat"),
(_, false) => Path.Combine(testBasePath, "mcs", "2.6.4", "lib", "mono", "2.0", "gmcs.exe")
};
string otherOptions = " -unsafe -o" + (flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ");
if (flags.HasFlag(CompilerOptions.Library))
{
otherOptions += "-t:library ";
}
else
{
otherOptions += "-t:exe ";
}
if (flags.HasFlag(CompilerOptions.UseDebug))
{
otherOptions += "-g ";
}
if (flags.HasFlag(CompilerOptions.Force32Bit))
{
otherOptions += "-platform:x86 ";
}
else
{
otherOptions += "-platform:anycpu ";
}
if (preprocessorSymbols.Count > 0)
{
otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
}
var command = WrapCompiler(mcsPath, $"{otherOptions}-out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}")
.WithValidation(CommandResultValidation.None);
//Console.WriteLine($"\"{command.TargetFilePath}\" {command.Arguments}");
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
}
Assert.That(result.ExitCode, Is.EqualTo(0), "mcs failed");
return results;
}
}
internal static DecompilerSettings GetSettings(CompilerOptions cscOptions)
{
if ((cscOptions & CompilerOptions.UseRoslynMask) != 0)
{
var langVersion = (cscOptions & CompilerOptions.UseRoslynMask) switch {
CompilerOptions.UseRoslyn1_3_2 => CSharp.LanguageVersion.CSharp6,
CompilerOptions.UseRoslyn2_10_0 => CSharp.LanguageVersion.CSharp7_3,
CompilerOptions.UseRoslyn3_11_0 => CSharp.LanguageVersion.CSharp9_0,
CompilerOptions.UseRoslyn4_14_0 => CSharp.LanguageVersion.CSharp13_0,
_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp14_0,
};
DecompilerSettings settings = new(langVersion) {
// Never use file-scoped namespaces
FileScopedNamespaces = false
};
return settings;
}
else
{
var settings = new DecompilerSettings(CSharp.LanguageVersion.CSharp5);
if ((cscOptions & CompilerOptions.UseMcsMask) != 0)
{
// we don't recompile with mcs but with roslyn, so we can use ref locals
settings.UseRefLocalsForAccurateOrderOfEvaluation = true;
}
return settings;
}
}
public static void CompileCSharpWithPdb(string assemblyName, Dictionary<string, string> sourceFiles,
CompilerOptions compilerOptions = CompilerOptions.None)
{
var parseOptions = new CSharpParseOptions(languageVersion: Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest);
if (compilerOptions.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
// Mirror the out-of-process csc '/features:runtime-async=on' flag so the compiler lowers
// async methods to the .NET 11 runtime-async form (MethodImplAsync bit, no state machine).
parseOptions = parseOptions.WithFeatures(new[] { new KeyValuePair<string, string>("runtime-async", "on") });
}
List<EmbeddedText> embeddedTexts = new List<EmbeddedText>();
List<SyntaxTree> syntaxTrees = new List<SyntaxTree>();
foreach (KeyValuePair<string, string> file in sourceFiles)
{
var sourceText = SourceText.From(file.Value, new UTF8Encoding(false), SourceHashAlgorithm.Sha256);
syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(sourceText, parseOptions, file.Key));
embeddedTexts.Add(EmbeddedText.FromSource(file.Key, sourceText));
}
var compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(assemblyName),
syntaxTrees, coreDefaultReferences.Select(r => MetadataReference.CreateFromFile(Path.Combine(RefAssembliesToolset.GetPath(CurrentNetCoreVersion.AppVersion), r))),
new CSharpCompilationOptions(
compilerOptions.HasFlag(CompilerOptions.Library)
? OutputKind.DynamicallyLinkedLibrary
: OutputKind.ConsoleApplication,
platform: Platform.AnyCpu,
optimizationLevel: OptimizationLevel.Release,
allowUnsafe: true,
deterministic: true
));
using (FileStream peStream = File.Open(assemblyName + ".dll", FileMode.OpenOrCreate, FileAccess.ReadWrite))
using (FileStream pdbStream = File.Open(assemblyName + ".pdb", FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
var emitResult = compilation.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: assemblyName + ".pdb"), embeddedTexts: embeddedTexts);
if (!emitResult.Success)
{
StringBuilder b = new StringBuilder("Compiler error:");
foreach (var diag in emitResult.Diagnostics)
{
b.AppendLine(diag.ToString());
}
throw new Exception(b.ToString());
}
}
}
internal static string GetSuffix(CompilerOptions cscOptions)
{
string suffix = "";
if ((cscOptions & CompilerOptions.Optimize) != 0)
suffix += ".opt";
if ((cscOptions & CompilerOptions.Force32Bit) != 0)
suffix += ".32";
if ((cscOptions & CompilerOptions.UseDebug) != 0)
suffix += ".dbg";
if ((cscOptions & CompilerOptions.TargetNet40) != 0)
suffix += ".net40";
if ((cscOptions & CompilerOptions.UseRoslyn1_3_2) != 0)
suffix += ".roslyn1";
if ((cscOptions & CompilerOptions.UseRoslyn2_10_0) != 0)
suffix += ".roslyn2";
if ((cscOptions & CompilerOptions.UseRoslyn3_11_0) != 0)
suffix += ".roslyn3";
if ((cscOptions & CompilerOptions.UseRoslyn4_14_0) != 0)
suffix += ".roslyn4";
if ((cscOptions & CompilerOptions.UseRoslynLatest) != 0)
suffix += ".roslyn";
if ((cscOptions & CompilerOptions.UseMcs2_6_4) != 0)
suffix += ".mcs2";
if ((cscOptions & CompilerOptions.UseMcs5_23) != 0)
suffix += ".mcs5";
if ((cscOptions & CompilerOptions.EnableRuntimeAsync) != 0)
suffix += ".runtimeasync";
return suffix;
}
public static async Task<(int ExitCode, string Output, string Error)> Run(string assemblyFileName)
{
var command = Cli.Wrap(assemblyFileName)
.WithValidation(CommandResultValidation.None);
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
return (result.ExitCode, result.StandardOutput, result.StandardError);
}
public static async Task<(int ExitCode, string Output, string Error)> RunWithTestRunner(string assemblyFileName, bool force32Bit)
{
string runnerRid;
if (OperatingSystem.IsWindows())
{
runnerRid = force32Bit ? "win-x86" : "win-x64";
}
else
{
// Safety net: 32-bit runs need a 32-bit runtime, which only exists on Windows.
// Configurations using Force32Bit are not enumerated on non-Windows platforms.
if (force32Bit)
Assert.Ignore("Force32Bit requires a 32-bit .NET runtime, which is only available on Windows.");
runnerRid = OperatingSystem.IsMacOS() ? "osx-arm64" : "linux-x64";
}
string testRunner = Path.Combine(testRunnerBasePath, runnerRid, "ICSharpCode.Decompiler.TestRunner" + ExecutableExtension);
var command = Cli.Wrap(testRunner)
.WithArguments(assemblyFileName)
.WithValidation(CommandResultValidation.None);
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
return (result.ExitCode, result.StandardOutput, result.StandardError);
}
public static Task<string> DecompileCSharp(string assemblyFileName, DecompilerSettings settings = null)
{
if (settings == null)
settings = new DecompilerSettings();
using (var file = new FileStream(assemblyFileName, FileMode.Open, FileAccess.Read))
{
var module = new PEFile(assemblyFileName, file, PEStreamOptions.PrefetchEntireImage);
string targetFramework = module.Metadata.DetectTargetFrameworkId();
var resolver = new UniversalAssemblyResolver(assemblyFileName, false,
targetFramework, null, PEStreamOptions.PrefetchMetadata);
resolver.AddSearchDirectory(RefAssembliesToolset.GetPath(targetFramework));
var typeSystem = new DecompilerTypeSystem(module, resolver, settings);
CSharpDecompiler decompiler = new CSharpDecompiler(typeSystem, settings);
decompiler.AstTransforms.Insert(0, new RemoveEmbeddedAttributes());
decompiler.AstTransforms.Insert(0, new RemoveCompilerAttribute());
decompiler.AstTransforms.Insert(0, new RemoveNamespaceMy());
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
var pdbFileName = Path.ChangeExtension(assemblyFileName, ".pdb");
if (File.Exists(pdbFileName))
decompiler.DebugInfoProvider = DebugInfoUtils.FromFile(module, pdbFileName);
var xmlDocFileName = Path.ChangeExtension(assemblyFileName, ".xml");
if (File.Exists(xmlDocFileName))
decompiler.DocumentationProvider = new XmlDocumentationProvider(xmlDocFileName);
var syntaxTree = decompiler.DecompileWholeModuleAsSingleFile(sortTypes: true);
StringWriter output = new StringWriter();
CSharpFormattingOptions formattingPolicy = CreateFormattingPolicyForTests();
var visitor = new CSharpOutputVisitor(output, formattingPolicy);
syntaxTree.AcceptVisitor(visitor);
string fileName = Path.GetTempFileName();
File.WriteAllText(fileName, output.ToString());
return Task.FromResult(fileName);
}
}
private static CSharpFormattingOptions CreateFormattingPolicyForTests()
{
var formattingPolicy = FormattingOptionsFactory.CreateSharpDevelop();
formattingPolicy.StatementBraceStyle = BraceStyle.NextLine;
formattingPolicy.CatchNewLinePlacement = NewLinePlacement.NewLine;
formattingPolicy.ElseNewLinePlacement = NewLinePlacement.NewLine;
formattingPolicy.FinallyNewLinePlacement = NewLinePlacement.NewLine;
formattingPolicy.SpaceBeforeAnonymousMethodParentheses = true;
return formattingPolicy;
}
public static async Task RunAndCompareOutput(string testFileName, string outputFile, string decompiledOutputFile, string decompiledCodeFile = null, bool useTestRunner = false, bool force32Bit = false)
{
string output1, output2, error1, error2;
int result1, result2;
if (useTestRunner)
{
(result1, output1, error1) = await RunWithTestRunner(outputFile, force32Bit).ConfigureAwait(false);
(result2, output2, error2) = await RunWithTestRunner(decompiledOutputFile, force32Bit).ConfigureAwait(false);
}
else
{
(result1, output1, error1) = await Run(outputFile).ConfigureAwait(false);
(result2, output2, error2) = await Run(decompiledOutputFile).ConfigureAwait(false);
}
Assert.That(result1, Is.EqualTo(0), "Exit code != 0; did the test case crash?" + Environment.NewLine + error1);
Assert.That(result2, Is.EqualTo(0), "Exit code != 0; did the decompiled code crash?" + Environment.NewLine + error2);
if (output1 != output2 || error1 != error2)
{
StringBuilder b = new StringBuilder();
b.AppendLine($"Test {testFileName} failed: output does not match.");
if (decompiledCodeFile != null)
{
b.AppendLine($"Decompiled code in {decompiledCodeFile}:line 1");
}
if (error1 != error2)
{
b.AppendLine("Got different error output.");
b.AppendLine("Original error:");
b.AppendLine(error1);
b.AppendLine();
b.AppendLine("Error after de+re-compiling:");
b.AppendLine(error2);
b.AppendLine();
}
if (output1 != output2)
{
string outputFileName = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(testFileName));
File.WriteAllText(outputFileName + ".original.out", output1);
File.WriteAllText(outputFileName + ".decompiled.out", output2);
int diffLine = 0;
string lastHeader = null;
Tuple<string, string> errorItem = null;
foreach (var pair in output1.Replace("\r", "").Split('\n').Zip(output2.Replace("\r", "").Split('\n'), Tuple.Create))
{
diffLine++;
if (pair.Item1 != pair.Item2)
{
errorItem = pair;
break;
}
if (pair.Item1.EndsWith(":", StringComparison.Ordinal))
{
lastHeader = pair.Item1;
}
}
b.AppendLine($"Output differs; first difference in line {diffLine}");
if (lastHeader != null)
{
b.AppendLine(lastHeader);
}
b.AppendLine($"{outputFileName}.original.out:line {diffLine}");
b.AppendLine(errorItem.Item1);
b.AppendLine($"{outputFileName}.decompiled.out:line {diffLine}");
b.AppendLine(errorItem.Item2);
}
Assert.Fail(b.ToString());
}
}
/// <summary>
/// Retries an IO operation that a virus scanner, the indexer or a compiler that has only
/// just exited can still hold a handle on, backing off between attempts. Every caller is
/// deleting a temp file, so a failure is reported and swallowed: the file is left behind,
/// which costs nothing, where throwing out of a fixture teardown reports an error for a
/// run that otherwise passed.
/// </summary>
internal static void RepeatOnIOError(Action action, int numTries = 8)
{
Exception lastError = null;
int delay = 10;
for (int i = 0; i < numTries; i++)
{
try
{
action();
return;
}
catch (IOException ex)
{
lastError = ex;
}
catch (UnauthorizedAccessException ex)
{
// potential virus scanner problem
lastError = ex;
}
Thread.Sleep(delay);
delay = Math.Min(delay * 2, 500);
}
TestContext.Out.WriteLine($"Cleanup could not delete a temp file after {numTries} tries, leaving it behind: {lastError?.Message}");
}
public static async Task SignAssembly(string assemblyPath, string keyFilePath)
{
if (!OperatingSystem.IsWindows())
Assert.Ignore("SignAssembly uses sn.exe from the Windows .NET Framework SDK; not available on this platform.");
string snPath = SdkUtility.GetSdkPath("sn.exe");
var command = Cli.Wrap(snPath)
.WithArguments($"-R \"{assemblyPath}\" \"{keyFilePath}\"")
.WithValidation(CommandResultValidation.None);
var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
Assert.That(result.ExitCode, Is.EqualTo(0), "sn failed");
if (!string.IsNullOrWhiteSpace(result.StandardOutput))
{
Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
}
if (!string.IsNullOrWhiteSpace(result.StandardError))
{
Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
}
}
public static async Task<string> FindMSBuild()
{
if (!OperatingSystem.IsWindows())
Assert.Ignore("FindMSBuild uses vswhere.exe to locate Visual Studio's MSBuild; not available on this platform.");
string path = vswhereToolset.GetVsWhere();
var result = await Cli.Wrap(path)
.WithArguments(@"-latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe")
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync().ConfigureAwait(false);
if (result.ExitCode != 0)
throw new InvalidOperationException("Could not find MSBuild");
return result.StandardOutput.TrimEnd();
}
}
public class CompilerResults
{
readonly HashSet<string> tempFiles = new(Decompiler.Util.Platform.FileNameComparer);
string pathToAssembly;
public string PathToAssembly {
get {
if (pathToAssembly == null)
{
pathToAssembly = TestsAssemblyOutput.GetTempFileName();
tempFiles.Add(pathToAssembly);
}
return pathToAssembly;
}
set {
if (pathToAssembly != null)
{
throw new InvalidOperationException("PathToAssembly can only be set once");
}
pathToAssembly = value;
}
}
public void DeleteTempFiles()
{
foreach (var file in tempFiles)
{
Tester.RepeatOnIOError(() => File.Delete(file));
}
}
public void AddTempFile(string file)
{
if (!Path.IsPathFullyQualified(file))
{
throw new InvalidOperationException("file must be a fully qualified path");
}
tempFiles.Add(file);
}
}
}