1050 lines
46 KiB
C#
1050 lines
46 KiB
C#
#if UNITY_EDITOR
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Unity.Burst;
|
|
using UnityEditor;
|
|
using UnityEditor.Build.Reporting;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
using Assembly = System.Reflection.Assembly;
|
|
using Debug = UnityEngine.Debug;
|
|
|
|
namespace Unity.Collections.Tests
|
|
{
|
|
/// <summary>
|
|
/// Base class for semi-automated burst compatibility testing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// To create Burst compatibility tests for your assembly, you must do the following:<para/> <para/>
|
|
///
|
|
/// 1. Set up a directory to contain the generated Burst compatibility code.<para/> <para/>
|
|
///
|
|
/// 2. Create a new asmdef in that directory. You should set up the references so you can access Burst and
|
|
/// your assembly. This new asmdef *must* support all platforms because the Burst compatibility tests use player
|
|
/// builds to compile the code in parallel.<para/> <para/>
|
|
///
|
|
/// 3. If you wish to test internal methods, you should make internals visible to the new asmdef you created in
|
|
/// step 2.<para/> <para/>
|
|
///
|
|
/// 4. Create a test scene (it can be empty) for the Burst compatibility tests to use when it builds a player.
|
|
///
|
|
/// 5. Create a new class and inherit BurstCompatibilityTests. Call the base constructor with the appropriate
|
|
/// arguments so BurstCompatibilityTests knows which assemblies to scan for the [GenerateTestsForBurstCompatibility] attribute and
|
|
/// where to put the generated Burst compatibility code. This new class should live in an editor only assembly and
|
|
/// the generated Burst compatibility code should be a part of the multiplatform asmdef you created in step 2.
|
|
/// <para/> <para/>
|
|
///
|
|
/// 6. If your generated code will live in your package directory, you may need to add the [EmbeddedPackageOnlyTest]
|
|
/// attribute to your new class.<para/> <para/>
|
|
///
|
|
/// 7. Start adding [GenerateTestsForBurstCompatibility] or [ExcludeFromBurstCompatTesting] attributes to your types or methods.<para/> <para/>
|
|
///
|
|
/// 8. In the test runner, run the test called CompatibilityTests that can be found nested under the name of your
|
|
/// new test class you implemented in step 4.<para/> <para/>
|
|
/// </remarks>
|
|
public abstract class BurstCompatibilityTests
|
|
{
|
|
private string m_GeneratedCodePath;
|
|
private HashSet<string> m_AssembliesToVerify = new HashSet<string>();
|
|
private string m_GeneratedCodeAssemblyName;
|
|
private readonly string m_TempBurstCompatibilityPath;
|
|
private readonly string m_TestScenePath;
|
|
private static readonly string s_GeneratedClassName = "_generated_burst_compat_tests";
|
|
|
|
/// <summary>
|
|
/// Sets up the code generator for Burst compatibility tests.
|
|
/// </summary>
|
|
/// <param name="assemblyNameToVerifyBurstCompatibility">Name of the assembly to verify Burst compatibility.</param>
|
|
/// <param name="generatedCodePath">Destination path for the generated Burst compatibility code.</param>
|
|
/// <param name="generatedCodeAssemblyName">Name of the assembly that will contain the generated Burst compatibility code.</param>
|
|
/// <param name="testScenePath">Path of the test scene to use when building a player.</param>
|
|
protected BurstCompatibilityTests(string assemblyNameToVerifyBurstCompatibility, string generatedCodePath, string generatedCodeAssemblyName, string testScenePath)
|
|
: this(new[] {assemblyNameToVerifyBurstCompatibility}, generatedCodePath, generatedCodeAssemblyName, testScenePath)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets up the code generator for Burst compatibility tests.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This constructor takes multiple assembly names to verify, which allows you to check multiple assemblies with
|
|
/// one test. Prefer to use this instead of separate tests that verify a single assembly if you need to minimize
|
|
/// CI time.
|
|
/// </remarks>
|
|
/// <param name="assemblyNamesToVerifyBurstCompatibility">Names of the assemblies to verify Burst compatibility.</param>
|
|
/// <param name="generatedCodePath">Destination path for the generated Burst compatibility code.</param>
|
|
/// <param name="generatedCodeAssemblyName">Name of the assembly that will contain the generated Burst compatibility code.</param>
|
|
/// <param name="testScenePath">Path of the test scene to use when building a player.</param>
|
|
protected BurstCompatibilityTests(string[] assemblyNamesToVerifyBurstCompatibility, string generatedCodePath, string generatedCodeAssemblyName, string testScenePath)
|
|
{
|
|
m_GeneratedCodePath = generatedCodePath;
|
|
|
|
foreach (var assemblyName in assemblyNamesToVerifyBurstCompatibility)
|
|
{
|
|
m_AssembliesToVerify.Add(assemblyName);
|
|
}
|
|
|
|
m_GeneratedCodeAssemblyName = generatedCodeAssemblyName;
|
|
m_TempBurstCompatibilityPath = Path.Combine("Temp", "BurstCompatibility", GetType().Name);
|
|
m_TestScenePath = testScenePath;
|
|
}
|
|
|
|
struct MethodData : IComparable<MethodData>
|
|
{
|
|
public MethodBase methodBase;
|
|
public Type InstanceType;
|
|
public Type[] MethodGenericTypeArguments;
|
|
public Type[] InstanceTypeGenericTypeArguments;
|
|
public Dictionary<string, Type> MethodGenericArgumentLookup;
|
|
public Dictionary<string, Type> InstanceTypeGenericArgumentLookup;
|
|
public string RequiredDefine;
|
|
public GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget CompileTarget;
|
|
|
|
public int CompareTo(MethodData other)
|
|
{
|
|
var lhs = methodBase;
|
|
var rhs = other.methodBase;
|
|
|
|
var ltn = methodBase.DeclaringType.FullName;
|
|
var rtn = other.methodBase.DeclaringType.FullName;
|
|
|
|
int tc = ltn.CompareTo(rtn);
|
|
if (tc != 0) return tc;
|
|
|
|
tc = lhs.Name.CompareTo(rhs.Name);
|
|
if (tc != 0) return tc;
|
|
|
|
var lp = lhs.GetParameters();
|
|
var rp = rhs.GetParameters();
|
|
if (lp.Length < rp.Length)
|
|
return -1;
|
|
if (lp.Length > rp.Length)
|
|
return 1;
|
|
|
|
var lb = new StringBuilder();
|
|
var rb = new StringBuilder();
|
|
for (int i = 0; i < lp.Length; ++i)
|
|
{
|
|
GetFullTypeName(lp[i].ParameterType, lb, this);
|
|
GetFullTypeName(rp[i].ParameterType, rb, other);
|
|
|
|
tc = lb.ToString().CompareTo(rb.ToString());
|
|
if (tc != 0)
|
|
return tc;
|
|
|
|
lb.Clear();
|
|
rb.Clear();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public Type ReplaceGeneric(Type genericType)
|
|
{
|
|
if (genericType.IsByRef)
|
|
{
|
|
genericType = genericType.GetElementType();
|
|
}
|
|
|
|
if (MethodGenericArgumentLookup == null & InstanceTypeGenericArgumentLookup == null)
|
|
{
|
|
throw new InvalidOperationException("For '{InstanceType.Name}.{Info.Name}', generic argument lookups are null! Did you forget to specify GenericTypeArguments in the [GenerateTestsForBurstCompatibility] attribute?");
|
|
}
|
|
|
|
bool hasMethodReplacement = MethodGenericArgumentLookup.ContainsKey(genericType.Name);
|
|
bool hasInstanceTypeReplacement = InstanceTypeGenericArgumentLookup.ContainsKey(genericType.Name);
|
|
|
|
if (hasMethodReplacement)
|
|
{
|
|
return MethodGenericArgumentLookup[genericType.Name];
|
|
}
|
|
else if (hasInstanceTypeReplacement)
|
|
{
|
|
return InstanceTypeGenericArgumentLookup[genericType.Name];
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException($"'{genericType.Name}' in '{InstanceType.Name}.{methodBase.Name}' has no generic type replacement in the generic argument lookups! Did you forget to specify GenericTypeArguments in the [GenerateTestsForBurstCompatibility] attribute?");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the code for the Burst compatibility tests.
|
|
/// </summary>
|
|
/// <param name="path">Path of the generated file.</param>
|
|
/// <param name="methodsTestedCount">Number of methods being tested.</param>
|
|
/// <returns>True if the file was generated successfully; false otherwise.</returns>
|
|
private bool UpdateGeneratedFile(string path, out int methodsTestedCount)
|
|
{
|
|
var buf = new StringBuilder();
|
|
var success = GetTestMethods(out MethodData[] methods);
|
|
|
|
if (!success)
|
|
{
|
|
methodsTestedCount = 0;
|
|
return false;
|
|
}
|
|
|
|
buf.AppendLine(
|
|
@"// auto-generated
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using NUnit.Framework;
|
|
using Unity.Burst;
|
|
using Unity.Collections;
|
|
using Unity.Collections.LowLevel.Unsafe;");
|
|
|
|
// This serves to force the player build to skip all the shader variants which dramatically
|
|
// reduces the player build time. Since we only care about burst compilation, this is fine
|
|
// and desirable. The reason why this code is generated is that as soon as this definition
|
|
// exists, the player build pipeline will start using it. Since we don't want to bloat user
|
|
// build pipelines with extra callbacks or modify their behavior, we generate the code here
|
|
// for test use only.
|
|
buf.AppendLine(@"
|
|
#if UNITY_EDITOR
|
|
using UnityEngine;
|
|
using UnityEditor.Build;
|
|
using UnityEditor.Rendering;
|
|
|
|
class BurstCompatibleSkipShaderVariants : IPreprocessShaders
|
|
{
|
|
public int callbackOrder => 0;
|
|
|
|
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
|
|
{
|
|
data.Clear();
|
|
}
|
|
}
|
|
#endif
|
|
");
|
|
|
|
buf.AppendLine("[BurstCompile]");
|
|
buf.AppendLine($"public unsafe class {s_GeneratedClassName}");
|
|
buf.AppendLine("{");
|
|
buf.AppendLine(" private delegate void TestFunc(IntPtr p);");
|
|
buf.AppendLine(" static unsafe ref T CreateRef<T>() where T : unmanaged => ref UnsafeUtility.AsRef<T>((void*)default);");
|
|
|
|
var overloadHandling = new Dictionary<string, int>();
|
|
|
|
foreach (var methodData in methods)
|
|
{
|
|
var method = methodData.methodBase;
|
|
var isGetter = method.Name.StartsWith("get_");
|
|
var isSetter = method.Name.StartsWith("set_");
|
|
var isProperty = isGetter | isSetter;
|
|
var isIndexer = method.Name.Equals("get_Item") || method.Name.Equals("set_Item");
|
|
var sourceName = isProperty ? method.Name.Substring(4) : method.Name;
|
|
|
|
var safeName = GetSafeName(methodData);
|
|
if (overloadHandling.ContainsKey(safeName))
|
|
{
|
|
int num = overloadHandling[safeName]++;
|
|
safeName += $"_overload{num}";
|
|
}
|
|
else
|
|
{
|
|
overloadHandling.Add(safeName, 0);
|
|
}
|
|
|
|
if (methodData.RequiredDefine != null)
|
|
{
|
|
buf.AppendLine($"#if {methodData.RequiredDefine}");
|
|
}
|
|
|
|
if (methodData.CompileTarget == GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget.Editor)
|
|
{
|
|
// Emit #if UNITY_EDITOR in case GenerateTestsForBurstCompatibility attribute doesn't have any required
|
|
// unity defines. We want to be sure that we actually test the editor only code when
|
|
// we are actually in the editor.
|
|
buf.AppendLine("#if UNITY_EDITOR");
|
|
}
|
|
|
|
buf.AppendLine(" [BurstCompile(CompileSynchronously = true)]");
|
|
buf.AppendLine($" public static void Burst_{safeName}(IntPtr p)");
|
|
buf.AppendLine(" {");
|
|
|
|
// Generate targets for out/ref parameters
|
|
var parameters = method.GetParameters();
|
|
for (int i = 0; i < parameters.Length; ++i)
|
|
{
|
|
var param = parameters[i];
|
|
var pt = param.ParameterType;
|
|
|
|
if (pt.IsGenericParameter ||
|
|
(pt.IsByRef && pt.GetElementType().IsGenericParameter))
|
|
{
|
|
pt = methodData.ReplaceGeneric(pt);
|
|
}
|
|
|
|
if (pt.IsGenericTypeDefinition)
|
|
{
|
|
pt = pt.MakeGenericType(methodData.InstanceTypeGenericTypeArguments);
|
|
}
|
|
|
|
if (pt.IsPointer)
|
|
{
|
|
TypeToString(pt, buf, methodData);
|
|
buf.Append($" v{i} = (");
|
|
TypeToString(pt, buf, methodData);
|
|
buf.AppendLine($") ((byte*)p + {i * 1024});");
|
|
} else if (HasAttribute<IsByRefLikeAttribute>(pt) || (pt.HasElementType && pt.GetElementType().IsPointer)) // is `ref struct`
|
|
{
|
|
buf.Append($" var v{i} = default(");
|
|
TypeToString(pt, buf, methodData);
|
|
buf.AppendLine(");");
|
|
}
|
|
else
|
|
{
|
|
buf.Append($" ref var v{i} = ref CreateRef<");
|
|
TypeToString(pt, buf, methodData);
|
|
buf.AppendLine(">();");
|
|
}
|
|
}
|
|
|
|
var info = method as MethodInfo;
|
|
var refStr = "";
|
|
var readonlyStr = "";
|
|
|
|
// [MayOnlyLiveInBlobStorage] forces types to be referred to by reference only, so we
|
|
// must be sure to make any local vars use ref whenever something is a ref return.
|
|
//
|
|
// Furthermore, we also care about readonly since some returns are ref readonly returns.
|
|
// Detecting readonly is done by checking the required custom modifiers for the InAttribute.
|
|
if (info != null && info.ReturnType.IsByRef)
|
|
{
|
|
refStr = "ref ";
|
|
|
|
foreach (var attr in info.ReturnParameter.GetRequiredCustomModifiers())
|
|
{
|
|
if (attr == typeof(InAttribute))
|
|
{
|
|
readonlyStr = "readonly ";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (method.IsStatic)
|
|
{
|
|
if (isGetter)
|
|
buf.Append($" {refStr}{readonlyStr}var __result = {refStr}");
|
|
TypeToString(methodData.InstanceType, buf, methodData);
|
|
buf.Append($".{sourceName}");
|
|
}
|
|
else
|
|
{
|
|
StringBuilder typeStringBuilder = new StringBuilder();
|
|
TypeToString(methodData.InstanceType, typeStringBuilder, methodData);
|
|
|
|
// ref structs need special handling, can't be used as type arguments
|
|
if (typeStringBuilder.ToString() == "Unity.Entities.SystemState" ||
|
|
typeStringBuilder.ToString() == "Unity.Entities.EntityQueryBuilder")
|
|
{
|
|
buf.Append($" ref var instance = ref *(");
|
|
buf.Append(typeStringBuilder);
|
|
buf.AppendLine("*)((void*)p);");
|
|
}
|
|
else
|
|
{
|
|
buf.Append($" ref var instance = ref UnsafeUtility.AsRef<");
|
|
buf.Append(typeStringBuilder);
|
|
buf.AppendLine(">((void*)p);");
|
|
}
|
|
|
|
if (isIndexer)
|
|
{
|
|
if (isGetter)
|
|
buf.Append($" {refStr}{readonlyStr}var __result = {refStr}instance");
|
|
else if (isSetter)
|
|
buf.Append(" instance");
|
|
}
|
|
else if (method.IsConstructor)
|
|
{
|
|
// Begin the setup for the constructor call. Arguments will be handled later.
|
|
buf.Append(" instance = new ");
|
|
TypeToString(methodData.InstanceType, buf, methodData);
|
|
}
|
|
else
|
|
{
|
|
if (isGetter)
|
|
buf.Append($" {refStr}{readonlyStr}var __result = {refStr}");
|
|
buf.Append($" instance.{sourceName}");
|
|
}
|
|
}
|
|
|
|
if (method.IsGenericMethod)
|
|
{
|
|
buf.Append("<");
|
|
var args = method.GetGenericArguments();
|
|
for (int i = 0; i < args.Length; ++i)
|
|
{
|
|
if (i > 0)
|
|
buf.Append(", ");
|
|
TypeToString(args[i], buf, methodData);
|
|
}
|
|
buf.Append(">");
|
|
}
|
|
|
|
// Make dummy arguments.
|
|
if (isIndexer)
|
|
{
|
|
buf.Append("[");
|
|
}
|
|
else
|
|
{
|
|
if (isGetter)
|
|
{
|
|
}
|
|
else if (isSetter)
|
|
buf.Append("=");
|
|
else
|
|
buf.Append("(");
|
|
}
|
|
|
|
for (int i = 0; i < parameters.Length; ++i)
|
|
{
|
|
// Close the indexer brace and assign the value if we're handling an indexer and setter.
|
|
if (isIndexer && isSetter && i + 1 == parameters.Length)
|
|
{
|
|
buf.Append($"] = v{i}");
|
|
break;
|
|
}
|
|
|
|
if (i > 0)
|
|
buf.Append(" ,");
|
|
|
|
// Close the indexer brace. This is separate from the setter logic above because the
|
|
// comma separating arguments is required for the getter case but not for the setter.
|
|
if (isIndexer && isGetter && i + 1 == parameters.Length)
|
|
{
|
|
buf.Append($"v{i}]");
|
|
break;
|
|
}
|
|
|
|
var param = parameters[i];
|
|
|
|
if (param.IsOut)
|
|
{
|
|
buf.Append("out ");
|
|
}
|
|
else if (param.IsIn)
|
|
{
|
|
buf.Append("in ");
|
|
}
|
|
else if (param.ParameterType.IsByRef)
|
|
{
|
|
buf.Append("ref ");
|
|
}
|
|
|
|
buf.Append($"v{i}");
|
|
}
|
|
|
|
if (!isProperty)
|
|
buf.Append(")");
|
|
|
|
buf.AppendLine(";");
|
|
buf.AppendLine(" }");
|
|
|
|
if (methodData.CompileTarget == GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget.Editor)
|
|
{
|
|
// Closes #if UNITY_EDITOR that surrounds the actual method/property being tested.
|
|
buf.AppendLine("#endif");
|
|
}
|
|
|
|
// Set up the call to BurstCompiler.CompileFunctionPointer only in the cases where it is necessary.
|
|
switch (methodData.CompileTarget)
|
|
{
|
|
case GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget.PlayerAndEditor:
|
|
case GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget.Editor:
|
|
{
|
|
buf.AppendLine("#if UNITY_EDITOR");
|
|
buf.AppendLine($" public static void BurstCompile_{safeName}()");
|
|
buf.AppendLine(" {");
|
|
buf.AppendLine($" BurstCompiler.CompileFunctionPointer<TestFunc>(Burst_{safeName});");
|
|
buf.AppendLine(" }");
|
|
buf.AppendLine("#endif");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (methodData.RequiredDefine != null)
|
|
{
|
|
buf.AppendLine($"#endif");
|
|
}
|
|
}
|
|
|
|
buf.AppendLine("}");
|
|
|
|
File.WriteAllText(path, buf.ToString());
|
|
methodsTestedCount = methods.Length;
|
|
return true;
|
|
}
|
|
|
|
static bool HasAttribute<T>(MemberInfo type)
|
|
{
|
|
foreach (var ca in type.CustomAttributes)
|
|
if (ca.AttributeType.IsEquivalentTo(typeof(T)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
private static void TypeToString(Type t, StringBuilder buf, in MethodData methodData)
|
|
{
|
|
if (t.IsPrimitive || t == typeof(void))
|
|
{
|
|
buf.Append(PrimitiveTypeToString(t));
|
|
return;
|
|
}
|
|
|
|
if (t.IsByRef)
|
|
{
|
|
TypeToString(t.GetElementType(), buf, methodData);
|
|
return;
|
|
}
|
|
|
|
// This should come after the IsByRef check above to avoid adding an extra asterisk.
|
|
// You could have a T*& (ref to a pointer) which causes t.IsByRef and t.IsPointer to both be true and if
|
|
// you check t.IsPointer first then descend down the types with t.GetElementType() you end up with
|
|
// T* which causes you to descend a second time then print an asterisk twice as you come back up the
|
|
// recursion.
|
|
if (t.IsPointer)
|
|
{
|
|
TypeToString(t.GetElementType(), buf, methodData);
|
|
buf.Append("*");
|
|
return;
|
|
}
|
|
|
|
GetFullTypeName(t, buf, methodData);
|
|
}
|
|
|
|
private static string PrimitiveTypeToString(Type type)
|
|
{
|
|
if (type == typeof(void))
|
|
return "void";
|
|
if (type == typeof(bool))
|
|
return "bool";
|
|
if (type == typeof(byte))
|
|
return "byte";
|
|
if (type == typeof(sbyte))
|
|
return "sbyte";
|
|
if (type == typeof(short))
|
|
return "short";
|
|
if (type == typeof(ushort))
|
|
return "ushort";
|
|
if (type == typeof(int))
|
|
return "int";
|
|
if (type == typeof(uint))
|
|
return "uint";
|
|
if (type == typeof(long))
|
|
return "long";
|
|
if (type == typeof(ulong))
|
|
return "ulong";
|
|
if (type == typeof(char))
|
|
return "char";
|
|
if (type == typeof(double))
|
|
return "double";
|
|
if (type == typeof(float))
|
|
return "float";
|
|
if (type == typeof(IntPtr))
|
|
return "IntPtr";
|
|
if (type == typeof(UIntPtr))
|
|
return "UIntPtr";
|
|
|
|
throw new InvalidOperationException($"{type} is not a primitive type");
|
|
}
|
|
|
|
private static void GetFullTypeName(Type type, StringBuilder buf, in MethodData methodData)
|
|
{
|
|
// If we encounter a generic parameter (typically T) then we should replace it with a real one
|
|
// specified by [GenerateTestsForBurstCompatibility(GenericTypeArguments = new [] { typeof(...) })].
|
|
if (type.IsGenericParameter)
|
|
{
|
|
GetFullTypeName(methodData.ReplaceGeneric(type), buf, methodData);
|
|
return;
|
|
}
|
|
|
|
if (type.DeclaringType != null)
|
|
{
|
|
GetFullTypeName(type.DeclaringType, buf, methodData);
|
|
buf.Append(".");
|
|
}
|
|
else
|
|
{
|
|
// These appends for the namespace used to be protected by an if check for Unity.Collections or
|
|
// Unity.Collections.LowLevel.Unsafe, but HashSetExtensions lives in both so just fully disambiguate
|
|
// by always appending the namespace.
|
|
buf.Append(type.Namespace);
|
|
buf.Append(".");
|
|
}
|
|
|
|
var name = type.Name;
|
|
|
|
var idx = name.IndexOf('`');
|
|
if (-1 != idx)
|
|
{
|
|
name = name.Remove(idx);
|
|
}
|
|
|
|
buf.Append(name);
|
|
|
|
if (type.IsConstructedGenericType || type.IsGenericTypeDefinition)
|
|
{
|
|
var gt = type.GetGenericArguments();
|
|
|
|
// Avoid printing out the generic arguments for cases like UnsafeParallelHashMap<TKey, TValue>.ParallelWriter.
|
|
// ParallelWriter is considered to be a generic type and will have two generic parameters inherited
|
|
// from UnsafeParallelHashMap. Because of this, if we don't do this check, we could code gen this:
|
|
//
|
|
// UnsafeParallelHashMap<int, int>.ParallelWriter<int, int>
|
|
//
|
|
// But we want:
|
|
//
|
|
// UnsafeParallelHashMap<int, int>.ParallelWriter
|
|
//
|
|
// ParallelWriter doesn't actually have generic arguments you can give it directly so it's not correct
|
|
// to give it generic arguments. If the nested type has the same number of generic arguments as its
|
|
// declaring type, then there should be no new generic arguments and therefore nothing to print.
|
|
if (type.IsNested && gt.Length == type.DeclaringType.GetGenericArguments().Length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
buf.Append("<");
|
|
|
|
for (int i = 0; i < gt.Length; ++i)
|
|
{
|
|
if (i > 0)
|
|
{
|
|
buf.Append(", ");
|
|
}
|
|
|
|
TypeToString(gt[i], buf, methodData);
|
|
}
|
|
|
|
buf.Append(">");
|
|
}
|
|
}
|
|
|
|
private static readonly Type[] EmptyGenericTypeArguments = { };
|
|
|
|
private bool GetTestMethods(out MethodData[] methods)
|
|
{
|
|
var seenMethods = new HashSet<MethodBase>();
|
|
var result = new List<MethodData>();
|
|
int errorCount = 0;
|
|
|
|
void LogError(string message)
|
|
{
|
|
++errorCount;
|
|
Debug.LogError(message);
|
|
}
|
|
|
|
void MaybeAddMethod(MethodBase m, Type[] methodGenericTypeArguments, Type[] declaredTypeGenericTypeArguments, string requiredDefine, MemberInfo attributeHolder, GenerateTestsForBurstCompatibilityAttribute.BurstCompatibleCompileTarget compileTarget)
|
|
{
|
|
if (m.IsPrivate)
|
|
{
|
|
// Private methods that were explicitly tagged as [GenerateTestsForBurstCompatibility] should generate an error to
|
|
// avoid users thinking the method is being tested when it actually isn't.
|
|
if (m.GetCustomAttribute<GenerateTestsForBurstCompatibilityAttribute>() != null)
|
|
{
|
|
// Just return and avoiding printing duplicate errors if we've already seen this method.
|
|
if (seenMethods.Contains(m))
|
|
return;
|
|
|
|
seenMethods.Add(m);
|
|
LogError($"GenerateTestsForBurstCompatibilityAttribute cannot be used with private methods, but found on private method `{m.DeclaringType}.{m}`. Make method public, internal, or ensure that this method is called by another public/internal method that is tested.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Burst IL post processing might create a new method that contains $ to ensure it doesn't
|
|
// conflict with any real names (as an example, it can append $BurstManaged to the original method name).
|
|
// However, trying to call that method directly in C# isn't possible because the $ is invalid for
|
|
// identifiers.
|
|
if (m.Name.Contains('$'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (attributeHolder.GetCustomAttribute<ObsoleteAttribute>() != null)
|
|
return;
|
|
if (attributeHolder.GetCustomAttribute<ExcludeFromBurstCompatTestingAttribute>() != null)
|
|
return;
|
|
if (attributeHolder.GetCustomAttribute<BurstDiscardAttribute>() != null)
|
|
return;
|
|
|
|
// If this is not a property but still has a special name, ignore it.
|
|
if (attributeHolder is MethodInfo && m.IsSpecialName)
|
|
return;
|
|
|
|
var methodGenericArgumentLookup = new Dictionary<string, Type>();
|
|
|
|
if (m.IsGenericMethodDefinition)
|
|
{
|
|
if (methodGenericTypeArguments == null)
|
|
{
|
|
LogError($"Method `{m.DeclaringType}.{m}` is generic but doesn't have a type array in its GenerateTestsForBurstCompatibility attribute");
|
|
return;
|
|
}
|
|
|
|
var genericArguments = m.GetGenericArguments();
|
|
|
|
if (genericArguments.Length != methodGenericTypeArguments.Length)
|
|
{
|
|
LogError($"Method `{m.DeclaringType}.{m}` is generic with {genericArguments.Length} generic parameters but GenerateTestsForBurstCompatibility attribute has {methodGenericTypeArguments.Length} types, they must be the same length!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
m = (m as MethodInfo).MakeGenericMethod(methodGenericTypeArguments);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogError($"Could not instantiate method `{m.DeclaringType}.{m}` with type arguments `{methodGenericTypeArguments}`.");
|
|
Debug.LogException(e);
|
|
return;
|
|
}
|
|
|
|
// Build up the generic name to type lookup for this method.
|
|
for (int i = 0; i < genericArguments.Length; ++i)
|
|
{
|
|
var name = genericArguments[i].Name;
|
|
var type = methodGenericTypeArguments[i];
|
|
|
|
try
|
|
{
|
|
methodGenericArgumentLookup.Add(name, type);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogError($"For method `{m.DeclaringType}.{m}`, could not add ({name}, {type}).");
|
|
Debug.LogException(e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
var instanceType = m.DeclaringType;
|
|
var instanceTypeGenericLookup = new Dictionary<string, Type>();
|
|
|
|
if (instanceType.IsGenericTypeDefinition)
|
|
{
|
|
var instanceGenericArguments = instanceType.GetGenericArguments();
|
|
|
|
if (declaredTypeGenericTypeArguments == null)
|
|
{
|
|
LogError($"Type `{m.DeclaringType}` is generic but doesn't have a type array in its GenerateTestsForBurstCompatibility attribute");
|
|
return;
|
|
}
|
|
|
|
if (instanceGenericArguments.Length != declaredTypeGenericTypeArguments.Length)
|
|
{
|
|
LogError($"Type `{instanceType}` is generic with {instanceGenericArguments.Length} generic parameters but GenerateTestsForBurstCompatibility attribute has {declaredTypeGenericTypeArguments.Length} types, they must be the same length!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
instanceType = instanceType.MakeGenericType(declaredTypeGenericTypeArguments);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogError($"Could not instantiate type `{instanceType}` with type arguments `{declaredTypeGenericTypeArguments}`.");
|
|
Debug.LogException(e);
|
|
return;
|
|
}
|
|
|
|
// Build up the generic name to type lookup for this method.
|
|
for (int i = 0; i < instanceGenericArguments.Length; ++i)
|
|
{
|
|
var name = instanceGenericArguments[i].Name;
|
|
var type = declaredTypeGenericTypeArguments[i];
|
|
|
|
try
|
|
{
|
|
instanceTypeGenericLookup.Add(name, type);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LogError($"For type `{instanceType}`, could not add ({name}, {type}).");
|
|
Debug.LogException(e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//if (m.GetParameters().Any((p) => !p.ParameterType.IsValueType && !p.ParameterType.IsPointer))
|
|
// return;
|
|
|
|
// These are crazy nested function names. They'll be covered anyway as the parent function is burst compatible.
|
|
if (m.Name.Contains('<'))
|
|
return;
|
|
|
|
if (seenMethods.Contains(m))
|
|
return;
|
|
|
|
seenMethods.Add(m);
|
|
result.Add(new MethodData
|
|
{
|
|
methodBase = m, InstanceType = instanceType, MethodGenericTypeArguments = methodGenericTypeArguments,
|
|
InstanceTypeGenericTypeArguments = declaredTypeGenericTypeArguments, RequiredDefine = requiredDefine,
|
|
MethodGenericArgumentLookup = methodGenericArgumentLookup, InstanceTypeGenericArgumentLookup = instanceTypeGenericLookup,
|
|
CompileTarget = compileTarget
|
|
});
|
|
}
|
|
|
|
var declaredTypeGenericArguments = new Dictionary<Type, Type[]>();
|
|
|
|
// Go through types tagged with [GenerateTestsForBurstCompatibility] and their methods before performing the direct method
|
|
// search (below this loop) to ensure that we get the declared type generic arguments.
|
|
//
|
|
// If we were to run the direct method search first, it's possible we would add the method to the seen list
|
|
// and then by the time we execute this loop we might skip it because we think we have seen the method
|
|
// already but we haven't grabbed the declared type generic arguments yet.
|
|
foreach (var t in TypeCache.GetTypesWithAttribute<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
if (!m_AssembliesToVerify.Contains(t.Assembly.GetName().Name))
|
|
continue;
|
|
|
|
foreach (var typeAttr in t.GetCustomAttributes<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
// As we go through all the types with [GenerateTestsForBurstCompatibility] on them, remember their GenericTypeArguments
|
|
// in case we encounter the type again when we do the direct method search later. When we do the
|
|
// direct method search, we don't have as easy access to the [GenerateTestsForBurstCompatibility] attribute on the
|
|
// type so just remember this now to make life easier.
|
|
declaredTypeGenericArguments[t] = typeAttr.GenericTypeArguments;
|
|
|
|
const BindingFlags flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public |
|
|
BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
|
|
foreach (var c in t.GetConstructors(flags))
|
|
{
|
|
bool hadAttributes = false;
|
|
foreach (var methodAttr in c.GetCustomAttributes<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
hadAttributes = true;
|
|
MaybeAddMethod(c, methodAttr.GenericTypeArguments, typeAttr.GenericTypeArguments, methodAttr.RequiredUnityDefine ?? typeAttr.RequiredUnityDefine, c, methodAttr.CompileTarget);
|
|
}
|
|
|
|
if(!hadAttributes)
|
|
{
|
|
MaybeAddMethod(c, EmptyGenericTypeArguments, typeAttr.GenericTypeArguments, typeAttr.RequiredUnityDefine, c, typeAttr.CompileTarget);
|
|
}
|
|
}
|
|
|
|
foreach (var m in t.GetMethods(flags))
|
|
{
|
|
// If this is a property getter/setter, the attributes will be stored on the property itself, not
|
|
// the method.
|
|
MemberInfo attributeHolder = m;
|
|
if (m.IsSpecialName && (m.Name.StartsWith("get_") || m.Name.StartsWith("set_")))
|
|
{
|
|
attributeHolder = m.DeclaringType.GetProperty(m.Name.Substring("get_".Length), flags);
|
|
Debug.Assert(attributeHolder != null, $"Failed to find property for {m.Name} in {m.DeclaringType.Name}");
|
|
}
|
|
|
|
bool hadAttributes = false;
|
|
foreach (var methodAttr in attributeHolder.GetCustomAttributes<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
hadAttributes = true;
|
|
MaybeAddMethod(m, methodAttr.GenericTypeArguments, typeAttr.GenericTypeArguments, methodAttr.RequiredUnityDefine ?? typeAttr.RequiredUnityDefine, attributeHolder, methodAttr.CompileTarget);
|
|
}
|
|
|
|
if (!hadAttributes)
|
|
{
|
|
MaybeAddMethod(m, EmptyGenericTypeArguments, typeAttr.GenericTypeArguments, typeAttr.RequiredUnityDefine, attributeHolder, typeAttr.CompileTarget);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Direct method search.
|
|
foreach (var m in TypeCache.GetMethodsWithAttribute<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
if (!m_AssembliesToVerify.Contains(m.DeclaringType.Assembly.GetName().Name))
|
|
continue;
|
|
|
|
// Look up the GenericTypeArguments on the declaring type that we probably got earlier. If the key
|
|
// doesn't exist then it means [GenerateTestsForBurstCompatibility] was only on the method and not the type, which is fine
|
|
// but if [GenerateTestsForBurstCompatibility] was on the type we should go ahead and use whatever GenericTypeArguments
|
|
// it may have had.
|
|
var typeGenericArguments = declaredTypeGenericArguments.ContainsKey(m.DeclaringType) ? declaredTypeGenericArguments[m.DeclaringType] : EmptyGenericTypeArguments;
|
|
|
|
foreach (var attr in m.GetCustomAttributes<GenerateTestsForBurstCompatibilityAttribute>())
|
|
{
|
|
MaybeAddMethod(m, attr.GenericTypeArguments, typeGenericArguments, attr.RequiredUnityDefine, m, attr.CompileTarget);
|
|
}
|
|
}
|
|
|
|
if (errorCount > 0)
|
|
{
|
|
methods = new MethodData[] {};
|
|
return false;
|
|
}
|
|
|
|
methods = result.ToArray();
|
|
Array.Sort(methods);
|
|
return true;
|
|
}
|
|
|
|
private static string GetSafeName(in MethodData methodData)
|
|
{
|
|
var method = methodData.methodBase;
|
|
return GetSafeName(method.DeclaringType, methodData) + "_" + r.Replace(method.Name, "__");
|
|
}
|
|
|
|
public static readonly Regex r = new Regex(@"[^A-Za-z_0-9]+");
|
|
|
|
private static string GetSafeName(Type t, in MethodData methodData)
|
|
{
|
|
var b = new StringBuilder();
|
|
GetFullTypeName(t, b, methodData);
|
|
return r.Replace(b.ToString(), "__");
|
|
}
|
|
|
|
Assembly GetAssemblyByName(string name)
|
|
{
|
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
foreach(var assembly in assemblies)
|
|
{
|
|
if(assembly.GetName().Name == name)
|
|
return assembly;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
private StreamWriter m_BurstCompileLogs;
|
|
|
|
// This is a log handler to save all the logs during burst compilation and eventually write to a file
|
|
// so it is easier to see all the logs without truncation in the test runner.
|
|
void LogHandler(string logString, string stackTrace, LogType type)
|
|
{
|
|
m_BurstCompileLogs.WriteLine(logString);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator CompatibilityTests()
|
|
{
|
|
int runCount = 0;
|
|
int successCount = 0;
|
|
bool playerBuildSucceeded = false;
|
|
var playerBuildTime = new TimeSpan();
|
|
var compileFunctionPointerTime = new TimeSpan();
|
|
|
|
try
|
|
{
|
|
if (!UpdateGeneratedFile(m_GeneratedCodePath, out var generatedCount))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
// Make another copy of the generated code and put it in Temp so it's easier to inspect.
|
|
var debugCodeGenTempDest = Path.Combine(m_TempBurstCompatibilityPath, "generated.cs");
|
|
Directory.CreateDirectory(m_TempBurstCompatibilityPath);
|
|
File.Copy(m_GeneratedCodePath, debugCodeGenTempDest, true);
|
|
Debug.Log($"Generated {generatedCount} Burst compatibility tests.");
|
|
Debug.Log($"You can inspect a copy of the generated code at: {Path.GetFullPath(debugCodeGenTempDest)}");
|
|
|
|
// BEWARE: this causes a domain reload and can cause variables to go null.
|
|
yield return new RecompileScripts();
|
|
|
|
var t = GetAssemblyByName(m_GeneratedCodeAssemblyName).GetType(s_GeneratedClassName);
|
|
if (t == null)
|
|
{
|
|
throw new ApplicationException($"could not find generated type {s_GeneratedClassName} in assembly {m_GeneratedCodeAssemblyName}");
|
|
}
|
|
|
|
var logPath = Path.Combine(m_TempBurstCompatibilityPath, "BurstCompileLog.txt");
|
|
m_BurstCompileLogs = File.CreateText(logPath);
|
|
Debug.Log($"Logs from Burst compilation written to: {Path.GetFullPath(logPath)}\n");
|
|
Application.logMessageReceived += LogHandler;
|
|
var stopwatch = new Stopwatch();
|
|
stopwatch.Start();
|
|
foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
|
{
|
|
if (m.Name.StartsWith("BurstCompile_"))
|
|
{
|
|
++runCount;
|
|
try
|
|
{
|
|
m.Invoke(null, null);
|
|
++successCount;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogException(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
compileFunctionPointerTime = stopwatch.Elapsed;
|
|
|
|
var buildOptions = new BuildPlayerOptions();
|
|
buildOptions.scenes = new[] { m_TestScenePath };
|
|
buildOptions.target = EditorUserBuildSettings.activeBuildTarget;
|
|
buildOptions.locationPathName = FileUtil.GetUniqueTempPathInProject();
|
|
// TODO: DOTS-4886
|
|
// Remove when fixed
|
|
// Made a development build due to a bug in the Editor causing Debug.isDebugBuild to be false
|
|
// on the next Domain reload after this build is made
|
|
buildOptions.options = BuildOptions.IncludeTestAssemblies | BuildOptions.Development;
|
|
|
|
// Temporary hack to work around issue where headless no-graphics CI pass would spit out
|
|
// `RenderTexture.Create with shadow sampling failed` error, causing this test to fail.
|
|
// Feature has been requested to NOT log this error when running headless.
|
|
var ignoreFailingMessagesCache = LogAssert.ignoreFailingMessages;
|
|
LogAssert.ignoreFailingMessages = true;
|
|
var buildReport = BuildPipeline.BuildPlayer(buildOptions);
|
|
LogAssert.ignoreFailingMessages = ignoreFailingMessagesCache;
|
|
|
|
playerBuildSucceeded = buildReport.summary.result == BuildResult.Succeeded;
|
|
playerBuildTime = buildReport.summary.totalTime;
|
|
}
|
|
finally
|
|
{
|
|
Application.logMessageReceived -= LogHandler;
|
|
m_BurstCompileLogs?.Close();
|
|
Debug.Log($"Player build duration: {playerBuildTime.TotalSeconds} s.");
|
|
|
|
if (playerBuildSucceeded)
|
|
{
|
|
Debug.Log("Burst AOT compile via player build succeeded.");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("Player build FAILED.");
|
|
}
|
|
|
|
Debug.Log($"Compile function pointer duration: {compileFunctionPointerTime.TotalSeconds} s.");
|
|
|
|
if (runCount != successCount)
|
|
{
|
|
Debug.LogError($"Burst compatibility tests failed; ran {runCount} editor tests, {successCount} OK, {runCount - successCount} FAILED.");
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($"Ran {runCount} editor Burst compatible tests, all OK.");
|
|
}
|
|
|
|
AssetDatabase.DeleteAsset(m_GeneratedCodePath);
|
|
}
|
|
|
|
yield return new RecompileScripts();
|
|
}
|
|
}
|
|
}
|
|
#endif
|