Ei kuvausta
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 46KB

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