暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BurstCompatibilityTests.cs 44KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. #if UNITY_EDITOR
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Reflection;
  9. using System.Runtime.InteropServices;
  10. using System.Text;
  11. using System.Text.RegularExpressions;
  12. using UnityEditor;
  13. using UnityEditor.Build.Reporting;
  14. using UnityEngine;
  15. using UnityEngine.TestTools;
  16. using Assembly = System.Reflection.Assembly;
  17. using Debug = UnityEngine.Debug;
  18. namespace Unity.Collections.Tests
  19. {
  20. /// <summary>
  21. /// Base class for semi-automated burst compatibility testing.
  22. /// </summary>
  23. /// <remarks>
  24. /// To create Burst compatibility tests for your assembly, you must do the following:<para/> <para/>
  25. ///
  26. /// 1. Set up a directory to contain the generated Burst compatibility code.<para/> <para/>
  27. ///
  28. /// 2. Create a new asmdef in that directory. You should set up the references so you can access Burst and
  29. /// your assembly. This new asmdef *must* support all platforms because the Burst compatibility tests use player
  30. /// builds to compile the code in parallel.<para/> <para/>
  31. ///
  32. /// 3. If you wish to test internal methods, you should make internals visible to the new asmdef you created in
  33. /// step 2.<para/> <para/>
  34. ///
  35. /// 4. Create a new class and inherit BurstCompatibilityTests. Call the base constructor with the appropriate
  36. /// arguments so BurstCompatibilityTests knows which assemblies to scan for the [BurstCompatible] attribute and
  37. /// where to put the generated Burst compatibility code. This new class should live in an editor only assembly and
  38. /// the generated Burst compatibility code should be a part of the multiplatform asmdef you created in step 2.
  39. /// <para/> <para/>
  40. ///
  41. /// 5. If your generated code will live in your package directory, you may need to add the [EmbeddedPackageOnlyTest]
  42. /// attribute to your new class.<para/> <para/>
  43. ///
  44. /// 6. Start adding [BurstCompatible] or [NotBurstCompatible] attributes to your types or methods.<para/> <para/>
  45. ///
  46. /// 7. In the test runner, run the test called CompatibilityTests that can be found nested under the name of your
  47. /// new test class you implemented in step 4.<para/> <para/>
  48. /// </remarks>
  49. public abstract class BurstCompatibilityTests
  50. {
  51. private string m_GeneratedCodePath;
  52. private HashSet<string> m_AssembliesToVerify = new HashSet<string>();
  53. private string m_GeneratedCodeAssemblyName;
  54. private readonly string m_TempBurstCompatibilityPath;
  55. private static readonly string s_GeneratedClassName = "_generated_burst_compat_tests";
  56. /// <summary>
  57. /// Sets up the code generator for Burst compatibility tests.
  58. /// </summary>
  59. /// <param name="assemblyNameToVerifyBurstCompatibility">Name of the assembly to verify Burst compatibility.</param>
  60. /// <param name="generatedCodePath">Destination path for the generated Burst compatibility code.</param>
  61. /// <param name="generatedCodeAssemblyName">Name of the assembly that will contain the generated Burst compatibility code.</param>
  62. protected BurstCompatibilityTests(string assemblyNameToVerifyBurstCompatibility, string generatedCodePath, string generatedCodeAssemblyName)
  63. : this(new[] {assemblyNameToVerifyBurstCompatibility}, generatedCodePath, generatedCodeAssemblyName)
  64. {
  65. }
  66. /// <summary>
  67. /// Sets up the code generator for Burst compatibility tests.
  68. /// </summary>
  69. /// <remarks>
  70. /// This constructor takes multiple assembly names to verify, which allows you to check multiple assemblies with
  71. /// one test. Prefer to use this instead of separate tests that verify a single assembly if you need to minimize
  72. /// CI time.
  73. /// </remarks>
  74. /// <param name="assemblyNamesToVerifyBurstCompatibility">Names of the assemblies to verify Burst compatibility.</param>
  75. /// <param name="generatedCodePath">Destination path for the generated Burst compatibility code.</param>
  76. /// <param name="generatedCodeAssemblyName">Name of the assembly that will contain the generated Burst compatibility code.</param>
  77. protected BurstCompatibilityTests(string[] assemblyNamesToVerifyBurstCompatibility, string generatedCodePath, string generatedCodeAssemblyName)
  78. {
  79. m_GeneratedCodePath = generatedCodePath;
  80. foreach (var assemblyName in assemblyNamesToVerifyBurstCompatibility)
  81. {
  82. m_AssembliesToVerify.Add(assemblyName);
  83. }
  84. m_GeneratedCodeAssemblyName = generatedCodeAssemblyName;
  85. m_TempBurstCompatibilityPath = Path.Combine("Temp", "BurstCompatibility", GetType().Name);
  86. }
  87. struct MethodData : IComparable<MethodData>
  88. {
  89. public MethodBase methodBase;
  90. public Type InstanceType;
  91. public Type[] MethodGenericTypeArguments;
  92. public Type[] InstanceTypeGenericTypeArguments;
  93. public Dictionary<string, Type> MethodGenericArgumentLookup;
  94. public Dictionary<string, Type> InstanceTypeGenericArgumentLookup;
  95. public string RequiredDefine;
  96. public BurstCompatibleAttribute.BurstCompatibleCompileTarget CompileTarget;
  97. public int CompareTo(MethodData other)
  98. {
  99. var lhs = methodBase;
  100. var rhs = other.methodBase;
  101. var ltn = methodBase.DeclaringType.FullName;
  102. var rtn = other.methodBase.DeclaringType.FullName;
  103. int tc = ltn.CompareTo(rtn);
  104. if (tc != 0) return tc;
  105. tc = lhs.Name.CompareTo(rhs.Name);
  106. if (tc != 0) return tc;
  107. var lp = lhs.GetParameters();
  108. var rp = rhs.GetParameters();
  109. if (lp.Length < rp.Length)
  110. return -1;
  111. if (lp.Length > rp.Length)
  112. return 1;
  113. var lb = new StringBuilder();
  114. var rb = new StringBuilder();
  115. for (int i = 0; i < lp.Length; ++i)
  116. {
  117. GetFullTypeName(lp[i].ParameterType, lb, this);
  118. GetFullTypeName(rp[i].ParameterType, rb, other);
  119. tc = lb.ToString().CompareTo(rb.ToString());
  120. if (tc != 0)
  121. return tc;
  122. lb.Clear();
  123. rb.Clear();
  124. }
  125. return 0;
  126. }
  127. public Type ReplaceGeneric(Type genericType)
  128. {
  129. if (genericType.IsByRef)
  130. {
  131. genericType = genericType.GetElementType();
  132. }
  133. if (MethodGenericArgumentLookup == null & InstanceTypeGenericArgumentLookup == null)
  134. {
  135. throw new InvalidOperationException("For '{InstanceType.Name}.{Info.Name}', generic argument lookups are null! Did you forget to specify GenericTypeArguments in the [BurstCompatible] attribute?");
  136. }
  137. bool hasMethodReplacement = MethodGenericArgumentLookup.ContainsKey(genericType.Name);
  138. bool hasInstanceTypeReplacement = InstanceTypeGenericArgumentLookup.ContainsKey(genericType.Name);
  139. if (hasMethodReplacement)
  140. {
  141. return MethodGenericArgumentLookup[genericType.Name];
  142. }
  143. else if (hasInstanceTypeReplacement)
  144. {
  145. return InstanceTypeGenericArgumentLookup[genericType.Name];
  146. }
  147. else
  148. {
  149. 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 [BurstCompatible] attribute?");
  150. }
  151. }
  152. }
  153. /// <summary>
  154. /// Generates the code for the Burst compatibility tests.
  155. /// </summary>
  156. /// <param name="path">Path of the generated file.</param>
  157. /// <param name="methodsTestedCount">Number of methods being tested.</param>
  158. /// <returns>True if the file was generated successfully; false otherwise.</returns>
  159. private bool UpdateGeneratedFile(string path, out int methodsTestedCount)
  160. {
  161. var buf = new StringBuilder();
  162. var success = GetTestMethods(out MethodData[] methods);
  163. if (!success)
  164. {
  165. methodsTestedCount = 0;
  166. return false;
  167. }
  168. buf.AppendLine(
  169. @"// auto-generated
  170. #if !NET_DOTS
  171. using System;
  172. using System.Collections.Generic;
  173. using NUnit.Framework;
  174. using Unity.Burst;
  175. using Unity.Collections;
  176. using Unity.Collections.LowLevel.Unsafe;");
  177. // This serves to force the player build to skip all the shader variants which dramatically
  178. // reduces the player build time. Since we only care about burst compilation, this is fine
  179. // and desirable. The reason why this code is generated is that as soon as this definition
  180. // exists, the player build pipeline will start using it. Since we don't want to bloat user
  181. // build pipelines with extra callbacks or modify their behavior, we generate the code here
  182. // for test use only.
  183. buf.AppendLine(@"
  184. #if UNITY_EDITOR
  185. using UnityEngine;
  186. using UnityEditor.Build;
  187. using UnityEditor.Rendering;
  188. class BurstCompatibleSkipShaderVariants : IPreprocessShaders
  189. {
  190. public int callbackOrder => 0;
  191. public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
  192. {
  193. data.Clear();
  194. }
  195. }
  196. #endif
  197. ");
  198. buf.AppendLine("[BurstCompile]");
  199. buf.AppendLine($"public unsafe class {s_GeneratedClassName}");
  200. buf.AppendLine("{");
  201. buf.AppendLine(" private delegate void TestFunc(IntPtr p);");
  202. var overloadHandling = new Dictionary<string, int>();
  203. foreach (var methodData in methods)
  204. {
  205. var method = methodData.methodBase;
  206. var isGetter = method.Name.StartsWith("get_");
  207. var isSetter = method.Name.StartsWith("set_");
  208. var isProperty = isGetter | isSetter;
  209. var isIndexer = method.Name.Equals("get_Item") || method.Name.Equals("set_Item");
  210. var sourceName = isProperty ? method.Name.Substring(4) : method.Name;
  211. var safeName = GetSafeName(methodData);
  212. if (overloadHandling.ContainsKey(safeName))
  213. {
  214. int num = overloadHandling[safeName]++;
  215. safeName += $"_overload{num}";
  216. }
  217. else
  218. {
  219. overloadHandling.Add(safeName, 0);
  220. }
  221. if (methodData.RequiredDefine != null)
  222. {
  223. buf.AppendLine($"#if {methodData.RequiredDefine}");
  224. }
  225. if (methodData.CompileTarget == BurstCompatibleAttribute.BurstCompatibleCompileTarget.Editor)
  226. {
  227. // Emit #if UNITY_EDITOR in case BurstCompatible attribute doesn't have any required
  228. // unity defines. We want to be sure that we actually test the editor only code when
  229. // we are actually in the editor.
  230. buf.AppendLine("#if UNITY_EDITOR");
  231. }
  232. buf.AppendLine(" [BurstCompile(CompileSynchronously = true)]");
  233. buf.AppendLine($" public static void Burst_{safeName}(IntPtr p)");
  234. buf.AppendLine(" {");
  235. // Generate targets for out/ref parameters
  236. var parameters = method.GetParameters();
  237. for (int i = 0; i < parameters.Length; ++i)
  238. {
  239. var param = parameters[i];
  240. var pt = param.ParameterType;
  241. if (pt.IsGenericParameter ||
  242. (pt.IsByRef && pt.GetElementType().IsGenericParameter))
  243. {
  244. pt = methodData.ReplaceGeneric(pt);
  245. }
  246. if (pt.IsGenericTypeDefinition)
  247. {
  248. pt = pt.MakeGenericType(methodData.InstanceTypeGenericTypeArguments);
  249. }
  250. if (pt.IsPointer)
  251. {
  252. TypeToString(pt, buf, methodData);
  253. buf.Append($" v{i} = (");
  254. TypeToString(pt, buf, methodData);
  255. buf.AppendLine($") ((byte*)p + {i * 1024});");
  256. }
  257. else
  258. {
  259. buf.Append($" var v{i} = default(");
  260. TypeToString(pt, buf, methodData);
  261. buf.AppendLine(");");
  262. }
  263. }
  264. var info = method as MethodInfo;
  265. var refStr = "";
  266. var readonlyStr = "";
  267. // [MayOnlyLiveInBlobStorage] forces types to be referred to by reference only, so we
  268. // must be sure to make any local vars use ref whenever something is a ref return.
  269. //
  270. // Furthermore, we also care about readonly since some returns are ref readonly returns.
  271. // Detecting readonly is done by checking the required custom modifiers for the InAttribute.
  272. if (info != null && info.ReturnType.IsByRef)
  273. {
  274. refStr = "ref ";
  275. if (info.ReturnParameter.GetRequiredCustomModifiers().Contains(typeof(InAttribute)))
  276. {
  277. readonlyStr = "readonly ";
  278. }
  279. }
  280. if (method.IsStatic)
  281. {
  282. if (isGetter)
  283. buf.Append($" {refStr}{readonlyStr}var __result = {refStr}");
  284. TypeToString(methodData.InstanceType, buf, methodData);
  285. buf.Append($".{sourceName}");
  286. }
  287. else
  288. {
  289. StringBuilder typeStringBuilder = new StringBuilder();
  290. TypeToString(methodData.InstanceType, typeStringBuilder, methodData);
  291. if (typeStringBuilder.ToString() == "Unity.Entities.SystemState")
  292. {
  293. buf.Append($" ref var instance = ref *(");
  294. buf.Append(typeStringBuilder);
  295. buf.AppendLine("*)((void*)p);");
  296. }
  297. else
  298. {
  299. buf.Append($" ref var instance = ref UnsafeUtility.AsRef<");
  300. buf.Append(typeStringBuilder);
  301. buf.AppendLine(">((void*)p);");
  302. }
  303. if (isIndexer)
  304. {
  305. if (isGetter)
  306. buf.Append($" {refStr}{readonlyStr}var __result = {refStr}instance");
  307. else if (isSetter)
  308. buf.Append(" instance");
  309. }
  310. else if (method.IsConstructor)
  311. {
  312. // Begin the setup for the constructor call. Arguments will be handled later.
  313. buf.Append(" instance = new ");
  314. TypeToString(methodData.InstanceType, buf, methodData);
  315. }
  316. else
  317. {
  318. if (isGetter)
  319. buf.Append($" {refStr}{readonlyStr}var __result = {refStr}");
  320. buf.Append($" instance.{sourceName}");
  321. }
  322. }
  323. if (method.IsGenericMethod)
  324. {
  325. buf.Append("<");
  326. var args = method.GetGenericArguments();
  327. for (int i = 0; i < args.Length; ++i)
  328. {
  329. if (i > 0)
  330. buf.Append(", ");
  331. TypeToString(args[i], buf, methodData);
  332. }
  333. buf.Append(">");
  334. }
  335. // Make dummy arguments.
  336. if (isIndexer)
  337. {
  338. buf.Append("[");
  339. }
  340. else
  341. {
  342. if (isGetter)
  343. {
  344. }
  345. else if (isSetter)
  346. buf.Append("=");
  347. else
  348. buf.Append("(");
  349. }
  350. for (int i = 0; i < parameters.Length; ++i)
  351. {
  352. // Close the indexer brace and assign the value if we're handling an indexer and setter.
  353. if (isIndexer && isSetter && i + 1 == parameters.Length)
  354. {
  355. buf.Append($"] = v{i}");
  356. break;
  357. }
  358. if (i > 0)
  359. buf.Append(" ,");
  360. // Close the indexer brace. This is separate from the setter logic above because the
  361. // comma separating arguments is required for the getter case but not for the setter.
  362. if (isIndexer && isGetter && i + 1 == parameters.Length)
  363. {
  364. buf.Append($"v{i}]");
  365. break;
  366. }
  367. var param = parameters[i];
  368. if (param.IsOut)
  369. {
  370. buf.Append("out ");
  371. }
  372. else if (param.IsIn)
  373. {
  374. buf.Append("in ");
  375. }
  376. else if (param.ParameterType.IsByRef)
  377. {
  378. buf.Append("ref ");
  379. }
  380. buf.Append($"v{i}");
  381. }
  382. if (!isProperty)
  383. buf.Append(")");
  384. buf.AppendLine(";");
  385. buf.AppendLine(" }");
  386. if (methodData.CompileTarget == BurstCompatibleAttribute.BurstCompatibleCompileTarget.Editor)
  387. {
  388. // Closes #if UNITY_EDITOR that surrounds the actual method/property being tested.
  389. buf.AppendLine("#endif");
  390. }
  391. // Set up the call to BurstCompiler.CompileFunctionPointer only in the cases where it is necessary.
  392. switch (methodData.CompileTarget)
  393. {
  394. case BurstCompatibleAttribute.BurstCompatibleCompileTarget.PlayerAndEditor:
  395. case BurstCompatibleAttribute.BurstCompatibleCompileTarget.Editor:
  396. {
  397. buf.AppendLine("#if UNITY_EDITOR");
  398. buf.AppendLine($" public static void BurstCompile_{safeName}()");
  399. buf.AppendLine(" {");
  400. buf.AppendLine($" BurstCompiler.CompileFunctionPointer<TestFunc>(Burst_{safeName});");
  401. buf.AppendLine(" }");
  402. buf.AppendLine("#endif");
  403. break;
  404. }
  405. }
  406. if (methodData.RequiredDefine != null)
  407. {
  408. buf.AppendLine($"#endif");
  409. }
  410. }
  411. buf.AppendLine("}");
  412. buf.AppendLine("#endif");
  413. File.WriteAllText(path, buf.ToString());
  414. methodsTestedCount = methods.Length;
  415. return true;
  416. }
  417. private static void TypeToString(Type t, StringBuilder buf, in MethodData methodData)
  418. {
  419. if (t.IsPrimitive || t == typeof(void))
  420. {
  421. buf.Append(PrimitiveTypeToString(t));
  422. return;
  423. }
  424. if (t.IsByRef)
  425. {
  426. TypeToString(t.GetElementType(), buf, methodData);
  427. return;
  428. }
  429. // This should come after the IsByRef check above to avoid adding an extra asterisk.
  430. // You could have a T*& (ref to a pointer) which causes t.IsByRef and t.IsPointer to both be true and if
  431. // you check t.IsPointer first then descend down the types with t.GetElementType() you end up with
  432. // T* which causes you to descend a second time then print an asterisk twice as you come back up the
  433. // recursion.
  434. if (t.IsPointer)
  435. {
  436. TypeToString(t.GetElementType(), buf, methodData);
  437. buf.Append("*");
  438. return;
  439. }
  440. GetFullTypeName(t, buf, methodData);
  441. }
  442. private static string PrimitiveTypeToString(Type type)
  443. {
  444. if (type == typeof(void))
  445. return "void";
  446. if (type == typeof(bool))
  447. return "bool";
  448. if (type == typeof(byte))
  449. return "byte";
  450. if (type == typeof(sbyte))
  451. return "sbyte";
  452. if (type == typeof(short))
  453. return "short";
  454. if (type == typeof(ushort))
  455. return "ushort";
  456. if (type == typeof(int))
  457. return "int";
  458. if (type == typeof(uint))
  459. return "uint";
  460. if (type == typeof(long))
  461. return "long";
  462. if (type == typeof(ulong))
  463. return "ulong";
  464. if (type == typeof(char))
  465. return "char";
  466. if (type == typeof(double))
  467. return "double";
  468. if (type == typeof(float))
  469. return "float";
  470. if (type == typeof(IntPtr))
  471. return "IntPtr";
  472. if (type == typeof(UIntPtr))
  473. return "UIntPtr";
  474. throw new InvalidOperationException($"{type} is not a primitive type");
  475. }
  476. private static void GetFullTypeName(Type type, StringBuilder buf, in MethodData methodData)
  477. {
  478. // If we encounter a generic parameter (typically T) then we should replace it with a real one
  479. // specified by [BurstCompatible(GenericTypeArguments = new [] { typeof(...) })].
  480. if (type.IsGenericParameter)
  481. {
  482. GetFullTypeName(methodData.ReplaceGeneric(type), buf, methodData);
  483. return;
  484. }
  485. if (type.DeclaringType != null)
  486. {
  487. GetFullTypeName(type.DeclaringType, buf, methodData);
  488. buf.Append(".");
  489. }
  490. else
  491. {
  492. // These appends for the namespace used to be protected by an if check for Unity.Collections or
  493. // Unity.Collections.LowLevel.Unsafe, but HashSetExtensions lives in both so just fully disambiguate
  494. // by always appending the namespace.
  495. buf.Append(type.Namespace);
  496. buf.Append(".");
  497. }
  498. var name = type.Name;
  499. var idx = name.IndexOf('`');
  500. if (-1 != idx)
  501. {
  502. name = name.Remove(idx);
  503. }
  504. buf.Append(name);
  505. if (type.IsConstructedGenericType || type.IsGenericTypeDefinition)
  506. {
  507. var gt = type.GetGenericArguments();
  508. // Avoid printing out the generic arguments for cases like UnsafeHashMap<TKey, TValue>.ParallelWriter.
  509. // ParallelWriter is considered to be a generic type and will have two generic parameters inherited
  510. // from UnsafeHashMap. Because of this, if we don't do this check, we could code gen this:
  511. //
  512. // UnsafeHashMap<int, int>.ParallelWriter<int, int>
  513. //
  514. // But we want:
  515. //
  516. // UnsafeHashMap<int, int>.ParallelWriter
  517. //
  518. // ParallelWriter doesn't actually have generic arguments you can give it directly so it's not correct
  519. // to give it generic arguments. If the nested type has the same number of generic arguments as its
  520. // declaring type, then there should be no new generic arguments and therefore nothing to print.
  521. if (type.IsNested && gt.Length == type.DeclaringType.GetGenericArguments().Length)
  522. {
  523. return;
  524. }
  525. buf.Append("<");
  526. for (int i = 0; i < gt.Length; ++i)
  527. {
  528. if (i > 0)
  529. {
  530. buf.Append(", ");
  531. }
  532. TypeToString(gt[i], buf, methodData);
  533. }
  534. buf.Append(">");
  535. }
  536. }
  537. private static readonly Type[] EmptyGenericTypeArguments = { };
  538. private bool GetTestMethods(out MethodData[] methods)
  539. {
  540. var seenMethods = new HashSet<MethodBase>();
  541. var result = new List<MethodData>();
  542. int errorCount = 0;
  543. void LogError(string message)
  544. {
  545. ++errorCount;
  546. Debug.LogError(message);
  547. }
  548. void MaybeAddMethod(MethodBase m, Type[] methodGenericTypeArguments, Type[] declaredTypeGenericTypeArguments, string requiredDefine, MemberInfo attributeHolder, BurstCompatibleAttribute.BurstCompatibleCompileTarget compileTarget)
  549. {
  550. if (m.IsPrivate)
  551. {
  552. // Private methods that were explicitly tagged as [BurstCompatible] should generate an error to
  553. // avoid users thinking the method is being tested when it actually isn't.
  554. if (m.GetCustomAttribute<BurstCompatibleAttribute>() != null)
  555. {
  556. // Just return and avoiding printing duplicate errors if we've already seen this method.
  557. if (seenMethods.Contains(m))
  558. return;
  559. seenMethods.Add(m);
  560. LogError($"BurstCompatibleAttribute 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.");
  561. }
  562. return;
  563. }
  564. // Burst IL post processing might create a new method that contains $ to ensure it doesn't
  565. // conflict with any real names (as an example, it can append $BurstManaged to the original method name).
  566. // However, trying to call that method directly in C# isn't possible because the $ is invalid for
  567. // identifiers.
  568. if (m.Name.Contains('$'))
  569. {
  570. return;
  571. }
  572. if (attributeHolder.GetCustomAttribute<ObsoleteAttribute>() != null)
  573. return;
  574. if (attributeHolder.GetCustomAttribute<NotBurstCompatibleAttribute>() != null)
  575. return;
  576. // If this is not a property but still has a special name, ignore it.
  577. if (attributeHolder is MethodInfo && m.IsSpecialName)
  578. return;
  579. var methodGenericArgumentLookup = new Dictionary<string, Type>();
  580. if (m.IsGenericMethodDefinition)
  581. {
  582. if (methodGenericTypeArguments == null)
  583. {
  584. LogError($"Method `{m.DeclaringType}.{m}` is generic but doesn't have a type array in its BurstCompatible attribute");
  585. return;
  586. }
  587. var genericArguments = m.GetGenericArguments();
  588. if (genericArguments.Length != methodGenericTypeArguments.Length)
  589. {
  590. LogError($"Method `{m.DeclaringType}.{m}` is generic with {genericArguments.Length} generic parameters but BurstCompatible attribute has {methodGenericTypeArguments.Length} types, they must be the same length!");
  591. return;
  592. }
  593. try
  594. {
  595. m = (m as MethodInfo).MakeGenericMethod(methodGenericTypeArguments);
  596. }
  597. catch (Exception e)
  598. {
  599. LogError($"Could not instantiate method `{m.DeclaringType}.{m}` with type arguments `{methodGenericTypeArguments}`.");
  600. Debug.LogException(e);
  601. return;
  602. }
  603. // Build up the generic name to type lookup for this method.
  604. for (int i = 0; i < genericArguments.Length; ++i)
  605. {
  606. var name = genericArguments[i].Name;
  607. var type = methodGenericTypeArguments[i];
  608. try
  609. {
  610. methodGenericArgumentLookup.Add(name, type);
  611. }
  612. catch (Exception e)
  613. {
  614. LogError($"For method `{m.DeclaringType}.{m}`, could not add ({name}, {type}).");
  615. Debug.LogException(e);
  616. return;
  617. }
  618. }
  619. }
  620. var instanceType = m.DeclaringType;
  621. var instanceTypeGenericLookup = new Dictionary<string, Type>();
  622. if (instanceType.IsGenericTypeDefinition)
  623. {
  624. var instanceGenericArguments = instanceType.GetGenericArguments();
  625. if (declaredTypeGenericTypeArguments == null)
  626. {
  627. LogError($"Type `{m.DeclaringType}` is generic but doesn't have a type array in its BurstCompatible attribute");
  628. return;
  629. }
  630. if (instanceGenericArguments.Length != declaredTypeGenericTypeArguments.Length)
  631. {
  632. LogError($"Type `{instanceType}` is generic with {instanceGenericArguments.Length} generic parameters but BurstCompatible attribute has {declaredTypeGenericTypeArguments.Length} types, they must be the same length!");
  633. return;
  634. }
  635. try
  636. {
  637. instanceType = instanceType.MakeGenericType(declaredTypeGenericTypeArguments);
  638. }
  639. catch (Exception e)
  640. {
  641. LogError($"Could not instantiate type `{instanceType}` with type arguments `{declaredTypeGenericTypeArguments}`.");
  642. Debug.LogException(e);
  643. return;
  644. }
  645. // Build up the generic name to type lookup for this method.
  646. for (int i = 0; i < instanceGenericArguments.Length; ++i)
  647. {
  648. var name = instanceGenericArguments[i].Name;
  649. var type = declaredTypeGenericTypeArguments[i];
  650. try
  651. {
  652. instanceTypeGenericLookup.Add(name, type);
  653. }
  654. catch (Exception e)
  655. {
  656. LogError($"For type `{instanceType}`, could not add ({name}, {type}).");
  657. Debug.LogException(e);
  658. return;
  659. }
  660. }
  661. }
  662. //if (m.GetParameters().Any((p) => !p.ParameterType.IsValueType && !p.ParameterType.IsPointer))
  663. // return;
  664. // These are crazy nested function names. They'll be covered anyway as the parent function is burst compatible.
  665. if (m.Name.Contains('<'))
  666. return;
  667. if (seenMethods.Contains(m))
  668. return;
  669. seenMethods.Add(m);
  670. result.Add(new MethodData
  671. {
  672. methodBase = m, InstanceType = instanceType, MethodGenericTypeArguments = methodGenericTypeArguments,
  673. InstanceTypeGenericTypeArguments = declaredTypeGenericTypeArguments, RequiredDefine = requiredDefine,
  674. MethodGenericArgumentLookup = methodGenericArgumentLookup, InstanceTypeGenericArgumentLookup = instanceTypeGenericLookup,
  675. CompileTarget = compileTarget
  676. });
  677. }
  678. var declaredTypeGenericArguments = new Dictionary<Type, Type[]>();
  679. // Go through types tagged with [BurstCompatible] and their methods before performing the direct method
  680. // search (below this loop) to ensure that we get the declared type generic arguments.
  681. //
  682. // If we were to run the direct method search first, it's possible we would add the method to the seen list
  683. // and then by the time we execute this loop we might skip it because we think we have seen the method
  684. // already but we haven't grabbed the declared type generic arguments yet.
  685. foreach (var t in TypeCache.GetTypesWithAttribute<BurstCompatibleAttribute>())
  686. {
  687. if (!m_AssembliesToVerify.Contains(t.Assembly.GetName().Name))
  688. continue;
  689. foreach (var typeAttr in t.GetCustomAttributes<BurstCompatibleAttribute>())
  690. {
  691. // As we go through all the types with [BurstCompatible] on them, remember their GenericTypeArguments
  692. // in case we encounter the type again when we do the direct method search later. When we do the
  693. // direct method search, we don't have as easy access to the [BurstCompatible] attribute on the
  694. // type so just remember this now to make life easier.
  695. declaredTypeGenericArguments[t] = typeAttr.GenericTypeArguments;
  696. const BindingFlags flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public |
  697. BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
  698. foreach (var c in t.GetConstructors(flags))
  699. {
  700. var attributes = c.GetCustomAttributes<BurstCompatibleAttribute>().ToArray();
  701. if (attributes.Length == 0)
  702. {
  703. MaybeAddMethod(c, EmptyGenericTypeArguments, typeAttr.GenericTypeArguments, typeAttr.RequiredUnityDefine, c, typeAttr.CompileTarget);
  704. }
  705. else
  706. {
  707. foreach (var methodAttr in attributes)
  708. {
  709. MaybeAddMethod(c, methodAttr.GenericTypeArguments, typeAttr.GenericTypeArguments, methodAttr.RequiredUnityDefine ?? typeAttr.RequiredUnityDefine, c, methodAttr.CompileTarget);
  710. }
  711. }
  712. }
  713. foreach (var m in t.GetMethods(flags))
  714. {
  715. // If this is a property getter/setter, the attributes will be stored on the property itself, not
  716. // the method.
  717. MemberInfo attributeHolder = m;
  718. if (m.IsSpecialName && (m.Name.StartsWith("get_") || m.Name.StartsWith("set_")))
  719. {
  720. attributeHolder = m.DeclaringType.GetProperty(m.Name.Substring("get_".Length), flags);
  721. Debug.Assert(attributeHolder != null, $"Failed to find property for {m.Name} in {m.DeclaringType.Name}");
  722. }
  723. var attributes = attributeHolder.GetCustomAttributes<BurstCompatibleAttribute>().ToArray();
  724. if (attributes.Length == 0)
  725. {
  726. MaybeAddMethod(m, EmptyGenericTypeArguments, typeAttr.GenericTypeArguments, typeAttr.RequiredUnityDefine, attributeHolder, typeAttr.CompileTarget);
  727. }
  728. else
  729. {
  730. foreach (var methodAttr in attributes)
  731. {
  732. MaybeAddMethod(m, methodAttr.GenericTypeArguments, typeAttr.GenericTypeArguments, methodAttr.RequiredUnityDefine ?? typeAttr.RequiredUnityDefine, attributeHolder, methodAttr.CompileTarget);
  733. }
  734. }
  735. }
  736. }
  737. }
  738. // Direct method search.
  739. foreach (var m in TypeCache.GetMethodsWithAttribute<BurstCompatibleAttribute>())
  740. {
  741. if (!m_AssembliesToVerify.Contains(m.DeclaringType.Assembly.GetName().Name))
  742. continue;
  743. // Look up the GenericTypeArguments on the declaring type that we probably got earlier. If the key
  744. // doesn't exist then it means [BurstCompatible] was only on the method and not the type, which is fine
  745. // but if [BurstCompatible] was on the type we should go ahead and use whatever GenericTypeArguments
  746. // it may have had.
  747. var typeGenericArguments = declaredTypeGenericArguments.ContainsKey(m.DeclaringType) ? declaredTypeGenericArguments[m.DeclaringType] : EmptyGenericTypeArguments;
  748. foreach (var attr in m.GetCustomAttributes<BurstCompatibleAttribute>())
  749. {
  750. MaybeAddMethod(m, attr.GenericTypeArguments, typeGenericArguments, attr.RequiredUnityDefine, m, attr.CompileTarget);
  751. }
  752. }
  753. if (errorCount > 0)
  754. {
  755. methods = new MethodData[] {};
  756. return false;
  757. }
  758. methods = result.ToArray();
  759. Array.Sort(methods);
  760. return true;
  761. }
  762. private static string GetSafeName(in MethodData methodData)
  763. {
  764. var method = methodData.methodBase;
  765. return GetSafeName(method.DeclaringType, methodData) + "_" + r.Replace(method.Name, "__");
  766. }
  767. public static readonly Regex r = new Regex(@"[^A-Za-z_0-9]+");
  768. private static string GetSafeName(Type t, in MethodData methodData)
  769. {
  770. var b = new StringBuilder();
  771. GetFullTypeName(t, b, methodData);
  772. return r.Replace(b.ToString(), "__");
  773. }
  774. Assembly GetAssemblyByName(string name)
  775. {
  776. var assemblies = AppDomain.CurrentDomain.GetAssemblies();
  777. return assemblies.SingleOrDefault(assembly => assembly.GetName().Name == name);
  778. }
  779. private StreamWriter m_BurstCompileLogs;
  780. // This is a log handler to save all the logs during burst compilation and eventually write to a file
  781. // so it is easier to see all the logs without truncation in the test runner.
  782. void LogHandler(string logString, string stackTrace, LogType type)
  783. {
  784. m_BurstCompileLogs.WriteLine(logString);
  785. }
  786. [UnityTest]
  787. public IEnumerator CompatibilityTests()
  788. {
  789. int runCount = 0;
  790. int successCount = 0;
  791. bool playerBuildSucceeded = false;
  792. var playerBuildTime = new TimeSpan();
  793. var compileFunctionPointerTime = new TimeSpan();
  794. try
  795. {
  796. if (!UpdateGeneratedFile(m_GeneratedCodePath, out var generatedCount))
  797. {
  798. yield break;
  799. }
  800. // Make another copy of the generated code and put it in Temp so it's easier to inspect.
  801. var debugCodeGenTempDest = Path.Combine(m_TempBurstCompatibilityPath, "generated.cs");
  802. Directory.CreateDirectory(m_TempBurstCompatibilityPath);
  803. File.Copy(m_GeneratedCodePath, debugCodeGenTempDest, true);
  804. Debug.Log($"Generated {generatedCount} Burst compatibility tests.");
  805. Debug.Log($"You can inspect a copy of the generated code at: {Path.GetFullPath(debugCodeGenTempDest)}");
  806. // BEWARE: this causes a domain reload and can cause variables to go null.
  807. yield return new RecompileScripts();
  808. var t = GetAssemblyByName(m_GeneratedCodeAssemblyName).GetType(s_GeneratedClassName);
  809. if (t == null)
  810. {
  811. throw new ApplicationException($"could not find generated type {s_GeneratedClassName} in assembly {m_GeneratedCodeAssemblyName}");
  812. }
  813. var logPath = Path.Combine(m_TempBurstCompatibilityPath, "BurstCompileLog.txt");
  814. m_BurstCompileLogs = File.CreateText(logPath);
  815. Debug.Log($"Logs from Burst compilation written to: {Path.GetFullPath(logPath)}\n");
  816. Application.logMessageReceived += LogHandler;
  817. var stopwatch = new Stopwatch();
  818. stopwatch.Start();
  819. foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Static))
  820. {
  821. if (m.Name.StartsWith("BurstCompile_"))
  822. {
  823. ++runCount;
  824. try
  825. {
  826. m.Invoke(null, null);
  827. ++successCount;
  828. }
  829. catch (Exception ex)
  830. {
  831. Debug.LogException(ex);
  832. }
  833. }
  834. }
  835. stopwatch.Stop();
  836. compileFunctionPointerTime = stopwatch.Elapsed;
  837. var buildOptions = new BuildPlayerOptions();
  838. buildOptions.target = EditorUserBuildSettings.activeBuildTarget;
  839. buildOptions.locationPathName = FileUtil.GetUniqueTempPathInProject();
  840. // TODO: https://unity3d.atlassian.net/browse/DOTS-4886
  841. // Remove when fixed
  842. // Made a development build due to a bug in the Editor causing Debug.isDebugBuild to be false
  843. // on the next Domain reload after this build is made
  844. buildOptions.options = BuildOptions.IncludeTestAssemblies | BuildOptions.Development;
  845. var buildReport = BuildPipeline.BuildPlayer(buildOptions);
  846. playerBuildSucceeded = buildReport.summary.result == BuildResult.Succeeded;
  847. playerBuildTime = buildReport.summary.totalTime;
  848. }
  849. finally
  850. {
  851. Application.logMessageReceived -= LogHandler;
  852. m_BurstCompileLogs?.Close();
  853. Debug.Log($"Player build duration: {playerBuildTime.TotalSeconds} s.");
  854. if (playerBuildSucceeded)
  855. {
  856. Debug.Log("Burst AOT compile via player build succeeded.");
  857. }
  858. else
  859. {
  860. Debug.LogError("Player build FAILED.");
  861. }
  862. Debug.Log($"Compile function pointer duration: {compileFunctionPointerTime.TotalSeconds} s.");
  863. if (runCount != successCount)
  864. {
  865. Debug.LogError($"Burst compatibility tests failed; ran {runCount} editor tests, {successCount} OK, {runCount - successCount} FAILED.");
  866. }
  867. else
  868. {
  869. Debug.Log($"Ran {runCount} editor Burst compatible tests, all OK.");
  870. }
  871. AssetDatabase.DeleteAsset(m_GeneratedCodePath);
  872. }
  873. yield return new RecompileScripts();
  874. }
  875. }
  876. }
  877. #endif