Skip to content

Commit

Permalink
[tests] Add new cecil tests to verify correct block literal usage in …
Browse files Browse the repository at this point in the history
…our own code. Fixes xamarin#15783.

This is the final step in our improved block support.

Fixes xamarin#15783.
  • Loading branch information
rolfbjarne committed Mar 14, 2023
1 parent 29633a6 commit 0065345
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 1 deletion.
120 changes: 120 additions & 0 deletions tests/cecil-tests/BlittablePInvokes.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;

using ObjCRuntime;

using Xamarin.Tests;
using Xamarin.Utils;

#nullable enable

namespace Cecil.Tests {
Expand Down Expand Up @@ -335,5 +341,119 @@ static bool IsPInvokeOK (MethodDefinition method)
return true;
}
}

[Test]
public void CheckForBlockLiterals ()
{
var failures = new HashSet<(string Message, string Location)> ();

foreach (var info in Helper.NetPlatformImplementationAssemblyDefinitions) {
var assembly = info.Assembly;
foreach (var type in assembly.EnumerateTypes ()) {
foreach (var method in type.EnumerateMethods (m => m.HasBody)) {
var body = method.Body;
foreach (var instr in body.Instructions) {
switch (instr.OpCode.Code) {
case Code.Call:
case Code.Calli:
case Code.Callvirt:
break;
default:
continue;
}

var targetMethod = (MethodReference) instr.Operand;
Assert.IsNotNull (targetMethod, "Null operand"); // If this ever fails, the code needs to be updated.

if (!targetMethod!.DeclaringType.Is ("ObjCRuntime", "BlockLiteral"))
continue;

switch (targetMethod.Name) {
case "SetupBlock":
case "SetupBlockUnsafe":
break;
default:
continue;
}

var location = method.RenderLocation (instr);
var message = $"The call to {targetMethod.Name} in {method.AsFullName ()} must be converted to new Block syntax.";
failures.Add (new (message, location));
}
}
}
}

var newFailures = failures.Where (v => !knownFailuresBlockLiterals.Contains (v.Message)).ToArray ();
var fixedFailures = knownFailuresBlockLiterals.Except (failures.Select (v => v.Message).ToHashSet ());

var printKnownFailures = newFailures.Any () || fixedFailures.Any ();
if (printKnownFailures) {
Console.WriteLine ("Printing all failures as known failures because they seem out of date:");
Console.WriteLine ("\t\tstatic HashSet<string> knownFailuresBlockLiterals = new HashSet<string> {");
foreach (var failure in failures.OrderBy (v => v))
Console.WriteLine ($"\t\t\t\"{failure.Message}\",");
Console.WriteLine ("\t\t};");
}

if (newFailures.Any ()) {
// Print any new failures with the local path for easy navigation (depending on the terminal and/or IDE you might just click on the path to open the corresponding file).
Console.WriteLine ($"Printing {newFailures.Count ()} new failures with local paths for easy navigation:");
foreach (var failure in newFailures.OrderBy (v => v))
Console.WriteLine ($" {failure.Location}: {failure.Message}");
}

Assert.IsEmpty (newFailures, "Failures");
Assert.IsEmpty (fixedFailures, "Known failures that aren't failing anymore - remove these from the list of known failures");
}

static HashSet<string> knownFailuresBlockLiterals = new HashSet<string> {
"The call to SetupBlock in ObjCRuntime.BlockLiteral.GetBlockForDelegate(System.Reflection.MethodInfo, System.Object, System.UInt32, System.String) must be converted to new Block syntax.",
"The call to SetupBlock in ObjCRuntime.BlockLiteral.SetupBlock(System.Delegate, System.Delegate) must be converted to new Block syntax.",
"The call to SetupBlock in ObjCRuntime.BlockLiteral.SetupBlockUnsafe(System.Delegate, System.Delegate) must be converted to new Block syntax.",
};

[Test]
public void CheckForMonoPInvokeCallback ()
{
var failures = new HashSet<(string Message, string Location)> ();

foreach (var info in Helper.NetPlatformImplementationAssemblyDefinitions) {
var assembly = info.Assembly;
foreach (var type in assembly.EnumerateTypes ()) {
foreach (var method in type.EnumerateMethods (m => m.HasCustomAttributes)) {
foreach (var ca in method.CustomAttributes) {
if (ca.AttributeType.Name != "MonoPInvokeCallbackAttribute")
continue;

var location = method.RenderLocation ();
var message = $"The method {method.AsFullName ()} has a MonoPInvokeCallback attribute and must be converted to an UnmanagedCallersOnly method.";
failures.Add (new (message, location));

break;
}
}
}
}

var newFailures = failures.Where (v => !knownFailuresMonoPInvokeCallback.Contains (v.Message)).ToArray ();
var fixedFailures = knownFailuresMonoPInvokeCallback.Except (failures.Select (v => v.Message).ToHashSet ());

if (newFailures.Any ()) {
// Print any new failures with the local path for easy navigation (depending on the terminal and/or IDE you might just click on the path to open the corresponding file).
Console.WriteLine ($"Printing {newFailures.Count ()} failures with local path for easy navigation:");
foreach (var failure in newFailures.OrderBy (v => v))
Console.WriteLine ($" {failure.Location}: {failure.Message}");
}

Assert.IsEmpty (newFailures, "New Failures");
Assert.IsEmpty (fixedFailures, "Known failures that aren't failing anymore - remove these from the list of known failures");
}

static HashSet<string> knownFailuresMonoPInvokeCallback = new HashSet<string> {
#if !NET8_0_OR_GREATER
"The method CoreFoundation.CFStream.OnCallback(System.IntPtr, System.IntPtr, System.IntPtr) has a MonoPInvokeCallback attribute and must be converted to an UnmanagedCallersOnly method.",
#endif
};
}
}
57 changes: 57 additions & 0 deletions tests/cecil-tests/CecilExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Text;

using Mono.Cecil;

using ObjCRuntime;

namespace Xamarin.Utils {
public static partial class CecilExtensions {

Expand Down Expand Up @@ -145,6 +148,60 @@ public static bool HasEditorBrowseableNeverAttribute (this ICustomAttributeProvi

return false;
}

public static bool HasBindingImplAttribute (this ICustomAttributeProvider? provider, out BindingImplOptions options)
{
options = BindingImplOptions.None;
if (provider is null)
return false;

if (provider.HasCustomAttributes) {
foreach (var attr in provider.CustomAttributes) {
if (!attr.AttributeType.Is ("ObjCRuntime", "BindingImplAttribute"))
continue;
options = (BindingImplOptions) attr.ConstructorArguments [0].Value;
return true;
}
}

var property = FindProperty (provider as MethodDefinition);
if (property is not null)
return HasBindingImplAttribute (property, out options);

return false;
}

public static PropertyDefinition? FindProperty (this MethodDefinition? accessor)
{
if (accessor is null)
return null;

if (!accessor.IsSpecialName)
return null;

if (!accessor.DeclaringType.HasProperties)
return null;

if (!accessor.Name.StartsWith ("get_", StringComparison.Ordinal) && !accessor.Name.StartsWith ("set_", StringComparison.Ordinal))
return null;

var propertyName = accessor.Name.Substring (4);
var properties = accessor.DeclaringType.Properties.Where (v => v.Name == propertyName);
foreach (var property in properties) {
if (property.GetMethod == accessor || property.SetMethod == accessor)
return property;
}

return null;
}
}
}

namespace ObjCRuntime {
[Flags]
public enum BindingImplOptions {
None = 0,
GeneratedCode = 1,
Optimizable = 2,
}
}
14 changes: 13 additions & 1 deletion tests/cecil-tests/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using NUnit.Framework;

using Mono.Cecil;
using Mono.Cecil.Cil;

using Xamarin.Tests;
using Xamarin.Utils;
Expand Down Expand Up @@ -348,7 +349,7 @@ static TestFixtureData CreateTestFixtureDataFromPath (string path)
return rv;
}

public static string RenderLocation (this IMemberDefinition member)
public static string RenderLocation (this IMemberDefinition member, Instruction? instruction = null)
{
if (member is null)
return string.Empty;
Expand All @@ -369,6 +370,17 @@ public static string RenderLocation (this IMemberDefinition member)

if (method.DebugInformation.HasSequencePoints) {
var seq = method.DebugInformation.SequencePoints [0];
if (instruction is not null) {
var instr = instruction;
while (instr is not null) {
var iseq = method.DebugInformation.GetSequencePoint (instr);
if (iseq is not null) {
seq = iseq;
break;
}
instr = instr.Previous;
}
}
return seq.Document.Url + ":" + seq.StartLine + " ";
}
return string.Empty;
Expand Down

0 comments on commit 0065345

Please sign in to comment.