CoverageTester.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // ***********************************************************************
  2. // Copyright (c) 2017 Unity Technologies. All rights reserved.
  3. //
  4. // Licensed under the ##LICENSENAME##.
  5. // See LICENSE.md file in the project root for full license information.
  6. //
  7. // This script depends on the Mono.Reflection.Disassembler by the JB Evain,
  8. // see https://github.com/jbevain/mono.reflection (it's under an MIT license).
  9. // ***********************************************************************
  10. #if ENABLE_COVERAGE_TEST
  11. using System.Collections.Generic;
  12. using System.Reflection;
  13. namespace Autodesk.Fbx
  14. {
  15. static class CoverageTester
  16. {
  17. // Important fact: MethodBase doesn't implement equality like you'd expect.
  18. // So we need to use RuntimeMethodHandle for keys.
  19. // Maps a method to the calls somebody told us we'll do through reflection
  20. // when we call that method. See RegisterReflectionCall.
  21. static Dictionary<System.RuntimeMethodHandle, List<MethodBase>> s_reflectionCalls = new Dictionary<System.RuntimeMethodHandle, List<MethodBase>> ();
  22. // Cache. Maps a method to the calls it may directly make, determined by
  23. // looking at the instructions of the method (and s_reflectionCalls).
  24. static Dictionary<System.RuntimeMethodHandle, List<MethodBase>> s_directCalls = new Dictionary<System.RuntimeMethodHandle, List<MethodBase>> ();
  25. // Cache. Maps a method to the calls it may recursively make. This is the
  26. // transitive closure of s_directCalls.
  27. static Dictionary<System.RuntimeMethodHandle, List<MethodBase>> s_calls = new Dictionary<System.RuntimeMethodHandle, List<MethodBase>> ();
  28. static void PreloadCache ()
  29. {
  30. if (s_calls.Count != 0) {
  31. return;
  32. }
  33. var types = new System.Type[] {
  34. typeof(object),
  35. typeof(string),
  36. typeof(int),
  37. typeof(System.Array),
  38. typeof(System.Type),
  39. typeof(System.Text.StringBuilder),
  40. typeof(CoverageTester),
  41. typeof(NUnit.Framework.Assert)
  42. };
  43. foreach (var typ in types) {
  44. foreach (var method in typ.GetMethods()) {
  45. GetCalls (method);
  46. }
  47. }
  48. }
  49. /// <summary>
  50. /// Dig through the instructions for 'method' and see which methods it
  51. /// calls directly. Includes calls invoked through reflection (if they were registered).
  52. ///
  53. /// This is cached, so the first call for any given function will be
  54. /// expensive but subsequent ones will be cheap.
  55. /// </summary>
  56. private static List<MethodBase> GetDirectCalls (MethodBase method)
  57. {
  58. // See if we've cached this already.
  59. List<MethodBase> calls;
  60. if (s_directCalls.TryGetValue (method.MethodHandle, out calls)) {
  61. return calls;
  62. }
  63. calls = new List<MethodBase> ();
  64. // Analyze the function and add those calls.
  65. IEnumerable<Mono.Reflection.Instruction> instructions;
  66. try {
  67. instructions = Mono.Reflection.Disassembler.GetInstructions (method);
  68. } catch (System.ArgumentException) {
  69. instructions = new Mono.Reflection.Instruction[0];
  70. }
  71. // We devirtualize calls using the 'constrained'
  72. // instruction hint, which is the instruction before the call
  73. // instruction.
  74. //
  75. // That trick only works in the context of 'top' being a generic
  76. // function (or a member function in a generic type), and
  77. // 'calledMethod' is being called on the generic type. In that
  78. // specific case, the CIL requires a 'constraint' instruction to be
  79. // emitted as a prefix to the 'callvirt' instruction. In other cases,
  80. // we don't get the prefix.
  81. //
  82. // We could get better devirtualization by interpreting the
  83. // instruction stream, but that would be much harder!
  84. //
  85. System.Type constraintType = null;
  86. foreach (var instruction in instructions) {
  87. // Is this a constraint instruction? If so, store it.
  88. if (instruction.OpCode == System.Reflection.Emit.OpCodes.Constrained) {
  89. constraintType = instruction.Operand as System.Type;
  90. continue;
  91. }
  92. // Otherwise it's maybe a call?
  93. MethodBase calledMethod = instruction.Operand as MethodBase;
  94. if (calledMethod == null) {
  95. continue;
  96. }
  97. // Devirtualize the function if we can.
  98. if (constraintType != null && calledMethod.DeclaringType != constraintType) {
  99. var parameters = calledMethod.GetParameters ();
  100. var types = new System.Type[parameters.Length];
  101. for (int i = 0, n = parameters.Length; i < n; ++i) {
  102. types [i] = parameters [i].ParameterType;
  103. }
  104. var specificMethod = constraintType.GetMethod (calledMethod.Name, types);
  105. if (specificMethod != null) {
  106. calledMethod = specificMethod;
  107. }
  108. }
  109. // We called something. Push it on the search stack, and
  110. // clear the constraint since we've used it up.
  111. calls.Add (calledMethod);
  112. constraintType = null;
  113. }
  114. // Also get the calls invoked through reflection, if any.
  115. List<MethodBase> reflectionCalls;
  116. s_reflectionCalls.TryGetValue (method.MethodHandle, out reflectionCalls);
  117. if (reflectionCalls != null) {
  118. calls.AddRange (reflectionCalls);
  119. }
  120. s_directCalls.Add (method.MethodHandle, calls);
  121. return calls;
  122. }
  123. /// <summary>
  124. /// Dig through the instructions for 'method' and see which methods it
  125. /// calls, and what methods those methods call in turn recursively.
  126. ///
  127. /// This is cached, so the first call for any given function will be
  128. /// expensive but subsequent ones will be cheap.
  129. /// </summary>
  130. public static List<MethodBase> GetCalls (MethodBase method)
  131. {
  132. // See if we've cached this already.
  133. List<MethodBase> calls;
  134. if (s_calls.TryGetValue (method.MethodHandle, out calls)) {
  135. return calls;
  136. }
  137. calls = new List<MethodBase> ();
  138. // Look at the current method and in DFS, find all the methods it calls.
  139. var stack = new List<MethodBase> ();
  140. var visited = new HashSet<System.RuntimeMethodHandle> ();
  141. stack.Add (method);
  142. while (stack.Count > 0) {
  143. var top = stack [stack.Count - 1];
  144. stack.RemoveAt (stack.Count - 1);
  145. if (!visited.Add (top.MethodHandle)) {
  146. continue;
  147. }
  148. calls.Add (top);
  149. // If we have already seen this method, we can copy all its calls
  150. // in and stop this branch of our search here.
  151. if (s_calls.ContainsKey (top.MethodHandle)) {
  152. foreach (var calledMethod in s_calls[top.MethodHandle]) {
  153. visited.Add (calledMethod.MethodHandle);
  154. calls.Add (calledMethod);
  155. }
  156. continue;
  157. }
  158. // Otherwise we get all the direct calls it makes, and add them to
  159. // the stack for recursive processing.
  160. stack.AddRange (GetDirectCalls (top));
  161. }
  162. // Store the result and return it.
  163. s_calls.Add (method.MethodHandle, calls);
  164. return calls;
  165. }
  166. /// <summary>
  167. /// Clear the cache; useful if you just registered a new reflection call.
  168. /// </summary>
  169. public static void ClearCache ()
  170. {
  171. s_calls = new Dictionary<System.RuntimeMethodHandle, List<MethodBase>> ();
  172. s_directCalls = new Dictionary<System.RuntimeMethodHandle, List<MethodBase>> ();
  173. }
  174. /// <summary>
  175. /// If you're using reflection to call methods, they won't be picked
  176. /// up automatically. This lets you add that information.
  177. ///
  178. /// You might want to clear the cache afterwards.
  179. /// </summary>
  180. public static void RegisterReflectionCall (MethodBase from, MethodBase to)
  181. {
  182. List<MethodBase> calls;
  183. if (s_reflectionCalls.TryGetValue (from.MethodHandle, out calls)) {
  184. calls.Add (to);
  185. } else {
  186. List<MethodBase> tos = new List<MethodBase> ();
  187. tos.Add (to);
  188. s_reflectionCalls [from.MethodHandle] = tos;
  189. }
  190. }
  191. /// <summary>
  192. /// Helper for TestCoverage, determines whether we called a function
  193. /// we're looking for -- just via a base class.
  194. /// </summary>
  195. private static bool DidCallBaseMethod (MethodInfo methodToCover,
  196. HashSet<System.RuntimeMethodHandle> calledMethods)
  197. {
  198. if ((object)methodToCover == null) {
  199. // mTC is a constructor, which means it can't have been called via
  200. // virtual function dispatch.
  201. return false;
  202. }
  203. var baseBaseMethod = methodToCover.GetBaseDefinition ();
  204. if (baseBaseMethod.MethodHandle == methodToCover.MethodHandle) {
  205. // mTC is the base definition, which means it can't have been called
  206. // via virtual function dispatch.
  207. return false;
  208. }
  209. if (calledMethods.Contains (baseBaseMethod.MethodHandle)) {
  210. // mTC's base method got called, so it might have been called via
  211. // virtual function dispatch.
  212. return true;
  213. }
  214. // The base-base method is at the top of the hierarchy. We might have
  215. // called something in the middle of the hierarchy, and neither test above
  216. // will have noticed.
  217. //
  218. // Look up the parent class. Find the method with the same name and types.
  219. // Repeat.
  220. var parameters = methodToCover.GetParameters ();
  221. var parameterTypes = new System.Type[parameters.Length];
  222. for (int i = 0; i < parameters.Length; ++i) {
  223. parameterTypes [i] = parameters [i].ParameterType;
  224. }
  225. System.Type baseClass = methodToCover.DeclaringType;
  226. MethodBase baseMethod;
  227. do {
  228. baseClass = baseClass.BaseType;
  229. baseMethod = baseClass.GetMethod (methodToCover.Name, parameterTypes);
  230. if (calledMethods.Contains (baseMethod.MethodHandle)) {
  231. return true;
  232. }
  233. } while (baseMethod != baseBaseMethod);
  234. return false;
  235. }
  236. /// <summary>
  237. /// Statically analyze the root methods, and check whether their static
  238. /// call graph might cover all the methods to cover.
  239. ///
  240. /// Every function in 'methods to cover' that *might* get called will
  241. /// be added to the 'hit' output. Functions that *definitely* won't be
  242. /// called get added to the 'missed' output.
  243. ///
  244. /// "Definite" fails in the face of reflection and virtual functions.
  245. /// Use RegisterReflectionCall to handle reflection.
  246. ///
  247. /// For virtuals, we rely on the tests being simple (not calling virtualized
  248. /// hierarchies of test frameworks). If there's a call to a base method,
  249. /// we say that call covers any derived method.
  250. ///
  251. /// The static analysis is very simplistic: we don't fold constants or
  252. /// eliminate dead code or devirtualize calls.
  253. /// </summary>
  254. public static bool TestCoverage (IEnumerable<MethodBase> MethodsToCover,
  255. IEnumerable<MethodBase> RootMethods,
  256. out List<MethodBase> out_HitMethods,
  257. out List<MethodBase> out_MissedMethods
  258. )
  259. {
  260. PreloadCache ();
  261. // MethodsToCover and RootMethods may have duplicates;
  262. // use 'unique' to avoid doing the work twice.
  263. var unique = new HashSet<System.RuntimeMethodHandle> ();
  264. // Collect up the handles we called.
  265. var calledMethods = new HashSet<System.RuntimeMethodHandle> ();
  266. unique.Clear ();
  267. foreach (var rootMethod in RootMethods) {
  268. if (!unique.Add (rootMethod.MethodHandle)) {
  269. continue;
  270. }
  271. foreach (var called in GetCalls(rootMethod)) {
  272. calledMethods.Add (called.MethodHandle);
  273. }
  274. }
  275. out_MissedMethods = new List<MethodBase> ();
  276. out_HitMethods = new List<MethodBase> ();
  277. unique.Clear ();
  278. foreach (var methodToCover in MethodsToCover) {
  279. if (!unique.Add (methodToCover.MethodHandle)) {
  280. continue;
  281. }
  282. // Did we call the method?
  283. if (calledMethods.Contains (methodToCover.MethodHandle)) {
  284. out_HitMethods.Add (methodToCover);
  285. continue;
  286. }
  287. // Did we call a base class declaration of the method?
  288. // If so, we might call the method we're looking for through
  289. // virtual function dispatch.
  290. if (DidCallBaseMethod (methodToCover as MethodInfo, calledMethods)) {
  291. out_HitMethods.Add (methodToCover);
  292. continue;
  293. }
  294. // No other excuses? We must have missed it.
  295. out_MissedMethods.Add (methodToCover);
  296. }
  297. if (out_MissedMethods.Count == 0) {
  298. return true;
  299. } else {
  300. return false;
  301. }
  302. }
  303. /// <summary>
  304. /// Collect all the methods of the type that we want to cover.
  305. /// Includes all public instance methods, all public static methods,
  306. /// all public constructors, all public property getters and setters.
  307. /// </summary>
  308. public static void CollectMethodsToCover (System.Type TypeToCover, List<MethodBase> MethodsToCover)
  309. {
  310. // Don't cover anything for enums, they're basically compiler-generated
  311. // types.
  312. if (TypeToCover.IsEnum) {
  313. return;
  314. }
  315. // We want to call all the methods of the proxy, including all the constructors.
  316. int firstIndex = MethodsToCover.Count;
  317. MethodsToCover.AddRange (TypeToCover.GetMethods ());
  318. MethodsToCover.AddRange (TypeToCover.GetConstructors ());
  319. // Testers will often use EqualityTester on the type. Register its
  320. // reflection calls.
  321. var eqTester = typeof(UnitTests.EqualityTester<>).MakeGenericType (TypeToCover);
  322. System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor (eqTester.TypeHandle);
  323. // In calling any method on the type, the static initializer will be invoked.
  324. if (TypeToCover.TypeInitializer != null) {
  325. for (int i = firstIndex, n = MethodsToCover.Count; i < n; ++i) {
  326. RegisterReflectionCall (MethodsToCover [i], TypeToCover.TypeInitializer);
  327. }
  328. }
  329. }
  330. /// <summary>
  331. /// Filter a list of methods to get the methods that the unit test
  332. /// framework will interpret as tests.
  333. /// </summary>
  334. public static void CollectTestMethods (IEnumerable<MethodBase> PotentialTestMethods, List<MethodBase> TestMethods)
  335. {
  336. foreach (var method in PotentialTestMethods) {
  337. // Check that the method is tagged [Test]
  338. if (method.GetCustomAttributes (typeof(NUnit.Framework.TestAttribute), true).Length == 0) {
  339. continue;
  340. }
  341. TestMethods.Add (method);
  342. /* Invoke the declaring type's static init so it registers its
  343. * reflection calls. */
  344. System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor (method.DeclaringType.TypeHandle);
  345. }
  346. }
  347. public static void CollectTestMethods (System.Type TestClass, List<MethodBase> TestMethods)
  348. {
  349. CollectTestMethods (TestClass.GetMethods (), TestMethods);
  350. }
  351. /// <summary>
  352. /// Simple interface for running an NUnit test.
  353. /// <code>
  354. /// [Test]
  355. /// public void TestCoverage() { CoverageTester.TestCoverage(typeof(ThingWeAreTesting), this.GetType()); }
  356. /// </code>
  357. /// </summary>
  358. public static void TestCoverage (System.Type TypeToCover, System.Type NUnitTestFramework)
  359. {
  360. var methodsToCover = new List<MethodBase> ();
  361. CollectMethodsToCover (TypeToCover, methodsToCover);
  362. // Our public test functions are what we can use to call that with.
  363. var testMethods = new List<MethodBase> ();
  364. CollectTestMethods (NUnitTestFramework.GetMethods (), testMethods);
  365. List<MethodBase> hitMethods;
  366. List<MethodBase> missedMethods;
  367. var coverageComplete = CoverageTester.TestCoverage (methodsToCover, testMethods, out hitMethods, out missedMethods);
  368. NUnit.Framework.Assert.That (
  369. () => coverageComplete,
  370. () => CoverageTester.MakeCoverageMessage (hitMethods, missedMethods));
  371. }
  372. public static string GetMethodSignature (MethodBase info)
  373. {
  374. var builder = new System.Text.StringBuilder ();
  375. if (info.IsConstructor) {
  376. builder.Append (info.DeclaringType.Name);
  377. } else {
  378. var method = info as MethodInfo;
  379. if (method != null) {
  380. builder.Append (method.ReturnType.Name);
  381. builder.Append (' ');
  382. }
  383. if (info.DeclaringType != null) {
  384. builder.Append (info.DeclaringType.Name);
  385. builder.Append ('.');
  386. }
  387. builder.Append (info.Name);
  388. }
  389. builder.Append ('(');
  390. var args = info.GetParameters ();
  391. if (args.Length > 0) {
  392. builder.Append (args [0].ParameterType.Name);
  393. }
  394. for (var i = 1; i < args.Length; ++i) {
  395. builder.Append (", ");
  396. builder.Append (args [i].ParameterType.Name);
  397. }
  398. builder.Append (')');
  399. return builder.ToString ();
  400. }
  401. static string[] GetUniqueSortedSignatures (IEnumerable<MethodBase> Methods)
  402. {
  403. var unique = new HashSet<System.RuntimeMethodHandle> ();
  404. // Eliminate duplicates
  405. var methods = new List<MethodBase> ();
  406. foreach (var method in Methods) {
  407. if (!unique.Add (method.MethodHandle)) {
  408. continue;
  409. }
  410. methods.Add (method);
  411. }
  412. unique.Clear ();
  413. // Sort first by declaring type name, then by method name, then by signature
  414. methods.Sort ((MethodBase a, MethodBase b) => {
  415. var aname = a.DeclaringType == null ? "" : a.DeclaringType.Name;
  416. var bname = b.DeclaringType == null ? "" : b.DeclaringType.Name;
  417. var namecompare = aname.CompareTo (bname);
  418. if (namecompare != 0) {
  419. return namecompare;
  420. }
  421. aname = a.Name;
  422. bname = b.Name;
  423. namecompare = aname.CompareTo (bname);
  424. if (namecompare != 0) {
  425. return namecompare;
  426. }
  427. aname = GetMethodSignature (a);
  428. bname = GetMethodSignature (b);
  429. namecompare = aname.CompareTo (bname);
  430. if (namecompare != 0) {
  431. return namecompare;
  432. }
  433. return 0;
  434. });
  435. // Convert to an array of string
  436. var signatures = new string[methods.Count];
  437. for (int i = 0, n = methods.Count; i < n; ++i) {
  438. signatures [i] = GetMethodSignature (methods [i]);
  439. }
  440. return signatures;
  441. }
  442. public static string MakeCoverageMessage (
  443. IEnumerable<MethodBase> HitMethods,
  444. IEnumerable<MethodBase> MissedMethods)
  445. {
  446. return string.Format ("Failed to call:\n\t{0}\nSucceeded to call:\n\t{1}",
  447. string.Join ("\n\t", GetUniqueSortedSignatures (MissedMethods)),
  448. string.Join ("\n\t", GetUniqueSortedSignatures (HitMethods)));
  449. }
  450. }
  451. }
  452. #endif