Skip to content

Commit

Permalink
use ILVerification library instead of ilverify tool (#313)
Browse files Browse the repository at this point in the history
ilverify used the same library so there's no loss of functionality / coverage.
btoh not launching a separate process for each test speeds test executation time
  • Loading branch information
adrianoc committed Oct 20, 2024
1 parent c29ca42 commit 6198464
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 42 deletions.
1 change: 1 addition & 0 deletions Cecilifier.Core.Tests/Cecilifier.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.ILVerification" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
Expand Down
105 changes: 64 additions & 41 deletions Cecilifier.Core.Tests/Framework/CecilifierTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text;
using Cecilifier.Core.Misc;
using Cecilifier.Core.Naming;
using Cecilifier.Core.Tests.Framework.AssemblyDiff;
using Cecilifier.Core.Tests.Framework.ILVerification;
using Cecilifier.Runtime;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil;
using Mono.Cecil.Rocks;
using NUnit.Framework;
using ILVerify;

namespace Cecilifier.Core.Tests.Framework;

Expand All @@ -38,47 +41,69 @@ private protected static string CompileExpectedTestAssembly(string cecilifiedAss
: CompilationServices.CompileDLL(targetPath, tbc, ReferencedAssemblies.GetTrustedAssembliesPath());
}

private protected void VerifyAssembly(string actualAssemblyPath, string expectedAssemblyPath, CecilifyTestOptions options)
class AssemblyResolver : IResolver
{
var dotnetRootPath = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (string.IsNullOrEmpty(dotnetRootPath))
private readonly Dictionary<string, PEReader> assemblyCache = new();
public PEReader ResolveAssembly(AssemblyNameInfo assemblyName)
{
Console.WriteLine($"Unable to resolve DOTNET_ROOT environment variable. Skping ilverify on {actualAssemblyPath}");
return;
if (assemblyCache.TryGetValue(assemblyName.Name, out var assembly))
return assembly;

var assemblyPath = referenceAssemblies.SingleOrDefault(assemblyPath => Path.GetFileName(Path.GetFileNameWithoutExtension(assemblyPath)) == assemblyName.Name);
if (assemblyPath != null)
{
var resolvedAssembly = new PEReader(File.OpenRead(assemblyPath));
assemblyCache[assemblyName.Name] = resolvedAssembly;
return resolvedAssembly;
}

return null;
}

var ignoreErrorsArg = options.IgnoredILErrors != null ? $" -g {options.IgnoredILErrors}" : string.Empty;
var ilVerifyStartInfo = new ProcessStartInfo
{
FileName = "ilverify",
Arguments = $"""{actualAssemblyPath} -r "{dotnetRootPath}/packs/Microsoft.NETCore.App.Ref/{Environment.Version}/ref/net{Environment.Version.Major}.{Environment.Version.Minor}/*.dll"{ignoreErrorsArg}""",
WindowStyle = ProcessWindowStyle.Normal,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};

using var ilverifyProcess = new Process
public PEReader ResolveModule(AssemblyNameInfo referencingAssembly, string fileName)
{
StartInfo = ilVerifyStartInfo
};

var output = new List<string>();
ilverifyProcess.OutputDataReceived += (_, arg) => output.Add(arg.Data);
ilverifyProcess.ErrorDataReceived += (_, arg) => output.Add(arg.Data);
return null;
}

ilverifyProcess.Start();
ilverifyProcess.BeginOutputReadLine();
ilverifyProcess.BeginErrorReadLine();
public AssemblyResolver(string dotnetRoot)
{
referenceAssemblies = Directory.GetFiles($"{dotnetRoot}/packs/Microsoft.NETCore.App.Ref/{Environment.Version}/ref/net{Environment.Version.Major}.{Environment.Version.Minor}", "*.dll");
}

if (!ilverifyProcess.WaitForExit(TimeSpan.FromSeconds(30)))
private string[] referenceAssemblies;
}

private protected void VerifyAssembly(string actualAssemblyPath, string expectedAssemblyPath, CecilifyTestOptions options)
{
var dotnetRootPath = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (string.IsNullOrEmpty(dotnetRootPath))
{
throw new TimeoutException($"ilverify ({ilverifyProcess.Id}) took more than 30 secs to process {actualAssemblyPath}");
Console.WriteLine($"Unable to resolve DOTNET_ROOT environment variable. Skping ilverify on {actualAssemblyPath}");
return;
}

if (ilverifyProcess.ExitCode != 0)
var v = new Verifier(new AssemblyResolver(dotnetRootPath), new VerifierOptions() { IncludeMetadataTokensInErrorMessages = true});
v.SetSystemModuleName(new AssemblyNameInfo("mscorlib"));
var assemblyReaderToVerify = new PEReader(new FileStream(actualAssemblyPath, FileMode.Open));
var verifierResults = v.Verify(assemblyReaderToVerify).ToArray();

if (verifierResults.Length > 0)
{
output.Add($"ilverify for {actualAssemblyPath} failed with exit code = {ilverifyProcess.ExitCode}.\n{(expectedAssemblyPath != null ? $"Expected path={expectedAssemblyPath}\n" : "")}");
var ignoredErrorsSpan = options.IgnoredILErrors.AsSpan();
Span<Range> splitPositions = stackalloc Range[ignoredErrorsSpan.Count('|') + 1];
var ignoredErrorsCount = ignoredErrorsSpan.Split(splitPositions, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
HashSet<VerifierError> ignoredErrors = new();
foreach (var ignoredErrorNamePosition in splitPositions.Slice(0, ignoredErrorsCount))
{
ignoredErrors.Add(Enum.Parse<VerifierError>(ignoredErrorsSpan[ignoredErrorNamePosition]));
}

List<string> output = new();
output.AddRange(verifierResults.Where(error => !ignoredErrors.Contains(error.Code)).Select(error => ILVerifierResult.From(error, assemblyReaderToVerify.GetMetadataReader()).GetErrorMessage()).ToArray());
if (output.Count == 0)
return;

output.Add($"ilverify for {actualAssemblyPath} failed.\n{(expectedAssemblyPath != null ? $"Expected path={expectedAssemblyPath}\n" : "")}");

if (options.CecilifiedCode != null)
{
Expand All @@ -101,7 +126,7 @@ protected CecilifyResult CecilifyAndExecute(Stream tbc, string testBasePath)
var references = ReferencedAssemblies.GetTrustedAssembliesPath().Where(a => !a.Contains("mscorlib"));
List<string> refsToCopy = [
typeof(ILParser).Assembly.Location,
typeof(TypeReference).Assembly.Location,
typeof(Mono.Cecil.TypeReference).Assembly.Location,
typeof(TypeHelpers).Assembly.Location
];

Expand Down Expand Up @@ -178,16 +203,14 @@ protected void CopyFilesNextToGeneratedExecutable(string cecilifierRunnerPath, L

private protected string GetILFrom(string actualAssemblyPath, string methodSignature)
{
using (var assembly = AssemblyDefinition.ReadAssembly(actualAssemblyPath))
using var assembly = Mono.Cecil.AssemblyDefinition.ReadAssembly(actualAssemblyPath);
var method = assembly.MainModule.Types.SelectMany(t => t.Methods).SingleOrDefault(m => m.FullName == methodSignature);
if (method == null)
{
var method = assembly.MainModule.Types.SelectMany(t => t.Methods).SingleOrDefault(m => m.FullName == methodSignature);
if (method == null)
{
Assert.Fail($"Method {methodSignature} could not be found in {actualAssemblyPath}");
}

return Formatter.FormatMethodBody(method).Replace("\t", "");
Assert.Fail($"Method {methodSignature} could not be found in {actualAssemblyPath}");
}

return Formatter.FormatMethodBody(method).Replace("\t", "");
}

private protected static string ReadToEnd(Stream tbc)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Reflection.Metadata;
using System.Text;
using Cecilifier.Core.Tests.Framework.ILVerification;

namespace Cecilifier.Core.Tests.Framework.Extensions;

public static class Extensions
{
public static string GetMethodSignature (this MethodDefinitionHandle handle, MetadataReader metadataReader)
{
var method = metadataReader.GetMethodDefinition (handle);
SignatureProvider signatureProvider = new();
StringBuilder sb = new();
var signature = method.DecodeSignature(signatureProvider, new object ());
sb.Append(metadataReader.GetString(method.Name));
sb.Append ('(');
int paramIndex = 0;
foreach (var typeName in signature.ParameterTypes) {
if (paramIndex > 0)
sb.Append (',');

sb.Append (typeName);

paramIndex++;
}

sb.Append (')');
return sb.ToString ();
}

public static string GetMethodDeclaringTypeFullName (this MethodDefinitionHandle handle, MetadataReader metadataReader)
{
var definition = metadataReader.GetMethodDefinition (handle);
var declaringType = definition.GetDeclaringType ();
return declaringType.GetTypeFullName (metadataReader);
}

public static string GetTypeFullName (this TypeDefinitionHandle handle, MetadataReader metadataReader)
{
var typeDefinition = metadataReader.GetTypeDefinition (handle);
var declaringType = typeDefinition.GetDeclaringType ();

var builder = new StringBuilder ();
if (!declaringType.IsNil)
{
builder.Append(GetTypeFullName (declaringType, metadataReader))
.Append ('+')
.Append (metadataReader.GetString (typeDefinition.Name));
} else
{
builder.Append(metadataReader.GetString (typeDefinition.Namespace))
.Append ('.')
.Append (metadataReader.GetString (typeDefinition.Name));
}

return builder.ToString ();
}

public static string GetTypeFullName (this TypeReferenceHandle handle, MetadataReader metadataReader)
{
var typeReference = metadataReader.GetTypeReference (handle);

var builder = new StringBuilder ();
builder.Append (metadataReader.GetString(typeReference.Namespace))
.Append ('.')
.Append (metadataReader.GetString(typeReference.Name));

return builder.ToString ();
}
}
65 changes: 65 additions & 0 deletions Cecilifier.Core.Tests/Framework/ILVerification/ILVerifierResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Reflection.Metadata;
using Cecilifier.Core.Tests.Framework.Extensions;
using System.Text;
using ILVerify;

namespace Cecilifier.Core.Tests.Framework.ILVerification;

public class ILVerifierResult
{
public readonly VerificationResult Result;
public readonly string TypeFullName;
public readonly string MethodSignature;

public ILVerifierResult(VerificationResult result, string typeFullName, string methodSignature)
{
Result = result;
TypeFullName = typeFullName;
MethodSignature = methodSignature;
}

public static ILVerifierResult From(VerificationResult r, MetadataReader metadataReader) =>
new(
r,
r.Type.IsNil
? r.Method.GetMethodDeclaringTypeFullName(metadataReader)
: r.Type.GetTypeFullName(metadataReader),
r.Method.IsNil
? string.Empty
: r.Method.GetMethodSignature(metadataReader));

public string GetErrorMessage()
{
var sb = new StringBuilder();
if (string.IsNullOrEmpty(MethodSignature))
sb.Append(TypeFullName);
else
{
sb.Append(TypeFullName);
sb.Append('.');
sb.Append(MethodSignature);
}

sb.Append($" - {Result.Code}: ");
sb.Append(Result.Message);
if (Result.ErrorArguments?.Length > 0)
{
sb.Append(" - ");
foreach (var argument in Result.ErrorArguments)
{
sb.Append(FormatArgument(argument));
}
}

return sb.ToString();

static string FormatArgument(ErrorArgument argument)
{
if (argument.Name == "Token" && argument.Value is int token)
return $" {argument.Name} 0x{token:X8}";
if (argument.Name == "Offset" && argument.Value is int offset)
return $" {argument.Name} IL_{offset:X4}";
return $" {argument.Name} {argument.Value}";
}
}
}
Loading

0 comments on commit 6198464

Please sign in to comment.