using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.ExceptionServices; namespace Glass.Testbed { /// /// Finds the test cases in an assembly without building any of them. /// public static class TestCaseCatalog { /// /// The namespace a case's category sits under. A case in Glass.Testbed.TestCases.Text is in the Text /// category. /// public const string RootNamespace = "Glass.Testbed.TestCases"; /// /// What a menu needs to list a case before it exists. /// public sealed class Entry { public readonly string Category; public readonly string Name; public readonly Type Type; public Entry(string category, string name, Type type) { Category = category; Name = name; Type = type; } } private static string CategoryOf(Type caseType) { string prefix = RootNamespace + "."; if (caseType.Namespace == null || !caseType.Namespace.StartsWith(prefix, StringComparison.Ordinal)) { throw new InvalidOperationException( $"{caseType.FullName} is a test case, so it belongs in a category namespace under " + $"{RootNamespace}."); } string returnMe = caseType.Namespace.Substring(prefix.Length); if (returnMe.Contains(".")) { throw new InvalidOperationException( $"{caseType.FullName} is in a namespace nested below its category {returnMe}."); } return returnMe; } private static ConstructorInfo ConstructorOf(Type caseType) { ConstructorInfo returnMe = caseType.GetConstructor(new[] { typeof(World) }) ?? caseType.GetConstructor(Type.EmptyTypes); if (returnMe == null) { throw new InvalidOperationException( $"{caseType.FullName} needs a public constructor that takes a {nameof(World)}, or none at all."); } return returnMe; } /// /// Returns every concrete test case in , ordered by category and then name. Throws /// for one that couldn't be built or placed, so a broken case fails at startup instead of when it's clicked. /// public static List Find(Assembly assembly) { return Find(assembly.GetTypes()); } /// /// Returns the concrete test cases among , as the assembly overload does. /// public static List Find(IEnumerable types) { var returnMe = new List(); foreach (Type type in types) { if (type.IsAbstract || type.IsGenericTypeDefinition || !typeof(TestCase).IsAssignableFrom(type)) { continue; } ConstructorOf(type); returnMe.Add(new Entry(CategoryOf(type), TestCase.NameFor(type), type)); } returnMe.Sort((left, right) => { int byCategory = string.CompareOrdinal(left.Category, right.Category); return byCategory != 0 ? byCategory : string.CompareOrdinal(left.Name, right.Name); }); return returnMe; } /// /// Builds the case describes, handing it if it takes one. /// public static TestCase Create(Entry entry, World world) { ConstructorInfo constructor = ConstructorOf(entry.Type); try { if (constructor.GetParameters().Length == 0) { return (TestCase)constructor.Invoke(null); } return (TestCase)constructor.Invoke(new object[] { world }); } catch (TargetInvocationException exception) { ExceptionDispatchInfo.Capture(exception.InnerException).Throw(); throw; } } } }