Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<MicrosoftNetCompilersToolsetVersion>$(MicrosoftCodeAnalysisVersion)</MicrosoftNetCompilersToolsetVersion>
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion>1.0.1-beta1.*</MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion>
<MicrosoftCodeAnalysisBannedApiAnalyzersVersion>3.3.2</MicrosoftCodeAnalysisBannedApiAnalyzersVersion>
<MicrosoftILVerificationVersion>7.0.0-preview.7.22375.6</MicrosoftILVerificationVersion>
<!-- This controls the version of the cecil package, or the version of cecil in the project graph
when we build the cecil submodule. The reference assembly package will depend on this version of cecil.
Keep this in sync with ProjectInfo.cs in the submodule. -->
Expand Down
3 changes: 2 additions & 1 deletion test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand Down Expand Up @@ -32,6 +32,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageReference Include="Microsoft.ILVerification" Version="$(MicrosoftILVerificationVersion)" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know where this package is built? I wonder if we should add a subscription (as a general comment, doesn't need to block this change).

<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<!-- This reference is purely so that the linker can resolve this
Expand Down
132 changes: 132 additions & 0 deletions test/Mono.Linker.Tests/TestCasesRunner/ILVerifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Runtime.Loader;
using ILVerify;
using Mono.Linker.Tests.Extensions;

#nullable enable
namespace Mono.Linker.Tests.TestCasesRunner
{
class ILVerifier : ILVerify.IResolver
{
Verifier _verifier;
NPath _assemblyFolder;
NPath _frameworkFolder;
Dictionary<string, PEReader> _assemblyCache;
AssemblyLoadContext _alc;

public IEnumerable<VerificationResult> Results { get; private set; }

public ILVerifier (NPath assemblyPath)
{
var assemblyName = assemblyPath.FileNameWithoutExtension;
_assemblyFolder = assemblyPath.Parent;
_assemblyCache = new Dictionary<string, PEReader> ();
_frameworkFolder = typeof (object).Assembly.Location.ToNPath ().Parent;
_alc = new AssemblyLoadContext (_assemblyFolder.FileName);
LoadAssembly ("mscorlib");
LoadAssembly ("System.Private.CoreLib");
LoadAssemblyFromPath (assemblyName, assemblyPath);

_verifier = new ILVerify.Verifier (
this,
new ILVerify.VerifierOptions {
SanityChecks = true,
IncludeMetadataTokensInErrorMessages = true
});
_verifier.SetSystemModuleName (new AssemblyName ("mscorlib"));

var allResults = _verifier.Verify (Resolve (assemblyName))
?? Enumerable.Empty<VerificationResult> ();

Results = allResults.Where (r => r.Code switch {
ILVerify.VerifierError.None
// Static interface methods cause this warning
or ILVerify.VerifierError.CallAbstract
// "Missing callVirt after constrained prefix - static interface methods cause this warning
or ILVerify.VerifierError.Constrained
// ex. localloc cannot be statically verified by ILVerify
or ILVerify.VerifierError.Unverifiable
// ref returning a ref local causes this warning but is okay
or VerifierError.ReturnPtrToStack
// Span indexing with indexer (ex. span[^4]) causes this warning
or VerifierError.InitOnly
=> false,
_ => true
});
}

PEReader LoadAssembly (string assemblyName)
{
if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader))
return reader;
var assembly = _alc.LoadFromAssemblyName (new AssemblyName (assemblyName));
reader = new PEReader (File.OpenRead (assembly.Location));
_assemblyCache.Add (assemblyName, reader);
return reader;
}

PEReader LoadAssemblyFromPath (string assemblyName, NPath pathToAssembly)
{
if (_assemblyCache.TryGetValue (assemblyName, out PEReader? reader))
return reader;
var assembly = _alc.LoadFromAssemblyPath (pathToAssembly);
reader = new PEReader (File.OpenRead (assembly.Location));
_assemblyCache.Add (assemblyName, reader);
return reader;
}

bool TryLoadAssemblyFromFolder (string assemblyName, NPath folder, [NotNullWhen (true)] out PEReader? peReader)
{
Assembly? assembly = null;
string assemblyPath = Path.Join (folder.ToString (), assemblyName);
if (File.Exists (assemblyPath + ".dll"))
assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".dll");
else if (File.Exists (assemblyPath + ".exe"))
assembly = _alc.LoadFromAssemblyPath (assemblyPath + ".exe");

if (assembly is not null) {
peReader = new PEReader (File.OpenRead (assembly.Location));
_assemblyCache.Add (assemblyName, peReader);
return true;
}
peReader = null;
return false;
}

PEReader? Resolve (string assemblyName)
{
PEReader? reader;
if (_assemblyCache.TryGetValue (assemblyName, out reader)) {
return reader;
}

if (TryLoadAssemblyFromFolder (assemblyName, _frameworkFolder, out reader))
return reader;

if (TryLoadAssemblyFromFolder (assemblyName, _assemblyFolder, out reader))
return reader;

return null;
}

PEReader? ILVerify.IResolver.ResolveAssembly (AssemblyName assemblyName)
=> Resolve (assemblyName.Name ?? assemblyName.FullName);

PEReader? ILVerify.IResolver.ResolveModule (AssemblyName referencingModule, string fileName)
=> Resolve (Path.GetFileNameWithoutExtension (fileName));

public string GetErrorMessage (VerificationResult result)
{
return $"IL Verification error:\n{result.Message}";
}
}
}
#nullable restore
62 changes: 59 additions & 3 deletions test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
Expand All @@ -13,6 +14,7 @@
using Mono.Linker.Tests.Cases.Expectations.Metadata;
using Mono.Linker.Tests.Extensions;
using NUnit.Framework;
using WellKnownType = ILLink.Shared.TypeSystemProxy.WellKnownType;

namespace Mono.Linker.Tests.TestCasesRunner
{
Expand Down Expand Up @@ -47,6 +49,32 @@ public ResultChecker (BaseAssemblyResolver originalsResolver, BaseAssemblyResolv
_linkedReaderParameters = linkedReaderParameters;
}

static void VerifyIL (NPath pathToAssembly)
{
var verifier = new ILVerifier (pathToAssembly);
foreach (var result in verifier.Results) {
if (result.Code == ILVerify.VerifierError.None)
continue;
Assert.Fail (verifier.GetErrorMessage (result));
}
}

static bool ShouldValidateIL (AssemblyDefinition inputAssembly)
{
if (HasAttribute (inputAssembly, nameof (SkipPeVerifyAttribute)))
return false;

var caaIsUnsafeFlag = (CustomAttributeArgument caa) =>
caa.Type.IsTypeOf (WellKnownType.System_String)
&& (string) caa.Value == "/unsafe";
var customAttributeHasUnsafeFlag = (CustomAttribute ca) => ca.ConstructorArguments.Any (caaIsUnsafeFlag);
if (GetCustomAttributes (inputAssembly, nameof (SetupCompileArgumentAttribute))
.Any (customAttributeHasUnsafeFlag))
return false;

return true;
}

public virtual void Check (LinkedTestCaseResult linkResult)
{
InitializeResolvers (linkResult);
Expand All @@ -57,6 +85,9 @@ public virtual void Check (LinkedTestCaseResult linkResult)
Assert.IsTrue (linkResult.OutputAssemblyPath.FileExists (), $"The linked output assembly was not found. Expected at {linkResult.OutputAssemblyPath}");
var linked = ResolveLinkedAssembly (linkResult.OutputAssemblyPath.FileNameWithoutExtension);

if (ShouldValidateIL (original))
VerifyIL (linkResult.OutputAssemblyPath);

InitialChecking (linkResult, original, linked);

PerformOutputAssemblyChecks (original, linkResult.OutputAssemblyPath.Parent);
Expand Down Expand Up @@ -1069,15 +1100,40 @@ bool IsTypeInOtherAssemblyAssertion (CustomAttribute attr)
}

static bool HasAttribute (ICustomAttributeProvider caProvider, string attributeName)
{
return TryGetCustomAttribute (caProvider, attributeName, out var _);
}

#nullable enable
static bool TryGetCustomAttribute (ICustomAttributeProvider caProvider, string attributeName, [NotNullWhen (true)] out CustomAttribute? customAttribute)
{
if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null) {
customAttribute = assembly.EntryPoint.DeclaringType.CustomAttributes
.FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null);
return customAttribute is not null;
}

if (caProvider is TypeDefinition type) {
customAttribute = type.CustomAttributes
.FirstOrDefault (attr => attr!.AttributeType.Name == attributeName, null);
return customAttribute is not null;
}
customAttribute = null;
return false;
}

static IEnumerable<CustomAttribute> GetCustomAttributes (ICustomAttributeProvider caProvider, string attributeName )
{
if (caProvider is AssemblyDefinition assembly && assembly.EntryPoint != null)
return assembly.EntryPoint.DeclaringType.CustomAttributes
.Any (attr => attr.AttributeType.Name == attributeName);
.Where (attr => attr!.AttributeType.Name == attributeName);

if (caProvider is TypeDefinition type)
return type.CustomAttributes.Any (attr => attr.AttributeType.Name == attributeName);
return type.CustomAttributes
.Where (attr => attr!.AttributeType.Name == attributeName);

return false;
return Enumerable.Empty<CustomAttribute> ();
}
#nullable restore
}
}