diff options
Diffstat (limited to 'test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs')
-rw-r--r-- | test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs new file mode 100644 index 000000000..5c4f4dbc9 --- /dev/null +++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Linker.Tests.TestCases; +using Mono.Linker.Tests.Extensions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.TestCasesRunner { + public class TestCaseCollector { + private readonly NPath _rootDirectory; + private readonly NPath _testCaseAssemblyPath; + + public TestCaseCollector (string rootDirectory, string testCaseAssemblyPath) + : this (rootDirectory.ToNPath (), testCaseAssemblyPath.ToNPath ()) + { + } + + public TestCaseCollector (NPath rootDirectory, NPath testCaseAssemblyPath) + { + _rootDirectory = rootDirectory; + _testCaseAssemblyPath = testCaseAssemblyPath; + } + + public IEnumerable<TestCase> Collect () + { + return Collect (AllSourceFiles ()); + } + + public TestCase Collect (NPath sourceFile) + { + return Collect (new [] { sourceFile }).First (); + } + + public IEnumerable<TestCase> Collect (IEnumerable<NPath> sourceFiles) + { + _rootDirectory.DirectoryMustExist (); + _testCaseAssemblyPath.FileMustExist (); + + using (var caseAssemblyDefinition = AssemblyDefinition.ReadAssembly (_testCaseAssemblyPath.ToString ())) { + foreach (var file in sourceFiles) { + TestCase testCase; + if (CreateCase (caseAssemblyDefinition, file, out testCase)) + yield return testCase; + } + } + } + + public IEnumerable<NPath> AllSourceFiles () + { + _rootDirectory.DirectoryMustExist (); + + foreach (var file in _rootDirectory.Files ("*.cs")) { + yield return file; + } + + foreach (var subDir in _rootDirectory.Directories ()) { + if (subDir.FileName == "bin" || subDir.FileName == "obj" || subDir.FileName == "Properties") + continue; + + foreach (var file in subDir.Files ("*.cs", true)) { + + var relativeParents = file.RelativeTo(_rootDirectory); + // Magic : Anything in a directory named Dependencies is assumed to be a dependency to a test case + // and never a test itself + // This makes life a little easier when writing these supporting files as it removes some constraints you would previously have + // had to follow such as ensuring a class exists that matches the file name and putting [NotATestCase] on that class + if (relativeParents.RecursiveParents.Any(p => p.Elements.Any() && p.FileName == "Dependencies")) + continue; + + // Magic: Anything in a directory named Individual is expected to be ran by it's own [Test] rather than as part of [TestCaseSource] + if (relativeParents.RecursiveParents.Any(p => p.Elements.Any() && p.FileName == "Individual")) + continue; + + yield return file; + } + } + } + + public TestCase CreateIndividualCase (Type testCaseType) + { + _rootDirectory.DirectoryMustExist (); + _testCaseAssemblyPath.FileMustExist (); + + var pathRelativeToAssembly = $"{testCaseType.FullName.Substring (testCaseType.Module.Name.Length - 3).Replace ('.', '/')}.cs"; + var fullSourcePath = _rootDirectory.Combine (pathRelativeToAssembly).FileMustExist (); + + using (var caseAssemblyDefinition = AssemblyDefinition.ReadAssembly (_testCaseAssemblyPath.ToString ())) + { + TestCase testCase; + if (!CreateCase (caseAssemblyDefinition, fullSourcePath, out testCase)) + throw new ArgumentException ($"Could not create a test case for `{testCaseType}`. Ensure the namespace matches it's location on disk"); + + return testCase; + } + } + + private bool CreateCase (AssemblyDefinition caseAssemblyDefinition, NPath sourceFile, out TestCase testCase) + { + var potentialCase = new TestCase (sourceFile, _rootDirectory, _testCaseAssemblyPath); + + var typeDefinition = FindTypeDefinition (caseAssemblyDefinition, potentialCase); + + testCase = null; + + if (typeDefinition == null) { + Console.WriteLine ($"Could not find the matching type for test case {sourceFile}. Ensure the file name and class name match"); + return false; + } + + if (typeDefinition.HasAttribute (nameof (NotATestCaseAttribute))) { + return false; + } + + // Verify the class as a static main method + var mainMethod = typeDefinition.Methods.FirstOrDefault (m => m.Name == "Main"); + + if (mainMethod == null) { + Console.WriteLine ($"{typeDefinition} in {sourceFile} is missing a Main() method"); + return false; + } + + if (!mainMethod.IsStatic) { + Console.WriteLine ($"The Main() method for {typeDefinition} in {sourceFile} should be static"); + return false; + } + + testCase = potentialCase; + return true; + } + + private static TypeDefinition FindTypeDefinition (AssemblyDefinition caseAssemblyDefinition, TestCase testCase) + { + var typeDefinition = caseAssemblyDefinition.MainModule.GetType (testCase.ReconstructedFullTypeName); + + // For all of the Test Cases, the full type name we constructed from the directory structure will be correct and we can successfully find + // the type from GetType. + if (typeDefinition != null) + return typeDefinition; + + // However, some of types are supporting types rather than test cases. and may not follow the standardized naming scheme of the test cases + // We still need to be able to locate these type defs so that we can parse some of the metadata on them. + // One example, Unity run's into this with it's tests that require a type UnityEngine.MonoBehaviours to exist. This tpe is defined in it's own + // file and it cannot follow our standardized naming directory & namespace naming scheme since the namespace must be UnityEngine + foreach (var type in caseAssemblyDefinition.MainModule.Types) { + // Let's assume we should never have to search for a test case that has no namespace. If we don't find the type from GetType, then o well, that's not a test case. + if (string.IsNullOrEmpty (type.Namespace)) + continue; + + if (type.Name == testCase.Name) { + // This isn't foolproof, but let's do a little extra vetting to make sure the type we found corresponds to the source file we are + // processing. + if (!testCase.SourceFile.ReadAllText ().Contains ($"namespace {type.Namespace}")) + continue; + + return type; + } + } + + return null; + } + } +}
\ No newline at end of file |