diff options
author | Jeremy Koritzinsky <jkoritzinsky@gmail.com> | 2019-01-09 16:02:36 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-09 16:02:36 -0800 |
commit | dbf50d82754a894ba53781dbb839198a46605c9f (patch) | |
tree | 85e1a320f42be954147e07808bfa0f75edca2180 /tests | |
parent | fe5f8d64ae098900eb6bd8b83bf19a7e60e06270 (diff) | |
download | coreclr-dbf50d82754a894ba53781dbb839198a46605c9f.tar.gz coreclr-dbf50d82754a894ba53781dbb839198a46605c9f.tar.bz2 coreclr-dbf50d82754a894ba53781dbb839198a46605c9f.zip |
Implement AssemblyDependencyResolver (#21896)
* 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.
Diffstat (limited to 'tests')
10 files changed, 1197 insertions, 0 deletions
diff --git a/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs new file mode 100644 index 0000000000..40d7a41f5e --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolver.cs @@ -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 index 0000000000..883d569f96 --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.cs @@ -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 index 0000000000..28bd068fc4 --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/AssemblyDependencyResolverTests.csproj @@ -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 index 0000000000..9e4821148b --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/CMakeLists.txt @@ -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 index 0000000000..1c212efd48 --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/HostPolicyMock.cs @@ -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 index 0000000000..b1a90d2af3 --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/HostpolicyMock.cpp @@ -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 index 0000000000..616b5ee8ac --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/InvalidHostingTest.cs @@ -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 index 0000000000..e7dcf201bc --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/NativeDependencyTests.cs @@ -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 index 0000000000..74532c6a78 --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/TestBase.cs @@ -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 index 0000000000..fd931f465d --- /dev/null +++ b/tests/src/Loader/AssemblyDependencyResolverTests/XPlatformUtils.cs @@ -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; + } + } +} |