Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/linker.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSven Boemer <sbomer@gmail.com>2019-02-20 01:16:58 +0300
committerMarek Safar <marek.safar@gmail.com>2019-02-20 01:16:58 +0300
commitfa9ccbdaf6907c69ef1bb117906f8f012218d57f (patch)
tree4d4cbe2d4079a35edca350a4e444831c2f9dd19a /test/Mono.Linker.Tests
parente64148792193bf7d706428a64632ebf7f8023d3b (diff)
Adopt new directory layout (#466)
This organizes the source and test projects as follows: - source projects go in `src/project/projectfile.csproj` - test projects go in `test/project/projectfile.csproj` The uniform layout of projects is part of the arcade onboarding (see https://github.com/mono/linker/issues/452).
Diffstat (limited to 'test/Mono.Linker.Tests')
-rw-r--r--test/Mono.Linker.Tests/Extensions/CecilExtensions.cs122
-rw-r--r--test/Mono.Linker.Tests/Extensions/NiceIO.cs922
-rw-r--r--test/Mono.Linker.Tests/Mono.Linker.Tests.csproj115
-rw-r--r--test/Mono.Linker.Tests/TestCases/IndividualTests.cs92
-rw-r--r--test/Mono.Linker.Tests/TestCases/TestCase.cs48
-rw-r--r--test/Mono.Linker.Tests/TestCases/TestDatabase.cs175
-rw-r--r--test/Mono.Linker.Tests/TestCases/TestSuites.cs154
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs746
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/CompilerOptions.cs14
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/ExpectationsProvider.cs17
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/FormattingUtils.cs59
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/ILCompiler.cs72
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/LinkXmlHelpers.cs30
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/LinkedTestCaseResult.cs19
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs180
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/LinkerDriver.cs8
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/ManagedCompilationResult.cs15
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/ObjectFactory.cs31
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/PeVerifier.cs147
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs629
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/SetupCompileInfo.cs15
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/SourceAndDestinationPair.cs9
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseAssemblyResolver.cs38
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseCollector.cs163
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs357
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseLinkerOptions.cs21
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseMetadaProvider.cs271
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestCaseSandbox.cs151
-rw-r--r--test/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs135
-rw-r--r--test/Mono.Linker.Tests/Tests/ParseResponseFileLinesTests.cs69
-rw-r--r--test/Mono.Linker.Tests/Tests/PreserveActionComparisonTests.cs30
-rw-r--r--test/Mono.Linker.Tests/Tests/TypeNameParserTests.cs121
-rw-r--r--test/Mono.Linker.Tests/packages.config6
33 files changed, 4981 insertions, 0 deletions
diff --git a/test/Mono.Linker.Tests/Extensions/CecilExtensions.cs b/test/Mono.Linker.Tests/Extensions/CecilExtensions.cs
new file mode 100644
index 000000000..747abf570
--- /dev/null
+++ b/test/Mono.Linker.Tests/Extensions/CecilExtensions.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Mono.Cecil;
+
+namespace Mono.Linker.Tests.Extensions {
+ public static class CecilExtensions {
+ public static IEnumerable<TypeDefinition> AllDefinedTypes (this AssemblyDefinition assemblyDefinition)
+ {
+ return assemblyDefinition.Modules.SelectMany (m => m.AllDefinedTypes ());
+ }
+
+ public static IEnumerable<TypeDefinition> AllDefinedTypes (this ModuleDefinition moduleDefinition)
+ {
+ foreach (var typeDefinition in moduleDefinition.Types) {
+ yield return typeDefinition;
+
+ foreach (var definition in typeDefinition.AllDefinedTypes ())
+ yield return definition;
+ }
+ }
+
+ public static IEnumerable<TypeDefinition> AllDefinedTypes (this TypeDefinition typeDefinition)
+ {
+ foreach (var nestedType in typeDefinition.NestedTypes) {
+ yield return nestedType;
+
+ foreach (var definition in nestedType.AllDefinedTypes ())
+ yield return definition;
+ }
+ }
+
+ public static IEnumerable<IMemberDefinition> AllMembers (this ModuleDefinition module)
+ {
+ foreach (var type in module.AllDefinedTypes ()) {
+ yield return type;
+
+ foreach (var member in type.AllMembers ())
+ yield return member;
+ }
+ }
+
+ public static IEnumerable<IMemberDefinition> AllMembers (this TypeDefinition type)
+ {
+ foreach (var field in type.Fields)
+ yield return field;
+
+ foreach (var prop in type.Properties)
+ yield return prop;
+
+ foreach (var method in type.Methods)
+ yield return method;
+
+ foreach (var @event in type.Events)
+ yield return @event;
+ }
+
+ public static bool HasAttribute (this ICustomAttributeProvider provider, string name)
+ {
+ return provider.CustomAttributes.Any (ca => ca.AttributeType.Name == name);
+ }
+
+ public static bool HasAttributeDerivedFrom (this ICustomAttributeProvider provider, string name)
+ {
+ return provider.CustomAttributes.Any (ca => ca.AttributeType.Resolve ().DerivesFrom (name));
+ }
+
+ public static bool DerivesFrom (this TypeDefinition type, string baseTypeName)
+ {
+ if (type.Name == baseTypeName)
+ return true;
+
+ if (type.BaseType == null)
+ return false;
+
+ if (type.BaseType.Name == baseTypeName)
+ return true;
+
+ return type.BaseType.Resolve ().DerivesFrom (baseTypeName);
+ }
+
+ public static PropertyDefinition GetPropertyDefinition (this MethodDefinition method)
+ {
+ if (!method.IsSetter && !method.IsGetter)
+ throw new ArgumentException ();
+
+ var propertyName = method.Name.Substring (4);
+ return method.DeclaringType.Properties.First (p => p.Name == propertyName);
+ }
+
+ public static string GetSignature (this MethodDefinition method)
+ {
+ var builder = new StringBuilder ();
+ builder.Append (method.Name);
+ if (method.HasGenericParameters) {
+ builder.Append ('<');
+
+ for (int i = 0; i < method.GenericParameters.Count - 1; i++)
+ builder.Append ($"{method.GenericParameters [i]},");
+
+ builder.Append ($"{method.GenericParameters [method.GenericParameters.Count - 1]}>");
+ }
+
+ builder.Append ("(");
+
+ if (method.HasParameters) {
+ for (int i = 0; i < method.Parameters.Count - 1; i++) {
+ // TODO: modifiers
+ // TODO: default values
+ builder.Append ($"{method.Parameters [i].ParameterType},");
+ }
+
+ builder.Append (method.Parameters [method.Parameters.Count - 1].ParameterType);
+ }
+
+ builder.Append (")");
+
+ return builder.ToString ();
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/Extensions/NiceIO.cs b/test/Mono.Linker.Tests/Extensions/NiceIO.cs
new file mode 100644
index 000000000..69b6f30f4
--- /dev/null
+++ b/test/Mono.Linker.Tests/Extensions/NiceIO.cs
@@ -0,0 +1,922 @@
+// The MIT License(MIT)
+// =====================
+//
+// Copyright © `2015-2017` `Lucas Meijer`
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Mono.Linker.Tests.Extensions
+{
+ public class NPath : IEquatable<NPath>, IComparable
+ {
+ private static readonly StringComparison PathStringComparison = IsLinux() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+
+ private readonly string[] _elements;
+ private readonly bool _isRelative;
+ private readonly string _driveLetter;
+
+ #region construction
+
+ public NPath(string path)
+ {
+ if (path == null)
+ throw new ArgumentNullException();
+
+ path = ParseDriveLetter(path, out _driveLetter);
+
+ if (path == "/")
+ {
+ _isRelative = false;
+ _elements = new string[] { };
+ }
+ else
+ {
+ var split = path.Split('/', '\\');
+
+ _isRelative = _driveLetter == null && IsRelativeFromSplitString(split);
+
+ _elements = ParseSplitStringIntoElements(split.Where(s => s.Length > 0).ToArray());
+ }
+ }
+
+ private NPath(string[] elements, bool isRelative, string driveLetter)
+ {
+ _elements = elements;
+ _isRelative = isRelative;
+ _driveLetter = driveLetter;
+ }
+
+ private string[] ParseSplitStringIntoElements(IEnumerable<string> inputs)
+ {
+ var stack = new List<string>();
+
+ foreach (var input in inputs.Where(input => input.Length != 0))
+ {
+ if (input == ".")
+ {
+ if ((stack.Count > 0) && (stack.Last() != "."))
+ continue;
+ }
+ else if (input == "..")
+ {
+ if (HasNonDotDotLastElement(stack))
+ {
+ stack.RemoveAt(stack.Count - 1);
+ continue;
+ }
+ if (!_isRelative)
+ throw new ArgumentException("You cannot create a path that tries to .. past the root");
+ }
+ stack.Add(input);
+ }
+ return stack.ToArray();
+ }
+
+ private static bool HasNonDotDotLastElement(List<string> stack)
+ {
+ return stack.Count > 0 && stack[stack.Count - 1] != "..";
+ }
+
+ private string ParseDriveLetter(string path, out string driveLetter)
+ {
+ if (path.Length >= 2 && path[1] == ':')
+ {
+ driveLetter = path[0].ToString();
+ return path.Substring(2);
+ }
+
+ driveLetter = null;
+ return path;
+ }
+
+ private static bool IsRelativeFromSplitString(string[] split)
+ {
+ if (split.Length < 2)
+ return true;
+
+ return split[0].Length != 0 || !split.Any(s => s.Length > 0);
+ }
+
+ public NPath Combine(params string[] append)
+ {
+ return Combine(append.Select(a => new NPath(a)).ToArray());
+ }
+
+ public NPath Combine(params NPath[] append)
+ {
+ if (!append.All(p => p.IsRelative))
+ throw new ArgumentException("You cannot .Combine a non-relative path");
+
+ return new NPath(ParseSplitStringIntoElements(_elements.Concat(append.SelectMany(p => p._elements))), _isRelative, _driveLetter);
+ }
+
+ public NPath Parent
+ {
+ get
+ {
+ if (_elements.Length == 0)
+ throw new InvalidOperationException("Parent is called on an empty path");
+
+ var newElements = _elements.Take(_elements.Length - 1).ToArray();
+
+ return new NPath(newElements, _isRelative, _driveLetter);
+ }
+ }
+
+ public NPath RelativeTo(NPath path)
+ {
+ if (!IsChildOf(path))
+ {
+ if (!IsRelative && !path.IsRelative && _driveLetter != path._driveLetter)
+ throw new ArgumentException("Path.RelativeTo() was invoked with two paths that are on different volumes. invoked on: " + ToString() + " asked to be made relative to: " + path);
+
+ NPath commonParent = null;
+ foreach (var parent in RecursiveParents)
+ {
+ commonParent = path.RecursiveParents.FirstOrDefault(otherParent => otherParent == parent);
+
+ if (commonParent != null)
+ break;
+ }
+
+ if (commonParent == null)
+ throw new ArgumentException("Path.RelativeTo() was unable to find a common parent between " + ToString() + " and " + path);
+
+ if (IsRelative && path.IsRelative && commonParent.IsEmpty())
+ throw new ArgumentException("Path.RelativeTo() was invoked with two relative paths that do not share a common parent. Invoked on: " + ToString() + " asked to be made relative to: " + path);
+
+ var depthDiff = path.Depth - commonParent.Depth;
+ return new NPath(Enumerable.Repeat("..", depthDiff).Concat(_elements.Skip(commonParent.Depth)).ToArray(), true, null);
+ }
+
+ return new NPath(_elements.Skip(path._elements.Length).ToArray(), true, null);
+ }
+
+ public NPath ChangeExtension(string extension)
+ {
+ ThrowIfRoot();
+
+ var newElements = (string[])_elements.Clone();
+ newElements[newElements.Length - 1] = Path.ChangeExtension(_elements[_elements.Length - 1], WithDot(extension));
+ if (extension == string.Empty)
+ newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.');
+ return new NPath(newElements, _isRelative, _driveLetter);
+ }
+ #endregion construction
+
+ #region inspection
+
+ public bool IsRelative
+ {
+ get { return _isRelative; }
+ }
+
+ public string FileName
+ {
+ get
+ {
+ ThrowIfRoot();
+
+ return _elements.Last();
+ }
+ }
+
+ public string FileNameWithoutExtension
+ {
+ get { return Path.GetFileNameWithoutExtension(FileName); }
+ }
+
+ public IEnumerable<string> Elements
+ {
+ get { return _elements; }
+ }
+
+ public int Depth
+ {
+ get { return _elements.Length; }
+ }
+
+ public bool Exists(string append = "")
+ {
+ return Exists(new NPath(append));
+ }
+
+ public bool Exists(NPath append)
+ {
+ return FileExists(append) || DirectoryExists(append);
+ }
+
+ public bool DirectoryExists(string append = "")
+ {
+ return DirectoryExists(new NPath(append));
+ }
+
+ public bool DirectoryExists(NPath append)
+ {
+ return Directory.Exists(Combine(append).ToString());
+ }
+
+ public bool FileExists(string append = "")
+ {
+ return FileExists(new NPath(append));
+ }
+
+ public bool FileExists(NPath append)
+ {
+ return File.Exists(Combine(append).ToString());
+ }
+
+ public string ExtensionWithDot
+ {
+ get
+ {
+ if (IsRoot)
+ throw new ArgumentException("A root directory does not have an extension");
+
+ var last = _elements.Last();
+ var index = last.LastIndexOf(".");
+ if (index < 0) return String.Empty;
+ return last.Substring(index);
+ }
+ }
+
+ public string InQuotes()
+ {
+ return "\"" + ToString() + "\"";
+ }
+
+ public string InQuotes(SlashMode slashMode)
+ {
+ return "\"" + ToString(slashMode) + "\"";
+ }
+
+ public override string ToString()
+ {
+ return ToString(SlashMode.Native);
+ }
+
+ public string ToString(SlashMode slashMode)
+ {
+ // Check if it's linux root /
+ if (IsRoot && string.IsNullOrEmpty(_driveLetter))
+ return Slash(slashMode).ToString();
+
+ if (_isRelative && _elements.Length == 0)
+ return ".";
+
+ var sb = new StringBuilder();
+ if (_driveLetter != null)
+ {
+ sb.Append(_driveLetter);
+ sb.Append(":");
+ }
+ if (!_isRelative)
+ sb.Append(Slash(slashMode));
+ var first = true;
+ foreach (var element in _elements)
+ {
+ if (!first)
+ sb.Append(Slash(slashMode));
+
+ sb.Append(element);
+ first = false;
+ }
+ return sb.ToString();
+ }
+
+ public static implicit operator string(NPath path)
+ {
+ return path.ToString();
+ }
+
+ static char Slash(SlashMode slashMode)
+ {
+ switch (slashMode)
+ {
+ case SlashMode.Backward:
+ return '\\';
+ case SlashMode.Forward:
+ return '/';
+ default:
+ return Path.DirectorySeparatorChar;
+ }
+ }
+
+ public override bool Equals(Object obj)
+ {
+ if (obj == null)
+ return false;
+
+ // If parameter cannot be cast to Point return false.
+ var p = obj as NPath;
+ if ((Object)p == null)
+ return false;
+
+ return Equals(p);
+ }
+
+ public bool Equals(NPath p)
+ {
+ if (p._isRelative != _isRelative)
+ return false;
+
+ if (!string.Equals(p._driveLetter, _driveLetter, PathStringComparison))
+ return false;
+
+ if (p._elements.Length != _elements.Length)
+ return false;
+
+ for (var i = 0; i != _elements.Length; i++)
+ if (!string.Equals(p._elements[i], _elements[i], PathStringComparison))
+ return false;
+
+ return true;
+ }
+
+ public static bool operator ==(NPath a, NPath b)
+ {
+ // If both are null, or both are same instance, return true.
+ if (ReferenceEquals(a, b))
+ return true;
+
+ // If one is null, but not both, return false.
+ if (((object)a == null) || ((object)b == null))
+ return false;
+
+ // Return true if the fields match:
+ return a.Equals(b);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ // Suitable nullity checks etc, of course :)
+ hash = hash * 23 + _isRelative.GetHashCode();
+ foreach (var element in _elements)
+ hash = hash * 23 + element.GetHashCode();
+ if (_driveLetter != null)
+ hash = hash * 23 + _driveLetter.GetHashCode();
+ return hash;
+ }
+ }
+
+ public int CompareTo(object obj)
+ {
+ if (obj == null)
+ return -1;
+
+ return this.ToString().CompareTo(((NPath)obj).ToString());
+ }
+
+ public static bool operator !=(NPath a, NPath b)
+ {
+ return !(a == b);
+ }
+
+ public bool HasExtension(params string[] extensions)
+ {
+ var extensionWithDotLower = ExtensionWithDot.ToLower();
+ return extensions.Any(e => WithDot(e).ToLower() == extensionWithDotLower);
+ }
+
+ private static string WithDot(string extension)
+ {
+ return extension.StartsWith(".") ? extension : "." + extension;
+ }
+
+ private bool IsEmpty()
+ {
+ return _elements.Length == 0;
+ }
+
+ public bool IsRoot
+ {
+ get { return _elements.Length == 0 && !_isRelative; }
+ }
+
+ #endregion inspection
+
+ #region directory enumeration
+
+ public IEnumerable<NPath> Files(string filter, bool recurse = false)
+ {
+ return Directory.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
+ }
+
+ public IEnumerable<NPath> Files(bool recurse = false)
+ {
+ return Files("*", recurse);
+ }
+
+ public IEnumerable<NPath> Contents(string filter, bool recurse = false)
+ {
+ return Files(filter, recurse).Concat(Directories(filter, recurse));
+ }
+
+ public IEnumerable<NPath> Contents(bool recurse = false)
+ {
+ return Contents("*", recurse);
+ }
+
+ public IEnumerable<NPath> Directories(string filter, bool recurse = false)
+ {
+ return Directory.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
+ }
+
+ public IEnumerable<NPath> Directories(bool recurse = false)
+ {
+ return Directories("*", recurse);
+ }
+
+ #endregion
+
+ #region filesystem writing operations
+ public NPath CreateFile()
+ {
+ ThrowIfRelative();
+ ThrowIfRoot();
+ EnsureParentDirectoryExists();
+ File.WriteAllBytes(ToString(), new byte[0]);
+ return this;
+ }
+
+ public NPath CreateFile(string file)
+ {
+ return CreateFile(new NPath(file));
+ }
+
+ public NPath CreateFile(NPath file)
+ {
+ if (!file.IsRelative)
+ throw new ArgumentException("You cannot call CreateFile() on an existing path with a non relative argument");
+ return Combine(file).CreateFile();
+ }
+
+ public NPath CreateDirectory()
+ {
+ ThrowIfRelative();
+
+ if (IsRoot)
+ throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString());
+
+ Directory.CreateDirectory(ToString());
+ return this;
+ }
+
+ public NPath CreateDirectory(string directory)
+ {
+ return CreateDirectory(new NPath(directory));
+ }
+
+ public NPath CreateDirectory(NPath directory)
+ {
+ if (!directory.IsRelative)
+ throw new ArgumentException("Cannot call CreateDirectory with an absolute argument");
+
+ return Combine(directory).CreateDirectory();
+ }
+
+ public NPath Copy(string dest)
+ {
+ return Copy(new NPath(dest));
+ }
+
+ public NPath Copy(string dest, Func<NPath, bool> fileFilter)
+ {
+ return Copy(new NPath(dest), fileFilter);
+ }
+
+ public NPath Copy(NPath dest)
+ {
+ return Copy(dest, p => true);
+ }
+
+ public NPath Copy(NPath dest, Func<NPath, bool> fileFilter)
+ {
+ ThrowIfRelative();
+ if (dest.IsRelative)
+ dest = Parent.Combine(dest);
+
+ if (dest.DirectoryExists())
+ return CopyWithDeterminedDestination(dest.Combine(FileName), fileFilter);
+
+ return CopyWithDeterminedDestination(dest, fileFilter);
+ }
+
+ public NPath MakeAbsolute()
+ {
+ if (!IsRelative)
+ return this;
+
+ return NPath.CurrentDirectory.Combine(this);
+ }
+
+ NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func<NPath, bool> fileFilter)
+ {
+ if (absoluteDestination.IsRelative)
+ throw new ArgumentException("absoluteDestination must be absolute");
+
+ if (FileExists())
+ {
+ if (!fileFilter(absoluteDestination))
+ return null;
+
+ absoluteDestination.EnsureParentDirectoryExists();
+
+ File.Copy(ToString(), absoluteDestination.ToString(), true);
+ return absoluteDestination;
+ }
+
+ if (DirectoryExists())
+ {
+ absoluteDestination.EnsureDirectoryExists();
+ foreach (var thing in Contents())
+ thing.CopyWithDeterminedDestination(absoluteDestination.Combine(thing.RelativeTo(this)), fileFilter);
+ return absoluteDestination;
+ }
+
+ throw new ArgumentException("Copy() called on path that doesnt exist: " + ToString());
+ }
+
+ public void Delete(DeleteMode deleteMode = DeleteMode.Normal)
+ {
+ ThrowIfRelative();
+
+ if (IsRoot)
+ throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString());
+
+ if (FileExists())
+ File.Delete(ToString());
+ else if (DirectoryExists())
+ try
+ {
+ Directory.Delete(ToString(), true);
+ }
+ catch (IOException)
+ {
+ if (deleteMode == DeleteMode.Normal)
+ throw;
+ }
+ else
+ throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString());
+ }
+
+ public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal)
+ {
+ ThrowIfRelative();
+
+ if (FileExists() || DirectoryExists())
+ Delete(deleteMode);
+ }
+
+ public NPath DeleteContents()
+ {
+ ThrowIfRelative();
+
+ if (IsRoot)
+ throw new NotSupportedException("DeleteContents is not supported on a root level directory because it would be dangerous:" + ToString());
+
+ if (FileExists())
+ throw new InvalidOperationException("It is not valid to perform this operation on a file");
+
+ if (DirectoryExists())
+ {
+ try
+ {
+ Files().Delete();
+ Directories().Delete();
+ }
+ catch (IOException)
+ {
+ if (Files(true).Any())
+ throw;
+ }
+
+ return this;
+ }
+
+ return EnsureDirectoryExists();
+ }
+
+ public static NPath CreateTempDirectory(string myprefix)
+ {
+ var random = new Random();
+ while (true)
+ {
+ var candidate = new NPath(Path.GetTempPath() + "/" + myprefix + "_" + random.Next());
+ if (!candidate.Exists())
+ return candidate.CreateDirectory();
+ }
+ }
+
+ public NPath Move(string dest)
+ {
+ return Move(new NPath(dest));
+ }
+
+ public NPath Move(NPath dest)
+ {
+ ThrowIfRelative();
+
+ if (IsRoot)
+ throw new NotSupportedException("Move is not supported on a root level directory because it would be dangerous:" + ToString());
+
+ if (dest.IsRelative)
+ return Move(Parent.Combine(dest));
+
+ if (dest.DirectoryExists())
+ return Move(dest.Combine(FileName));
+
+ if (FileExists())
+ {
+ dest.EnsureParentDirectoryExists();
+ File.Move(ToString(), dest.ToString());
+ return dest;
+ }
+
+ if (DirectoryExists())
+ {
+ Directory.Move(ToString(), dest.ToString());
+ return dest;
+ }
+
+ throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString());
+ }
+
+ #endregion
+
+ #region special paths
+
+ public static NPath CurrentDirectory
+ {
+ get
+ {
+ return new NPath(Directory.GetCurrentDirectory());
+ }
+ }
+
+ public static NPath HomeDirectory
+ {
+ get
+ {
+ if (Path.DirectorySeparatorChar == '\\')
+ return new NPath(Environment.GetEnvironmentVariable("USERPROFILE"));
+ return new NPath(Environment.GetEnvironmentVariable("HOME"));
+ }
+ }
+
+ public static NPath SystemTemp
+ {
+ get
+ {
+ return new NPath(Path.GetTempPath());
+ }
+ }
+
+ #endregion
+
+ private void ThrowIfRelative()
+ {
+ if (_isRelative)
+ throw new ArgumentException("You are attempting an operation on a Path that requires an absolute path, but the path is relative");
+ }
+
+ private void ThrowIfRoot()
+ {
+ if (IsRoot)
+ throw new ArgumentException("You are attempting an operation that is not valid on a root level directory");
+ }
+
+ public NPath EnsureDirectoryExists(string append = "")
+ {
+ return EnsureDirectoryExists(new NPath(append));
+ }
+
+ public NPath EnsureDirectoryExists(NPath append)
+ {
+ var combined = Combine(append);
+ if (combined.DirectoryExists())
+ return combined;
+ combined.EnsureParentDirectoryExists();
+ combined.CreateDirectory();
+ return combined;
+ }
+
+ public NPath EnsureParentDirectoryExists()
+ {
+ var parent = Parent;
+ parent.EnsureDirectoryExists();
+ return parent;
+ }
+
+ public NPath FileMustExist()
+ {
+ if (!FileExists())
+ throw new FileNotFoundException("File was expected to exist : " + ToString());
+
+ return this;
+ }
+
+ public NPath DirectoryMustExist()
+ {
+ if (!DirectoryExists())
+ throw new DirectoryNotFoundException("Expected directory to exist : " + ToString());
+
+ return this;
+ }
+
+ public bool IsChildOf(string potentialBasePath)
+ {
+ return IsChildOf(new NPath(potentialBasePath));
+ }
+
+ public bool IsChildOf(NPath potentialBasePath)
+ {
+ if ((IsRelative && !potentialBasePath.IsRelative) || !IsRelative && potentialBasePath.IsRelative)
+ throw new ArgumentException("You can only call IsChildOf with two relative paths, or with two absolute paths");
+
+ // If the other path is the root directory, then anything is a child of it as long as it's not a Windows path
+ if (potentialBasePath.IsRoot)
+ {
+ if (_driveLetter != potentialBasePath._driveLetter)
+ return false;
+ return true;
+ }
+
+ if (IsEmpty())
+ return false;
+
+ if (Equals(potentialBasePath))
+ return true;
+
+ return Parent.IsChildOf(potentialBasePath);
+ }
+
+ public IEnumerable<NPath> RecursiveParents
+ {
+ get
+ {
+ var candidate = this;
+ while (true)
+ {
+ if (candidate.IsEmpty())
+ yield break;
+
+ candidate = candidate.Parent;
+ yield return candidate;
+ }
+ }
+ }
+
+ public NPath ParentContaining(string needle)
+ {
+ return ParentContaining(new NPath(needle));
+ }
+
+ public NPath ParentContaining(NPath needle)
+ {
+ ThrowIfRelative();
+
+ return RecursiveParents.FirstOrDefault(p => p.Exists(needle));
+ }
+
+ public NPath WriteAllText(string contents)
+ {
+ ThrowIfRelative();
+ EnsureParentDirectoryExists();
+ File.WriteAllText(ToString(), contents);
+ return this;
+ }
+
+ public string ReadAllText()
+ {
+ ThrowIfRelative();
+ return File.ReadAllText(ToString());
+ }
+
+ public NPath WriteAllLines(string[] contents)
+ {
+ ThrowIfRelative();
+ EnsureParentDirectoryExists();
+ File.WriteAllLines(ToString(), contents);
+ return this;
+ }
+
+ public string[] ReadAllLines()
+ {
+ ThrowIfRelative();
+ return File.ReadAllLines(ToString());
+ }
+
+ public IEnumerable<NPath> CopyFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
+ {
+ destination.EnsureDirectoryExists();
+ return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Copy(destination.Combine(file.RelativeTo(this)))).ToArray();
+ }
+
+ public IEnumerable<NPath> MoveFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
+ {
+ if (IsRoot)
+ throw new NotSupportedException("MoveFiles is not supported on this directory because it would be dangerous:" + ToString());
+
+ destination.EnsureDirectoryExists();
+ return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Move(destination.Combine(file.RelativeTo(this)))).ToArray();
+ }
+
+ static bool AlwaysTrue(NPath p)
+ {
+ return true;
+ }
+
+ private static bool IsLinux()
+ {
+ return Directory.Exists("/proc");
+ }
+ }
+
+ public static class Extensions
+ {
+ public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, string dest)
+ {
+ return Copy(self, new NPath(dest));
+ }
+
+ public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, NPath dest)
+ {
+ if (dest.IsRelative)
+ throw new ArgumentException("When copying multiple files, the destination cannot be a relative path");
+ dest.EnsureDirectoryExists();
+ return self.Select(p => p.Copy(dest.Combine(p.FileName))).ToArray();
+ }
+
+ public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, string dest)
+ {
+ return Move(self, new NPath(dest));
+ }
+
+ public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, NPath dest)
+ {
+ if (dest.IsRelative)
+ throw new ArgumentException("When moving multiple files, the destination cannot be a relative path");
+ dest.EnsureDirectoryExists();
+ return self.Select(p => p.Move(dest.Combine(p.FileName))).ToArray();
+ }
+
+ public static IEnumerable<NPath> Delete(this IEnumerable<NPath> self)
+ {
+ foreach (var p in self)
+ p.Delete();
+ return self;
+ }
+
+ public static IEnumerable<string> InQuotes(this IEnumerable<NPath> self, SlashMode forward = SlashMode.Native)
+ {
+ return self.Select(p => p.InQuotes(forward));
+ }
+
+ public static NPath ToNPath(this string path)
+ {
+ return new NPath(path);
+ }
+ }
+
+ public enum SlashMode
+ {
+ Native,
+ Forward,
+ Backward
+ }
+
+ public enum DeleteMode
+ {
+ Normal,
+ Soft
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
new file mode 100644
index 000000000..7da7cae3d
--- /dev/null
+++ b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
@@ -0,0 +1,115 @@
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.50727</ProductVersion>
+ <ProjectGuid>{400A1561-B6B6-482D-9E4C-3DDAEDE5BD07}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <AssemblyName>Mono.Linker.Tests</AssemblyName>
+ <StartupObject>
+ </StartupObject>
+ <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
+ <RootNamespace>Mono.Linker.Tests</RootNamespace>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Xml" />
+ <Reference Include="System.Core" />
+ <Reference Include="nunit.framework">
+ <HintPath>..\..\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="Properties\" />
+ <Folder Include="TestCasesRunner\" />
+ <Folder Include="Extensions\" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\external\cecil\Mono.Cecil.csproj">
+ <Project>{D68133BD-1E63-496E-9EDE-4FBDBF77B486}</Project>
+ <Name>Mono.Cecil</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\external\cecil\symbols\mdb\Mono.Cecil.Mdb.csproj">
+ <Project>{8559dd7f-a16f-46d0-a05a-9139faeba8fd}</Project>
+ <Name>Mono.Cecil.Mdb</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\external\cecil\symbols\pdb\Mono.Cecil.Pdb.csproj">
+ <Project>{63e6915c-7ea4-4d76-ab28-0d7191eea626}</Project>
+ <Name>Mono.Cecil.Pdb</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\linker\Mono.Linker.csproj">
+ <Project>{DD28E2B1-057B-4B4D-A04D-B2EBD9E76E46}</Project>
+ <Name>Mono.Linker</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Mono.Linker.Tests.Cases\Mono.Linker.Tests.Cases.csproj">
+ <Project>{B6BEE6AA-ADA0-4E1D-9A17-FBF2936F82B5}</Project>
+ <Name>Mono.Linker.Tests.Cases</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Mono.Linker.Tests.Cases.Expectations\Mono.Linker.Tests.Cases.Expectations.csproj">
+ <Project>{2C26601F-3E2F-45B9-A02F-58EE9296E19E}</Project>
+ <Name>Mono.Linker.Tests.Cases.Expectations</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="TestCasesRunner\CompilerOptions.cs" />
+ <Compile Include="TestCasesRunner\FormattingUtils.cs" />
+ <Compile Include="TestCasesRunner\ILCompiler.cs" />
+ <Compile Include="TestCasesRunner\SourceAndDestinationPair.cs" />
+ <Compile Include="TestCasesRunner\SetupCompileInfo.cs" />
+ <Compile Include="TestCasesRunner\PeVerifier.cs" />
+ <Compile Include="TestCasesRunner\TestCaseAssemblyResolver.cs" />
+ <Compile Include="TestCases\IndividualTests.cs" />
+ <Compile Include="TestCases\TestSuites.cs" />
+ <Compile Include="TestCases\TestDatabase.cs" />
+ <Compile Include="Tests\PreserveActionComparisonTests.cs" />
+ <Compile Include="TestCasesRunner\LinkXmlHelpers.cs" />
+ <Compile Include="TestCasesRunner\LinkedTestCaseResult.cs" />
+ <Compile Include="TestCasesRunner\ManagedCompilationResult.cs" />
+ <Compile Include="TestCasesRunner\TestCaseCollector.cs" />
+ <Compile Include="TestCasesRunner\TestCaseLinkerOptions.cs" />
+ <Compile Include="TestCasesRunner\TestRunner.cs" />
+ <Compile Include="TestCases\TestCase.cs" />
+ <Compile Include="Extensions\NiceIO.cs" />
+ <Compile Include="Extensions\CecilExtensions.cs" />
+ <Compile Include="TestCasesRunner\ExpectationsProvider.cs" />
+ <Compile Include="TestCasesRunner\AssemblyChecker.cs" />
+ <Compile Include="TestCasesRunner\LinkerArgumentBuilder.cs" />
+ <Compile Include="TestCasesRunner\LinkerDriver.cs" />
+ <Compile Include="TestCasesRunner\ObjectFactory.cs" />
+ <Compile Include="TestCasesRunner\ResultChecker.cs" />
+ <Compile Include="TestCasesRunner\TestCaseCompiler.cs" />
+ <Compile Include="TestCasesRunner\TestCaseMetadaProvider.cs" />
+ <Compile Include="TestCasesRunner\TestCaseSandbox.cs" />
+ <Compile Include="Tests\TypeNameParserTests.cs" />
+ <Compile Include="Tests\ParseResponseFileLinesTests.cs" />
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCases/IndividualTests.cs b/test/Mono.Linker.Tests/TestCases/IndividualTests.cs
new file mode 100644
index 000000000..c8c1a8833
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCases/IndividualTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using System.Xml;
+using Mono.Linker.Tests.Cases.References.Individual;
+using Mono.Linker.Tests.Cases.Tracing.Individual;
+using Mono.Linker.Tests.TestCases;
+using Mono.Linker.Tests.TestCasesRunner;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCases
+{
+ [TestFixture]
+ public class IndividualTests
+ {
+ [Test]
+ public void CanSkipUnresolved ()
+ {
+ var testcase = CreateIndividualCase (typeof (CanSkipUnresolved));
+ var result = Run (testcase);
+
+ // We can't use the ResultChecker on the output because there will be unresolved types/methods
+ // Let's just make sure that the output assembly exists. That's enough to verify that the linker didn't throw due to the
+ // missing types/methods
+ if (!result.OutputAssemblyPath.Exists ())
+ Assert.Fail ($"The linked assembly is missing. Should have existed at {result.OutputAssemblyPath}");
+ }
+
+ [Test]
+ public void CanEnableDependenciesDump ()
+ {
+ var testcase = CreateIndividualCase (typeof (CanEnableDependenciesDump));
+ var result = Run (testcase);
+
+ var outputPath = result.OutputAssemblyPath.Parent.Combine (Tracer.DefaultDependenciesFileName);
+ if (!outputPath.Exists ())
+ Assert.Fail ($"The dependency dump file is missing. Expected it to exist at {outputPath}");
+ }
+
+ [Test]
+ public void CanDumpDependenciesToUncompressedXml ()
+ {
+ var testcase = CreateIndividualCase (typeof (CanDumpDependenciesToUncompressedXml));
+ var result = Run (testcase);
+
+ var outputPath = result.OutputAssemblyPath.Parent.Combine ("linker-dependencies.xml");
+ if (!outputPath.Exists ())
+ Assert.Fail($"The dependency dump file is missing. Expected it to exist at {outputPath}");
+
+ // Do a basic check to verify that the contents of the file are uncompressed xml
+ using (var reader = new XmlTextReader (outputPath.ToString ())) {
+ reader.Read ();
+ reader.Read ();
+ reader.Read ();
+ Assert.That (reader.Name, Is.EqualTo ("dependencies"), $"Expected to be at the dependencies element, but the current node name is `{reader.Name}`");
+ }
+ }
+
+ [Test]
+ public void CanEnableReducedTracing ()
+ {
+ var testcase = CreateIndividualCase (typeof (CanEnableReducedTracing));
+ var result = Run (testcase);
+
+ // Note: This name needs to match what is setup in the test case arguments to the linker
+ const string expectedDependenciesFileName = "linker-dependencies.xml";
+ var outputPath = result.OutputAssemblyPath.Parent.Combine (expectedDependenciesFileName);
+ if (!outputPath.Exists ())
+ Assert.Fail($"The dependency dump file is missing. Expected it to exist at {outputPath}");
+
+ // Let's go a little bit further and make sure it looks like reducing tracking actually worked.
+ // This is intentionally a loose assertion. This test isn't meant to verify how reduced tracing works,
+ // it's here to make sure that enabling the option enables the behavior.
+ var lineCount = outputPath.ReadAllLines ().Length;
+
+ // When reduced tracing is not enabled there are around 16k of lines in the output file.
+ // With reduced tracing there should be less than 65, but to be safe, we'll check for less than 100.
+ const int expectedMaxLines = 100;
+ Assert.That (lineCount, Is.LessThan (expectedMaxLines), $"There were `{lineCount}` lines in the dump file. This is more than expected max of {expectedMaxLines} and likely indicates reduced tracing was not enabled. Dump file can be found at: {outputPath}");
+ }
+
+ private TestCase CreateIndividualCase (Type testCaseType)
+ {
+ return TestDatabase.CreateCollector ().CreateIndividualCase (testCaseType);
+ }
+
+ protected virtual LinkedTestCaseResult Run (TestCase testCase)
+ {
+ var runner = new TestRunner (new ObjectFactory ());
+ return runner.Run (testCase);
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCases/TestCase.cs b/test/Mono.Linker.Tests/TestCases/TestCase.cs
new file mode 100644
index 000000000..790e41102
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCases/TestCase.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Linq;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCases {
+ public class TestCase {
+ public TestCase (NPath sourceFile, NPath rootCasesDirectory, NPath originalTestCaseAssemblyPath)
+ {
+ SourceFile = sourceFile;
+ OriginalTestCaseAssemblyPath = originalTestCaseAssemblyPath;
+ Name = sourceFile.FileNameWithoutExtension;
+ DisplayName = $"{sourceFile.RelativeTo (rootCasesDirectory).Parent.ToString (SlashMode.Forward).Replace ('/', '.')}.{sourceFile.FileNameWithoutExtension}";
+
+ // A little hacky, but good enough for name. No reason why namespace & type names
+ // should not follow the directory structure
+ ReconstructedFullTypeName = $"{sourceFile.Parent.RelativeTo (rootCasesDirectory.Parent).ToString (SlashMode.Forward).Replace ('/', '.')}.{sourceFile.FileNameWithoutExtension}";
+
+ var firstParentRelativeToRoot = SourceFile.RelativeTo (rootCasesDirectory).Elements.First ();
+ TestSuiteDirectory = rootCasesDirectory.Combine (firstParentRelativeToRoot);
+ }
+
+ public string Name { get; }
+
+ public string DisplayName { get; }
+
+ public NPath SourceFile { get; }
+
+ public NPath OriginalTestCaseAssemblyPath { get; }
+
+ public string ReconstructedFullTypeName { get; }
+
+ public bool HasLinkXmlFile {
+ get { return SourceFile.ChangeExtension ("xml").FileExists (); }
+ }
+
+ public NPath LinkXmlFile {
+ get
+ {
+ if (!HasLinkXmlFile)
+ throw new InvalidOperationException ("This test case does not have a link xml file");
+
+ return SourceFile.ChangeExtension ("xml");
+ }
+ }
+
+ public NPath TestSuiteDirectory { get; }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCases/TestDatabase.cs b/test/Mono.Linker.Tests/TestCases/TestDatabase.cs
new file mode 100644
index 000000000..62a1a0009
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCases/TestDatabase.cs
@@ -0,0 +1,175 @@
+using System.Linq;
+using System.Collections.Generic;
+using NUnit.Framework;
+using System.Runtime.CompilerServices;
+using System.IO;
+using Mono.Linker.Tests.TestCasesRunner;
+
+namespace Mono.Linker.Tests.TestCases
+{
+ public static class TestDatabase
+ {
+ private static TestCase[] _cachedAllCases;
+
+ public static IEnumerable<TestCaseData> XmlTests()
+ {
+ return NUnitCasesBySuiteName("LinkXml");
+ }
+
+ public static IEnumerable<TestCaseData> BasicTests()
+ {
+ return NUnitCasesBySuiteName("Basic");
+ }
+
+ public static IEnumerable<TestCaseData> AttributeTests()
+ {
+ return NUnitCasesBySuiteName("Attributes");
+ }
+
+ public static IEnumerable<TestCaseData> AttributeDebuggerTests ()
+ {
+ return NUnitCasesBySuiteName ("Attributes.Debugger");
+ }
+
+ public static IEnumerable<TestCaseData> GenericsTests()
+ {
+ return NUnitCasesBySuiteName("Generics");
+ }
+
+ public static IEnumerable<TestCaseData> CoreLinkTests()
+ {
+ return NUnitCasesBySuiteName("CoreLink");
+ }
+
+ public static IEnumerable<TestCaseData> StaticsTests()
+ {
+ return NUnitCasesBySuiteName("Statics");
+ }
+
+ public static IEnumerable<TestCaseData> InteropTests()
+ {
+ return NUnitCasesBySuiteName("Interop");
+ }
+
+ public static IEnumerable<TestCaseData> ReferencesTests()
+ {
+ return NUnitCasesBySuiteName("References");
+ }
+
+ public static IEnumerable<TestCaseData> ResourcesTests ()
+ {
+ return NUnitCasesBySuiteName ("Resources");
+ }
+
+ public static IEnumerable<TestCaseData> TypeForwardingTests ()
+ {
+ return NUnitCasesBySuiteName ("TypeForwarding");
+ }
+
+ public static IEnumerable<TestCaseData> TestFrameworkTests ()
+ {
+ return NUnitCasesBySuiteName ("TestFramework");
+ }
+
+ public static IEnumerable<TestCaseData> ReflectionTests ()
+ {
+ return NUnitCasesBySuiteName ("Reflection");
+ }
+
+ public static IEnumerable<TestCaseData> SymbolsTests ()
+ {
+ return NUnitCasesBySuiteName ("Symbols");
+ }
+
+ public static IEnumerable<TestCaseData> PreserveDependenciesTests ()
+ {
+ return NUnitCasesBySuiteName ("PreserveDependencies");
+ }
+
+ public static IEnumerable<TestCaseData> LibrariesTests ()
+ {
+ return NUnitCasesBySuiteName ("Libraries");
+ }
+
+ public static IEnumerable<TestCaseData> AdvancedTests ()
+ {
+ return NUnitCasesBySuiteName ("Advanced");
+ }
+
+ public static IEnumerable<TestCaseData> InheritanceInterfaceTests ()
+ {
+ return NUnitCasesBySuiteName ("Inheritance.Interfaces");
+ }
+
+ public static IEnumerable<TestCaseData> InheritanceAbstractClassTests ()
+ {
+ return NUnitCasesBySuiteName ("Inheritance.AbstractClasses");
+ }
+
+ public static IEnumerable<TestCaseData> InheritanceVirtualMethodsTests ()
+ {
+ return NUnitCasesBySuiteName ("Inheritance.VirtualMethods");
+ }
+
+ public static IEnumerable<TestCaseData> InheritanceComplexTests ()
+ {
+ return NUnitCasesBySuiteName ("Inheritance.Complex");
+ }
+
+ public static IEnumerable<TestCaseData> BCLFeaturesTests ()
+ {
+ return NUnitCasesBySuiteName ("BCLFeatures");
+ }
+
+ public static IEnumerable<TestCaseData> CommandLineTests ()
+ {
+ return NUnitCasesBySuiteName ("CommandLine");
+ }
+
+ public static TestCaseCollector CreateCollector ()
+ {
+ string rootSourceDirectory;
+ string testCaseAssemblyPath;
+ GetDirectoryPaths (out rootSourceDirectory, out testCaseAssemblyPath);
+ return new TestCaseCollector (rootSourceDirectory, testCaseAssemblyPath);
+ }
+
+ static IEnumerable<TestCase> AllCases ()
+ {
+ if (_cachedAllCases == null)
+ _cachedAllCases = CreateCollector ()
+ .Collect ()
+ .OrderBy (c => c.DisplayName)
+ .ToArray ();
+
+ return _cachedAllCases;
+ }
+
+ static IEnumerable<TestCaseData> NUnitCasesBySuiteName(string suiteName)
+ {
+ return AllCases()
+ .Where(c => c.TestSuiteDirectory.FileName == suiteName)
+ .Select(c => CreateNUnitTestCase(c, c.DisplayName.Substring(suiteName.Length + 1)))
+ .OrderBy(c => c.TestName);
+ }
+
+ static TestCaseData CreateNUnitTestCase(TestCase testCase, string displayName)
+ {
+ var data = new TestCaseData(testCase);
+ data.SetName(displayName);
+ return data;
+ }
+
+ static void GetDirectoryPaths(out string rootSourceDirectory, out string testCaseAssemblyPath, [CallerFilePath] string thisFile = null)
+ {
+ var thisDirectory = Path.GetDirectoryName(thisFile);
+ rootSourceDirectory = Path.GetFullPath(Path.Combine(thisDirectory, "..", "..", "Mono.Linker.Tests.Cases"));
+#if DEBUG
+ var configDirectoryName = "Debug";
+#else
+ var configDirectoryName = "Release";
+#endif
+ testCaseAssemblyPath = Path.GetFullPath(Path.Combine(rootSourceDirectory, "bin", configDirectoryName, "Mono.Linker.Tests.Cases.dll"));
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCases/TestSuites.cs b/test/Mono.Linker.Tests/TestCases/TestSuites.cs
new file mode 100644
index 000000000..994448f68
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCases/TestSuites.cs
@@ -0,0 +1,154 @@
+using Mono.Linker.Tests.TestCasesRunner;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCases
+{
+ [TestFixture]
+ public class All
+ {
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.BasicTests))]
+ public void BasicTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.AdvancedTests))]
+ public void AdvancedTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.XmlTests))]
+ public void XmlTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.AttributeTests))]
+ public void AttributesTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.AttributeDebuggerTests))]
+ public void AttributesDebuggerTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.GenericsTests))]
+ public void GenericsTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.StaticsTests))]
+ public void StaticsTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.CoreLinkTests))]
+ public void CoreLinkTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.InteropTests))]
+ public void InteropTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource(typeof(TestDatabase), nameof(TestDatabase.ReferencesTests))]
+ public void ReferencesTests(TestCase testCase)
+ {
+ Run(testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.ResourcesTests))]
+ public void ResourcesTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.TypeForwardingTests))]
+ public void TypeForwardingTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource(typeof (TestDatabase), nameof (TestDatabase.TestFrameworkTests))]
+ public void TestFrameworkTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.ReflectionTests))]
+ public void ReflectionTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.PreserveDependenciesTests))]
+ public void PreserveDependenciesTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.SymbolsTests))]
+ public void SymbolsTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.LibrariesTests))]
+ public void LibrariesTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.InheritanceInterfaceTests))]
+ public void InheritanceInterfaceTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.InheritanceAbstractClassTests))]
+ public void InheritanceAbstractClassTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.InheritanceVirtualMethodsTests))]
+ public void InheritanceVirtualMethodsTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.InheritanceComplexTests))]
+ public void InheritanceComplexTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.BCLFeaturesTests))]
+ public void BCLFeaturesTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.CommandLineTests))]
+ public void CommandLineTests (TestCase testCase)
+ {
+ Run (testCase);
+ }
+
+ protected virtual void Run (TestCase testCase)
+ {
+ var runner = new TestRunner (new ObjectFactory ());
+ var linkedResult = runner.Run (testCase);
+ new ResultChecker ().Check (linkedResult);
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs b/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs
new file mode 100644
index 000000000..9893a82a1
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs
@@ -0,0 +1,746 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Extensions;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class AssemblyChecker {
+ readonly AssemblyDefinition originalAssembly, linkedAssembly;
+
+ HashSet<string> linkedMembers;
+ HashSet<string> verifiedGeneratedFields = new HashSet<string> ();
+ HashSet<string> verifiedEventMethods = new HashSet<string>();
+ HashSet<string> verifiedGeneratedTypes = new HashSet<string> ();
+
+ public AssemblyChecker (AssemblyDefinition original, AssemblyDefinition linked)
+ {
+ this.originalAssembly = original;
+ this.linkedAssembly = linked;
+ }
+
+ public void Verify ()
+ {
+ // TODO: Implement fully, probably via custom Kept attribute
+ Assert.IsFalse (linkedAssembly.MainModule.HasExportedTypes);
+
+ VerifyCustomAttributes (originalAssembly, linkedAssembly);
+ VerifySecurityAttributes (originalAssembly, linkedAssembly);
+
+ foreach (var originalModule in originalAssembly.Modules)
+ VerifyModule (originalModule, linkedAssembly.Modules.FirstOrDefault (m => m.Name == originalModule.Name));
+
+ VerifyResources (originalAssembly, linkedAssembly);
+ VerifyReferences (originalAssembly, linkedAssembly);
+
+ linkedMembers = new HashSet<string> (linkedAssembly.MainModule.AllMembers ().Select (s => {
+ return s.FullName;
+ }), StringComparer.Ordinal);
+
+ var membersToAssert = originalAssembly.MainModule.Types;
+ foreach (var originalMember in membersToAssert) {
+ var td = originalMember as TypeDefinition;
+ if (td != null) {
+ if (td.Name == "<Module>") {
+ linkedMembers.Remove (td.Name);
+ continue;
+ }
+
+ TypeDefinition linkedType = linkedAssembly.MainModule.GetType (originalMember.FullName);
+ VerifyTypeDefinition (td, linkedType);
+ linkedMembers.Remove (td.FullName);
+
+ continue;
+ }
+
+ throw new NotImplementedException ($"Don't know how to check member of type {originalMember.GetType ()}");
+ }
+
+ Assert.IsEmpty (linkedMembers, "Linked output includes unexpected member");
+ }
+
+ protected virtual void VerifyModule (ModuleDefinition original, ModuleDefinition linked)
+ {
+ // We never link away a module today so let's make sure the linked one isn't null
+ if (linked == null)
+ Assert.Fail ($"Linked assembly `{original.Assembly.Name.Name}` is missing module `{original.Name}`");
+
+ VerifyCustomAttributes (original, linked);
+ }
+
+ protected virtual void VerifyTypeDefinition (TypeDefinition original, TypeDefinition linked)
+ {
+ if (linked != null && verifiedGeneratedTypes.Contains (linked.FullName))
+ return;
+
+ ModuleDefinition linkedModule = linked?.Module;
+
+ //
+ // Little bit complex check to allow easier test writting to match
+ // - It has [Kept] attribute or any variation of it
+ // - It contains Main method
+ // - It contains at least one member which has [Kept] attribute (not recursive)
+ //
+ bool expectedKept =
+ original.HasAttributeDerivedFrom (nameof (KeptAttribute)) ||
+ (linked != null && linkedModule.Assembly.EntryPoint?.DeclaringType == linked) ||
+ original.AllMembers ().Any (l => l.HasAttribute (nameof (KeptAttribute)));
+
+ if (!expectedKept) {
+ if (linked != null)
+ Assert.Fail ($"Type `{original}' should have been removed");
+
+ return;
+ }
+
+ VerifyTypeDefinitionKept (original, linked);
+ }
+
+ protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDefinition linked)
+ {
+ if (linked == null)
+ Assert.Fail ($"Type `{original}' should have been kept");
+
+ if (!original.IsInterface)
+ VerifyBaseType (original, linked);
+
+ VerifyInterfaces (original, linked);
+ VerifyPseudoAttributes (original, linked);
+ VerifyGenericParameters (original, linked);
+ VerifyCustomAttributes (original, linked);
+ VerifySecurityAttributes (original, linked);
+
+ VerifyFixedBufferFields (original, linked);
+
+ foreach (var td in original.NestedTypes) {
+ VerifyTypeDefinition (td, linked?.NestedTypes.FirstOrDefault (l => td.FullName == l.FullName));
+ linkedMembers.Remove (td.FullName);
+ }
+
+ // Need to check properties before fields so that the KeptBackingFieldAttribute is handled correctly
+ foreach (var p in original.Properties) {
+ VerifyProperty (p, linked?.Properties.FirstOrDefault (l => p.Name == l.Name), linked);
+ linkedMembers.Remove (p.FullName);
+ }
+ // Need to check events before fields so that the KeptBackingFieldAttribute is handled correctly
+ foreach (var e in original.Events) {
+ VerifyEvent (e, linked?.Events.FirstOrDefault (l => e.Name == l.Name), linked);
+ linkedMembers.Remove (e.FullName);
+ }
+
+ // Need to check delegate cache fields before the normal field check
+ VerifyDelegateBackingFields (original, linked);
+
+ foreach (var f in original.Fields) {
+ if (verifiedGeneratedFields.Contains (f.FullName))
+ continue;
+ VerifyField (f, linked?.Fields.FirstOrDefault (l => f.Name == l.Name));
+ linkedMembers.Remove (f.FullName);
+ }
+
+ foreach (var m in original.Methods) {
+ if (verifiedEventMethods.Contains (m.FullName))
+ continue;
+ VerifyMethod (m, linked?.Methods.FirstOrDefault (l => m.GetSignature () == l.GetSignature ()));
+ linkedMembers.Remove (m.FullName);
+ }
+ }
+
+ void VerifyBaseType (TypeDefinition src, TypeDefinition linked)
+ {
+ string expectedBaseName;
+ var expectedBaseGenericAttr = src.CustomAttributes.FirstOrDefault (w => w.AttributeType.Name == nameof (KeptBaseTypeAttribute) && w.ConstructorArguments.Count > 1);
+ if (expectedBaseGenericAttr != null) {
+ StringBuilder builder = new StringBuilder ();
+ builder.Append (expectedBaseGenericAttr.ConstructorArguments [0].Value);
+ builder.Append ("<");
+ bool separator = false;
+ foreach (var caa in (CustomAttributeArgument[])expectedBaseGenericAttr.ConstructorArguments [1].Value) {
+ if (separator)
+ builder.Append (",");
+ else
+ separator = true;
+
+ var arg = (CustomAttributeArgument)caa.Value;
+ builder.Append (arg.Value);
+ }
+
+ builder.Append (">");
+ expectedBaseName = builder.ToString ();
+ } else {
+ var defaultBaseType = src.IsEnum ? "System.Enum" : src.IsValueType ? "System.ValueType" : "System.Object";
+ expectedBaseName = GetCustomAttributeCtorValues<object> (src, nameof (KeptBaseTypeAttribute)).FirstOrDefault ()?.ToString () ?? defaultBaseType;
+ }
+ Assert.AreEqual (expectedBaseName, linked.BaseType?.FullName, $"Incorrect base type on : {linked.Name}");
+ }
+
+ void VerifyInterfaces (TypeDefinition src, TypeDefinition linked)
+ {
+ var expectedInterfaces = new HashSet<string> (GetCustomAttributeCtorValues<object> (src, nameof (KeptInterfaceAttribute)).Select (val => val.ToString ()));
+ if (expectedInterfaces.Count == 0) {
+ Assert.IsFalse (linked.HasInterfaces, $"Type `{src}' has unexpected interfaces");
+ } else {
+ foreach (var iface in linked.Interfaces) {
+ Assert.IsTrue (expectedInterfaces.Remove (iface.InterfaceType.FullName), $"Type `{src}' interface `{iface.InterfaceType.FullName}' should have been removed");
+ }
+
+ Assert.IsEmpty (expectedInterfaces, $"Unexpected interfaces on {src}");
+ }
+ }
+
+ void VerifyField (FieldDefinition src, FieldDefinition linked)
+ {
+ bool expectedKept = ShouldBeKept (src);
+
+ if (!expectedKept) {
+ if (linked != null)
+ Assert.Fail ($"Field `{src}' should have been removed");
+
+ return;
+ }
+
+ VerifyFieldKept (src, linked);
+ }
+
+ void VerifyFieldKept (FieldDefinition src, FieldDefinition linked)
+ {
+ if (linked == null)
+ Assert.Fail ($"Field `{src}' should have been kept");
+
+ Assert.AreEqual (src?.Constant, linked?.Constant, $"Field `{src}' value");
+
+ VerifyPseudoAttributes (src, linked);
+ VerifyCustomAttributes (src, linked);
+ }
+
+ void VerifyProperty (PropertyDefinition src, PropertyDefinition linked, TypeDefinition linkedType)
+ {
+ VerifyMemberBackingField (src, linkedType);
+
+ bool expectedKept = ShouldBeKept (src);
+
+ if (!expectedKept) {
+ if (linked != null)
+ Assert.Fail ($"Property `{src}' should have been removed");
+
+ return;
+ }
+
+ if (linked == null)
+ Assert.Fail ($"Property `{src}' should have been kept");
+
+ Assert.AreEqual (src?.Constant, linked?.Constant, $"Property `{src}' value");
+
+ VerifyPseudoAttributes (src, linked);
+ VerifyCustomAttributes (src, linked);
+ }
+
+ void VerifyEvent (EventDefinition src, EventDefinition linked, TypeDefinition linkedType)
+ {
+ VerifyMemberBackingField (src, linkedType);
+
+ bool expectedKept = ShouldBeKept (src);
+
+ if (!expectedKept) {
+ if (linked != null)
+ Assert.Fail ($"Event `{src}' should have been removed");
+
+ return;
+ }
+
+ if (linked == null)
+ Assert.Fail ($"Event `{src}' should have been kept");
+
+ if (src.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptEventAddMethodAttribute))) {
+ VerifyMethodInternal (src.AddMethod, linked.AddMethod, true);
+ verifiedEventMethods.Add (src.AddMethod.FullName);
+ linkedMembers.Remove (src.AddMethod.FullName);
+ }
+
+ if (src.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptEventRemoveMethodAttribute))) {
+ VerifyMethodInternal (src.RemoveMethod, linked.RemoveMethod, true);
+ verifiedEventMethods.Add (src.RemoveMethod.FullName);
+ linkedMembers.Remove (src.RemoveMethod.FullName);
+ }
+
+ VerifyPseudoAttributes (src, linked);
+ VerifyCustomAttributes (src, linked);
+ }
+
+ void VerifyMethod (MethodDefinition src, MethodDefinition linked)
+ {
+ bool expectedKept = ShouldMethodBeKept (src);
+ VerifyMethodInternal (src, linked, expectedKept);
+ }
+
+
+ void VerifyMethodInternal (MethodDefinition src, MethodDefinition linked, bool expectedKept)
+ {
+ if (!expectedKept) {
+ if (linked != null)
+ Assert.Fail ($"Method `{src.FullName}' should have been removed");
+
+ return;
+ }
+
+ VerifyMethodKept (src, linked);
+ }
+
+ void VerifyMemberBackingField (IMemberDefinition src, TypeDefinition linkedType)
+ {
+ var keptBackingFieldAttribute = src.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.Name == nameof (KeptBackingFieldAttribute));
+ if (keptBackingFieldAttribute == null)
+ return;
+
+ var backingFieldName = src.MetadataToken.TokenType == TokenType.Property
+ ? $"<{src.Name}>k__BackingField" : src.Name;
+ var srcField = src.DeclaringType.Fields.FirstOrDefault (f => f.Name == backingFieldName);
+
+ if (srcField == null) {
+ // Can add more here if necessary
+ backingFieldName = backingFieldName.Replace ("System.Int32", "int");
+ backingFieldName = backingFieldName.Replace ("System.String", "string");
+ backingFieldName = backingFieldName.Replace ("System.Char", "char");
+
+ srcField = src.DeclaringType.Fields.FirstOrDefault (f => f.Name == backingFieldName);
+ }
+
+ if (srcField == null)
+ Assert.Fail ($"{src.MetadataToken.TokenType} `{src}', could not locate the expected backing field {backingFieldName}");
+
+ VerifyFieldKept (srcField, linkedType?.Fields.FirstOrDefault (l => srcField.Name == l.Name));
+ verifiedGeneratedFields.Add (srcField.FullName);
+ linkedMembers.Remove (srcField.FullName);
+ }
+
+ protected virtual void VerifyMethodKept (MethodDefinition src, MethodDefinition linked)
+ {
+ if (linked == null)
+ Assert.Fail ($"Method `{src.FullName}' should have been kept");
+
+ VerifyPseudoAttributes (src, linked);
+ VerifyGenericParameters (src, linked);
+ VerifyCustomAttributes (src, linked);
+ VerifyParameters (src, linked);
+ VerifySecurityAttributes (src, linked);
+ VerifyArrayInitializers (src, linked);
+ VerifyMethodBody (src, linked);
+ }
+
+ protected virtual void VerifyMethodBody (MethodDefinition src, MethodDefinition linked)
+ {
+ if (!src.HasBody)
+ return;
+
+ VerifyExceptionHandlers (src, linked);
+ VerifyInstructions (src, linked);
+ VerifyLocals (src, linked);
+ }
+
+ protected static void VerifyInstructions (MethodDefinition src, MethodDefinition linked)
+ {
+ VerifyBodyProperties (
+ src,
+ linked,
+ nameof (ExpectedInstructionSequenceAttribute),
+ nameof (ExpectBodyModifiedAttribute),
+ "instructions",
+ m => m.Body.Instructions.Select (ins => ins.OpCode.ToString ().ToLower()).ToArray (),
+ attr => GetStringArrayAttributeValue (attr).Select (v => v.ToLower ()).ToArray ());
+ }
+
+ static void VerifyExceptionHandlers (MethodDefinition src, MethodDefinition linked)
+ {
+ VerifyBodyProperties (
+ src,
+ linked,
+ nameof (ExpectedExceptionHandlerSequenceAttribute),
+ nameof (ExpectExceptionHandlersModifiedAttribute),
+ "exception handlers",
+ m => m.Body.ExceptionHandlers.Select (h => h.HandlerType.ToString ().ToLower ()).ToArray (),
+ attr => GetStringArrayAttributeValue (attr).Select (v => v.ToLower ()).ToArray ());
+ }
+
+ static void VerifyLocals (MethodDefinition src, MethodDefinition linked)
+ {
+ VerifyBodyProperties (
+ src,
+ linked,
+ nameof (ExpectedLocalsSequenceAttribute),
+ nameof (ExpectLocalsModifiedAttribute),
+ "locals",
+ m => m.Body.Variables.Select (v => v.VariableType.ToString ()).ToArray (),
+ attr => GetStringOrTypeArrayAttributeValue (attr).ToArray ());
+ }
+
+ protected static void VerifyBodyProperties (MethodDefinition src, MethodDefinition linked, string sequenceAttributeName, string expectModifiedAttributeName,
+ string propertyDescription,
+ Func<MethodDefinition, string []> valueCollector,
+ Func<CustomAttribute, string []> getExpectFromSequenceAttribute)
+ {
+ var expectedSequenceAttribute = src.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.Name == sequenceAttributeName);
+ var linkedValues = valueCollector (linked);
+ var srcValues = valueCollector (src);
+
+ if (src.CustomAttributes.Any (attr => attr.AttributeType.Name == expectModifiedAttributeName)) {
+ Assert.That (
+ linkedValues,
+ Is.Not.EquivalentTo (srcValues),
+ $"Expected method `{src} to have {propertyDescription} modified, however, the {propertyDescription} were the same as the original\n{FormattingUtils.FormatSequenceCompareFailureMessage (linkedValues, srcValues)}");
+ } else if (expectedSequenceAttribute != null) {
+ var expected = getExpectFromSequenceAttribute(expectedSequenceAttribute).ToArray();
+ Assert.That(
+ linkedValues,
+ Is.EquivalentTo (expected),
+ $"Expected method `{src} to have it's {propertyDescription} modified, however, the sequence of {propertyDescription} does not match the expected value\n{FormattingUtils.FormatSequenceCompareFailureMessage2 (linkedValues, expected, srcValues)}");
+ } else {
+ Assert.That(
+ linkedValues,
+ Is.EquivalentTo (srcValues),
+ $"Expected method `{src} to have it's {propertyDescription} unchanged, however, the {propertyDescription} differ from the original\n{FormattingUtils.FormatSequenceCompareFailureMessage (linkedValues, srcValues)}");
+ }
+ }
+
+ void VerifyReferences (AssemblyDefinition original, AssemblyDefinition linked)
+ {
+ var expected = original.MainModule.AllDefinedTypes ()
+ .SelectMany (t => GetCustomAttributeCtorValues<string> (t, nameof (KeptReferenceAttribute)))
+ .Select (ReduceAssemblyFileNameOrNameToNameOnly)
+ .ToArray ();
+
+ /*
+ - The test case will always need to have at least 1 reference.
+ - Forcing all tests to define their expected references seems tedious
+
+ Given the above, let's assume that when no [KeptReference] attributes are present,
+ the test case does not want to make any assertions regarding references.
+
+ Once 1 kept reference attribute is used, the test will need to define all of of it's expected references
+ */
+ if (expected.Length == 0)
+ return;
+
+ var actual = linked.MainModule.AssemblyReferences
+ .Select (name => name.Name)
+ .ToArray ();
+
+ Assert.That (actual, Is.EquivalentTo (expected));
+ }
+
+ string ReduceAssemblyFileNameOrNameToNameOnly (string fileNameOrAssemblyName)
+ {
+ if (fileNameOrAssemblyName.EndsWith (".dll") || fileNameOrAssemblyName.EndsWith (".exe") || fileNameOrAssemblyName.EndsWith (".winmd"))
+ return System.IO.Path.GetFileNameWithoutExtension (fileNameOrAssemblyName);
+
+ // It must already be just the assembly name
+ return fileNameOrAssemblyName;
+ }
+
+ void VerifyResources (AssemblyDefinition original, AssemblyDefinition linked)
+ {
+ var expectedResources = original.MainModule.AllDefinedTypes ()
+ .SelectMany (t => GetCustomAttributeCtorValues<string> (t, nameof (KeptResourceAttribute)));
+
+ Assert.That (linked.MainModule.Resources.Select (r => r.Name), Is.EquivalentTo (expectedResources));
+ }
+
+ protected virtual void VerifyPseudoAttributes (MethodDefinition src, MethodDefinition linked)
+ {
+ var expected = (MethodAttributes) GetExpectedPseudoAttributeValue(src, (uint) src.Attributes);
+ Assert.AreEqual (expected, linked.Attributes, $"Method `{src}' pseudo attributes did not match expected");
+ }
+
+ protected virtual void VerifyPseudoAttributes (TypeDefinition src, TypeDefinition linked)
+ {
+ var expected = (TypeAttributes) GetExpectedPseudoAttributeValue(src, (uint) src.Attributes);
+ Assert.AreEqual (expected, linked.Attributes, $"Type `{src}' pseudo attributes did not match expected");
+ }
+
+ protected virtual void VerifyPseudoAttributes (FieldDefinition src, FieldDefinition linked)
+ {
+ var expected = (FieldAttributes) GetExpectedPseudoAttributeValue(src, (uint) src.Attributes);
+ Assert.AreEqual (expected, linked.Attributes, $"Field `{src}' pseudo attributes did not match expected");
+ }
+
+ protected virtual void VerifyPseudoAttributes (PropertyDefinition src, PropertyDefinition linked)
+ {
+ var expected = (PropertyAttributes) GetExpectedPseudoAttributeValue(src, (uint) src.Attributes);
+ Assert.AreEqual (expected, linked.Attributes, $"Property `{src}' pseudo attributes did not match expected");
+ }
+
+ protected virtual void VerifyPseudoAttributes (EventDefinition src, EventDefinition linked)
+ {
+ var expected = (EventAttributes) GetExpectedPseudoAttributeValue(src, (uint) src.Attributes);
+ Assert.AreEqual (expected, linked.Attributes, $"Event `{src}' pseudo attributes did not match expected");
+ }
+
+ protected virtual void VerifyCustomAttributes (ICustomAttributeProvider src, ICustomAttributeProvider linked)
+ {
+ var expectedAttrs = GetExpectedAttributes (src).ToList ();
+ var linkedAttrs = FilterLinkedAttributes (linked).ToList ();
+
+ Assert.That (linkedAttrs, Is.EquivalentTo (expectedAttrs), $"Custom attributes on `{src}' are not matching");
+ }
+
+ protected virtual void VerifySecurityAttributes (ICustomAttributeProvider src, ISecurityDeclarationProvider linked)
+ {
+ var expectedAttrs = GetCustomAttributeCtorValues<object> (src, nameof (KeptSecurityAttribute))
+ .Select (attr => attr.ToString ())
+ .ToList ();
+
+ var linkedAttrs = FilterLinkedSecurityAttributes (linked).ToList ();
+
+ Assert.That (linkedAttrs, Is.EquivalentTo (expectedAttrs), $"Security attributes on `{src}' are not matching");
+ }
+
+ protected virtual void VerifyArrayInitializers (MethodDefinition src, MethodDefinition linked)
+ {
+ var expectedIndicies = GetCustomAttributeCtorValues<object> (src, nameof (KeptInitializerData))
+ .Cast<int> ()
+ .ToArray ();
+
+ var expectKeptAll = src.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptInitializerData) && !attr.HasConstructorArguments);
+
+ if (expectedIndicies.Length == 0 && !expectKeptAll)
+ return;
+
+ if (!src.HasBody)
+ Assert.Fail ($"`{nameof (KeptInitializerData)}` cannot be used on methods that don't have bodies");
+
+ var srcImplementationDetails = src.Module.Types.FirstOrDefault (t => string.IsNullOrEmpty (t.Namespace) && t.Name.StartsWith ("<PrivateImplementationDetails>"));
+
+ if (srcImplementationDetails == null)
+ Assert.Fail ("Could not locate <PrivateImplementationDetails> in the original assembly. Does your test use initializers?");
+
+ var linkedImplementationDetails = linked.Module.Types.FirstOrDefault (t => string.IsNullOrEmpty (t.Namespace) && t.Name.StartsWith ("<PrivateImplementationDetails>"));
+
+ if (linkedImplementationDetails == null)
+ Assert.Fail ("Could not locate <PrivateImplementationDetails> in the linked assembly");
+
+ var possibleInitializerFields = src.Body.Instructions
+ .Where (ins => IsLdtokenOnPrivateImplementationDetails (srcImplementationDetails, ins))
+ .Select (ins => ((FieldReference)ins.Operand).Resolve ())
+ .ToArray ();
+
+ if (possibleInitializerFields.Length == 0)
+ Assert.Fail ($"`{src}` does not make use of any initializers");
+
+ if (expectKeptAll) {
+ foreach (var srcField in possibleInitializerFields) {
+ var linkedField = linkedImplementationDetails.Fields.FirstOrDefault (f => f.Name == srcField.Name);
+ VerifyInitializerField (srcField, linkedField);
+ }
+ } else {
+ foreach (var index in expectedIndicies) {
+ if (index < 0 || index > possibleInitializerFields.Length)
+ Assert.Fail($"Invalid expected index `{index}` in {src}. Value must be between 0 and {expectedIndicies.Length}");
+
+ var srcField = possibleInitializerFields[index];
+ var linkedField = linkedImplementationDetails.Fields.FirstOrDefault (f => f.Name == srcField.Name);
+
+ VerifyInitializerField (srcField, linkedField);
+ }
+ }
+ }
+
+ void VerifyInitializerField (FieldDefinition src, FieldDefinition linked)
+ {
+ VerifyFieldKept (src, linked);
+ verifiedGeneratedFields.Add (linked.FullName);
+ linkedMembers.Remove (linked.FullName);
+ VerifyTypeDefinitionKept (src.FieldType.Resolve (), linked.FieldType.Resolve ());
+ linkedMembers.Remove (linked.FieldType.FullName);
+ linkedMembers.Remove (linked.DeclaringType.FullName);
+ verifiedGeneratedTypes.Add (linked.DeclaringType.FullName);
+ }
+
+ static bool IsLdtokenOnPrivateImplementationDetails (TypeDefinition privateImplementationDetails, Instruction instruction)
+ {
+ if (instruction.OpCode.Code == Code.Ldtoken && instruction.Operand is FieldReference field)
+ {
+ return field.DeclaringType.Resolve () == privateImplementationDetails;
+ }
+
+ return false;
+ }
+
+ protected static IEnumerable<string> GetExpectedAttributes (ICustomAttributeProvider original)
+ {
+ foreach (var expectedAttrs in GetCustomAttributeCtorValues<object> (original, nameof (KeptAttributeAttribute)))
+ yield return expectedAttrs.ToString ();
+
+ // The name of the generated fixed buffer type is a little tricky.
+ // Some versions of csc name it `<fieldname>e__FixedBuffer0`
+ // while mcs and other versions of csc name it `<fieldname>__FixedBuffer0`
+ if (original is TypeDefinition srcDefinition && srcDefinition.Name.Contains ("__FixedBuffer")) {
+ var name = srcDefinition.Name.Substring (1, srcDefinition.Name.IndexOf('>') - 1);
+ var fixedField = srcDefinition.DeclaringType.Fields.FirstOrDefault (f => f.Name == name);
+ if (fixedField == null)
+ Assert.Fail ($"Could not locate original fixed field for {srcDefinition}");
+
+ foreach (var additionalExpectedAttributesFromFixedField in GetCustomAttributeCtorValues<object> (fixedField, nameof (KeptAttributeOnFixedBufferTypeAttribute)))
+ yield return additionalExpectedAttributesFromFixedField.ToString ();
+
+ }
+ }
+
+ /// <summary>
+ /// Filters out some attributes that should not be taken into consideration when checking the linked result against the expected result
+ /// </summary>
+ /// <param name="linked"></param>
+ /// <returns></returns>
+ protected virtual IEnumerable<string> FilterLinkedAttributes (ICustomAttributeProvider linked)
+ {
+ foreach (var attr in linked.CustomAttributes) {
+ switch (attr.AttributeType.FullName) {
+ case "System.Runtime.CompilerServices.RuntimeCompatibilityAttribute":
+ case "System.Runtime.CompilerServices.CompilerGeneratedAttribute":
+ continue;
+
+ // When mcs is used to compile the test cases, backing fields end up with this attribute on them
+ case "System.Diagnostics.DebuggerBrowsableAttribute":
+ continue;
+
+ case "System.Runtime.CompilerServices.CompilationRelaxationsAttribute":
+ if (linked is AssemblyDefinition)
+ continue;
+ break;
+ }
+
+ yield return attr.AttributeType.FullName;
+ }
+ }
+
+ protected virtual IEnumerable<string> FilterLinkedSecurityAttributes (ISecurityDeclarationProvider linked)
+ {
+ return linked.SecurityDeclarations
+ .SelectMany (d => d.SecurityAttributes)
+ .Select (attr => attr.AttributeType.ToString ());
+ }
+
+ void VerifyFixedBufferFields (TypeDefinition src, TypeDefinition linked)
+ {
+ var fields = src.Fields.Where (f => f.CustomAttributes.Any (attr => attr.AttributeType.Name == nameof (KeptFixedBufferAttribute)));
+
+ foreach (var field in fields) {
+ // The name of the generated fixed buffer type is a little tricky.
+ // Some versions of csc name it `<fieldname>e__FixedBuffer0`
+ // while mcs and other versions of csc name it `<fieldname>__FixedBuffer0`
+ var originalCompilerGeneratedBufferType = src.NestedTypes.FirstOrDefault (t => t.FullName.Contains ($"<{field.Name}>") && t.FullName.Contains ("__FixedBuffer"));
+ if (originalCompilerGeneratedBufferType == null)
+ Assert.Fail ($"Could not locate original compiler generated fixed buffer type for field {field}");
+
+ var linkedCompilerGeneratedBufferType = linked.NestedTypes.FirstOrDefault (t => t.Name == originalCompilerGeneratedBufferType.Name);
+ if (linkedCompilerGeneratedBufferType == null)
+ Assert.Fail ($"Missing expected type {originalCompilerGeneratedBufferType}");
+
+ // Have to verify the field before the type
+ var originalElementField = originalCompilerGeneratedBufferType.Fields.FirstOrDefault (f => f.Name == "FixedElementField");
+ if (originalElementField == null)
+ Assert.Fail ($"Could not locate original compiler generated FixedElementField on {originalCompilerGeneratedBufferType}");
+
+ var linkedField = linkedCompilerGeneratedBufferType?.Fields.FirstOrDefault (l => l.Name == originalElementField.Name);
+ VerifyFieldKept (originalElementField, linkedField);
+ verifiedGeneratedFields.Add (originalElementField.FullName);
+ linkedMembers.Remove (originalElementField.FullName);
+
+ VerifyTypeDefinitionKept(originalCompilerGeneratedBufferType, linkedCompilerGeneratedBufferType);
+ verifiedGeneratedTypes.Add(originalCompilerGeneratedBufferType.FullName);
+ }
+ }
+
+ void VerifyDelegateBackingFields (TypeDefinition src, TypeDefinition linked)
+ {
+ var expectedFieldNames = GetCustomAttributeCtorValues<string> (src, nameof (KeptDelegateCacheFieldAttribute))
+ .Select (unique => $"<>f__mg$cache{unique}")
+ .ToList ();
+
+ if (expectedFieldNames.Count == 0)
+ return;
+
+ foreach (var srcField in src.Fields) {
+ if (!expectedFieldNames.Contains (srcField.Name))
+ continue;
+
+ var linkedField = linked?.Fields.FirstOrDefault (l => l.Name == srcField.Name);
+ VerifyFieldKept (srcField, linkedField);
+ verifiedGeneratedFields.Add (srcField.FullName);
+ linkedMembers.Remove (srcField.FullName);
+ }
+ }
+
+ void VerifyGenericParameters (IGenericParameterProvider src, IGenericParameterProvider linked)
+ {
+ Assert.AreEqual (src.HasGenericParameters, linked.HasGenericParameters);
+ if (src.HasGenericParameters) {
+ for (int i = 0; i < src.GenericParameters.Count; ++i) {
+ // TODO: Verify constraints
+ VerifyCustomAttributes (src.GenericParameters [i], linked.GenericParameters [i]);
+ }
+ }
+ }
+
+ void VerifyParameters (IMethodSignature src, IMethodSignature linked)
+ {
+ Assert.AreEqual (src.HasParameters, linked.HasParameters);
+ if (src.HasParameters) {
+ for (int i = 0; i < src.Parameters.Count; ++i) {
+ VerifyCustomAttributes (src.Parameters [i], linked.Parameters [i]);
+ }
+ }
+ }
+
+ protected virtual bool ShouldMethodBeKept (MethodDefinition method)
+ {
+ var srcSignature = method.GetSignature ();
+ return ShouldBeKept (method, srcSignature) || method.DeclaringType.Module.EntryPoint == method;
+ }
+
+ protected virtual bool ShouldBeKept<T> (T member, string signature = null) where T : MemberReference, ICustomAttributeProvider
+ {
+ if (member.HasAttribute (nameof (KeptAttribute)))
+ return true;
+
+ ICustomAttributeProvider cap = (ICustomAttributeProvider)member.DeclaringType;
+ if (cap == null)
+ return false;
+
+ return GetCustomAttributeCtorValues<string> (cap, nameof (KeptMemberAttribute)).Any (a => a == (signature ?? member.Name));
+ }
+
+ protected static uint GetExpectedPseudoAttributeValue (ICustomAttributeProvider provider, uint sourceValue)
+ {
+ var removals = provider.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (RemovedPseudoAttributeAttribute)).ToArray ();
+ var adds = provider.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (AddedPseudoAttributeAttribute)).ToArray ();
+
+ return removals.Aggregate (sourceValue, (accum, item) => accum & ~((uint) item.ConstructorArguments [0].Value)) |
+ adds.Aggregate ((uint)0, (acum, item) => acum | (uint) item.ConstructorArguments [0].Value);
+ }
+
+ protected static IEnumerable<T> GetCustomAttributeCtorValues<T> (ICustomAttributeProvider provider, string attributeName) where T : class
+ {
+ return provider.CustomAttributes.
+ Where (w => w.AttributeType.Name == attributeName && w.Constructor.Parameters.Count == 1).
+ Select (l => l.ConstructorArguments [0].Value as T);
+ }
+
+ protected static IEnumerable<string> GetStringOrTypeArrayAttributeValue (CustomAttribute attribute)
+ {
+ foreach (var arg in ((CustomAttributeArgument[]) attribute.ConstructorArguments [0].Value)) {
+ if (arg.Value is TypeReference tRef)
+ yield return tRef.ToString ();
+ else
+ yield return (string) arg.Value;
+ }
+ }
+
+ protected static IEnumerable<string> GetStringArrayAttributeValue (CustomAttribute attribute)
+ {
+ return ((CustomAttributeArgument[]) attribute.ConstructorArguments [0].Value)?.Select (arg => arg.Value.ToString ());
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/CompilerOptions.cs b/test/Mono.Linker.Tests/TestCasesRunner/CompilerOptions.cs
new file mode 100644
index 000000000..7e584e904
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/CompilerOptions.cs
@@ -0,0 +1,14 @@
+using System;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class CompilerOptions {
+ public NPath OutputPath;
+ public NPath[] SourceFiles;
+ public string[] Defines;
+ public NPath[] References;
+ public NPath[] Resources;
+ public string[] AdditionalArguments;
+ public string CompilerToUse;
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ExpectationsProvider.cs b/test/Mono.Linker.Tests/TestCasesRunner/ExpectationsProvider.cs
new file mode 100644
index 000000000..8b3baf0e3
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/ExpectationsProvider.cs
@@ -0,0 +1,17 @@
+using Mono.Cecil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public static class ExpectationsProvider {
+
+ public static bool IsAssemblyAssertion (CustomAttribute attr)
+ {
+ return attr.AttributeType.Name == nameof (KeptAssemblyAttribute) || attr.AttributeType.Name == nameof (RemovedAssemblyAttribute);
+ }
+
+ public static bool IsSymbolAssertion (CustomAttribute attr)
+ {
+ return attr.AttributeType.Name == nameof (KeptSymbolsAttribute) || attr.AttributeType.Name == nameof (RemovedSymbolsAttribute);
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/FormattingUtils.cs b/test/Mono.Linker.Tests/TestCasesRunner/FormattingUtils.cs
new file mode 100644
index 000000000..97c14abb2
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/FormattingUtils.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public static class FormattingUtils {
+ public static string FormatSequenceCompareFailureMessage (IEnumerable<string> actual, IEnumerable<string> expected)
+ {
+ var builder = new StringBuilder ();
+ builder.AppendLine ("---------------");
+ builder.AppendLine ($"Expected/Original (Total : {expected.Count ()})");
+ builder.AppendLine ("---------------");
+ // Format in a quoted array form for easier copying into a expected sequence attribute
+ builder.AppendLine (expected.Select (c => $"\"{c}\",").AggregateWithNewLine ());
+ builder.AppendLine ("---------------");
+ builder.AppendLine ($"Actual/Linked (Total : {actual.Count ()})");
+ builder.AppendLine ("---------------");
+ // Format in a quoted array form for easier copying into a expected sequence attribute
+ builder.AppendLine (actual.Select(c => $"\"{c}\",").AggregateWithNewLine ());
+ builder.AppendLine ("---------------");
+ return builder.ToString ();
+ }
+
+ public static string FormatSequenceCompareFailureMessage2 (IEnumerable<string> actual, IEnumerable<string> expected, IEnumerable<string> original)
+ {
+ var builder = new StringBuilder ();
+ builder.AppendLine ("---------------");
+ builder.AppendLine ($"Expected (Total : {expected.Count ()})");
+ builder.AppendLine ("---------------");
+ // Format in a quoted array form for easier copying into a expected sequence attribute
+ builder.AppendLine (expected.Select(c => $"\"{c}\",").AggregateWithNewLine ());
+ builder.AppendLine ("---------------");
+ builder.AppendLine ($"Actual/Linked (Total : {actual.Count ()})");
+ builder.AppendLine ("---------------");
+ // Format in a quoted array form for easier copying into a expected sequence attribute
+ builder.AppendLine (actual.Select(c => $"\"{c}\",").AggregateWithNewLine ());
+ builder.AppendLine ("---------------");
+ builder.AppendLine ($"Original (Total : {original.Count()})");
+ builder.AppendLine ("---------------");
+ // Format in a quoted array form for easier copying into a expected sequence attribute
+ builder.AppendLine (original.Select(c => $"\"{c}\",").AggregateWithNewLine ());
+ builder.AppendLine ("---------------");
+ return builder.ToString ();
+ }
+
+ private static string AggregateWithNewLine (this IEnumerable<string> elements)
+ {
+ return elements.AggregateWith (System.Environment.NewLine);
+ }
+
+ private static string AggregateWith (this IEnumerable<string> elements, string separator)
+ {
+ if (elements.Any ())
+ return elements.Aggregate ((buff, s) => buff + separator + s);
+
+ return string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ILCompiler.cs b/test/Mono.Linker.Tests/TestCasesRunner/ILCompiler.cs
new file mode 100644
index 000000000..517bcb273
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/ILCompiler.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Mono.Linker.Tests.Extensions;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class ILCompiler {
+ private readonly string _ilasmExecutable;
+
+ public ILCompiler ()
+ {
+ _ilasmExecutable = Environment.OSVersion.Platform == PlatformID.Win32NT ? LocateIlasmOnWindows ().ToString () : "ilasm";
+ }
+
+ public ILCompiler (string ilasmExecutable)
+ {
+ _ilasmExecutable = ilasmExecutable;
+ }
+
+ public NPath Compile (CompilerOptions options)
+ {
+ var capturedOutput = new List<string> ();
+ var process = new Process ();
+ SetupProcess (process, options);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.OutputDataReceived += (sender, args) => capturedOutput.Add (args.Data);
+ process.Start ();
+ process.BeginOutputReadLine ();
+ process.WaitForExit ();
+
+ if (process.ExitCode != 0)
+ {
+ Assert.Fail($"Failed to compile IL assembly : {options.OutputPath}\n{capturedOutput.Aggregate ((buff, s) => buff + Environment.NewLine + s)}");
+ }
+
+ return options.OutputPath;
+ }
+
+ protected virtual void SetupProcess (Process process, CompilerOptions options)
+ {
+ process.StartInfo.FileName = _ilasmExecutable;
+ process.StartInfo.Arguments = BuildArguments (options);
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ }
+
+ private string BuildArguments (CompilerOptions options)
+ {
+ var args = new StringBuilder();
+ args.Append(options.OutputPath.ExtensionWithDot == ".dll" ? "/dll" : "/exe");
+ args.Append($" /out:{options.OutputPath.InQuotes ()}");
+ args.Append($" {options.SourceFiles.Aggregate (string.Empty, (buff, file) => $"{buff} {file.InQuotes ()}")}");
+ return args.ToString ();
+ }
+
+ public static NPath LocateIlasmOnWindows ()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ throw new InvalidOperationException ("This method should only be called on windows");
+
+ var possiblePath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory ().ToNPath ().Combine ("ilasm.exe");
+ if (possiblePath.FileExists ())
+ return possiblePath;
+
+ throw new InvalidOperationException ("Could not locate a ilasm.exe executable");
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/LinkXmlHelpers.cs b/test/Mono.Linker.Tests/TestCasesRunner/LinkXmlHelpers.cs
new file mode 100644
index 000000000..c7e70cf71
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/LinkXmlHelpers.cs
@@ -0,0 +1,30 @@
+using System.Text;
+using Mono.Cecil;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public static class LinkXmlHelpers {
+ public static void WriteXmlFileToPreserveEntryPoint (NPath targetProgram, NPath xmlFile)
+ {
+ using (var assembly = AssemblyDefinition.ReadAssembly (targetProgram.ToString ())) {
+ var method = assembly.EntryPoint;
+
+ var sb = new StringBuilder ();
+ sb.AppendLine ("<linker>");
+
+ sb.AppendLine (" <assembly fullname=\"" + assembly.FullName + "\">");
+
+ if (method != null) {
+ sb.AppendLine (" <type fullname=\"" + method.DeclaringType.FullName + "\">");
+ sb.AppendLine (" <method name=\"" + method.Name + "\"/>");
+ sb.AppendLine (" </type>");
+ }
+
+ sb.AppendLine (" </assembly>");
+
+ sb.AppendLine ("</linker>");
+ xmlFile.WriteAllText (sb.ToString ());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/LinkedTestCaseResult.cs b/test/Mono.Linker.Tests/TestCasesRunner/LinkedTestCaseResult.cs
new file mode 100644
index 000000000..2d05f6d09
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/LinkedTestCaseResult.cs
@@ -0,0 +1,19 @@
+using Mono.Linker.Tests.Extensions;
+using Mono.Linker.Tests.TestCases;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class LinkedTestCaseResult {
+ public readonly TestCase TestCase;
+ public readonly NPath InputAssemblyPath;
+ public readonly NPath OutputAssemblyPath;
+ public readonly NPath ExpectationsAssemblyPath;
+
+ public LinkedTestCaseResult (TestCase testCase, NPath inputAssemblyPath, NPath outputAssemblyPath, NPath expectationsAssemblyPath)
+ {
+ TestCase = testCase;
+ InputAssemblyPath = inputAssemblyPath;
+ OutputAssemblyPath = outputAssemblyPath;
+ ExpectationsAssemblyPath = expectationsAssemblyPath;
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs b/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs
new file mode 100644
index 000000000..98f93d782
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs
@@ -0,0 +1,180 @@
+using System.Collections.Generic;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class LinkerArgumentBuilder {
+ private readonly List<string> _arguments = new List<string> ();
+ private readonly TestCaseMetadaProvider _metadaProvider;
+
+ public LinkerArgumentBuilder (TestCaseMetadaProvider metadaProvider)
+ {
+ _metadaProvider = metadaProvider;
+ }
+
+ public virtual void AddSearchDirectory (NPath directory)
+ {
+ Append ("-d");
+ Append (directory.ToString ());
+ }
+
+ public virtual void AddOutputDirectory (NPath directory)
+ {
+ Append ("-o");
+ Append (directory.ToString ());
+ }
+
+ public virtual void AddLinkXmlFile (NPath path)
+ {
+ Append ("-x");
+ Append (path.ToString ());
+ }
+
+ public virtual void AddResponseFile (NPath path)
+ {
+ Append ($"@{path}");
+ }
+
+ public virtual void AddCoreLink (string value)
+ {
+ Append ("-c");
+ Append (value);
+ }
+
+ public virtual void AddUserLink (string value)
+ {
+ Append ("-u");
+ Append (value);
+ }
+
+ public virtual void LinkFromAssembly (string fileName)
+ {
+ Append ("-a");
+ Append (fileName);
+ }
+
+ public virtual void LinkFromPublicAndFamily (string fileName)
+ {
+ Append ("-r");
+ Append (fileName);
+ }
+
+ public virtual void IncludeBlacklist (bool value)
+ {
+ Append ("-z");
+ Append (value ? "true" : "false");
+ }
+
+ public virtual void AddIl8n (string value)
+ {
+ Append ("-l");
+ Append (value);
+ }
+
+ public virtual void AddKeepTypeForwarderOnlyAssemblies (string value)
+ {
+ if (bool.Parse (value))
+ Append ("-t");
+ }
+
+ public virtual void AddLinkSymbols (string value)
+ {
+ Append ("-b");
+ Append (value);
+ }
+
+ public virtual void AddKeepDebugMembers (string value)
+ {
+ Append ("-v");
+ Append (value);
+ }
+
+ public virtual void AddAssemblyAction (string action, string assembly)
+ {
+ Append ("-p");
+ Append (action);
+ Append (assembly);
+ }
+
+ public virtual void AddSkipUnresolved (bool skipUnresolved)
+ {
+ if (skipUnresolved) {
+ Append ("--skip-unresolved");
+ Append ("true");
+ }
+ }
+
+ public virtual void AddStripResources (bool stripResources)
+ {
+ if (!stripResources) {
+ Append ("--strip-resources");
+ Append ("false");
+ }
+ }
+
+ public string [] ToArgs ()
+ {
+ return _arguments.ToArray ();
+ }
+
+ protected void Append (string arg)
+ {
+ _arguments.Add (arg);
+ }
+
+ public virtual void AddAdditionalArgument (string flag, string [] values)
+ {
+ Append (flag);
+ if (values != null) {
+ foreach (var val in values)
+ Append (val);
+ }
+ }
+
+ public virtual void ProcessTestInputAssembly (NPath inputAssemblyPath)
+ {
+ if (_metadaProvider.LinkPublicAndFamily ())
+ LinkFromPublicAndFamily (inputAssemblyPath.ToString ());
+ else
+ LinkFromAssembly (inputAssemblyPath.ToString ());
+ }
+
+ public virtual void ProcessOptions (TestCaseLinkerOptions options)
+ {
+ if (options.CoreAssembliesAction != null)
+ AddCoreLink (options.CoreAssembliesAction);
+
+ if (options.UserAssembliesAction != null)
+ AddUserLink (options.UserAssembliesAction);
+
+ if (options.AssembliesAction != null) {
+ foreach (var entry in options.AssembliesAction)
+ AddAssemblyAction (entry.Key, entry.Value);
+ }
+
+ // Running the blacklist step causes a ton of stuff to be preserved. That's good for normal use cases, but for
+ // our test cases that pollutes the results
+ IncludeBlacklist (options.IncludeBlacklistStep);
+
+ if (!string.IsNullOrEmpty (options.Il8n))
+ AddIl8n (options.Il8n);
+
+ if (!string.IsNullOrEmpty (options.KeepTypeForwarderOnlyAssemblies))
+ AddKeepTypeForwarderOnlyAssemblies (options.KeepTypeForwarderOnlyAssemblies);
+
+ if (!string.IsNullOrEmpty (options.LinkSymbols))
+ AddLinkSymbols (options.LinkSymbols);
+
+ if (!string.IsNullOrEmpty (options.KeepDebugMembers))
+ AddKeepDebugMembers (options.KeepDebugMembers);
+
+ AddSkipUnresolved (options.SkipUnresolved);
+
+ AddStripResources (options.StripResources);
+
+ // Unity uses different argument format and needs to be able to translate to their format. In order to make that easier
+ // we keep the information in flag + values format for as long as we can so that this information doesn't have to be parsed out of a single string
+ foreach (var additionalArgument in options.AdditionalArguments)
+ AddAdditionalArgument (additionalArgument.Key, additionalArgument.Value);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/LinkerDriver.cs b/test/Mono.Linker.Tests/TestCasesRunner/LinkerDriver.cs
new file mode 100644
index 000000000..f76898873
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/LinkerDriver.cs
@@ -0,0 +1,8 @@
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class LinkerDriver {
+ public virtual void Link (string [] args)
+ {
+ new Driver (args).Run ();
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ManagedCompilationResult.cs b/test/Mono.Linker.Tests/TestCasesRunner/ManagedCompilationResult.cs
new file mode 100644
index 000000000..ccce993f9
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/ManagedCompilationResult.cs
@@ -0,0 +1,15 @@
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class ManagedCompilationResult {
+ public ManagedCompilationResult (NPath inputAssemblyPath, NPath expectationsAssemblyPath)
+ {
+ InputAssemblyPath = inputAssemblyPath;
+ ExpectationsAssemblyPath = expectationsAssemblyPath;
+ }
+
+ public NPath InputAssemblyPath { get; }
+
+ public NPath ExpectationsAssemblyPath { get; }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ObjectFactory.cs b/test/Mono.Linker.Tests/TestCasesRunner/ObjectFactory.cs
new file mode 100644
index 000000000..71d1c3859
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/ObjectFactory.cs
@@ -0,0 +1,31 @@
+using Mono.Cecil;
+using Mono.Linker.Tests.TestCases;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class ObjectFactory {
+ public virtual TestCaseSandbox CreateSandbox (TestCase testCase)
+ {
+ return new TestCaseSandbox (testCase);
+ }
+
+ public virtual TestCaseCompiler CreateCompiler (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider)
+ {
+ return new TestCaseCompiler (sandbox, metadataProvider);
+ }
+
+ public virtual LinkerDriver CreateLinker ()
+ {
+ return new LinkerDriver ();
+ }
+
+ public virtual TestCaseMetadaProvider CreateMetadataProvider (TestCase testCase, AssemblyDefinition fullTestCaseAssemblyDefinition)
+ {
+ return new TestCaseMetadaProvider (testCase, fullTestCaseAssemblyDefinition);
+ }
+
+ public virtual LinkerArgumentBuilder CreateLinkerArgumentBuilder (TestCaseMetadaProvider metadataProvider)
+ {
+ return new LinkerArgumentBuilder (metadataProvider);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/PeVerifier.cs b/test/Mono.Linker.Tests/TestCasesRunner/PeVerifier.cs
new file mode 100644
index 000000000..dd3d5e37c
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/PeVerifier.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.Win32;
+using Mono.Cecil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Extensions;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class PeVerifier {
+ private readonly string _peExecutable;
+
+ public PeVerifier ()
+ {
+ _peExecutable = Environment.OSVersion.Platform == PlatformID.Win32NT ? FindPeExecutableFromRegistry ().ToString () : "pedump";
+ }
+
+ public PeVerifier (string peExecutable)
+ {
+ _peExecutable = peExecutable;
+ }
+
+ public virtual void Check (LinkedTestCaseResult linkResult, AssemblyDefinition original)
+ {
+ bool skipCheckEntirely;
+ HashSet<string> assembliesToSkip;
+ ProcessSkipAttributes (linkResult, original, out skipCheckEntirely, out assembliesToSkip);
+
+ if (skipCheckEntirely)
+ return;
+
+ foreach (var file in linkResult.OutputAssemblyPath.Parent.Files ()) {
+ if (file.ExtensionWithDot != ".exe" && file.ExtensionWithDot != ".dll")
+ continue;
+
+ // Always skip the I18N assemblies, for some reason they end up in the output directory on OSX.
+ // verification of these fails due to native pointers
+ if (file.FileName.StartsWith ("I18N"))
+ continue;
+
+ if (assembliesToSkip.Contains (file.FileName))
+ continue;
+
+ CheckAssembly (file);
+ }
+ }
+
+ private void ProcessSkipAttributes (LinkedTestCaseResult linkResult, AssemblyDefinition original, out bool skipCheckEntirely, out HashSet<string> assembliesToSkip)
+ {
+ var peVerifyAttrs = original.MainModule.GetType (linkResult.TestCase.ReconstructedFullTypeName).CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (SkipPeVerifyAttribute));
+ skipCheckEntirely = false;
+ assembliesToSkip = new HashSet<string> ();
+ foreach (var attr in peVerifyAttrs) {
+ var ctorArg = attr.ConstructorArguments.FirstOrDefault ();
+
+ if (!attr.HasConstructorArguments) {
+ skipCheckEntirely = true;
+ } else if (ctorArg.Type.Name == nameof (SkipPeVerifyForToolchian)) {
+ var skipToolchain = (SkipPeVerifyForToolchian)ctorArg.Value;
+
+ if (skipToolchain == SkipPeVerifyForToolchian.Pedump) {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ skipCheckEntirely = true;
+ }
+ else
+ throw new ArgumentException ($"Unhandled platform and toolchain values of {Environment.OSVersion.Platform} and {skipToolchain}");
+ } else if (ctorArg.Type.Name == nameof (String)) {
+ assembliesToSkip.Add ((string)ctorArg.Value);
+ } else {
+ throw new ArgumentException ($"Unhandled constructor argument type of {ctorArg.Type} on {nameof (SkipPeVerifyAttribute)}");
+ }
+ }
+ }
+
+ private void CheckAssembly (NPath assemblyPath)
+ {
+ var capturedOutput = new List<string> ();
+ var process = new Process ();
+ SetupProcess (process, assemblyPath);
+ process.StartInfo.RedirectStandardOutput = true;
+ process.OutputDataReceived += (sender, args) => capturedOutput.Add (args.Data);
+ process.Start ();
+ process.BeginOutputReadLine ();
+ process.WaitForExit ();
+
+ if (process.ExitCode != 0) {
+ Assert.Fail ($"Invalid IL detected in {assemblyPath}\n{capturedOutput.Aggregate ((buff, s) => buff + Environment.NewLine + s)}");
+ }
+ }
+
+ protected virtual void SetupProcess (Process process, NPath assemblyPath)
+ {
+ var exeArgs = Environment.OSVersion.Platform == PlatformID.Win32NT ? $"/nologo {assemblyPath.InQuotes ()}" : $"--verify metadata,code {assemblyPath.InQuotes ()}";
+ process.StartInfo.FileName = _peExecutable;
+ process.StartInfo.Arguments = exeArgs;
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
+
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT) {
+ process.StartInfo.Environment ["MONO_PATH"] = assemblyPath.Parent.ToString ();
+ }
+ }
+
+ public static NPath FindPeExecutableFromRegistry ()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ throw new InvalidOperationException ("This method should only be called on windows");
+
+ NPath result;
+ if (TryFindPeExecutableFromRegustrySubfolder ("NETFXSDK", out result))
+ return result;
+ if (TryFindPeExecutableFromRegustrySubfolder ("Windows", out result))
+ return result;
+
+ throw new InvalidOperationException ("Could not locate a peverify.exe executable");
+ }
+
+ private static bool TryFindPeExecutableFromRegustrySubfolder (string subfolder, out NPath peVerifyPath)
+ {
+ var keyPath = $"SOFTWARE\\Wow6432Node\\Microsoft\\Microsoft SDKs\\{subfolder}";
+ var key = Registry.LocalMachine.OpenSubKey (keyPath);
+
+ foreach (var sdkKeyName in key.GetSubKeyNames ().OrderBy (name => new Version (name.TrimStart ('v').TrimEnd ('A'))).Reverse ()) {
+ var sdkKey = Registry.LocalMachine.OpenSubKey ($"{keyPath}\\{sdkKeyName}");
+
+ var sdkDir = (string)sdkKey.GetValue ("InstallationFolder");
+ if (string.IsNullOrEmpty (sdkDir))
+ continue;
+
+ var binDir = sdkDir.ToNPath ().Combine ("bin");
+ if (!binDir.Exists ())
+ continue;
+
+ foreach (var netSdkDirs in binDir.Directories ().OrderBy (dir => dir.FileName)) {
+ peVerifyPath = netSdkDirs.Combine ("PEVerify.exe");
+ if (peVerifyPath.FileExists ())
+ return true;
+ }
+ }
+ peVerifyPath = null;
+ return false;
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs
new file mode 100644
index 000000000..545e381cd
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs
@@ -0,0 +1,629 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Extensions;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class ResultChecker
+ {
+ readonly BaseAssemblyResolver _originalsResolver;
+ readonly BaseAssemblyResolver _linkedResolver;
+ readonly ReaderParameters _originalReaderParameters;
+ readonly ReaderParameters _linkedReaderParameters;
+ readonly PeVerifier _peVerifier;
+
+ public ResultChecker ()
+ : this(new TestCaseAssemblyResolver (), new TestCaseAssemblyResolver (), new PeVerifier (),
+ new ReaderParameters
+ {
+ SymbolReaderProvider = new DefaultSymbolReaderProvider (false)
+ },
+ new ReaderParameters
+ {
+ SymbolReaderProvider = new DefaultSymbolReaderProvider (false)
+ })
+ {
+ }
+
+ public ResultChecker (BaseAssemblyResolver originalsResolver, BaseAssemblyResolver linkedResolver, PeVerifier peVerifier,
+ ReaderParameters originalReaderParameters, ReaderParameters linkedReaderParameters)
+ {
+ _originalsResolver = originalsResolver;
+ _linkedResolver = linkedResolver;
+ _peVerifier = peVerifier;
+ _originalReaderParameters = originalReaderParameters;
+ _linkedReaderParameters = linkedReaderParameters;
+ }
+
+ public virtual void Check (LinkedTestCaseResult linkResult)
+ {
+ Assert.IsTrue (linkResult.OutputAssemblyPath.FileExists (), $"The linked output assembly was not found. Expected at {linkResult.OutputAssemblyPath}");
+
+ InitializeResolvers (linkResult);
+
+ try
+ {
+ var original = ResolveOriginalsAssembly (linkResult.ExpectationsAssemblyPath.FileNameWithoutExtension);
+ PerformOutputAssemblyChecks (original, linkResult.OutputAssemblyPath.Parent);
+ PerformOutputSymbolChecks (original, linkResult.OutputAssemblyPath.Parent);
+
+ var linked = ResolveLinkedAssembly (linkResult.OutputAssemblyPath.FileNameWithoutExtension);
+
+ CreateAssemblyChecker (original, linked).Verify ();
+
+ VerifyLinkingOfOtherAssemblies (original);
+
+ _peVerifier.Check (linkResult, original);
+
+ AdditionalChecking (linkResult, original, linked);
+ }
+ finally
+ {
+ _originalsResolver.Dispose ();
+ _linkedResolver.Dispose ();
+ }
+ }
+
+ protected virtual AssemblyChecker CreateAssemblyChecker (AssemblyDefinition original, AssemblyDefinition linked)
+ {
+ return new AssemblyChecker (original, linked);
+ }
+
+ void InitializeResolvers (LinkedTestCaseResult linkedResult)
+ {
+ _originalsResolver.AddSearchDirectory (linkedResult.ExpectationsAssemblyPath.Parent.ToString ());
+ _linkedResolver.AddSearchDirectory (linkedResult.OutputAssemblyPath.Parent.ToString ());
+ }
+
+ protected AssemblyDefinition ResolveLinkedAssembly (string assemblyName)
+ {
+ var cleanAssemblyName = assemblyName;
+ if (assemblyName.EndsWith(".exe") || assemblyName.EndsWith(".dll"))
+ cleanAssemblyName = System.IO.Path.GetFileNameWithoutExtension (assemblyName);
+ return _linkedResolver.Resolve (new AssemblyNameReference (cleanAssemblyName, null), _linkedReaderParameters);
+ }
+
+ protected AssemblyDefinition ResolveOriginalsAssembly (string assemblyName)
+ {
+ var cleanAssemblyName = assemblyName;
+ if (assemblyName.EndsWith (".exe") || assemblyName.EndsWith (".dll"))
+ cleanAssemblyName = Path.GetFileNameWithoutExtension (assemblyName);
+ return _originalsResolver.Resolve (new AssemblyNameReference (cleanAssemblyName, null), _originalReaderParameters);
+ }
+
+ void PerformOutputAssemblyChecks (AssemblyDefinition original, NPath outputDirectory)
+ {
+ var assembliesToCheck = original.MainModule.Types.SelectMany (t => t.CustomAttributes).Where (attr => ExpectationsProvider.IsAssemblyAssertion(attr));
+
+ foreach (var assemblyAttr in assembliesToCheck) {
+ var name = (string) assemblyAttr.ConstructorArguments.First ().Value;
+ var expectedPath = outputDirectory.Combine (name);
+
+ if (assemblyAttr.AttributeType.Name == nameof (RemovedAssemblyAttribute))
+ Assert.IsFalse (expectedPath.FileExists (), $"Expected the assembly {name} to not exist in {outputDirectory}, but it did");
+ else if (assemblyAttr.AttributeType.Name == nameof (KeptAssemblyAttribute))
+ Assert.IsTrue (expectedPath.FileExists (), $"Expected the assembly {name} to exist in {outputDirectory}, but it did not");
+ else
+ throw new NotImplementedException($"Unknown assembly assertion of type {assemblyAttr.AttributeType}");
+ }
+ }
+
+ void PerformOutputSymbolChecks (AssemblyDefinition original, NPath outputDirectory)
+ {
+ var symbolFilesToCheck = original.MainModule.Types.SelectMany (t => t.CustomAttributes).Where (ExpectationsProvider.IsSymbolAssertion);
+
+ foreach (var symbolAttr in symbolFilesToCheck) {
+ if (symbolAttr.AttributeType.Name == nameof (RemovedSymbolsAttribute))
+ VerifyRemovedSymbols (symbolAttr, outputDirectory);
+ else if (symbolAttr.AttributeType.Name == nameof (KeptSymbolsAttribute))
+ VerifyKeptSymbols (symbolAttr);
+ else
+ throw new NotImplementedException($"Unknown symbol file assertion of type {symbolAttr.AttributeType}");
+ }
+ }
+
+ void VerifyKeptSymbols (CustomAttribute symbolsAttribute)
+ {
+ var assemblyName = (string) symbolsAttribute.ConstructorArguments [0].Value;
+ var originalAssembly = ResolveOriginalsAssembly (assemblyName);
+ var linkedAssembly = ResolveLinkedAssembly (assemblyName);
+
+ if (linkedAssembly.MainModule.SymbolReader == null)
+ Assert.Fail ($"Missing symbols for assembly `{linkedAssembly.MainModule.FileName}`");
+
+ if (linkedAssembly.MainModule.SymbolReader.GetType () != originalAssembly.MainModule.SymbolReader.GetType ())
+ Assert.Fail ($"Expected symbol provider of type `{originalAssembly.MainModule.SymbolReader}`, but was `{linkedAssembly.MainModule.SymbolReader}`");
+ }
+
+ void VerifyRemovedSymbols (CustomAttribute symbolsAttribute, NPath outputDirectory)
+ {
+ var assemblyName = (string) symbolsAttribute.ConstructorArguments [0].Value;
+ try
+ {
+ var linkedAssembly = ResolveLinkedAssembly (assemblyName);
+
+ if (linkedAssembly.MainModule.SymbolReader != null)
+ Assert.Fail ($"Expected no symbols to be found for assembly `{linkedAssembly.MainModule.FileName}`, however, symbols were found of type {linkedAssembly.MainModule.SymbolReader}");
+ } catch (AssemblyResolutionException) {
+ // If we failed to resolve, then the entire assembly may be gone.
+ // The assembly being gone confirms that embedded pdbs were removed, but technically, for the other symbol types, the symbol file could still exist on disk
+ // let's check to make sure that it does not.
+ var possibleSymbolFilePath = outputDirectory.Combine ($"{assemblyName}").ChangeExtension ("pdb");
+ if (possibleSymbolFilePath.Exists ())
+ Assert.Fail ($"Expected no symbols to be found for assembly `{assemblyName}`, however, a symbol file was found at {possibleSymbolFilePath}");
+
+ possibleSymbolFilePath = outputDirectory.Combine ($"{assemblyName}.mdb");
+ if (possibleSymbolFilePath.Exists ())
+ Assert.Fail ($"Expected no symbols to be found for assembly `{assemblyName}`, however, a symbol file was found at {possibleSymbolFilePath}");
+ }
+ }
+
+ protected virtual void AdditionalChecking (LinkedTestCaseResult linkResult, AssemblyDefinition original, AssemblyDefinition linked)
+ {
+ }
+
+ void VerifyLinkingOfOtherAssemblies (AssemblyDefinition original)
+ {
+ var checks = BuildOtherAssemblyCheckTable (original);
+
+ try {
+ foreach (var assemblyName in checks.Keys) {
+ using (var linkedAssembly = ResolveLinkedAssembly (assemblyName)) {
+ foreach (var checkAttrInAssembly in checks [assemblyName])
+ {
+ var attributeTypeName = checkAttrInAssembly.AttributeType.Name;
+ if (attributeTypeName == nameof (KeptAllTypesAndMembersInAssemblyAttribute)) {
+ VerifyKeptAllTypesAndMembersInAssembly (linkedAssembly);
+ continue;
+ }
+
+ if (attributeTypeName == nameof (KeptAttributeInAssemblyAttribute)) {
+ VerifyKeptAttributeInAssembly (checkAttrInAssembly, linkedAssembly);
+ continue;
+ }
+
+ if (attributeTypeName == nameof (RemovedAttributeInAssembly)) {
+ VerifyRemovedAttributeInAssembly (checkAttrInAssembly, linkedAssembly);
+ continue;
+ }
+
+ var expectedTypeName = checkAttrInAssembly.ConstructorArguments [1].Value.ToString ();
+ var linkedType = linkedAssembly.MainModule.GetType (expectedTypeName);
+
+ if (linkedType == null && linkedAssembly.MainModule.HasExportedTypes) {
+ linkedType = linkedAssembly.MainModule.ExportedTypes
+ .FirstOrDefault (exported => exported.FullName == expectedTypeName)
+ ?.Resolve ();
+ }
+
+ switch (attributeTypeName) {
+ case nameof (RemovedTypeInAssemblyAttribute):
+ if (linkedType != null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been removed");
+ GetOriginalTypeFromInAssemblyAttribute (checkAttrInAssembly);
+ break;
+ case nameof (KeptTypeInAssemblyAttribute):
+ if (linkedType == null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been kept");
+ break;
+ case nameof (RemovedInterfaceOnTypeInAssemblyAttribute):
+ if (linkedType == null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been kept");
+ VerifyRemovedInterfaceOnTypeInAssembly (checkAttrInAssembly, linkedType);
+ break;
+ case nameof (KeptInterfaceOnTypeInAssemblyAttribute):
+ if (linkedType == null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been kept");
+ VerifyKeptInterfaceOnTypeInAssembly (checkAttrInAssembly, linkedType);
+ break;
+ case nameof (RemovedMemberInAssemblyAttribute):
+ if (linkedType == null)
+ continue;
+
+ VerifyRemovedMemberInAssembly (checkAttrInAssembly, linkedType);
+ break;
+ case nameof (KeptBaseOnTypeInAssemblyAttribute):
+ if (linkedType == null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been kept");
+ VerifyKeptBaseOnTypeInAssembly (checkAttrInAssembly, linkedType);
+ break;
+ case nameof (KeptMemberInAssemblyAttribute):
+ if (linkedType == null)
+ Assert.Fail ($"Type `{expectedTypeName}' should have been kept");
+
+ VerifyKeptMemberInAssembly (checkAttrInAssembly, linkedType);
+ break;
+ case nameof (RemovedForwarderAttribute):
+ if (linkedAssembly.MainModule.ExportedTypes.Any (l => l.Name == expectedTypeName))
+ Assert.Fail ($"Forwarder `{expectedTypeName}' should have been removed");
+
+ break;
+ case nameof (KeptResourceInAssemblyAttribute):
+ VerifyKeptResourceInAssembly (checkAttrInAssembly);
+ break;
+ case nameof (RemovedResourceInAssemblyAttribute):
+ VerifyRemovedResourceInAssembly (checkAttrInAssembly);
+ break;
+ case nameof (KeptReferencesInAssemblyAttribute):
+ VerifyKeptReferencesInAssembly (checkAttrInAssembly);
+ break;
+ default:
+ UnhandledOtherAssemblyAssertion (expectedTypeName, checkAttrInAssembly, linkedType);
+ break;
+ }
+ }
+ }
+ }
+ } catch (AssemblyResolutionException e) {
+ Assert.Fail ($"Failed to resolve linked assembly `{e.AssemblyReference.Name}`. It must not exist in any of the output directories:\n\t{_linkedResolver.GetSearchDirectories ().Aggregate ((buff, s) => $"{buff}\n\t{s}")}\n");
+ }
+ }
+
+ void VerifyKeptAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly)
+ {
+ VerifyAttributeInAssembly(inAssemblyAttribute, linkedAssembly, VerifyCustomAttributeKept);
+ }
+
+ void VerifyRemovedAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly)
+ {
+ VerifyAttributeInAssembly (inAssemblyAttribute, linkedAssembly, VerifyCustomAttributeRemoved);
+ }
+
+ void VerifyAttributeInAssembly (CustomAttribute inAssemblyAttribute, AssemblyDefinition linkedAssembly, Action<ICustomAttributeProvider, string> assertExpectedAttribute)
+ {
+ var assemblyName = (string) inAssemblyAttribute.ConstructorArguments [0].Value;
+ string expectedAttributeTypeName;
+ var attributeTypeOrTypeName = inAssemblyAttribute.ConstructorArguments [1].Value;
+ if (attributeTypeOrTypeName is TypeReference typeReference) {
+ expectedAttributeTypeName = typeReference.FullName;
+ } else {
+ expectedAttributeTypeName = attributeTypeOrTypeName.ToString ();
+ }
+
+ if (inAssemblyAttribute.ConstructorArguments.Count == 2) {
+ // Assembly
+ assertExpectedAttribute (linkedAssembly, expectedAttributeTypeName);
+ return;
+ }
+
+ // We are asserting on type or member
+ var typeOrTypeName = inAssemblyAttribute.ConstructorArguments [2].Value;
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute.ConstructorArguments[0].Value.ToString (), typeOrTypeName);
+ if (originalType == null)
+ Assert.Fail ($"Invalid test assertion. The original `{assemblyName}` does not contain a type `{typeOrTypeName}`");
+
+ var linkedType = linkedAssembly.MainModule.GetType (originalType.FullName);
+ if (linkedType == null)
+ Assert.Fail ($"Missing expected type `{typeOrTypeName}` in `{assemblyName}`");
+
+ if (inAssemblyAttribute.ConstructorArguments.Count == 3) {
+ assertExpectedAttribute (linkedType, expectedAttributeTypeName);
+ return;
+ }
+
+ // we are asserting on a member
+ string memberName = (string) inAssemblyAttribute.ConstructorArguments [3].Value;
+
+ // We will find the matching type from the original assembly first that way we can confirm
+ // that the name defined in the attribute corresponds to a member that actually existed
+ var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (originalFieldMember != null) {
+ var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (linkedField == null)
+ Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been kept");
+
+ assertExpectedAttribute (linkedField, expectedAttributeTypeName);
+ return;
+ }
+
+ var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (originalPropertyMember != null) {
+ var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (linkedProperty == null)
+ Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been kept");
+
+ assertExpectedAttribute (linkedProperty, expectedAttributeTypeName);
+ return;
+ }
+
+ var originalMethodMember = originalType.Methods.FirstOrDefault (m => m.GetSignature () == memberName);
+ if (originalMethodMember != null) {
+ var linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature () == memberName);
+ if (linkedMethod == null)
+ Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been kept");
+
+ assertExpectedAttribute (linkedMethod, expectedAttributeTypeName);
+ return;
+ }
+
+ Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`");
+ }
+
+ void VerifyCustomAttributeKept (ICustomAttributeProvider provider, string expectedAttributeTypeName)
+ {
+ var match = provider.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.FullName == expectedAttributeTypeName);
+ if (match == null)
+ Assert.Fail ($"Expected `{provider}` to have an attribute of type `{expectedAttributeTypeName}`");
+ }
+
+ void VerifyCustomAttributeRemoved (ICustomAttributeProvider provider, string expectedAttributeTypeName)
+ {
+ var match = provider.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.FullName == expectedAttributeTypeName);
+ if (match != null)
+ Assert.Fail ($"Expected `{provider}` to no longer have an attribute of type `{expectedAttributeTypeName}`");
+ }
+
+ void VerifyRemovedInterfaceOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType)
+ {
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute);
+
+ var interfaceAssemblyName = inAssemblyAttribute.ConstructorArguments [2].Value.ToString ();
+ var interfaceType = inAssemblyAttribute.ConstructorArguments [3].Value;
+
+ var originalInterface = GetOriginalTypeFromInAssemblyAttribute (interfaceAssemblyName, interfaceType);
+ if (!originalType.HasInterfaces)
+ Assert.Fail ("Invalid assertion. Original type does not have any interfaces");
+
+ var originalInterfaceImpl = GetMatchingInterfaceImplementationOnType (originalType, originalInterface.FullName);
+ if (originalInterfaceImpl == null)
+ Assert.Fail ($"Invalid assertion. Original type never had an interface of type `{originalInterface}`");
+
+ var linkedInterfaceImpl = GetMatchingInterfaceImplementationOnType (linkedType, originalInterface.FullName);
+ if (linkedInterfaceImpl != null)
+ Assert.Fail ($"Expected `{linkedType}` to no longer have an interface of type {originalInterface.FullName}");
+ }
+
+ void VerifyKeptInterfaceOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType)
+ {
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute);
+
+ var interfaceAssemblyName = inAssemblyAttribute.ConstructorArguments [2].Value.ToString ();
+ var interfaceType = inAssemblyAttribute.ConstructorArguments [3].Value;
+
+ var originalInterface = GetOriginalTypeFromInAssemblyAttribute (interfaceAssemblyName, interfaceType);
+ if (!originalType.HasInterfaces)
+ Assert.Fail ("Invalid assertion. Original type does not have any interfaces");
+
+ var originalInterfaceImpl = GetMatchingInterfaceImplementationOnType (originalType, originalInterface.FullName);
+ if (originalInterfaceImpl == null)
+ Assert.Fail ($"Invalid assertion. Original type never had an interface of type `{originalInterface}`");
+
+ var linkedInterfaceImpl = GetMatchingInterfaceImplementationOnType (linkedType, originalInterface.FullName);
+ if (linkedInterfaceImpl == null)
+ Assert.Fail ($"Expected `{linkedType}` to have interface of type {originalInterface.FullName}");
+ }
+
+ void VerifyKeptBaseOnTypeInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType)
+ {
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute);
+
+ var baseAssemblyName = inAssemblyAttribute.ConstructorArguments [2].Value.ToString ();
+ var baseType = inAssemblyAttribute.ConstructorArguments [3].Value;
+
+ var originalBase = GetOriginalTypeFromInAssemblyAttribute (baseAssemblyName, baseType);
+ if (originalType.BaseType.Resolve () != originalBase)
+ Assert.Fail ("Invalid assertion. Original type's base does not match the expected base");
+
+ Assert.That (originalBase.FullName, Is.EqualTo (linkedType.BaseType.FullName),
+ $"Incorrect base on `{linkedType.FullName}`. Expected `{originalBase.FullName}` but was `{linkedType.BaseType.FullName}`");
+ }
+
+ protected static InterfaceImplementation GetMatchingInterfaceImplementationOnType (TypeDefinition type, string expectedInterfaceTypeName)
+ {
+ return type.Interfaces.FirstOrDefault (impl =>
+ {
+ var resolvedImpl = impl.InterfaceType.Resolve ();
+
+ if (resolvedImpl == null)
+ Assert.Fail ($"Failed to resolve interface : `{impl.InterfaceType}` on `{type}`");
+
+ return resolvedImpl.FullName == expectedInterfaceTypeName;
+ });
+ }
+
+ void VerifyRemovedMemberInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType)
+ {
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute);
+ foreach (var memberNameAttr in (CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments [2].Value) {
+ string memberName = (string) memberNameAttr.Value;
+
+ // We will find the matching type from the original assembly first that way we can confirm
+ // that the name defined in the attribute corresponds to a member that actually existed
+ var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (originalFieldMember != null) {
+ var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (linkedField != null)
+ Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been removed");
+
+ continue;
+ }
+
+ var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (originalPropertyMember != null) {
+ var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (linkedProperty != null)
+ Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been removed");
+
+ continue;
+ }
+
+ var originalMethodMember = originalType.Methods.FirstOrDefault (m => m.GetSignature () == memberName);
+ if (originalMethodMember != null) {
+ var linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature () == memberName);
+ if (linkedMethod != null)
+ Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been removed");
+
+ continue;
+ }
+
+ Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`");
+ }
+ }
+
+ void VerifyKeptMemberInAssembly (CustomAttribute inAssemblyAttribute, TypeDefinition linkedType)
+ {
+ var originalType = GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute);
+ foreach (var memberNameAttr in (CustomAttributeArgument[]) inAssemblyAttribute.ConstructorArguments [2].Value) {
+ string memberName = (string) memberNameAttr.Value;
+
+ // We will find the matching type from the original assembly first that way we can confirm
+ // that the name defined in the attribute corresponds to a member that actually existed
+
+ if (TryVerifyKeptMemberInAssemblyAsField (memberName, originalType, linkedType))
+ continue;
+
+ if (TryVerifyKeptMemberInAssemblyAsProperty (memberName, originalType, linkedType))
+ continue;
+
+ if (TryVerifyKeptMemberInAssemblyAsMethod (memberName, originalType, linkedType))
+ continue;
+
+ Assert.Fail ($"Invalid test assertion. No member named `{memberName}` exists on the original type `{originalType}`");
+ }
+ }
+
+ protected virtual bool TryVerifyKeptMemberInAssemblyAsField (string memberName, TypeDefinition originalType, TypeDefinition linkedType)
+ {
+ var originalFieldMember = originalType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (originalFieldMember != null) {
+ var linkedField = linkedType.Fields.FirstOrDefault (m => m.Name == memberName);
+ if (linkedField == null)
+ Assert.Fail ($"Field `{memberName}` on Type `{originalType}` should have been kept");
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected virtual bool TryVerifyKeptMemberInAssemblyAsProperty (string memberName, TypeDefinition originalType, TypeDefinition linkedType)
+ {
+ var originalPropertyMember = originalType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (originalPropertyMember != null) {
+ var linkedProperty = linkedType.Properties.FirstOrDefault (m => m.Name == memberName);
+ if (linkedProperty == null)
+ Assert.Fail ($"Property `{memberName}` on Type `{originalType}` should have been kept");
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected virtual bool TryVerifyKeptMemberInAssemblyAsMethod (string memberName, TypeDefinition originalType, TypeDefinition linkedType)
+ {
+ var originalMethodMember = originalType.Methods.FirstOrDefault (m => m.GetSignature() == memberName);
+ if (originalMethodMember != null) {
+ var linkedMethod = linkedType.Methods.FirstOrDefault (m => m.GetSignature() == memberName);
+ if (linkedMethod == null)
+ Assert.Fail ($"Method `{memberName}` on Type `{originalType}` should have been kept");
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void VerifyKeptReferencesInAssembly (CustomAttribute inAssemblyAttribute)
+ {
+ var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments [0].Value.ToString ());
+ var expectedReferenceNames = ((CustomAttributeArgument []) inAssemblyAttribute.ConstructorArguments [1].Value).Select (attr => (string) attr.Value);
+ Assert.That (assembly.MainModule.AssemblyReferences.Select (asm => asm.Name), Is.EquivalentTo (expectedReferenceNames));
+ }
+
+ void VerifyKeptResourceInAssembly (CustomAttribute inAssemblyAttribute)
+ {
+ var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments [0].Value.ToString ());
+ var resourceName = inAssemblyAttribute.ConstructorArguments [1].Value.ToString ();
+
+ Assert.That (assembly.MainModule.Resources.Select (r => r.Name), Has.Member (resourceName));
+ }
+
+ void VerifyRemovedResourceInAssembly (CustomAttribute inAssemblyAttribute)
+ {
+ var assembly = ResolveLinkedAssembly (inAssemblyAttribute.ConstructorArguments [0].Value.ToString ());
+ var resourceName = inAssemblyAttribute.ConstructorArguments [1].Value.ToString ();
+
+ Assert.That (assembly.MainModule.Resources.Select (r => r.Name), Has.No.Member (resourceName));
+ }
+
+ void VerifyKeptAllTypesAndMembersInAssembly (AssemblyDefinition linked)
+ {
+ var original = ResolveOriginalsAssembly (linked.MainModule.Assembly.Name.Name);
+
+ if (original == null)
+ Assert.Fail ($"Failed to resolve original assembly {linked.MainModule.Assembly.Name.Name}");
+
+ var originalTypes = original.AllDefinedTypes ().ToDictionary (t => t.FullName);
+ var linkedTypes = linked.AllDefinedTypes ().ToDictionary (t => t.FullName);
+
+ var missingInLinked = originalTypes.Keys.Except (linkedTypes.Keys);
+
+ Assert.That (missingInLinked, Is.Empty, $"Expected all types to exist in the linked assembly, but one or more were missing");
+
+ foreach (var originalKvp in originalTypes) {
+ var linkedType = linkedTypes [originalKvp.Key];
+
+ var originalMembers = originalKvp.Value.AllMembers ().Select (m => m.FullName);
+ var linkedMembers = linkedType.AllMembers ().Select (m => m.FullName);
+
+ var missingMembersInLinked = originalMembers.Except (linkedMembers);
+
+ Assert.That (missingMembersInLinked, Is.Empty, $"Expected all members of `{originalKvp.Key}`to exist in the linked assembly, but one or more were missing");
+ }
+ }
+
+ protected TypeDefinition GetOriginalTypeFromInAssemblyAttribute (CustomAttribute inAssemblyAttribute)
+ {
+ return GetOriginalTypeFromInAssemblyAttribute (inAssemblyAttribute.ConstructorArguments [0].Value.ToString (), inAssemblyAttribute.ConstructorArguments [1].Value);
+ }
+
+ protected TypeDefinition GetOriginalTypeFromInAssemblyAttribute (string assemblyName, object typeOrTypeName)
+ {
+ var attributeValueAsTypeReference = typeOrTypeName as TypeReference;
+ if (attributeValueAsTypeReference != null)
+ return attributeValueAsTypeReference.Resolve ();
+
+ var assembly = ResolveOriginalsAssembly (assemblyName);
+
+ var expectedTypeName = typeOrTypeName.ToString ();
+ var originalType = assembly.MainModule.GetType (expectedTypeName);
+ if (originalType == null)
+ Assert.Fail ($"Invalid test assertion. Unable to locate the original type `{expectedTypeName}.`");
+ return originalType;
+ }
+
+ Dictionary<string, List<CustomAttribute>> BuildOtherAssemblyCheckTable (AssemblyDefinition original)
+ {
+ var checks = new Dictionary<string, List<CustomAttribute>> ();
+
+ foreach (var typeWithRemoveInAssembly in original.AllDefinedTypes ()) {
+ foreach (var attr in typeWithRemoveInAssembly.CustomAttributes.Where (IsTypeInOtherAssemblyAssertion)) {
+ var assemblyName = (string) attr.ConstructorArguments [0].Value;
+ List<CustomAttribute> checksForAssembly;
+ if (!checks.TryGetValue (assemblyName, out checksForAssembly))
+ checks [assemblyName] = checksForAssembly = new List<CustomAttribute> ();
+
+ checksForAssembly.Add (attr);
+ }
+ }
+
+ return checks;
+ }
+
+ protected virtual void UnhandledOtherAssemblyAssertion (string expectedTypeName, CustomAttribute checkAttrInAssembly, TypeDefinition linkedType)
+ {
+ throw new NotImplementedException ($"Type {expectedTypeName}, has an unknown other assembly attribute of type {checkAttrInAssembly.AttributeType}");
+ }
+
+ bool IsTypeInOtherAssemblyAssertion (CustomAttribute attr)
+ {
+ return attr.AttributeType.Resolve ().DerivesFrom (nameof (BaseInAssemblyAttribute));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/SetupCompileInfo.cs b/test/Mono.Linker.Tests/TestCasesRunner/SetupCompileInfo.cs
new file mode 100644
index 000000000..a8f0525cd
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/SetupCompileInfo.cs
@@ -0,0 +1,15 @@
+using System;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class SetupCompileInfo {
+ public string OutputName;
+ public NPath[] SourceFiles;
+ public string[] Defines;
+ public string[] References;
+ public NPath[] Resources;
+ public string AdditionalArguments;
+ public string CompilerToUse;
+ public bool AddAsReference;
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/SourceAndDestinationPair.cs b/test/Mono.Linker.Tests/TestCasesRunner/SourceAndDestinationPair.cs
new file mode 100644
index 000000000..762887a13
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/SourceAndDestinationPair.cs
@@ -0,0 +1,9 @@
+using System;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class SourceAndDestinationPair {
+ public NPath Source;
+ public string DestinationFileName;
+ }
+}
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseAssemblyResolver.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseAssemblyResolver.cs
new file mode 100644
index 000000000..0c3c82583
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseAssemblyResolver.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using Mono.Cecil;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestCaseAssemblyResolver : DefaultAssemblyResolver {
+ readonly HashSet<IDisposable> itemsToDispose;
+
+ public TestCaseAssemblyResolver ()
+ {
+ itemsToDispose = new HashSet<IDisposable> ();
+ }
+
+ public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderParameters parameters)
+ {
+ var assembly = base.Resolve (name, parameters);
+
+ if (assembly == null)
+ return null;
+
+ // Don't do any caching because the reader parameters could be different each time
+ // but we still want to track items that need to be disposed for easy clean up
+ itemsToDispose.Add (assembly);
+
+ if (assembly.MainModule.SymbolReader != null)
+ itemsToDispose.Add (assembly.MainModule.SymbolReader);
+ return assembly;
+ }
+
+ protected override void Dispose (bool disposing)
+ {
+ foreach (var item in itemsToDispose)
+ item.Dispose ();
+
+ base.Dispose (disposing);
+ }
+ }
+}
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
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs
new file mode 100644
index 000000000..afa1eb6a3
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs
@@ -0,0 +1,357 @@
+using System;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Mono.Linker.Tests.Extensions;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestCaseCompiler {
+ static string _cachedWindowsCscPath = null;
+ protected readonly TestCaseMetadaProvider _metadataProvider;
+ protected readonly TestCaseSandbox _sandbox;
+ protected readonly ILCompiler _ilCompiler;
+
+ public TestCaseCompiler (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider)
+ : this(sandbox, metadataProvider, new ILCompiler ())
+ {
+ }
+
+ public TestCaseCompiler (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider, ILCompiler ilCompiler)
+ {
+ _ilCompiler = ilCompiler;
+ _sandbox = sandbox;
+ _metadataProvider = metadataProvider;
+ }
+
+ public NPath CompileTestIn (NPath outputDirectory, string outputName, IEnumerable<string> sourceFiles, string[] commonReferences, string[] mainAssemblyReferences, IEnumerable<string> defines, NPath[] resources, string[] additionalArguments)
+ {
+ var originalCommonReferences = commonReferences.Select (r => r.ToNPath ()).ToArray ();
+ var originalDefines = defines?.ToArray () ?? new string [0];
+
+ Prepare (outputDirectory);
+
+ var compiledReferences = CompileBeforeTestCaseAssemblies (outputDirectory, originalCommonReferences, originalDefines).ToArray ();
+ var allTestCaseReferences = originalCommonReferences
+ .Concat (compiledReferences)
+ .Concat (mainAssemblyReferences.Select (r => r.ToNPath ()))
+ .ToArray ();
+
+ var options = CreateOptionsForTestCase (
+ outputDirectory.Combine (outputName),
+ sourceFiles.Select (s => s.ToNPath ()).ToArray (),
+ allTestCaseReferences,
+ originalDefines,
+ resources,
+ additionalArguments);
+ var testAssembly = CompileAssembly (options);
+
+
+ // The compile after step is used by tests to mess around with the input to the linker. Generally speaking, it doesn't seem like we would ever want to mess with the
+ // expectations assemblies because this would undermine our ability to inspect them for expected results during ResultChecking. The UnityLinker UnresolvedHandling tests depend on this
+ // behavior of skipping the after test compile
+ if (outputDirectory != _sandbox.ExpectationsDirectory)
+ CompileAfterTestCaseAssemblies (outputDirectory, originalCommonReferences, originalDefines);
+
+ return testAssembly;
+ }
+
+ protected virtual void Prepare (NPath outputDirectory)
+ {
+ }
+
+ protected virtual CompilerOptions CreateOptionsForTestCase (NPath outputPath, NPath[] sourceFiles, NPath[] references, string[] defines, NPath[] resources, string[] additionalArguments)
+ {
+ return new CompilerOptions
+ {
+ OutputPath = outputPath,
+ SourceFiles = sourceFiles,
+ References = references,
+ Defines = defines.Concat (_metadataProvider.GetDefines ()).ToArray (),
+ Resources = resources,
+ AdditionalArguments = additionalArguments,
+ CompilerToUse = _metadataProvider.GetCSharpCompilerToUse ()
+ };
+ }
+
+ protected virtual CompilerOptions CreateOptionsForSupportingAssembly (SetupCompileInfo setupCompileInfo, NPath outputDirectory, NPath[] sourceFiles, NPath[] references, string[] defines, NPath[] resources)
+ {
+ var allDefines = defines.Concat (setupCompileInfo.Defines ?? new string [0]).ToArray ();
+ var allReferences = references.Concat (setupCompileInfo.References?.Select (p => MakeSupportingAssemblyReferencePathAbsolute (outputDirectory, p)) ?? new NPath [0]).ToArray ();
+ string[] additionalArguments = string.IsNullOrEmpty (setupCompileInfo.AdditionalArguments) ? null : new[] { setupCompileInfo.AdditionalArguments };
+ return new CompilerOptions
+ {
+ OutputPath = outputDirectory.Combine (setupCompileInfo.OutputName),
+ SourceFiles = sourceFiles,
+ References = allReferences,
+ Defines = allDefines,
+ Resources = resources,
+ AdditionalArguments = additionalArguments,
+ CompilerToUse = setupCompileInfo.CompilerToUse?.ToLower ()
+ };
+ }
+
+ private IEnumerable<NPath> CompileBeforeTestCaseAssemblies (NPath outputDirectory, NPath[] references, string[] defines)
+ {
+ foreach (var setupCompileInfo in _metadataProvider.GetSetupCompileAssembliesBefore ())
+ {
+ var options = CreateOptionsForSupportingAssembly (
+ setupCompileInfo,
+ outputDirectory,
+ CollectSetupBeforeSourcesFiles (setupCompileInfo),
+ references,
+ defines,
+ CollectSetupBeforeResourcesFiles (setupCompileInfo));
+ var output = CompileAssembly (options);
+ if (setupCompileInfo.AddAsReference)
+ yield return output;
+ }
+ }
+
+ private void CompileAfterTestCaseAssemblies (NPath outputDirectory, NPath[] references, string[] defines)
+ {
+ foreach (var setupCompileInfo in _metadataProvider.GetSetupCompileAssembliesAfter ())
+ {
+ var options = CreateOptionsForSupportingAssembly (
+ setupCompileInfo,
+ outputDirectory,
+ CollectSetupAfterSourcesFiles (setupCompileInfo),
+ references,
+ defines,
+ CollectSetupAfterResourcesFiles (setupCompileInfo));
+ CompileAssembly (options);
+ }
+ }
+
+ private NPath[] CollectSetupBeforeSourcesFiles (SetupCompileInfo info)
+ {
+ return CollectSourceFilesFrom (_sandbox.BeforeReferenceSourceDirectoryFor (info.OutputName));
+ }
+
+ private NPath[] CollectSetupAfterSourcesFiles (SetupCompileInfo info)
+ {
+ return CollectSourceFilesFrom (_sandbox.AfterReferenceSourceDirectoryFor (info.OutputName));
+ }
+
+ private NPath[] CollectSetupBeforeResourcesFiles (SetupCompileInfo info)
+ {
+ return _sandbox.BeforeReferenceResourceDirectoryFor (info.OutputName).Files ().ToArray ();
+ }
+
+ private NPath[] CollectSetupAfterResourcesFiles (SetupCompileInfo info)
+ {
+ return _sandbox.AfterReferenceResourceDirectoryFor (info.OutputName).Files ().ToArray ();
+ }
+
+ private static NPath[] CollectSourceFilesFrom (NPath directory)
+ {
+ var sourceFiles = directory.Files ("*.cs").ToArray ();
+ if (sourceFiles.Length > 0)
+ return sourceFiles;
+
+ sourceFiles = directory.Files ("*.il").ToArray ();
+ if (sourceFiles.Length > 0)
+ return sourceFiles;
+
+ throw new FileNotFoundException ($"Didn't find any sources files in {directory}");
+ }
+
+ protected static NPath MakeSupportingAssemblyReferencePathAbsolute (NPath outputDirectory, string referenceFileName)
+ {
+ // Not a good idea to use a full path in a test, but maybe someone is trying to quickly test something locally
+ if (Path.IsPathRooted (referenceFileName))
+ return referenceFileName.ToNPath ();
+
+ var possiblePath = outputDirectory.Combine (referenceFileName);
+ if (possiblePath.FileExists ())
+ return possiblePath;
+
+ return referenceFileName.ToNPath();
+ }
+
+ protected NPath CompileAssembly (CompilerOptions options)
+ {
+ if (options.SourceFiles.Any (path => path.ExtensionWithDot == ".cs"))
+ return CompileCSharpAssembly (options);
+
+ if (options.SourceFiles.Any (path => path.ExtensionWithDot == ".il"))
+ return CompileIlAssembly (options);
+
+ throw new NotSupportedException ($"Unable to compile sources files with extension `{options.SourceFiles.First ().ExtensionWithDot}`");
+ }
+
+ protected virtual NPath CompileCSharpAssemblyWithDefaultCompiler (CompilerOptions options)
+ {
+ var compilerOptions = CreateCodeDomCompilerOptions (options);
+ var provider = CodeDomProvider.CreateProvider ("C#");
+ var result = provider.CompileAssemblyFromFile (compilerOptions, options.SourceFiles.Select (p => p.ToString ()).ToArray ());
+ if (!result.Errors.HasErrors)
+ return compilerOptions.OutputAssembly.ToNPath ();
+
+ var errors = new StringBuilder ();
+ foreach (var error in result.Errors)
+ errors.AppendLine (error.ToString ());
+ throw new Exception ("Compilation errors: " + errors);
+ }
+
+ protected virtual NPath CompileCSharpAssemblyWithCsc (CompilerOptions options)
+ {
+ return CompileCSharpAssemblyWithExternalCompiler (LocateCscExecutable (), options);
+ }
+
+ protected virtual NPath CompileCSharpAssemblyWithMsc(CompilerOptions options)
+ {
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ CompileCSharpAssemblyWithExternalCompiler (LocateMcsExecutable (), options);
+
+ return CompileCSharpAssemblyWithDefaultCompiler (options);
+ }
+
+ protected NPath CompileCSharpAssemblyWithExternalCompiler (string executable, CompilerOptions options)
+ {
+ var capturedOutput = new List<string> ();
+ var process = new Process ();
+ process.StartInfo.FileName = executable;
+ process.StartInfo.Arguments = OptionsToCompilerCommandLineArguments (options);
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.OutputDataReceived += (sender, args) => capturedOutput.Add (args.Data);
+ process.Start ();
+ process.BeginOutputReadLine ();
+ process.WaitForExit ();
+
+ if (process.ExitCode != 0)
+ Assert.Fail ($"Failed to compile assembly with csc: {options.OutputPath}\n{capturedOutput.Aggregate ((buff, s) => buff + Environment.NewLine + s)}");
+
+ return options.OutputPath;
+ }
+
+ static string LocateCscExecutable ()
+ {
+ if (Environment.OSVersion.Platform != PlatformID.Win32NT)
+ return "csc";
+
+ if (_cachedWindowsCscPath != null)
+ return _cachedWindowsCscPath;
+
+ var capturedOutput = new List<string> ();
+ var process = new Process ();
+
+ var vswherePath = Environment.ExpandEnvironmentVariables ("%ProgramFiles(x86)%\\Microsoft Visual Studio\\Installer\\vswhere.exe");
+ if (!File.Exists (vswherePath))
+ Assert.Fail ($"Unable to locate csc.exe on windows because vshwere.exe was not found at {vswherePath}");
+
+ process.StartInfo.FileName = vswherePath;
+ process.StartInfo.Arguments = "-latest -products * -requires Microsoft.Component.MSBuild -property installationPath";
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.OutputDataReceived += (sender, args) => capturedOutput.Add (args.Data);
+ process.Start ();
+ process.BeginOutputReadLine ();
+ process.WaitForExit ();
+
+ if (process.ExitCode != 0)
+ Assert.Fail ($"vswhere.exe failed with :\n{capturedOutput.Aggregate ((buff, s) => buff + Environment.NewLine + s)}");
+
+ if (capturedOutput.Count == 0 || string.IsNullOrEmpty (capturedOutput [0]))
+ Assert.Fail ("vswhere.exe was unable to locate an install directory");
+
+ var installPath = capturedOutput [0].Trim ().ToNPath ();
+
+ if (!installPath.Exists ())
+ Assert.Fail ($"No install found at {installPath}");
+
+ // Do a search for the roslyn directory for a little bit of furture proofing since it normally lives under
+ // a versioned msbuild directory
+ foreach (var roslynDirectory in installPath.Directories ("Roslyn", true)) {
+ var possibleCscPath = roslynDirectory.Combine ("csc.exe");
+ if (possibleCscPath.Exists ()) {
+ _cachedWindowsCscPath = possibleCscPath.ToString ();
+ return _cachedWindowsCscPath;
+ }
+ }
+
+ Assert.Fail ("Unable to locate a roslyn csc.exe");
+ return null;
+ }
+
+ static string LocateMcsExecutable ()
+ {
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ Assert.Ignore ("We don't have a universal way of locating mcs on Windows");
+
+ return "mcs";
+ }
+
+ protected string OptionsToCompilerCommandLineArguments (CompilerOptions options)
+ {
+ var builder = new StringBuilder ();
+ builder.Append ($"/out:{options.OutputPath}");
+ var target = options.OutputPath.ExtensionWithDot == ".exe" ? "exe" : "library";
+ builder.Append ($" /target:{target}");
+ if (options.Defines != null && options.Defines.Length > 0)
+ builder.Append (options.Defines.Aggregate (string.Empty, (buff, arg) => $"{buff} /define:{arg}"));
+
+ builder.Append (options.References.Aggregate (string.Empty, (buff, arg) => $"{buff} /r:{arg}"));
+
+ if (options.Resources != null && options.Resources.Length > 0)
+ builder.Append (options.Resources.Aggregate (string.Empty, (buff, arg) => $"{buff} /res:{arg}"));
+
+ if (options.AdditionalArguments != null && options.AdditionalArguments.Length > 0)
+ builder.Append (options.AdditionalArguments.Aggregate (string.Empty, (buff, arg) => $"{buff} {arg}"));
+
+ builder.Append (options.SourceFiles.Aggregate (string.Empty, (buff, arg) => $"{buff} {arg}"));
+
+ return builder.ToString ();
+ }
+
+ protected NPath CompileCSharpAssembly (CompilerOptions options)
+ {
+ if (string.IsNullOrEmpty (options.CompilerToUse))
+ return CompileCSharpAssemblyWithDefaultCompiler (options);
+
+ if (options.CompilerToUse == "csc")
+ return CompileCSharpAssemblyWithCsc (options);
+
+ if (options.CompilerToUse == "mcs")
+ return CompileCSharpAssemblyWithMsc (options);
+
+ throw new ArgumentException ($"Invalid compiler value `{options.CompilerToUse}`");
+ }
+
+ protected NPath CompileIlAssembly (CompilerOptions options)
+ {
+ return _ilCompiler.Compile (options);
+ }
+
+ private CompilerParameters CreateCodeDomCompilerOptions (CompilerOptions options)
+ {
+ var compilerParameters = new CompilerParameters
+ {
+ OutputAssembly = options.OutputPath.ToString (),
+ GenerateExecutable = options.OutputPath.FileName.EndsWith (".exe")
+ };
+
+ compilerParameters.CompilerOptions = options.Defines?.Aggregate (string.Empty, (buff, arg) => $"{buff} /define:{arg}");
+
+ compilerParameters.ReferencedAssemblies.AddRange (options.References.Select (r => r.ToString ()).ToArray ());
+
+ if (options.Resources != null)
+ compilerParameters.EmbeddedResources.AddRange (options.Resources.Select (r => r.ToString ()).ToArray ());
+
+ if (options.AdditionalArguments != null) {
+ var combinedValues = options.AdditionalArguments.Aggregate (string.Empty, (buff, arg) => $"{buff} {arg}");
+ compilerParameters.CompilerOptions = $"{compilerParameters.CompilerOptions} {combinedValues}";
+ }
+
+ return compilerParameters;
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseLinkerOptions.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseLinkerOptions.cs
new file mode 100644
index 000000000..f13b4eafc
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseLinkerOptions.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestCaseLinkerOptions
+ {
+ public string CoreAssembliesAction;
+ public string UserAssembliesAction;
+ public List<KeyValuePair<string, string>> AssembliesAction = new List<KeyValuePair<string, string>> ();
+
+ public string Il8n;
+ public bool IncludeBlacklistStep;
+ public string KeepTypeForwarderOnlyAssemblies;
+ public string KeepDebugMembers;
+ public string LinkSymbols;
+ public bool SkipUnresolved;
+ public bool StripResources;
+
+ public List<KeyValuePair<string, string[]>> AdditionalArguments = new List<KeyValuePair<string, string[]>> ();
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseMetadaProvider.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseMetadaProvider.cs
new file mode 100644
index 000000000..a41b398ff
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseMetadaProvider.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Mono.Cecil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Cases.Expectations.Metadata;
+using Mono.Linker.Tests.Extensions;
+using Mono.Linker.Tests.TestCases;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestCaseMetadaProvider {
+ protected readonly TestCase _testCase;
+ protected readonly AssemblyDefinition _fullTestCaseAssemblyDefinition;
+ protected readonly TypeDefinition _testCaseTypeDefinition;
+
+ public TestCaseMetadaProvider (TestCase testCase, AssemblyDefinition fullTestCaseAssemblyDefinition)
+ {
+ _testCase = testCase;
+ _fullTestCaseAssemblyDefinition = fullTestCaseAssemblyDefinition;
+ // The test case types are never nested so we don't need to worry about that
+ _testCaseTypeDefinition = fullTestCaseAssemblyDefinition.MainModule.GetType (_testCase.ReconstructedFullTypeName);
+
+ if (_testCaseTypeDefinition == null)
+ throw new InvalidOperationException ($"Could not find the type definition for {_testCase.Name} in {_testCase.SourceFile}");
+ }
+
+ public virtual TestCaseLinkerOptions GetLinkerOptions ()
+ {
+ var tclo = new TestCaseLinkerOptions {
+ Il8n = GetOptionAttributeValue (nameof (Il8nAttribute), "none"),
+ IncludeBlacklistStep = GetOptionAttributeValue (nameof (IncludeBlacklistStepAttribute), false),
+ KeepTypeForwarderOnlyAssemblies = GetOptionAttributeValue (nameof (KeepTypeForwarderOnlyAssembliesAttribute), string.Empty),
+ KeepDebugMembers = GetOptionAttributeValue (nameof (SetupLinkerKeepDebugMembersAttribute), string.Empty),
+ LinkSymbols = GetOptionAttributeValue (nameof (SetupLinkerLinkSymbolsAttribute), string.Empty),
+ CoreAssembliesAction = GetOptionAttributeValue<string> (nameof (SetupLinkerCoreActionAttribute), null),
+ UserAssembliesAction = GetOptionAttributeValue<string> (nameof (SetupLinkerUserActionAttribute), null),
+ SkipUnresolved = GetOptionAttributeValue (nameof (SkipUnresolvedAttribute), false),
+ StripResources = GetOptionAttributeValue (nameof (StripResourcesAttribute), true)
+ };
+
+ foreach (var assemblyAction in _testCaseTypeDefinition.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (SetupLinkerActionAttribute)))
+ {
+ var ca = assemblyAction.ConstructorArguments;
+ tclo.AssembliesAction.Add (new KeyValuePair<string, string> ((string)ca [0].Value, (string)ca [1].Value));
+ }
+
+ foreach (var additionalArgumentAttr in _testCaseTypeDefinition.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (SetupLinkerArgumentAttribute)))
+ {
+ var ca = additionalArgumentAttr.ConstructorArguments;
+ var values = ((CustomAttributeArgument [])ca [1].Value)?.Select (arg => arg.Value.ToString ()).ToArray ();
+ tclo.AdditionalArguments.Add (new KeyValuePair<string, string []> ((string)ca [0].Value, values));
+ }
+
+ return tclo;
+ }
+
+ public virtual IEnumerable<string> GetCommonReferencedAssemblies (NPath workingDirectory)
+ {
+ yield return workingDirectory.Combine ("Mono.Linker.Tests.Cases.Expectations.dll").ToString ();
+ yield return "mscorlib.dll";
+ }
+
+ public virtual IEnumerable<string> GetReferencedAssemblies (NPath workingDirectory)
+ {
+ foreach (var fileName in GetReferenceValues ()) {
+ if (fileName.StartsWith ("System.", StringComparison.Ordinal) || fileName.StartsWith ("Mono.", StringComparison.Ordinal) || fileName.StartsWith ("Microsoft.", StringComparison.Ordinal))
+ yield return fileName;
+ else
+ // Drop any relative path information. Sandboxing will have taken care of copying the reference to the directory
+ yield return workingDirectory.Combine (Path.GetFileName (fileName));
+ }
+ }
+
+ public virtual IEnumerable<string> GetReferenceDependencies ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (ReferenceDependencyAttribute))
+ .Select (attr => (string) attr.ConstructorArguments [0].Value);
+ }
+
+ public virtual IEnumerable<string> GetReferenceValues ()
+ {
+ foreach (var referenceAttr in _testCaseTypeDefinition.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (ReferenceAttribute)))
+ yield return (string) referenceAttr.ConstructorArguments.First ().Value;
+ }
+
+ public virtual IEnumerable<SourceAndDestinationPair> GetResources ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SetupCompileResourceAttribute))
+ .Select (GetSourceAndRelativeDestinationValue);
+ }
+
+ public virtual IEnumerable<SourceAndDestinationPair> GetResponseFiles ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SetupLinkerResponseFileAttribute))
+ .Select (GetSourceAndRelativeDestinationValue);
+ }
+
+ public virtual IEnumerable<NPath> GetExtraLinkerSearchDirectories ()
+ {
+ yield break;
+ }
+
+ public virtual bool IsIgnored (out string reason)
+ {
+ var ignoreAttribute = _testCaseTypeDefinition.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.Name == nameof (IgnoreTestCaseAttribute));
+ if (ignoreAttribute != null) {
+ reason = (string)ignoreAttribute.ConstructorArguments.First ().Value;
+ return true;
+ }
+
+ reason = null;
+ return false;
+ }
+
+ public virtual IEnumerable<SourceAndDestinationPair> AdditionalFilesToSandbox ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SandboxDependencyAttribute))
+ .Select (GetSourceAndRelativeDestinationValue);
+ }
+
+ public virtual IEnumerable<SetupCompileInfo> GetSetupCompileAssembliesBefore ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SetupCompileBeforeAttribute))
+ .Select (CreateSetupCompileAssemblyInfo);
+ }
+
+ public virtual IEnumerable<SetupCompileInfo> GetSetupCompileAssembliesAfter ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SetupCompileAfterAttribute))
+ .Select (CreateSetupCompileAssemblyInfo);
+ }
+
+ public virtual IEnumerable<string> GetDefines ()
+ {
+ // There are a few tests related to native pdbs where the assertions are different between windows and non-windows
+ // To enable test cases to define different expected behavior we set this special define
+ if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+ yield return "WIN32";
+
+ foreach (var attr in _testCaseTypeDefinition.CustomAttributes.Where (attr => attr.AttributeType.Name == nameof (DefineAttribute)))
+ yield return (string) attr.ConstructorArguments.First ().Value;
+ }
+
+ public virtual bool LinkPublicAndFamily()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .FirstOrDefault (attr => attr.AttributeType.Name == nameof (SetupLinkerLinkPublicAndFamilyAttribute)) != null;
+ }
+
+ public virtual string GetAssemblyName ()
+ {
+ var asLibraryAttribute = _testCaseTypeDefinition.CustomAttributes
+ .FirstOrDefault (attr => attr.AttributeType.Name == nameof (SetupCompileAsLibraryAttribute));
+ var defaultName = asLibraryAttribute == null ? "test.exe" : "test.dll";
+ return GetOptionAttributeValue (nameof (SetupCompileAssemblyNameAttribute), defaultName);
+ }
+
+ public virtual string GetCSharpCompilerToUse ()
+ {
+ return GetOptionAttributeValue (nameof (SetupCSharpCompilerToUseAttribute), string.Empty).ToLower ();
+ }
+
+ public virtual IEnumerable<string> GetSetupCompilerArguments ()
+ {
+ return _testCaseTypeDefinition.CustomAttributes
+ .Where (attr => attr.AttributeType.Name == nameof (SetupCompileArgumentAttribute))
+ .Select (attr => (string) attr.ConstructorArguments.First ().Value);
+ }
+
+ T GetOptionAttributeValue<T> (string attributeName, T defaultValue)
+ {
+ var attribute = _testCaseTypeDefinition.CustomAttributes.FirstOrDefault (attr => attr.AttributeType.Name == attributeName);
+ if (attribute != null)
+ return (T) attribute.ConstructorArguments.First ().Value;
+
+ return defaultValue;
+ }
+
+ SourceAndDestinationPair GetSourceAndRelativeDestinationValue (CustomAttribute attribute)
+ {
+ var fullSource = SourceFileForAttributeArgumentValue (attribute.ConstructorArguments.First ().Value);
+ var destinationFileName = (string) attribute.ConstructorArguments [1].Value;
+ return new SourceAndDestinationPair
+ {
+ Source = fullSource,
+ DestinationFileName = string.IsNullOrEmpty (destinationFileName) ? fullSource.FileName : destinationFileName
+ };
+ }
+
+ private SetupCompileInfo CreateSetupCompileAssemblyInfo (CustomAttribute attribute)
+ {
+ var ctorArguments = attribute.ConstructorArguments;
+ return new SetupCompileInfo
+ {
+ OutputName = (string) ctorArguments [0].Value,
+ SourceFiles = SourceFilesForAttributeArgument (ctorArguments [1]),
+ References = ((CustomAttributeArgument []) ctorArguments [2].Value)?.Select (arg => arg.Value.ToString ()).ToArray (),
+ Defines = ((CustomAttributeArgument []) ctorArguments [3].Value)?.Select (arg => arg.Value.ToString ()).ToArray (),
+ Resources = ((CustomAttributeArgument []) ctorArguments [4].Value)?.Select (arg => MakeSourceTreeFilePathAbsolute (arg.Value.ToString ())).ToArray (),
+ AdditionalArguments = (string) ctorArguments [5].Value,
+ CompilerToUse = (string) ctorArguments [6].Value,
+ AddAsReference = ctorArguments.Count >= 8 ? (bool) ctorArguments [7].Value : true
+ };
+ }
+
+ protected NPath MakeSourceTreeFilePathAbsolute (string value)
+ {
+ return _testCase.SourceFile.Parent.Combine (value);
+ }
+
+ protected NPath[] SourceFilesForAttributeArgument (CustomAttributeArgument attributeArgument)
+ {
+ return ((CustomAttributeArgument []) attributeArgument.Value)
+ .Select (attributeArg => SourceFileForAttributeArgumentValue (attributeArg.Value))
+ .Distinct ()
+ .ToArray ();
+ }
+
+ protected virtual NPath SourceFileForAttributeArgumentValue (object value)
+ {
+ var valueAsTypeRef = value as TypeReference;
+ if (valueAsTypeRef != null) {
+ // Use the parent type for locating the source file
+ var parentType = ParentMostType (valueAsTypeRef);
+ var pathRelativeToAssembly = $"{parentType.FullName.Substring (parentType.Module.Name.Length - 3).Replace ('.', '/')}.cs".ToNPath ();
+ var pathElements = pathRelativeToAssembly.Elements.ToArray ();
+ var topMostDirectoryName = pathElements [0];
+ var topMostDirectory = _testCase.SourceFile.RecursiveParents.Reverse ().FirstOrDefault (d => !d.IsRoot && d.FileName == topMostDirectoryName);
+
+ if (topMostDirectory == null) {
+ // Before giving up, try and detect the naming scheme for tests that use a dot in the top level directory name.
+ // Ex:
+ // Attributes.Debugger
+ // + 1 because the file name is one of the elements
+ if (pathElements.Length >= 3) {
+ topMostDirectoryName = $"{pathElements[0]}.{pathElements[1]}";
+ topMostDirectory = _testCase.SourceFile.RecursiveParents.Reverse ().FirstOrDefault (d => !d.IsRoot && d.FileName == topMostDirectoryName);
+ pathRelativeToAssembly = topMostDirectoryName.ToNPath ().Combine (pathElements.Skip (2).Aggregate (new NPath (string.Empty), (path, s) => path.Combine (s)));
+ }
+
+ if (topMostDirectory == null)
+ throw new ArgumentException ($"Unable to locate the source file for type {valueAsTypeRef}. Could not locate directory {topMostDirectoryName}. Ensure the type name matches the file name. And the namespace match the directory structure on disk");
+ }
+
+ var fullPath = topMostDirectory.Parent.Combine (pathRelativeToAssembly);
+
+ if (!fullPath.Exists ())
+ throw new ArgumentException ($"Unable to locate the source file for type {valueAsTypeRef}. Expected {fullPath}. Ensure the type name matches the file name. And the namespace match the directory structure on disk");
+
+ return fullPath;
+ }
+
+ return MakeSourceTreeFilePathAbsolute (value.ToString ());
+ }
+
+ static TypeReference ParentMostType (TypeReference type)
+ {
+ if (!type.IsNested)
+ return type;
+
+ return ParentMostType (type.DeclaringType);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestCaseSandbox.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseSandbox.cs
new file mode 100644
index 000000000..5ec502878
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestCaseSandbox.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Extensions;
+using Mono.Linker.Tests.TestCases;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestCaseSandbox {
+ protected readonly TestCase _testCase;
+ protected readonly NPath _directory;
+
+ public TestCaseSandbox (TestCase testCase)
+ : this (testCase, NPath.SystemTemp)
+ {
+ }
+
+ public TestCaseSandbox (TestCase testCase, NPath rootTemporaryDirectory)
+ : this (testCase, rootTemporaryDirectory, string.Empty)
+ {
+ }
+
+ public TestCaseSandbox (TestCase testCase, string rootTemporaryDirectory, string namePrefix)
+ : this (testCase, rootTemporaryDirectory.ToNPath (), namePrefix)
+ {
+ }
+
+ public TestCaseSandbox (TestCase testCase, NPath rootTemporaryDirectory, string namePrefix)
+ {
+ _testCase = testCase;
+ var name = string.IsNullOrEmpty (namePrefix) ? "linker_tests" : $"{namePrefix}_linker_tests";
+ _directory = rootTemporaryDirectory.Combine (name);
+
+ _directory.DeleteContents ();
+
+ InputDirectory = _directory.Combine ("input").EnsureDirectoryExists ();
+ OutputDirectory = _directory.Combine ("output").EnsureDirectoryExists ();
+ ExpectationsDirectory = _directory.Combine ("expectations").EnsureDirectoryExists ();
+ ResourcesDirectory = _directory.Combine ("resources").EnsureDirectoryExists ();
+ }
+
+ public NPath InputDirectory { get; }
+
+ public NPath OutputDirectory { get; }
+
+ public NPath ExpectationsDirectory { get; }
+
+ public NPath ResourcesDirectory { get; }
+
+ public IEnumerable<NPath> SourceFiles {
+ get { return _directory.Files ("*.cs"); }
+ }
+
+ public IEnumerable<NPath> LinkXmlFiles {
+ get { return InputDirectory.Files ("*.xml"); }
+ }
+
+ public IEnumerable<NPath> ResponseFiles {
+ get { return InputDirectory.Files ("*.rsp"); }
+ }
+
+ public IEnumerable<NPath> ResourceFiles => ResourcesDirectory.Files ();
+
+ public virtual void Populate (TestCaseMetadaProvider metadataProvider)
+ {
+ _testCase.SourceFile.Copy (_directory);
+
+ if (_testCase.HasLinkXmlFile)
+ _testCase.LinkXmlFile.Copy (InputDirectory);
+
+ CopyToInputAndExpectations (GetExpectationsAssemblyPath ());
+
+ foreach (var dep in metadataProvider.AdditionalFilesToSandbox ()) {
+ var destination = _directory.Combine (dep.DestinationFileName);
+ dep.Source.FileMustExist ().Copy (destination);
+
+ // In a few niche tests we need to copy pre-built assemblies directly into the input directory.
+ // When this is done, we also need to copy them into the expectations directory so that if they are used
+ // as references we can still compile the expectations version of the assemblies
+ if (destination.Parent == InputDirectory)
+ dep.Source.Copy (ExpectationsDirectory.Combine (destination.RelativeTo (InputDirectory)));
+ }
+
+ // Copy non class library dependencies to the sandbox
+ foreach (var fileName in metadataProvider.GetReferenceValues ()) {
+ if (!fileName.StartsWith ("System.", StringComparison.Ordinal) && !fileName.StartsWith ("Mono.", StringComparison.Ordinal) && !fileName.StartsWith ("Microsoft.", StringComparison.Ordinal))
+ CopyToInputAndExpectations (_testCase.SourceFile.Parent.Combine (fileName.ToNPath ()));
+ }
+
+ foreach (var referenceDependency in metadataProvider.GetReferenceDependencies ())
+ CopyToInputAndExpectations (_testCase.SourceFile.Parent.Combine (referenceDependency.ToNPath()));
+
+ foreach (var res in metadataProvider.GetResources ()) {
+ res.Source.FileMustExist ().Copy (ResourcesDirectory.Combine (res.DestinationFileName));
+ }
+
+ foreach (var res in metadataProvider.GetResponseFiles()) {
+ res.Source.FileMustExist ().Copy (InputDirectory.Combine (res.DestinationFileName));
+ }
+
+ foreach (var compileRefInfo in metadataProvider.GetSetupCompileAssembliesBefore ())
+ {
+ var destination = BeforeReferenceSourceDirectoryFor (compileRefInfo.OutputName).EnsureDirectoryExists ();
+ compileRefInfo.SourceFiles.Copy (destination);
+
+ destination = BeforeReferenceResourceDirectoryFor (compileRefInfo.OutputName).EnsureDirectoryExists ();
+ compileRefInfo.Resources?.Copy (destination);
+ }
+
+ foreach (var compileRefInfo in metadataProvider.GetSetupCompileAssembliesAfter ())
+ {
+ var destination = AfterReferenceSourceDirectoryFor (compileRefInfo.OutputName).EnsureDirectoryExists ();
+ compileRefInfo.SourceFiles.Copy (destination);
+
+ destination = AfterReferenceResourceDirectoryFor (compileRefInfo.OutputName).EnsureDirectoryExists ();
+ compileRefInfo.Resources?.Copy (destination);
+ }
+ }
+
+ private static NPath GetExpectationsAssemblyPath ()
+ {
+ return new Uri (typeof (KeptAttribute).Assembly.CodeBase).LocalPath.ToNPath ();
+ }
+
+ protected void CopyToInputAndExpectations (NPath source)
+ {
+ source.Copy (InputDirectory);
+ source.Copy (ExpectationsDirectory);
+ }
+
+ public NPath BeforeReferenceSourceDirectoryFor (string outputName)
+ {
+ return _directory.Combine ($"ref_source_before_{Path.GetFileNameWithoutExtension (outputName)}");
+ }
+
+ public NPath AfterReferenceSourceDirectoryFor (string outputName)
+ {
+ return _directory.Combine ($"ref_source_after_{Path.GetFileNameWithoutExtension (outputName)}");
+ }
+
+ public NPath BeforeReferenceResourceDirectoryFor (string outputName)
+ {
+ return _directory.Combine ($"ref_resource_before_{Path.GetFileNameWithoutExtension (outputName)}");
+ }
+
+ public NPath AfterReferenceResourceDirectoryFor (string outputName)
+ {
+ return _directory.Combine ($"ref_resource_after_{Path.GetFileNameWithoutExtension (outputName)}");
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs b/test/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs
new file mode 100644
index 000000000..fc421882e
--- /dev/null
+++ b/test/Mono.Linker.Tests/TestCasesRunner/TestRunner.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Mono.Cecil;
+using Mono.Linker.Tests.Extensions;
+using Mono.Linker.Tests.TestCases;
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests.TestCasesRunner {
+ public class TestRunner {
+ private readonly ObjectFactory _factory;
+
+ public TestRunner (ObjectFactory factory)
+ {
+ _factory = factory;
+ }
+
+ public virtual LinkedTestCaseResult Run (TestCase testCase)
+ {
+ using (var fullTestCaseAssemblyDefinition = AssemblyDefinition.ReadAssembly (testCase.OriginalTestCaseAssemblyPath.ToString ())) {
+ var metadataProvider = _factory.CreateMetadataProvider (testCase, fullTestCaseAssemblyDefinition);
+
+ string ignoreReason;
+ if (metadataProvider.IsIgnored (out ignoreReason))
+ Assert.Ignore (ignoreReason);
+
+ var sandbox = Sandbox (testCase, metadataProvider);
+ var compilationResult = Compile (sandbox, metadataProvider);
+ PrepForLink (sandbox, compilationResult);
+ return Link (testCase, sandbox, compilationResult, metadataProvider);
+ }
+ }
+
+ private TestCaseSandbox Sandbox (TestCase testCase, TestCaseMetadaProvider metadataProvider)
+ {
+ var sandbox = _factory.CreateSandbox (testCase);
+ sandbox.Populate (metadataProvider);
+ return sandbox;
+ }
+
+ private ManagedCompilationResult Compile (TestCaseSandbox sandbox, TestCaseMetadaProvider metadataProvider)
+ {
+ var inputCompiler = _factory.CreateCompiler (sandbox, metadataProvider);
+ var expectationsCompiler = _factory.CreateCompiler (sandbox, metadataProvider);
+ var sourceFiles = sandbox.SourceFiles.Select(s => s.ToString()).ToArray();
+
+ var assemblyName = metadataProvider.GetAssemblyName ();
+
+ var commonReferences = metadataProvider.GetCommonReferencedAssemblies(sandbox.InputDirectory).ToArray ();
+ var mainAssemblyReferences = metadataProvider.GetReferencedAssemblies(sandbox.InputDirectory).ToArray ();
+ var resources = sandbox.ResourceFiles.ToArray ();
+ var additionalArguments = metadataProvider.GetSetupCompilerArguments ().ToArray ();
+
+ var expectationsCommonReferences = metadataProvider.GetCommonReferencedAssemblies (sandbox.ExpectationsDirectory).ToArray ();
+ var expectationsMainAssemblyReferences = metadataProvider.GetReferencedAssemblies (sandbox.ExpectationsDirectory).ToArray ();
+
+ var inputTask = Task.Run(() => inputCompiler.CompileTestIn (sandbox.InputDirectory, assemblyName, sourceFiles, commonReferences, mainAssemblyReferences, null, resources, additionalArguments));
+ var expectationsTask = Task.Run(() => expectationsCompiler.CompileTestIn (sandbox.ExpectationsDirectory, assemblyName, sourceFiles, expectationsCommonReferences, expectationsMainAssemblyReferences, new[] {"INCLUDE_EXPECTATIONS"}, resources, additionalArguments));
+
+ NPath inputAssemblyPath = null;
+ NPath expectationsAssemblyPath = null;
+ try {
+ inputAssemblyPath = GetResultOfTaskThatMakesNUnitAssertions (inputTask);
+ expectationsAssemblyPath = GetResultOfTaskThatMakesNUnitAssertions (expectationsTask);
+ } catch (Exception) {
+ // If completing the input assembly task threw, we need to wait for the expectations task to complete before continuing
+ // otherwise we could set the next test up for a race condition with the expectations compilation over access to the sandbox directory
+ if (inputAssemblyPath == null && expectationsAssemblyPath == null)
+ {
+ try {
+ expectationsTask.Wait ();
+ } catch (Exception) {
+ // Don't care, we want to throw the first exception
+ }
+ }
+
+ throw;
+ }
+ return new ManagedCompilationResult (inputAssemblyPath, expectationsAssemblyPath);
+ }
+
+ protected virtual void PrepForLink (TestCaseSandbox sandbox, ManagedCompilationResult compilationResult)
+ {
+ }
+
+ private LinkedTestCaseResult Link (TestCase testCase, TestCaseSandbox sandbox, ManagedCompilationResult compilationResult, TestCaseMetadaProvider metadataProvider)
+ {
+ var linker = _factory.CreateLinker ();
+ var builder = _factory.CreateLinkerArgumentBuilder (metadataProvider);
+
+ AddLinkOptions (sandbox, compilationResult, builder, metadataProvider);
+
+ linker.Link (builder.ToArgs ());
+
+ return new LinkedTestCaseResult (testCase, compilationResult.InputAssemblyPath, sandbox.OutputDirectory.Combine (compilationResult.InputAssemblyPath.FileName), compilationResult.ExpectationsAssemblyPath);
+ }
+
+ protected virtual void AddLinkOptions (TestCaseSandbox sandbox, ManagedCompilationResult compilationResult, LinkerArgumentBuilder builder, TestCaseMetadaProvider metadataProvider)
+ {
+ var caseDefinedOptions = metadataProvider.GetLinkerOptions ();
+
+ builder.AddOutputDirectory (sandbox.OutputDirectory);
+ foreach (var linkXmlFile in sandbox.LinkXmlFiles)
+ builder.AddLinkXmlFile (linkXmlFile);
+
+ foreach (var linkXmlFile in sandbox.ResponseFiles)
+ builder.AddResponseFile (linkXmlFile);
+
+ builder.AddSearchDirectory (sandbox.InputDirectory);
+ foreach (var extraSearchDir in metadataProvider.GetExtraLinkerSearchDirectories ())
+ builder.AddSearchDirectory (extraSearchDir);
+
+ builder.ProcessOptions (caseDefinedOptions);
+
+ builder.ProcessTestInputAssembly (compilationResult.InputAssemblyPath);
+ }
+
+ private T GetResultOfTaskThatMakesNUnitAssertions<T> (Task<T> task)
+ {
+ try {
+ return task.Result;
+ } catch (AggregateException e) {
+ if (e.InnerException != null) {
+ if (e.InnerException is AssertionException
+ || e.InnerException is SuccessException
+ || e.InnerException is IgnoreException
+ || e.InnerException is InconclusiveException)
+ throw e.InnerException;
+ }
+
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/Tests/ParseResponseFileLinesTests.cs b/test/Mono.Linker.Tests/Tests/ParseResponseFileLinesTests.cs
new file mode 100644
index 000000000..c8c407520
--- /dev/null
+++ b/test/Mono.Linker.Tests/Tests/ParseResponseFileLinesTests.cs
@@ -0,0 +1,69 @@
+using NUnit.Framework;
+using System.Collections.Generic;
+
+namespace Mono.Linker.Tests {
+ [TestFixture]
+ public class ParseResponseFileLinesTests {
+ [Test]
+ public void TestOneArg ()
+ {
+ TestParseResponseFileLines (@"abc", new string [] { @"abc" });
+ }
+
+ [Test]
+ public void TestTwoArgsOnOneLine ()
+ {
+ TestParseResponseFileLines (@"abc def", new string [] { @"abc", @"def" });
+ }
+
+ [Test]
+ public void TestTwoArgsOnTwoLine ()
+ {
+ TestParseResponseFileLines (@"abc
+def", new string [] { @"abc", @"def" });
+ }
+
+ [Test]
+ public void TestOneSlashWithoutQuote ()
+ {
+ TestParseResponseFileLines (@"\", new string [] { @"\" });
+ }
+
+ [Test]
+ public void TestTwoSlashesWithoutQuote ()
+ {
+ TestParseResponseFileLines (@"\\", new string [] { @"\\" });
+ }
+
+ [Test]
+ public void TestOneSlashWithQuote ()
+ {
+ TestParseResponseFileLines (@"""x \"" y""", new string [] { @"x "" y" });
+ }
+
+ [Test]
+ public void TestTwoSlashesWithQuote ()
+ {
+ TestParseResponseFileLines (@"""Trailing Slash\\""", new string [] { @"Trailing Slash\" });
+ }
+
+ [Test]
+ public void TestWindowsPath ()
+ {
+ TestParseResponseFileLines (@"C:\temp\test.txt", new string [] { @"C:\temp\test.txt" });
+ }
+
+ [Test]
+ public void TestLinuxPath ()
+ {
+ TestParseResponseFileLines (@"/tmp/test.txt", new string [] { @"/tmp/test.txt" });
+ }
+
+ private void TestParseResponseFileLines (string v1, string [] v2)
+ {
+ var result = new Queue<string> ();
+ Driver.ParseResponseFileLines (v1.Split ('\n'), result);
+ Assert.That (result, Is.EquivalentTo (v2));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/Tests/PreserveActionComparisonTests.cs b/test/Mono.Linker.Tests/Tests/PreserveActionComparisonTests.cs
new file mode 100644
index 000000000..21c881468
--- /dev/null
+++ b/test/Mono.Linker.Tests/Tests/PreserveActionComparisonTests.cs
@@ -0,0 +1,30 @@
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests
+{
+ [TestFixture]
+ public class PreserveActionComparisonTests
+ {
+ [TestCase (TypePreserve.All, TypePreserve.All, TypePreserve.All)]
+ [TestCase (TypePreserve.All, TypePreserve.Methods, TypePreserve.All)]
+ [TestCase (TypePreserve.All, TypePreserve.Fields, TypePreserve.All)]
+ [TestCase (TypePreserve.All, TypePreserve.Nothing, TypePreserve.All)]
+ [TestCase (TypePreserve.Methods, TypePreserve.All, TypePreserve.All)]
+ [TestCase (TypePreserve.Methods, TypePreserve.Methods, TypePreserve.Methods)]
+ [TestCase (TypePreserve.Methods, TypePreserve.Fields, TypePreserve.All)]
+ [TestCase (TypePreserve.Methods, TypePreserve.Nothing, TypePreserve.Methods)]
+ [TestCase (TypePreserve.Fields, TypePreserve.All, TypePreserve.All)]
+ [TestCase (TypePreserve.Fields, TypePreserve.Methods, TypePreserve.All)]
+ [TestCase (TypePreserve.Fields, TypePreserve.Fields, TypePreserve.Fields)]
+ [TestCase (TypePreserve.Fields, TypePreserve.Nothing, TypePreserve.Fields)]
+ [TestCase (TypePreserve.Nothing, TypePreserve.All, TypePreserve.All)]
+ [TestCase (TypePreserve.Nothing, TypePreserve.Methods, TypePreserve.Methods)]
+ [TestCase (TypePreserve.Nothing, TypePreserve.Fields, TypePreserve.Fields)]
+ [TestCase (TypePreserve.Nothing, TypePreserve.Nothing, TypePreserve.Nothing)]
+ public void VerifyBehaviorOfChoosePreserveActionWhichPreservesTheMost (TypePreserve left, TypePreserve right, TypePreserve expected)
+ {
+ Assert.That (expected, Is.EqualTo (AnnotationStore.ChoosePreserveActionWhichPreservesTheMost (left, right)));
+ Assert.That (expected, Is.EqualTo (AnnotationStore.ChoosePreserveActionWhichPreservesTheMost (right, left)));
+ }
+ }
+}
diff --git a/test/Mono.Linker.Tests/Tests/TypeNameParserTests.cs b/test/Mono.Linker.Tests/Tests/TypeNameParserTests.cs
new file mode 100644
index 000000000..b5d124619
--- /dev/null
+++ b/test/Mono.Linker.Tests/Tests/TypeNameParserTests.cs
@@ -0,0 +1,121 @@
+using NUnit.Framework;
+
+namespace Mono.Linker.Tests {
+ [TestFixture]
+ public class TypeNameParserTests {
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_Null ()
+ {
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (null, out string typeName, out string assemblyName), Is.False);
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_FullyQualified ()
+ {
+ var value = typeof (TypeNameParserTests).AssemblyQualifiedName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo (typeof (TypeNameParserTests).FullName));
+ Assert.That (assemblyName, Is.EqualTo (typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_NameAndAssemblyOnly ()
+ {
+ var value = $"{typeof (TypeNameParserTests).FullName}, {typeof (TypeNameParserTests).Assembly.GetName ().Name}";
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo (typeof (TypeNameParserTests).FullName));
+ Assert.That (assemblyName, Is.EqualTo (typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_NameOnly ()
+ {
+ var value = typeof (TypeNameParserTests).FullName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo (typeof (TypeNameParserTests).FullName));
+ Assert.That (assemblyName, Is.Null);
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_GenericType_FullyQualified ()
+ {
+ var value = typeof (SampleGenericType<,>).AssemblyQualifiedName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo($"{typeof (TypeNameParserTests).FullName}/SampleGenericType`2"));
+ Assert.That (assemblyName, Is.EqualTo(typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_GenericType_NameAndAssemblyOnly ()
+ {
+ var value = $"{typeof (SampleGenericType<,>).FullName}, {typeof (TypeNameParserTests).Assembly.GetName ().Name}";
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo ($"{typeof (TypeNameParserTests).FullName}/SampleGenericType`2"));
+ Assert.That (assemblyName, Is.EqualTo (typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_GenericType_NameOnly ()
+ {
+ var value = typeof (SampleGenericType<,>).FullName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo ($"{typeof (TypeNameParserTests).FullName}/SampleGenericType`2"));
+ Assert.That (assemblyName, Is.Null);
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_NestedType_FullyQualified ()
+ {
+ var value = typeof (SampleNestedType).AssemblyQualifiedName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo($"{typeof (TypeNameParserTests).FullName}/{nameof (SampleNestedType)}"));
+ Assert.That (assemblyName, Is.EqualTo(typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_NestedType_NameAndAssemblyOnly ()
+ {
+ var value = $"{typeof (SampleNestedType).FullName}, {typeof (TypeNameParserTests).Assembly.GetName().Name}";
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo ($"{typeof (TypeNameParserTests).FullName}/{nameof (SampleNestedType)}"));
+ Assert.That (assemblyName, Is.EqualTo (typeof (TypeNameParserTests).Assembly.GetName ().Name));
+ }
+
+ [Test]
+ public void TryParseTypeAssemblyQualifiedName_NestedType_NameOnly ()
+ {
+ var value = typeof (SampleNestedType).FullName;
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (value, out string typeName, out string assemblyName), Is.True);
+ Assert.That (typeName, Is.EqualTo ($"{typeof (TypeNameParserTests).FullName}/{nameof (SampleNestedType)}"));
+ Assert.That (assemblyName, Is.Null);
+ }
+
+ [Test]
+ public void MissingTypeName ()
+ {
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (", System", out string typeName, out string assemblyName), Is.False);
+ Assert.That (typeName, Is.Null);
+ Assert.That (assemblyName, Is.Null);
+ }
+
+
+ [TestCase ("A[]][")]
+ [TestCase ("A][")]
+ [TestCase ("A[")]
+ [TestCase (", , ")]
+ [TestCase (", , , ")]
+ [TestCase (", , , , ")]
+ public void InvalidValues (string name)
+ {
+ Assert.That (TypeNameParser.TryParseTypeAssemblyQualifiedName (name, out string typeName, out string assemblyName), Is.False);
+ Assert.That (typeName, Is.Null);
+ Assert.That (assemblyName, Is.Null);
+ }
+
+ class SampleNestedType {
+ }
+
+ class SampleGenericType<T1, T2> {
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Mono.Linker.Tests/packages.config b/test/Mono.Linker.Tests/packages.config
new file mode 100644
index 000000000..e49d840c2
--- /dev/null
+++ b/test/Mono.Linker.Tests/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="NUnit" version="3.6.1" targetFramework="net462" />
+ <package id="NUnit.ConsoleRunner" version="3.6.1" targetFramework="net462" />
+ <package id="NUnit.Extension.NUnitV2ResultWriter" version="3.5.0" targetFramework="net462" />
+</packages>