Implement AssemblyDependencyResolver (#21896)
authorJeremy Koritzinsky <jkoritzinsky@gmail.com>
Thu, 10 Jan 2019 00:02:36 +0000 (16:02 -0800)
committerGitHub <noreply@github.com>
Thu, 10 Jan 2019 00:02:36 +0000 (16:02 -0800)
* Implementation of ComponentDependencyResolver

PInvokes into hostpolicy.dll (which should live next to the runtime and thus always be reachable).
If the PInvoke fails (missing hostpolicy.dll) we will fail for now.

Adds tests for the API into CoreCLR repo. The main reason is that with corerun
we can easily mock the hostpolicy.dll since there's none to start with.
Writing the same tests in CoreFX or any other place which starts the runtime through
hostpolicy would require test-only functionality to exist in either the class itself
or in the hostpolicy.

* Fix test project file to work outside of VS

* Better test cleanup to not leave artifacts on disk.

* CDR native resolution tests

 Add native resolution tests

* Implements detailed error reporting for ComponentDependencyResolver.

Registers error writer with the hostpolicy to receive detailed errors. Uses that in the exception.

Modifications to the mock and the tests to be able to verify the functionality.

* Revert overly eager cleanup

* Change public API surface naming to match the approved API surface.

* Fix nits.

* Fix renames.

13 files changed:
src/System.Private.CoreLib/Resources/Strings.resx
src/System.Private.CoreLib/System.Private.CoreLib.csproj
src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.csproj [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/CMakeLists.txt [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/HostPolicyMock.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/HostpolicyMock.cpp [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/InvalidHostingTest.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/NativeDependencyTests.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/TestBase.cs [new file with mode: 0644]
tests/src/Loader/AssemblyDependencyResolverTests/XPlatformUtils.cs [new file with mode: 0644]

index 5b28972..b13bce2 100644 (file)
   <data name="Argument_PrecisionTooLarge" xml:space="preserve">
     <value>Precision cannot be larger than {0}.</value>
   </data>
+  <data name="AssemblyDependencyResolver_FailedToLoadHostpolicy" xml:space="preserve">
+    <value>Cannot load hostpolicy library. AssemblyDependencyResolver is currently only supported if the runtime is hosted through hostpolicy library.</value>
+  </data>
+  <data name="AssemblyDependencyResolver_FailedToResolveDependencies" xml:space="preserve">
+    <value>Dependency resolution failed for component {0} with error code {1}. Detailed error: {2}</value>
+  </data>
   <data name="Arg_EnumNotCloneable" xml:space="preserve">
     <value>The supplied object does not implement ICloneable.</value>
   </data>
index 0be3c65..779688e 100644 (file)
     <Compile Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SafeHandle.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\InteropServices\SEHException.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\Loader\AssemblyLoadContext.cs" />
+    <Compile Include="$(BclSourcesRoot)\System\Runtime\Loader\AssemblyDependencyResolver.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\MemoryFailPoint.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\Serialization\FormatterServices.cs" />
     <Compile Include="$(BclSourcesRoot)\System\Runtime\Versioning\CompatibilitySwitch.cs" />
diff --git a/src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs b/src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs
new file mode 100644 (file)
index 0000000..a05b71c
--- /dev/null
@@ -0,0 +1,312 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using Internal.IO;
+
+namespace System.Runtime.Loader
+{
+    public sealed class AssemblyDependencyResolver
+    {
+        /// <summary>
+        /// The name of the neutral culture (same value as in Variables::Init in CoreCLR)
+        /// </summary>
+        private const string NeutralCultureName = "neutral";
+
+        /// <summary>
+        /// The extension of resource assembly (same as in BindSatelliteResourceByResourceRoots in CoreCLR)
+        /// </summary>
+        private const string ResourceAssemblyExtension = ".dll";
+
+        private readonly Dictionary<string, string> _assemblyPaths;
+        private readonly string[] _nativeSearchPaths;
+        private readonly string[] _resourceSearchPaths;
+        private readonly string[] _assemblyDirectorySearchPaths;
+
+        public AssemblyDependencyResolver(string componentAssemblyPath)
+        {
+            string assemblyPathsList = null;
+            string nativeSearchPathsList = null;
+            string resourceSearchPathsList = null;
+            int returnCode = 0;
+
+            StringBuilder errorMessage = new StringBuilder();
+            try
+            {
+                // Setup error writer for this thread. This makes the hostpolicy redirect all error output
+                // to the writer specified. Have to store the previous writer to set it back once this is done.
+                corehost_error_writer_fn errorWriter = new corehost_error_writer_fn(message =>
+                {
+                    errorMessage.AppendLine(message);
+                });
+
+                IntPtr errorWriterPtr = Marshal.GetFunctionPointerForDelegate(errorWriter);
+                IntPtr previousErrorWriterPtr = corehost_set_error_writer(errorWriterPtr);
+
+                try
+                {
+                    // Call hostpolicy to do the actual work of finding .deps.json, parsing it and extracting
+                    // information from it.
+                    returnCode = corehost_resolve_component_dependencies(
+                        componentAssemblyPath,
+                        (assembly_paths, native_search_paths, resource_search_paths) =>
+                        {
+                            assemblyPathsList = assembly_paths;
+                            nativeSearchPathsList = native_search_paths;
+                            resourceSearchPathsList = resource_search_paths;
+                        });
+                }
+                finally
+                {
+                    // Reset the error write to the one used before
+                    corehost_set_error_writer(previousErrorWriterPtr);
+                }
+            }
+            catch (EntryPointNotFoundException entryPointNotFoundException)
+            {
+                throw new InvalidOperationException(SR.AssemblyDependencyResolver_FailedToLoadHostpolicy, entryPointNotFoundException);
+            }
+            catch (DllNotFoundException dllNotFoundException)
+            {
+                throw new InvalidOperationException(SR.AssemblyDependencyResolver_FailedToLoadHostpolicy, dllNotFoundException);
+            }
+
+            if (returnCode != 0)
+            {
+                // Something went wrong - report a failure
+                throw new InvalidOperationException(SR.Format(
+                    SR.AssemblyDependencyResolver_FailedToResolveDependencies,
+                    componentAssemblyPath,
+                    returnCode,
+                    errorMessage.ToString()));
+            }
+
+            string[] assemblyPaths = SplitPathsList(assemblyPathsList);
+
+            // Assembly simple names are case insensitive per the runtime behavior
+            // (see SimpleNameToFileNameMapTraits for the TPA lookup hash).
+            _assemblyPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            foreach (string assemblyPath in assemblyPaths)
+            {
+                _assemblyPaths.Add(Path.GetFileNameWithoutExtension(assemblyPath), assemblyPath);
+            }
+
+            _nativeSearchPaths = SplitPathsList(nativeSearchPathsList);
+            _resourceSearchPaths = SplitPathsList(resourceSearchPathsList);
+
+            _assemblyDirectorySearchPaths = new string[1] { Path.GetDirectoryName(componentAssemblyPath) };
+        }
+
+        public string ResolveAssemblyToPath(AssemblyName assemblyName)
+        {
+            // Determine if the assembly name is for a satellite assembly or not
+            // This is the same logic as in AssemblyBinder::BindByTpaList in CoreCLR
+            // - If the culture name is non-empty and it's not 'neutral' 
+            // - The culture name is the value of the AssemblyName.Culture.Name 
+            //     (CoreCLR gets this and stores it as the culture name in the internal assembly name)
+            //     AssemblyName.CultureName is just a shortcut to AssemblyName.Culture.Name.
+            if (!string.IsNullOrEmpty(assemblyName.CultureName) && 
+                !string.Equals(assemblyName.CultureName, NeutralCultureName, StringComparison.OrdinalIgnoreCase))
+            {
+                // Load satellite assembly
+                // Search resource search paths by appending the culture name and the expected assembly file name.
+                // Copies the logic in BindSatelliteResourceByResourceRoots in CoreCLR.
+                // Note that the runtime will also probe APP_PATHS the same way, but that feature is effectively 
+                // being deprecated, so we chose to not support the same behavior for components.
+                foreach (string searchPath in _resourceSearchPaths)
+                {
+                    string assemblyPath = Path.Combine(
+                        searchPath,
+                        assemblyName.CultureName,
+                        assemblyName.Name + ResourceAssemblyExtension);
+                    if (File.Exists(assemblyPath))
+                    {
+                        return assemblyPath;
+                    }
+                }
+            }
+            else
+            {
+                // Load code assembly - simply look it up in the dictionary by its simple name.
+                if (_assemblyPaths.TryGetValue(assemblyName.Name, out string assemblyPath))
+                {
+                    // Only returnd the assembly if it exists on disk - this is to make the behavior of the API
+                    // consistent. Resource and native resolutions will only return existing files
+                    // so assembly resolution should do the same.
+                    if (File.Exists(assemblyPath))
+                    {
+                        return assemblyPath;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        public string ResolveUnmanagedDllToPath(string unmanagedDllName)
+        {
+            string[] searchPaths;
+            if (unmanagedDllName.Contains(Path.DirectorySeparatorChar))
+            {
+                // Library names with absolute or relative path can't be resolved
+                // using the component .deps.json as that defines simple names.
+                // So instead use the component directory as the lookup path.
+                searchPaths = _assemblyDirectorySearchPaths;
+            }
+            else
+            {
+                searchPaths = _nativeSearchPaths;
+            }
+
+            bool isRelativePath = !Path.IsPathFullyQualified(unmanagedDllName);
+            foreach (LibraryNameVariation libraryNameVariation in DetermineLibraryNameVariations(unmanagedDllName, isRelativePath))
+            {
+                string libraryName = libraryNameVariation.Prefix + unmanagedDllName + libraryNameVariation.Suffix;
+                foreach (string searchPath in searchPaths)
+                {
+                    string libraryPath = Path.Combine(searchPath, libraryName);
+                    if (File.Exists(libraryPath))
+                    {
+                        return libraryPath;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private static string[] SplitPathsList(string pathsList)
+        {
+            if (pathsList == null)
+            {
+                return Array.Empty<string>();
+            }
+            else
+            {
+                return pathsList.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
+            }
+        }
+
+        private struct LibraryNameVariation
+        {
+            public string Prefix;
+            public string Suffix;
+
+            public LibraryNameVariation(string prefix, string suffix)
+            {
+                Prefix = prefix;
+                Suffix = suffix;
+            }
+        }
+
+#if PLATFORM_WINDOWS
+        private const CharSet HostpolicyCharSet = CharSet.Unicode;
+        private const string LibraryNameSuffix = ".dll";
+
+        private IEnumerable<LibraryNameVariation> DetermineLibraryNameVariations(string libName, bool isRelativePath)
+        {
+            // This is a copy of the logic in DetermineLibNameVariations in dllimport.cpp in CoreCLR
+
+            yield return new LibraryNameVariation(string.Empty, string.Empty);
+
+            if (isRelativePath &&
+                !libName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) &&
+                !libName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+            {
+                yield return new LibraryNameVariation(string.Empty, LibraryNameSuffix);
+            }
+        }
+#else
+        private const CharSet HostpolicyCharSet = CharSet.Ansi;
+
+        private const string LibraryNamePrefix = "lib";
+#if PLATFORM_OSX
+        private const string LibraryNameSuffix = ".dylib";
+#else
+        private const string LibraryNameSuffix = ".so";
+#endif
+
+        private IEnumerable<LibraryNameVariation> DetermineLibraryNameVariations(string libName, bool isRelativePath)
+        {
+            // This is a copy of the logic in DetermineLibNameVariations in dllimport.cpp in CoreCLR
+
+            if (!isRelativePath)
+            {
+                yield return new LibraryNameVariation(string.Empty, string.Empty);
+            }
+            else
+            {
+                bool containsSuffix = false;
+                int indexOfSuffix = libName.IndexOf(LibraryNameSuffix);
+                if (indexOfSuffix >= 0)
+                {
+                    indexOfSuffix += LibraryNameSuffix.Length;
+                    containsSuffix = indexOfSuffix == libName.Length || libName[indexOfSuffix] == '.';
+                }
+
+                bool containsDelim = libName.Contains(Path.DirectorySeparatorChar);
+
+                if (containsSuffix)
+                {
+                    yield return new LibraryNameVariation(string.Empty, string.Empty);
+                    if (!containsDelim)
+                    {
+                        yield return new LibraryNameVariation(LibraryNamePrefix, string.Empty);
+                    }
+                    yield return new LibraryNameVariation(string.Empty, LibraryNameSuffix);
+                    if (!containsDelim)
+                    {
+                        yield return new LibraryNameVariation(LibraryNamePrefix, LibraryNameSuffix);
+                    }
+                }
+                else
+                {
+                    yield return new LibraryNameVariation(string.Empty, LibraryNameSuffix);
+                    if (!containsDelim)
+                    {
+                        yield return new LibraryNameVariation(LibraryNamePrefix, LibraryNameSuffix);
+                    }
+                    yield return new LibraryNameVariation(string.Empty, string.Empty);
+                    if (!containsDelim)
+                    {
+                        yield return new LibraryNameVariation(LibraryNamePrefix, string.Empty);
+                    }
+                }
+            }
+
+            yield return new LibraryNameVariation(string.Empty, string.Empty);
+
+            if (isRelativePath &&
+                !libName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) &&
+                !libName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+            {
+                yield return new LibraryNameVariation(string.Empty, LibraryNameSuffix);
+            }
+        }
+#endif
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = HostpolicyCharSet)]
+        internal delegate void corehost_resolve_component_dependencies_result_fn(
+            string assembly_paths,
+            string native_search_paths,
+            string resource_search_paths);
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = HostpolicyCharSet)]
+        internal delegate void corehost_error_writer_fn(
+            string message);
+
+#pragma warning disable BCL0015 // Disable Pinvoke analyzer errors.
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern int corehost_resolve_component_dependencies(
+            string component_main_assembly_path,
+            corehost_resolve_component_dependencies_result_fn result);
+
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern IntPtr corehost_set_error_writer(IntPtr error_writer);
+#pragma warning restore
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs
new file mode 100644 (file)
index 0000000..40d7a41
--- /dev/null
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.Reflection;
+
+namespace AssemblyDependencyResolverTests
+{
+    /// <summary>
+    /// Temporary until the actual public API gets propagated through CoreFX.
+    /// </summary>
+    public class AssemblyDependencyResolver
+    {
+        private object _implementation;
+        private Type _implementationType;
+        private MethodInfo _resolveAssemblyPathInfo;
+        private MethodInfo _resolveUnmanagedDllPathInfo;
+
+        public AssemblyDependencyResolver(string componentAssemblyPath)
+        {
+            _implementationType = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyDependencyResolver");
+            _resolveAssemblyPathInfo = _implementationType.GetMethod("ResolveAssemblyToPath");
+            _resolveUnmanagedDllPathInfo = _implementationType.GetMethod("ResolveUnmanagedDllToPath");
+
+            try
+            {
+                _implementation = Activator.CreateInstance(_implementationType, componentAssemblyPath);
+            }
+            catch (TargetInvocationException tie)
+            {
+                throw tie.InnerException;
+            }
+        }
+
+        public string ResolveAssemblyToPath(AssemblyName assemblyName)
+        {
+            try
+            {
+                return (string)_resolveAssemblyPathInfo.Invoke(_implementation, new object[] { assemblyName });
+            }
+            catch (TargetInvocationException tie)
+            {
+                throw tie.InnerException;
+            }
+        }
+
+        public string ResolveUnmanagedDllToPath(string unmanagedDllName)
+        {
+            try
+            {
+                return (string)_resolveUnmanagedDllPathInfo.Invoke(_implementation, new object[] { unmanagedDllName });
+            }
+            catch (TargetInvocationException tie)
+            {
+                throw tie.InnerException;
+            }
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.cs b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.cs
new file mode 100644 (file)
index 0000000..883d569
--- /dev/null
@@ -0,0 +1,319 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.IO;
+using System.Reflection;
+using Xunit;
+
+namespace AssemblyDependencyResolverTests
+{
+    class AssemblyDependencyResolverTests : TestBase
+    {
+        string _componentDirectory;
+        string _componentAssemblyPath;
+
+        protected override void Initialize()
+        {
+            HostPolicyMock.Initialize(TestBasePath, CoreRoot);
+            _componentDirectory = Path.Combine(TestBasePath, $"TestComponent_{Guid.NewGuid().ToString().Substring(0, 8)}");
+            Directory.CreateDirectory(_componentDirectory);
+            _componentAssemblyPath = CreateMockAssembly("TestComponent.dll");
+        }
+
+        protected override void Cleanup()
+        {
+            if (Directory.Exists(_componentDirectory))
+            {
+                Directory.Delete(_componentDirectory, recursive: true);
+            }
+        }
+
+        public void TestComponentLoadFailure()
+        {
+            const string errorMessageFirstLine = "First line: failure";
+            const string errorMessageSecondLine = "Second line: value";
+
+            using (HostPolicyMock.MockValues_corehost_set_error_writer errorWriterMock = 
+                HostPolicyMock.Mock_corehost_set_error_writer())
+            {
+                using (HostPolicyMock.MockValues_corehost_resolve_componet_dependencies resolverMock = 
+                    HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                        134,
+                        "",
+                        "",
+                        ""))
+                {
+                    // When the resolver is called, emulate error behavior
+                    // which is to write to the error writer some error message.
+                    resolverMock.Callback = (string componentAssemblyPath) =>
+                    {
+                        Assert.NotNull(errorWriterMock.LastSetErrorWriter);
+                        errorWriterMock.LastSetErrorWriter(errorMessageFirstLine);
+                        errorWriterMock.LastSetErrorWriter(errorMessageSecondLine);
+                    };
+
+                    string message = Assert.Throws<InvalidOperationException>(() =>
+                    {
+                        AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                            Path.Combine(TestBasePath, _componentAssemblyPath));
+                    }).Message;
+
+                    Assert.Contains("134", message);
+                    Assert.Contains(
+                        errorMessageFirstLine + Environment.NewLine + errorMessageSecondLine,
+                        message);
+
+                    // After everything is done, the error writer should be reset.
+                    Assert.Null(errorWriterMock.LastSetErrorWriter);
+                }
+            }
+        }
+
+        public void TestComponentLoadFailureWithPreviousErrorWriter()
+        {
+            IntPtr previousWriter = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(
+                (HostPolicyMock.ErrorWriterDelegate)((string _) => { Assert.True(false, "Should never get here"); }));
+
+            using (HostPolicyMock.MockValues_corehost_set_error_writer errorWriterMock =
+                HostPolicyMock.Mock_corehost_set_error_writer(previousWriter))
+            {
+                using (HostPolicyMock.MockValues_corehost_resolve_componet_dependencies resolverMock =
+                    HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                        134,
+                        "",
+                        "",
+                        ""))
+                {
+                    Assert.Throws<InvalidOperationException>(() =>
+                    {
+                        AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                            Path.Combine(TestBasePath, _componentAssemblyPath));
+                    });
+
+                    // After everything is done, the error writer should be reset to the original value.
+                    Assert.Equal(previousWriter, errorWriterMock.LastSetErrorWriterPtr);
+                }
+            }
+        }
+
+        public void TestAssembly()
+        {
+            string assemblyDependencyPath = CreateMockAssembly("AssemblyDependency.dll");
+
+            IntPtr previousWriter = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(
+                (HostPolicyMock.ErrorWriterDelegate)((string _) => { Assert.True(false, "Should never get here"); }));
+
+            using (HostPolicyMock.MockValues_corehost_set_error_writer errorWriterMock =
+                HostPolicyMock.Mock_corehost_set_error_writer(previousWriter))
+            {
+                using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                    0,
+                    assemblyDependencyPath,
+                    "",
+                    ""))
+                {
+                    AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                        Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                    Assert.Equal(
+                        assemblyDependencyPath,
+                        resolver.ResolveAssemblyToPath(new AssemblyName("AssemblyDependency")));
+
+                    // After everything is done, the error writer should be reset to the original value.
+                    Assert.Equal(previousWriter, errorWriterMock.LastSetErrorWriterPtr);
+                }
+            }
+        }
+
+        public void TestAssemblyWithNoRecord()
+        {
+            // If the reqest is for assembly which is not listed in .deps.json
+            // the resolver should return null.
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                "",
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Null(resolver.ResolveAssemblyToPath(new AssemblyName("AssemblyWithNoRecord")));
+            }
+        }
+
+        public void TestAssemblyWithMissingFile()
+        {
+            // Even if the .deps.json can resolve the request, if the file is not present
+            // the resolution should still return null.
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                Path.Combine(_componentDirectory, "NonExistingAssembly.dll"),
+                "",
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Null(resolver.ResolveAssemblyToPath(new AssemblyName("NonExistingAssembly")));
+            }
+        }
+
+        public void TestSingleResource()
+        {
+            string enResourcePath = CreateMockAssembly($"en{Path.DirectorySeparatorChar}TestComponent.resources.dll");
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                "",
+                _componentDirectory))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    enResourcePath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("TestComponent.resources, Culture=en")));
+            }
+        }
+
+        public void TestMutipleResourcesWithSameBasePath()
+        {
+            string enResourcePath = CreateMockAssembly($"en{Path.DirectorySeparatorChar}TestComponent.resources.dll");
+            string csResourcePath = CreateMockAssembly($"cs{Path.DirectorySeparatorChar}TestComponent.resources.dll");
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                "",
+                _componentDirectory))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    enResourcePath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("TestComponent.resources, Culture=en")));
+                Assert.Equal(
+                    csResourcePath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("TestComponent.resources, Culture=cs")));
+            }
+        }
+
+        public void TestMutipleResourcesWithDifferentBasePath()
+        {
+            string enResourcePath = CreateMockAssembly($"en{Path.DirectorySeparatorChar}TestComponent.resources.dll");
+            string frResourcePath = CreateMockAssembly($"SubComponent{Path.DirectorySeparatorChar}fr{Path.DirectorySeparatorChar}TestComponent.resources.dll");
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                "",
+                $"{_componentDirectory}{Path.PathSeparator}{Path.GetDirectoryName(Path.GetDirectoryName(frResourcePath))}"))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    enResourcePath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("TestComponent.resources, Culture=en")));
+                Assert.Equal(
+                    frResourcePath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("TestComponent.resources, Culture=fr")));
+            }
+        }
+
+        public void TestAssemblyWithNeutralCulture()
+        {
+            string neutralAssemblyPath = CreateMockAssembly("NeutralAssembly.dll");
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                neutralAssemblyPath,
+                "",
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    neutralAssemblyPath,
+                    resolver.ResolveAssemblyToPath(new AssemblyName("NeutralAssembly, Culture=neutral")));
+            }
+        }
+
+        public void TestSingleNativeDependency()
+        {
+            string nativeLibraryPath = CreateMockStandardNativeLibrary("native", "Single");
+
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                Path.GetDirectoryName(nativeLibraryPath),
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    nativeLibraryPath,
+                    resolver.ResolveUnmanagedDllToPath("Single"));
+            }
+        }
+
+        public void TestMultipleNativeDependencies()
+        {
+            string oneNativeLibraryPath = CreateMockStandardNativeLibrary($"native{Path.DirectorySeparatorChar}one", "One");
+            string twoNativeLibraryPath = CreateMockStandardNativeLibrary($"native{Path.DirectorySeparatorChar}two", "Two");
+
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                $"{Path.GetDirectoryName(oneNativeLibraryPath)}{Path.PathSeparator}{Path.GetDirectoryName(twoNativeLibraryPath)}",
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                Assert.Equal(
+                    oneNativeLibraryPath,
+                    resolver.ResolveUnmanagedDllToPath("One"));
+                Assert.Equal(
+                    twoNativeLibraryPath,
+                    resolver.ResolveUnmanagedDllToPath("Two"));
+            }
+        }
+
+        private string CreateMockAssembly(string relativePath)
+        {
+            string fullPath = Path.Combine(_componentDirectory, relativePath);
+            if (!File.Exists(fullPath))
+            {
+                string directory = Path.GetDirectoryName(fullPath);
+                if (!Directory.Exists(directory))
+                {
+                    Directory.CreateDirectory(directory);
+                }
+
+                File.WriteAllText(fullPath, "Mock assembly");
+            }
+
+            return fullPath;
+        }
+
+        private string CreateMockStandardNativeLibrary(string relativePath, string simpleName)
+        {
+            return CreateMockAssembly(
+                relativePath + Path.DirectorySeparatorChar + XPlatformUtils.GetStandardNativeLibraryFileName(simpleName));
+        }
+
+        public static int Main()
+        {
+            return TestBase.RunTests(
+                // It's important that the invalid hosting test runs first as it relies on the ability
+                // to delete (if it's there) the hostpolicy.dll. All other tests will end up loading the dll
+                // and thus locking it.
+                typeof(InvalidHostingTest),
+                typeof(AssemblyDependencyResolverTests),
+                typeof(NativeDependencyTests));
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.csproj b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.csproj
new file mode 100644 (file)
index 0000000..28bd068
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <CLRTestKind>BuildAndRun</CLRTestKind>
+    <CLRTestPriority>1</CLRTestPriority>
+    <ProjectGuid>{ABB86728-A3E0-4489-BD97-A0BAB00B322F}</ProjectGuid>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+  </PropertyGroup>
+  <PropertyGroup>
+    <DefineConstants Condition="$(OSGroup) == 'Windows_NT'">WINDOWS</DefineConstants>
+    <DefineConstants Condition="$(OSGroup) == 'OSX'">OSX</DefineConstants>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="AssemblyDependencyResolver.cs" />
+    <Compile Include="AssemblyDependencyResolverTests.cs" />
+    <Compile Include="HostPolicyMock.cs" />
+    <Compile Include="InvalidHostingTest.cs" />
+    <Compile Include="NativeDependencyTests.cs" />
+    <Compile Include="TestBase.cs" />
+    <Compile Include="XPlatformUtils.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="CMakeLists.txt" />
+  </ItemGroup>
+  <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+</Project>
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/CMakeLists.txt b/tests/src/Loader/AssemblyDependencyResolverTests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9e48211
--- /dev/null
@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 2.6)
+project (hostpolicy)
+
+set(SOURCES HostpolicyMock.cpp )
+add_library(hostpolicy SHARED ${SOURCES})
+
+install(TARGETS hostpolicy DESTINATION bin)
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/HostPolicyMock.cs b/tests/src/Loader/AssemblyDependencyResolverTests/HostPolicyMock.cs
new file mode 100644 (file)
index 0000000..1c212ef
--- /dev/null
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace AssemblyDependencyResolverTests
+{
+    class HostPolicyMock
+    {
+#if WINDOWS
+        private const CharSet HostpolicyCharSet = CharSet.Unicode;
+#else
+        private const CharSet HostpolicyCharSet = CharSet.Ansi;
+#endif
+
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern int Set_corehost_resolve_component_dependencies_Values(
+            int returnValue,
+            string assemblyPaths,
+            string nativeSearchPaths,
+            string resourceSearchPaths);
+
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern void Set_corehost_set_error_writer_returnValue(IntPtr error_writer);
+
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern IntPtr Get_corehost_set_error_writer_lastSet_error_writer();
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = HostpolicyCharSet)]
+        internal delegate void Callback_corehost_resolve_component_dependencies(
+            string component_main_assembly_path);
+
+        [DllImport("hostpolicy", CharSet = HostpolicyCharSet)]
+        private static extern void Set_corehost_resolve_component_dependencies_Callback(
+            IntPtr callback);
+
+        private static Type _assemblyDependencyResolverType;
+        private static Type _corehost_error_writer_fnType;
+
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = HostpolicyCharSet)]
+        public delegate void ErrorWriterDelegate(string message);
+
+        public static string DeleteExistingHostpolicy(string coreRoot)
+        {
+            string hostPolicyFileName = XPlatformUtils.GetStandardNativeLibraryFileName("hostpolicy");
+            string destinationPath = Path.Combine(coreRoot, hostPolicyFileName);
+            if (File.Exists(destinationPath))
+            {
+                File.Delete(destinationPath);
+            }
+
+            return destinationPath;
+        }
+
+        public static void Initialize(string testBasePath, string coreRoot)
+        {
+            string hostPolicyFileName = XPlatformUtils.GetStandardNativeLibraryFileName("hostpolicy");
+            string destinationPath = DeleteExistingHostpolicy(coreRoot);
+
+            File.Copy(
+                Path.Combine(testBasePath, hostPolicyFileName),
+                destinationPath);
+
+            _assemblyDependencyResolverType = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyDependencyResolver");
+
+            // This is needed for marshalling of function pointers to work - requires private access to the CDR unfortunately
+            // Delegate marshalling doesn't support casting delegates to anything but the original type
+            // so we need to use the original type.
+            _corehost_error_writer_fnType = _assemblyDependencyResolverType.GetNestedType("corehost_error_writer_fn", System.Reflection.BindingFlags.NonPublic);
+        }
+
+        public static MockValues_corehost_resolve_componet_dependencies Mock_corehost_resolve_componet_dependencies(
+            int returnValue,
+            string assemblyPaths,
+            string nativeSearchPaths,
+            string resourceSearchPaths)
+        {
+            Set_corehost_resolve_component_dependencies_Values(
+                returnValue,
+                assemblyPaths,
+                nativeSearchPaths,
+                resourceSearchPaths);
+
+            return new MockValues_corehost_resolve_componet_dependencies();
+        }
+
+        internal class MockValues_corehost_resolve_componet_dependencies : IDisposable
+        {
+            public Action<string> Callback
+            {
+                set
+                {
+                    var callback = new Callback_corehost_resolve_component_dependencies(value);
+                    if (callback != null)
+                    {
+                        Set_corehost_resolve_component_dependencies_Callback(
+                            Marshal.GetFunctionPointerForDelegate(callback));
+                    }
+                    else
+                    {
+                        Set_corehost_resolve_component_dependencies_Callback(IntPtr.Zero);
+                    }
+                }
+            }
+
+            public void Dispose()
+            {
+                Set_corehost_resolve_component_dependencies_Values(
+                    -1,
+                    string.Empty,
+                    string.Empty,
+                    string.Empty);
+                Set_corehost_resolve_component_dependencies_Callback(IntPtr.Zero);
+            }
+        }
+
+        public static MockValues_corehost_set_error_writer Mock_corehost_set_error_writer()
+        {
+            return Mock_corehost_set_error_writer(IntPtr.Zero);
+        }
+
+        public static MockValues_corehost_set_error_writer Mock_corehost_set_error_writer(IntPtr existingErrorWriter)
+        {
+            Set_corehost_set_error_writer_returnValue(existingErrorWriter);
+
+            return new MockValues_corehost_set_error_writer();
+        }
+
+        internal class MockValues_corehost_set_error_writer : IDisposable
+        {
+            public IntPtr LastSetErrorWriterPtr
+            {
+                get
+                {
+                    return Get_corehost_set_error_writer_lastSet_error_writer();
+                }
+            }
+
+            public Action<string> LastSetErrorWriter
+            {
+                get
+                {
+                    IntPtr errorWriterPtr = LastSetErrorWriterPtr;
+                    if (errorWriterPtr == IntPtr.Zero)
+                    {
+                        return null;
+                    }
+                    else
+                    {
+                        Delegate d = Marshal.GetDelegateForFunctionPointer(errorWriterPtr, _corehost_error_writer_fnType);
+                        return (string message) => { d.DynamicInvoke(message); };
+                    }
+                }
+            }
+
+            public void Dispose()
+            {
+                Set_corehost_set_error_writer_returnValue(IntPtr.Zero);
+            }
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/HostpolicyMock.cpp b/tests/src/Loader/AssemblyDependencyResolverTests/HostpolicyMock.cpp
new file mode 100644 (file)
index 0000000..b1a90d2
--- /dev/null
@@ -0,0 +1,100 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// Mock implementation of the hostpolicy.cpp exported methods.
+// Used for testing CoreCLR/Corlib functionality which calls into hostpolicy.
+
+#include <string>
+
+// dllexport
+#if defined _WIN32
+
+#define SHARED_API extern "C" __declspec(dllexport)
+typedef wchar_t char_t;
+typedef std::wstring string_t;
+
+#else //!_Win32
+
+#if __GNUC__ >= 4
+#define SHARED_API extern "C" __attribute__ ((visibility ("default")))
+#else
+#define SHARED_API extern "C"
+#endif
+
+typedef char char_t;
+typedef std::string string_t;
+
+#endif //_WIN32
+
+int g_corehost_resolve_component_dependencies_returnValue = -1;
+string_t g_corehost_resolve_component_dependencies_assemblyPaths;
+string_t g_corehost_resolve_component_dependencies_nativeSearchPaths;
+string_t g_corehost_resolve_component_dependencies_resourceSearchPaths;
+
+typedef void(*Callback_corehost_resolve_component_dependencies)(const char_t *component_main_assembly_path);
+Callback_corehost_resolve_component_dependencies g_corehost_resolve_component_dependencies_Callback;
+
+typedef void(*corehost_resolve_component_dependencies_result_fn)(
+    const char_t* assembly_paths,
+    const char_t* native_search_paths,
+    const char_t* resource_search_paths);
+
+SHARED_API int corehost_resolve_component_dependencies(
+    const char_t *component_main_assembly_path,
+    corehost_resolve_component_dependencies_result_fn result)
+{
+    if (g_corehost_resolve_component_dependencies_Callback != NULL)
+    {
+        g_corehost_resolve_component_dependencies_Callback(component_main_assembly_path);
+    }
+
+    if (g_corehost_resolve_component_dependencies_returnValue == 0)
+    {
+        result(
+            g_corehost_resolve_component_dependencies_assemblyPaths.data(),
+            g_corehost_resolve_component_dependencies_nativeSearchPaths.data(),
+            g_corehost_resolve_component_dependencies_resourceSearchPaths.data());
+    }
+
+    return g_corehost_resolve_component_dependencies_returnValue;
+}
+
+SHARED_API void Set_corehost_resolve_component_dependencies_Values(
+    int returnValue,
+    const char_t *assemblyPaths,
+    const char_t *nativeSearchPaths,
+    const char_t *resourceSearchPaths)
+{
+    g_corehost_resolve_component_dependencies_returnValue = returnValue;
+    g_corehost_resolve_component_dependencies_assemblyPaths.assign(assemblyPaths);
+    g_corehost_resolve_component_dependencies_nativeSearchPaths.assign(nativeSearchPaths);
+    g_corehost_resolve_component_dependencies_resourceSearchPaths.assign(resourceSearchPaths);
+}
+
+SHARED_API void Set_corehost_resolve_component_dependencies_Callback(
+    Callback_corehost_resolve_component_dependencies callback)
+{
+    g_corehost_resolve_component_dependencies_Callback = callback;
+}
+
+
+typedef void(*corehost_error_writer_fn)(const char_t* message);
+corehost_error_writer_fn g_corehost_set_error_writer_lastSet_error_writer;
+corehost_error_writer_fn g_corehost_set_error_writer_returnValue;
+
+SHARED_API corehost_error_writer_fn corehost_set_error_writer(corehost_error_writer_fn error_writer)
+{
+    g_corehost_set_error_writer_lastSet_error_writer = error_writer;
+    return g_corehost_set_error_writer_returnValue;
+}
+
+SHARED_API void Set_corehost_set_error_writer_returnValue(corehost_error_writer_fn error_writer)
+{
+    g_corehost_set_error_writer_returnValue = error_writer;
+}
+
+SHARED_API corehost_error_writer_fn Get_corehost_set_error_writer_lastSet_error_writer()
+{
+    return g_corehost_set_error_writer_lastSet_error_writer;
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/InvalidHostingTest.cs b/tests/src/Loader/AssemblyDependencyResolverTests/InvalidHostingTest.cs
new file mode 100644 (file)
index 0000000..616b5ee
--- /dev/null
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.IO;
+using Xunit;
+
+namespace AssemblyDependencyResolverTests
+{
+    class InvalidHostingTest : TestBase
+    {
+        private string _componentDirectory;
+        private string _componentAssemblyPath;
+        private string _officialHostPolicyPath;
+        private string _localHostPolicyPath;
+        private string _renamedHostPolicyPath;
+
+        protected override void Initialize()
+        {
+            // Make sure there's no hostpolicy available
+            _officialHostPolicyPath = HostPolicyMock.DeleteExistingHostpolicy(CoreRoot);
+            string hostPolicyFileName = XPlatformUtils.GetStandardNativeLibraryFileName("hostpolicy");
+            _localHostPolicyPath = Path.Combine(TestBasePath, hostPolicyFileName);
+            _renamedHostPolicyPath = Path.Combine(TestBasePath, hostPolicyFileName + "_renamed");
+            if (File.Exists(_renamedHostPolicyPath))
+            {
+                File.Delete(_renamedHostPolicyPath);
+            }
+            File.Move(_localHostPolicyPath, _renamedHostPolicyPath);
+
+            _componentDirectory = Path.Combine(TestBasePath, $"InvalidHostingComponent_{Guid.NewGuid().ToString().Substring(0, 8)}");
+            Directory.CreateDirectory(_componentDirectory);
+            _componentAssemblyPath = Path.Combine(_componentDirectory, "InvalidHostingComponent.dll");
+            File.WriteAllText(_componentAssemblyPath, "Mock assembly");
+        }
+
+        protected override void Cleanup()
+        {
+            if (Directory.Exists(_componentDirectory))
+            {
+                Directory.Delete(_componentDirectory, recursive: true);
+            }
+
+            if (File.Exists(_renamedHostPolicyPath))
+            {
+                File.Move(_renamedHostPolicyPath, _localHostPolicyPath);
+            }
+        }
+
+        public void TestMissingHostPolicy()
+        {
+            object innerException = Assert.Throws<InvalidOperationException>(() =>
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+            }).InnerException;
+
+            Assert.IsType<DllNotFoundException>(innerException);
+        }
+
+        // Note: No good way to test the missing entry point case where hostpolicy.dll
+        // exists, but it doesn't have the right entry points.
+        // Loading a "wrong" hostpolicy.dll into the process is non-revertable operation
+        // so we would not be able to run other tests along side this one.
+        // Having a standalone .exe just for that one test is not worth it.
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/NativeDependencyTests.cs b/tests/src/Loader/AssemblyDependencyResolverTests/NativeDependencyTests.cs
new file mode 100644 (file)
index 0000000..e7dcf20
--- /dev/null
@@ -0,0 +1,323 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace AssemblyDependencyResolverTests
+{
+    class NativeDependencyTests : TestBase
+    {
+        string _componentDirectory;
+        string _componentAssemblyPath;
+
+        protected override void Initialize()
+        {
+            HostPolicyMock.Initialize(TestBasePath, CoreRoot);
+            _componentDirectory = Path.Combine(TestBasePath, $"TestComponent_{Guid.NewGuid().ToString().Substring(0, 8)}");
+
+            Directory.CreateDirectory(_componentDirectory);
+            _componentAssemblyPath = CreateMockFile("TestComponent.dll");
+        }
+
+        protected override void Cleanup()
+        {
+            if (Directory.Exists(_componentDirectory))
+            {
+                Directory.Delete(_componentDirectory, recursive: true);
+            }
+        }
+
+        public void TestSimpleNameAndNoPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}", "{0}", OS.Windows | OS.OSX | OS.Linux);
+        }
+
+        public void TestSimpleNameAndNoPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}.dll", "{0}", OS.Windows);
+            ValidateNativeLibraryResolutions("{0}.dylib", "{0}", OS.OSX);
+            ValidateNativeLibraryResolutions("{0}.so", "{0}", OS.Linux);
+        }
+
+        public void TestSimpleNameAndLibPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}", "{0}", OS.OSX | OS.Linux);
+        }
+
+        public void TestRelativeNameAndLibPrefixAndNoSuffix()
+        {
+            // The lib prefix is not added if the lookup is a relative path.
+            ValidateNativeLibraryWithRelativeLookupResolutions("lib{0}", "{0}", 0);
+        }
+
+        public void TestSimpleNameAndLibPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}.dll", "{0}", 0);
+            ValidateNativeLibraryResolutions("lib{0}.dylib", "{0}", OS.OSX);
+            ValidateNativeLibraryResolutions("lib{0}.so", "{0}", OS.Linux);
+        }
+
+        public void TestNameWithSuffixAndNoPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}", "{0}.dll", 0);
+            ValidateNativeLibraryResolutions("{0}", "{0}.dylib", 0);
+            ValidateNativeLibraryResolutions("{0}", "{0}.so", 0);
+        }
+
+        public void TestNameWithSuffixAndNoPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}.dll", "{0}.dll", OS.Windows | OS.OSX | OS.Linux);
+            ValidateNativeLibraryResolutions("{0}.dylib", "{0}.dylib", OS.Windows | OS.OSX | OS.Linux);
+            ValidateNativeLibraryResolutions("{0}.so", "{0}.so", OS.Windows | OS.OSX | OS.Linux);
+        }
+
+        public void TestNameWithSuffixAndNoPrefixAndDoubleSuffix()
+        {
+            // Unixes add the suffix even if one is already present.
+            ValidateNativeLibraryResolutions("{0}.dll.dll", "{0}.dll", 0);
+            ValidateNativeLibraryResolutions("{0}.dylib.dylib", "{0}.dylib", OS.OSX);
+            ValidateNativeLibraryResolutions("{0}.so.so", "{0}.so", OS.Linux);
+        }
+
+        public void TestNameWithSuffixAndPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}", "{0}.dll", 0);
+            ValidateNativeLibraryResolutions("lib{0}", "{0}.dylib", 0);
+            ValidateNativeLibraryResolutions("lib{0}", "{0}.so", 0);
+        }
+
+        public void TestNameWithSuffixAndPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}.dll", "{0}.dll", OS.OSX | OS.Linux);
+            ValidateNativeLibraryResolutions("lib{0}.dylib", "{0}.dylib", OS.OSX | OS.Linux);
+            ValidateNativeLibraryResolutions("lib{0}.so", "{0}.so", OS.OSX | OS.Linux);
+        }
+
+        public void TestRelativeNameWithSuffixAndPrefixAndSuffix()
+        {
+            // The lib prefix is not added if the lookup is a relative path
+            ValidateNativeLibraryWithRelativeLookupResolutions("lib{0}.dll", "{0}.dll", 0);
+            ValidateNativeLibraryWithRelativeLookupResolutions("lib{0}.dylib", "{0}.dylib", 0);
+            ValidateNativeLibraryWithRelativeLookupResolutions("lib{0}.so", "{0}.so", 0);
+        }
+
+        public void TestNameWithPrefixAndNoPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}", "lib{0}", 0);
+        }
+
+        public void TestNameWithPrefixAndPrefixAndNoSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}", "lib{0}", OS.Windows | OS.OSX | OS.Linux);
+        }
+
+        public void TestNameWithPrefixAndNoPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("{0}.dll", "lib{0}", 0);
+            ValidateNativeLibraryResolutions("{0}.dylib", "lib{0}", 0);
+            ValidateNativeLibraryResolutions("{0}.so", "lib{0}", 0);
+        }
+
+        public void TestNameWithPrefixAndPrefixAndSuffix()
+        {
+            ValidateNativeLibraryResolutions("lib{0}.dll", "lib{0}", OS.Windows);
+            ValidateNativeLibraryResolutions("lib{0}.dylib", "lib{0}", OS.OSX);
+            ValidateNativeLibraryResolutions("lib{0}.so", "lib{0}", OS.Linux);
+        }
+
+        public void TestWindowsAddsSuffixEvenWithOnePresent()
+        {
+            ValidateNativeLibraryResolutions("{0}.ext.dll", "{0}.ext", OS.Windows);
+        }
+
+        public void TestWindowsDoesntAddSuffixWhenExectubaleIsPresent()
+        {
+            ValidateNativeLibraryResolutions("{0}.dll.dll", "{0}.dll", 0);
+            ValidateNativeLibraryResolutions("{0}.dll.exe", "{0}.dll", 0);
+            ValidateNativeLibraryResolutions("{0}.exe.dll", "{0}.exe", 0);
+            ValidateNativeLibraryResolutions("{0}.exe.exe", "{0}.exe", 0);
+        }
+
+        private void TestLookupWithSuffixPrefersUnmodifiedSuffixOnUnixes()
+        {
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.dylib", "lib{0}.dylib", "{0}.dylib", OS.OSX);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.so", "lib{0}.so", "{0}.so", OS.Linux);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.dylib", "{0}.dylib.dylib", "{0}.dylib", OS.OSX);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.so", "{0}.so.so", "{0}.so", OS.Linux);
+        }
+
+        private void TestLookupWithoutSuffixPrefersWithSuffixOnUnixes()
+        {
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.dylib", "lib{0}.dylib", "{0}", OS.OSX);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.so", "lib{0}.so", "{0}", OS.Linux);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.dylib", "{0}", "{0}", OS.OSX);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.so", "{0}", "{0}", OS.Linux);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.dylib", "lib{0}", "{0}", OS.OSX);
+            ValidateNativeLibraryResolutionsWithTwoFiles("{0}.so", "lib{0}", "{0}", OS.Linux);
+        }
+
+        public void TestFullPathLookupWithMatchingFileName()
+        {
+            ValidateFullPathNativeLibraryResolutions("{0}", "{0}", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("{0}.dll", "{0}.dll", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("{0}.dylib", "{0}.dylib", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("{0}.so", "{0}.so", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("lib{0}", "lib{0}", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dll", "lib{0}.dll", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dylib", "lib{0}.dylib", OS.Windows | OS.OSX | OS.Linux);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.so", "lib{0}.so", OS.Windows | OS.OSX | OS.Linux);
+        }
+
+        public void TestFullPathLookupWithDifferentFileName()
+        {
+            ValidateFullPathNativeLibraryResolutions("lib{0}", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("{0}.dll", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("{0}.dylib", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("{0}.so", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dll", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dylib", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.so", "{0}", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dll", "{0}.dll", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.dylib", "{0}.dylib", 0);
+            ValidateFullPathNativeLibraryResolutions("lib{0}.so", "{0}.so", 0);
+        }
+
+        [Flags]
+        private enum OS
+        {
+            Windows = 0x1,
+            OSX = 0x2,
+            Linux = 0x4
+        }
+
+        private void ValidateNativeLibraryResolutions(
+            string fileNamePattern,
+            string lookupNamePattern,
+            OS resolvesOnOSes)
+        {
+            string newDirectory = Guid.NewGuid().ToString().Substring(0, 8);
+            string nativeLibraryPath = CreateMockFile(Path.Combine(newDirectory, string.Format(fileNamePattern, "NativeLibrary")));
+            ValidateNativeLibraryResolutions(
+                Path.GetDirectoryName(nativeLibraryPath),
+                nativeLibraryPath,
+                string.Format(lookupNamePattern, "NativeLibrary"),
+                resolvesOnOSes);
+        }
+
+        private void ValidateNativeLibraryWithRelativeLookupResolutions(
+            string fileNamePattern,
+            string lookupNamePattern,
+            OS resolvesOnOSes)
+        {
+            string newDirectory = Guid.NewGuid().ToString().Substring(0, 8);
+            string nativeLibraryPath = CreateMockFile(Path.Combine(newDirectory, string.Format(fileNamePattern, "NativeLibrary")));
+            ValidateNativeLibraryResolutions(
+                Path.GetDirectoryName(Path.GetDirectoryName(nativeLibraryPath)),
+                nativeLibraryPath,
+                Path.Combine(newDirectory, string.Format(lookupNamePattern, "NativeLibrary")),
+                resolvesOnOSes);
+        }
+
+        private void ValidateFullPathNativeLibraryResolutions(
+            string fileNamePattern,
+            string lookupNamePattern,
+            OS resolvesOnOSes)
+        {
+            string newDirectory = Guid.NewGuid().ToString().Substring(0, 8);
+            string nativeLibraryPath = CreateMockFile(Path.Combine(newDirectory, string.Format(fileNamePattern, "NativeLibrary")));
+            ValidateNativeLibraryResolutions(
+                Path.GetDirectoryName(nativeLibraryPath),
+                nativeLibraryPath,
+                Path.Combine(Path.GetDirectoryName(nativeLibraryPath), string.Format(lookupNamePattern, "NativeLibrary")),
+                resolvesOnOSes);
+        }
+
+        private void ValidateNativeLibraryResolutionsWithTwoFiles(
+            string fileNameToResolvePattern,
+            string otherFileNamePattern,
+            string lookupNamePattern,
+            OS resolvesOnOSes)
+        {
+            string newDirectory = Guid.NewGuid().ToString().Substring(0, 8);
+            string nativeLibraryPath = CreateMockFile(Path.Combine(newDirectory, string.Format(fileNameToResolvePattern, "NativeLibrary")));
+            CreateMockFile(Path.Combine(newDirectory, string.Format(otherFileNamePattern, "NativeLibrary")));
+            ValidateNativeLibraryResolutions(
+                Path.GetDirectoryName(nativeLibraryPath),
+                nativeLibraryPath,
+                string.Format(lookupNamePattern, "NativeLibrary"),
+                resolvesOnOSes);
+        }
+
+        private void ValidateNativeLibraryResolutions(
+            string nativeLibraryPaths,
+            string expectedResolvedFilePath,
+            string lookupName,
+            OS resolvesOnOSes)
+        {
+            using (HostPolicyMock.Mock_corehost_resolve_componet_dependencies(
+                0,
+                "",
+                $"{nativeLibraryPaths}",
+                ""))
+            {
+                AssemblyDependencyResolver resolver = new AssemblyDependencyResolver(
+                    Path.Combine(TestBasePath, _componentAssemblyPath));
+
+                string result = resolver.ResolveUnmanagedDllToPath(lookupName);
+                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                {
+                    if (resolvesOnOSes.HasFlag(OS.Windows))
+                    {
+                        Assert.Equal(expectedResolvedFilePath, result);
+                    }
+                    else
+                    {
+                        Assert.Null(result);
+                    }
+                }
+                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+                {
+                    if (resolvesOnOSes.HasFlag(OS.OSX))
+                    {
+                        Assert.Equal(expectedResolvedFilePath, result);
+                    }
+                    else
+                    {
+                        Assert.Null(result);
+                    }
+                }
+                else
+                {
+                    if (resolvesOnOSes.HasFlag(OS.Linux))
+                    {
+                        Assert.Equal(expectedResolvedFilePath, result);
+                    }
+                    else
+                    {
+                        Assert.Null(result);
+                    }
+                }
+            }
+        }
+
+        private string CreateMockFile(string relativePath)
+        {
+            string fullPath = Path.Combine(_componentDirectory, relativePath);
+            if (!File.Exists(fullPath))
+            {
+                string directory = Path.GetDirectoryName(fullPath);
+                if (!Directory.Exists(directory))
+                {
+                    Directory.CreateDirectory(directory);
+                }
+
+                File.WriteAllText(fullPath, "Mock file");
+            }
+
+            return fullPath;
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/TestBase.cs b/tests/src/Loader/AssemblyDependencyResolverTests/TestBase.cs
new file mode 100644 (file)
index 0000000..74532c6
--- /dev/null
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace AssemblyDependencyResolverTests
+{
+    class TestBase
+    {
+        protected string TestBasePath { get; private set; }
+        protected string BinaryBasePath { get; private set; }
+        protected string CoreRoot { get; private set; }
+
+        protected virtual void Initialize()
+        {
+        }
+
+        protected virtual void Cleanup()
+        {
+        }
+
+        public static int RunTests(params Type[] testTypes)
+        {
+            int result = 100;
+            foreach (Type testType in testTypes)
+            {
+                int testResult = RunTestsForType(testType);
+                if (testResult != 100)
+                {
+                    result = testResult;
+                }
+            }
+
+            return result;
+        }
+
+        private static int RunTestsForType(Type testType)
+        {
+            string testBasePath = Path.GetDirectoryName(testType.Assembly.Location);
+
+            TestBase runner = (TestBase)Activator.CreateInstance(testType);
+            runner.TestBasePath = testBasePath;
+            runner.BinaryBasePath = Path.GetDirectoryName(testBasePath);
+            runner.CoreRoot = GetCoreRoot();
+
+            try
+            {
+                runner.Initialize();
+
+                runner.RunTestsForInstance(runner);
+                return runner._retValue;
+            }
+            finally
+            {
+                runner.Cleanup();
+            }
+        }
+
+        private int _retValue = 100;
+        private void RunSingleTest(Action test, string testName = null)
+        {
+            testName = testName ?? test.Method.Name;
+
+            try
+            {
+                Console.WriteLine($"{testName} Start");
+                test();
+                Console.WriteLine($"{testName} PASSED.");
+            }
+            catch (Exception exe)
+            {
+                Console.WriteLine($"{testName} FAILED:");
+                Console.WriteLine(exe.ToString());
+                _retValue = -1;
+            }
+        }
+
+        private void RunTestsForInstance(object testClass)
+        {
+            foreach (MethodInfo m in testClass.GetType()
+                .GetMethods(BindingFlags.Instance | BindingFlags.Public)
+                .Where(m => m.Name.StartsWith("Test") && m.GetParameters().Length == 0))
+            {
+                RunSingleTest(() => m.Invoke(testClass, new object[0]), m.Name);
+            }
+        }
+
+        private static string GetCoreRoot()
+        {
+            string value = Environment.GetEnvironmentVariable("CORE_ROOT");
+            if (value == null)
+            {
+                value = Directory.GetCurrentDirectory();
+            }
+
+            return value;
+        }
+    }
+}
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/XPlatformUtils.cs b/tests/src/Loader/AssemblyDependencyResolverTests/XPlatformUtils.cs
new file mode 100644 (file)
index 0000000..fd931f4
--- /dev/null
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+namespace AssemblyDependencyResolverTests
+{
+    class XPlatformUtils
+    {
+#if WINDOWS
+        public const string NativeLibraryPrefix = "";
+        public const string NativeLibrarySuffix = ".dll";
+#else
+        public const string NativeLibraryPrefix = "lib";
+#if OSX
+        public const string NativeLibrarySuffix = ".dylib";
+#else
+        public const string NativeLibrarySuffix = ".so";
+#endif
+#endif
+
+        public static string GetStandardNativeLibraryFileName(string simpleName)
+        {
+            return NativeLibraryPrefix + simpleName + NativeLibrarySuffix;
+        }
+    }
+}