From 39d52ba64f6bd2fdb96371d15a821f2c0ef55915 Mon Sep 17 00:00:00 2001 From: Chris Hamons Date: Fri, 11 Feb 2022 09:11:02 -0600 Subject: [PATCH] [tests] Add disabled NET6 availability attribute test (#14106) --- tests/cecil-tests/AttributeTest.cs | 259 +++++++++++++++++++++++++++++ tests/cecil-tests/Helper.cs | 28 ++++ 2 files changed, 287 insertions(+) create mode 100644 tests/cecil-tests/AttributeTest.cs diff --git a/tests/cecil-tests/AttributeTest.cs b/tests/cecil-tests/AttributeTest.cs new file mode 100644 index 0000000000..d8604e9208 --- /dev/null +++ b/tests/cecil-tests/AttributeTest.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics; +using System.IO; + +using NUnit.Framework; + +using Mono.Cecil; + +using Xamarin.Utils; +using Xamarin.Tests; + +#nullable enable + +namespace Cecil.Tests { + + // [TestFixture] + public class AttributeTest { + // https://github.com/xamarin/xamarin-macios/issues/10170 + // Every binding class that has net6 availability attributes on a method/property + // must have one matching every platform listed on the availabilities of the class + // + // Example: + // [SupportedOSPlatform("ios1.0")] + // [SupportedOSPlatform("maccatalyst1.0")] + // class TestType + // { + // public static void Original () { } + // + // [SupportedOSPlatform(""ios2.0"")] + // public static void Extension () { } + // } + // + // In this example, Extension is _not_ considered part of maccatalyst1.0 + // Because having _any_ SupportedOSPlatform implies only the set explicitly set on that member + // + // This test should find Extension, note that it has an ios attribute, + // and insist that some maccatalyst must also be set. + // [TestCaseSource (typeof (Helper), "NetPlatformAssemblies")] + public void ChildElementsListAvailabilityForAllPlatformsOnParent (string assemblyPath) + { + var assembly = Helper.GetAssembly (assemblyPath); + if (assembly is null) { + Assert.Ignore ("{assemblyPath} could not be found (might be disabled in build)"); + return; + } + +#if DEBUG + Console.WriteLine(assemblyPath); +#endif + + HashSet found = new HashSet (); + foreach (var prop in Helper.FilterProperties (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckAllPlatformsOnParent (prop, prop.FullName, prop.DeclaringType, found); + } + foreach (var meth in Helper.FilterMethods (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckAllPlatformsOnParent (meth, meth.FullName, meth.DeclaringType, found); + } + foreach (var field in Helper.FilterFields (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckAllPlatformsOnParent (field, field.FullName, field.DeclaringType, found); + } + + Assert.That (found, Is.Empty); + } + + void CheckAllPlatformsOnParent (ICustomAttributeProvider item, string fullName, TypeDefinition parent, HashSet found) + { + // XXX - For now skip generated code until associated generator.cs changes are in + if (Ignore (fullName) || HasCodegenAttribute (item)) { + return; + } +// #if DEBUG +// const string Filter = "AppKit"; +// if (!fullName.Contains (" " + Filter)) { +// return; +// } +// #endif + + var parentAvailability = GetAvailabilityAttributes (parent, includeUnsupported: false).ToList (); +// // This is true in theory, but our code should be explicit and list every platform individually +// // This can be re-enabled if that decision is reverted. + +// // iOS implies maccatalyst, but only for parent scope +// if (parentAvailability.Contains("ios") && !parentAvailability.Contains("maccatalyst")) { +// parentAvailability.Append("maccatalyst"); +// } + + var myAvailability = GetAvailabilityAttributes (item, includeUnsupported: true); + if (!FirstContainsAllOfSecond (myAvailability, parentAvailability)) { + DebugPrint (fullName, parentAvailability, myAvailability); + found.Add (fullName); + } + } + + // https://github.com/xamarin/xamarin-macios/issues/10170 + // Every binding class that has net6 any availability attributes on a method/property + // must have an introduced for the current platform. + // + // Example: + // class TestType + // { + // public static void Original () { } + // + // [SupportedOSPlatform(""ios2.0"")] + // public static void Extension () { } + // } + // + // When run against mac, this fails as Extension does not include a mac supported of any kind attribute + // [TestCaseSource (typeof (Helper), "NetPlatformAssemblies")] + public void AllAttributedItemsMustIncludeCurrentPlatform (string assemblyPath) + { + var assembly = Helper.GetAssembly (assemblyPath); + if (assembly is null) { + Assert.Ignore ("{assemblyPath} could not be found (might be disabled in build)"); + return; + } + +#if DEBUG + Console.WriteLine(assemblyPath); +#endif + + string platformName = AssemblyToAttributeName (assemblyPath); + + HashSet found = new HashSet (); + foreach (var type in Helper.FilterTypes (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckCurrentPlatformIncludedIfAny (type, platformName, type.FullName, type.DeclaringType, found); + } + foreach (var prop in Helper.FilterProperties (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckCurrentPlatformIncludedIfAny (prop, platformName, prop.FullName, prop.DeclaringType, found); + } + foreach (var meth in Helper.FilterMethods (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckCurrentPlatformIncludedIfAny (meth, platformName, meth.FullName, meth.DeclaringType, found); + } + foreach (var field in Helper.FilterFields (assembly, a => HasAnyAvailabilityAttribute (a, includeUnsupported: true))) { + CheckCurrentPlatformIncludedIfAny (field, platformName, field.FullName, field.DeclaringType, found); + } + + Assert.That (found, Is.Empty); + } + + void CheckCurrentPlatformIncludedIfAny (ICustomAttributeProvider item, string platformName, string fullName, TypeDefinition parent, HashSet found) + { + // XXX - For now skip generated code until associated generator.cs changes are in + if (Ignore (fullName) || HasCodegenAttribute (item)) { + return; + } + + if (HasAnyAvailabilityAttribute (item, true)) { + var supportedAttributes = item.CustomAttributes.Where (a => IsSupportedAttribute (a)); + if (!supportedAttributes.Any (a => FindAvailabilityKind (a) == platformName)) { + found.Add (fullName); + } + } + } + + string AssemblyToAttributeName (string assemblyPath) + { + var baseName = Path.GetFileName (assemblyPath); + if (Configuration.GetBaseLibraryName (TargetFramework.DotNet_5_0_iOS.Platform) == baseName) + return "ios"; + if (Configuration.GetBaseLibraryName (TargetFramework.DotNet_5_0_tvOS.Platform) == baseName) + return "tvos"; + if (Configuration.GetBaseLibraryName (TargetFramework.DotNet_5_0_macOS.Platform) == baseName) + return "macos"; + if (Configuration.GetBaseLibraryName (TargetFramework.DotNet_5_0_MacCatalyst.Platform) == baseName) + return "maccatalyst"; + throw new NotImplementedException (); + } + + [Conditional ("DEBUG")] + void DebugPrint (string fullName, IEnumerable parentAvailability, IEnumerable myAvailability) + { + Console.WriteLine (fullName); + Console.WriteLine ("Parent: " + string.Join (" ", parentAvailability)); + Console.WriteLine ("Mine: " + string.Join (" ", myAvailability)); + Console.WriteLine (); + } + + bool Ignore (string fullName) + { + switch (fullName) { + default: + return false; + } + } + + bool FirstContainsAllOfSecond (IEnumerable first, IEnumerable second) + { + var firstSet = new HashSet (first); + return second.All (s => firstSet.Contains (s)); + } + + IEnumerable GetAvailabilityAttributes (ICustomAttributeProvider provider, bool includeUnsupported) => GetAvailabilityAttributes (provider.CustomAttributes, includeUnsupported); + + IEnumerable GetAvailabilityAttributes (IEnumerable attributes, bool includeUnsupported) + { + var availability = new List (); + foreach (var attribute in attributes.Where (a => IsAvailabilityAttribute (a, includeUnsupported))) { + var kind = FindAvailabilityKind (attribute); + if (kind is not null) { + availability.Add (kind); + } + } + return availability; + } + + // Unfortunate state we need to keep since I can't see to walk "up" from a + // MethodDefinition get_Foo or SetFoo to see it's container's properties + HashSet HasCodegenPropertyImpl = new HashSet (); + bool HasCodegenAttribute (ICustomAttributeProvider provider) + { + // get/set don't have BindingImpl directly, it is on the parent context + if (provider is MethodDefinition method) { + var property = method.DeclaringType.Properties.FirstOrDefault (v => v.Name == method.Name.Substring (4)); + if (property != null && property.CustomAttributes.Any (IsBindingImplAttribute)) { + return true; + } + } + return provider.CustomAttributes.Any (IsBindingImplAttribute); + } + + string? FindAvailabilityKind (CustomAttribute attribute) + { + if (attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments [0].Type.Name == "String") { + string full = (string) attribute.ConstructorArguments [0].Value; + switch (full) { + case string s when full.StartsWith ("ios", StringComparison.Ordinal): + return "ios"; + case string s when full.StartsWith ("tvos"): + return "tvos"; + case string s when full.StartsWith ("macos"): + return "macos"; + case string s when full.StartsWith ("maccatalyst"): + return "maccatalyst"; + case string s when full.StartsWith ("watchos"): + return null; // WatchOS is ignored for comparision + default: + throw new System.NotImplementedException ($"Unknown platform kind: {full}"); + } + } + return null; + } + + bool HasAnyAvailabilityAttribute (ICustomAttributeProvider provider, bool includeUnsupported) + { + return provider.CustomAttributes.Any (a => IsAvailabilityAttribute (a, includeUnsupported)); + } + + bool IsAvailabilityAttribute (CustomAttribute attribute, bool includeUnsupported) + { + return IsSupportedAttribute (attribute) || + (includeUnsupported && attribute.AttributeType.Name == "UnsupportedOSPlatformAttribute"); + } + + bool IsSupportedAttribute (CustomAttribute attribute) => attribute.AttributeType.Name == "SupportedOSPlatformAttribute"; + bool IsBindingImplAttribute (CustomAttribute attribute) => attribute.AttributeType.Name == "BindingImplAttribute"; + } +} diff --git a/tests/cecil-tests/Helper.cs b/tests/cecil-tests/Helper.cs index d3fd43a514..54392f9344 100644 --- a/tests/cecil-tests/Helper.cs +++ b/tests/cecil-tests/Helper.cs @@ -104,6 +104,34 @@ namespace Cecil.Tests { yield break; } + public static IEnumerable FilterFields (AssemblyDefinition assembly, Func? filter) + { + foreach (var module in assembly.Modules) { + foreach (var type in module.Types) { + foreach (var field in FilterFields (type, filter)) + yield return field; + } + } + yield break; + } + + static IEnumerable FilterFields (TypeDefinition type, Func? filter) + { + if (type.HasFields) { + foreach (var field in type.Fields) { + if ((filter is null) || filter (field)) + yield return field; + } + } + if (type.HasNestedTypes) { + foreach (var nested in type.NestedTypes) { + foreach (var field in FilterFields (nested, filter)) + yield return field; + } + } + yield break; + } + public static string GetBCLDirectory (string assembly) { var rv = string.Empty;