From 47aab33f042271a8e6ee0f615a0977c213dfb7ab Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Thu, 6 Apr 2023 15:36:03 -0700 Subject: [PATCH] Move startup hook tests targeting `StartupHookProvider` out of hosting tests (#84338) --- .../StartupHookFake/StartupHookFake.csproj | 12 - .../StartupHookInvalidAssembly.dll | 1 - .../StartupHookWithInstanceMethod.cs | 15 - .../StartupHookWithInstanceMethod.csproj | 8 - ...HookWithMultipleIncorrectSignatures.csproj | 8 - .../StartupHookWithNonPublicMethod.cs | 13 - .../StartupHookWithNonPublicMethod.csproj | 8 - .../StartupHookWithOverload.cs | 21 - .../StartupHookWithOverload.csproj | 8 - .../StartupHookWithParameter.cs | 15 - .../StartupHookWithParameter.csproj | 8 - .../StartupHookWithReturnType.cs | 16 - .../StartupHookWithReturnType.csproj | 8 - .../StartupHookWithoutInitializeMethod.cs | 15 - .../StartupHookWithoutInitializeMethod.csproj | 8 - .../StartupHookWithoutStartupHookType.cs | 15 - .../StartupHookWithoutStartupHookType.csproj | 8 - .../HostActivation.Tests/StartupHooks.cs | 574 +----------------- src/tests/Loader/StartupHooks/Hook.cs | 82 +++ src/tests/Loader/StartupHooks/Hooks/Basic.cs | 21 + .../Loader/StartupHooks/Hooks/Basic.csproj | 9 + .../Hooks/Invalid/InstanceMethod.csproj | 10 + .../Hooks/Invalid/InvalidHook.cs} | 18 +- .../MultipleIncorrectSignatures.csproj | 10 + .../Hooks/Invalid/NoInitializeMethod.csproj | 9 + .../Hooks/Invalid/NonVoidReturn.csproj | 10 + .../Hooks/Invalid/NotParameterless.csproj | 10 + .../StartupHooks/Hooks/PrivateInitialize.cs | 15 + .../Hooks/PrivateInitialize.csproj | 9 + .../Loader/StartupHooks/StartupHookTests.cs | 194 ++++++ .../StartupHooks/StartupHookTests.csproj | 29 + src/tests/issues.targets | 3 + 32 files changed, 431 insertions(+), 759 deletions(-) delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookFake.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookInvalidAssembly.dll delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.csproj delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.cs delete mode 100644 src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.csproj create mode 100644 src/tests/Loader/StartupHooks/Hook.cs create mode 100644 src/tests/Loader/StartupHooks/Hooks/Basic.cs create mode 100644 src/tests/Loader/StartupHooks/Hooks/Basic.csproj create mode 100644 src/tests/Loader/StartupHooks/Hooks/Invalid/InstanceMethod.csproj rename src/{installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.cs => tests/Loader/StartupHooks/Hooks/Invalid/InvalidHook.cs} (71%) create mode 100644 src/tests/Loader/StartupHooks/Hooks/Invalid/MultipleIncorrectSignatures.csproj create mode 100644 src/tests/Loader/StartupHooks/Hooks/Invalid/NoInitializeMethod.csproj create mode 100644 src/tests/Loader/StartupHooks/Hooks/Invalid/NonVoidReturn.csproj create mode 100644 src/tests/Loader/StartupHooks/Hooks/Invalid/NotParameterless.csproj create mode 100644 src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.cs create mode 100644 src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.csproj create mode 100644 src/tests/Loader/StartupHooks/StartupHookTests.cs create mode 100644 src/tests/Loader/StartupHooks/StartupHookTests.csproj diff --git a/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookFake.csproj b/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookFake.csproj deleted file mode 100644 index f55862545f6a5..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookFake.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - - - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookInvalidAssembly.dll b/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookInvalidAssembly.dll deleted file mode 100644 index d487a97925e3d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookFake/StartupHookInvalidAssembly.dll +++ /dev/null @@ -1 +0,0 @@ -Used to test assembly load failure in startup hook path. \ No newline at end of file diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.cs deleted file mode 100644 index cb39d711013c8..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - public void Initialize() - { - // This hook should not be called because it's an instance - // method. Instead, the startup hook provider code should - // throw an exception. - Console.WriteLine("Hello from startup hook with instance method!"); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithInstanceMethod/StartupHookWithInstanceMethod.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.cs deleted file mode 100644 index e5b3df4bbc377..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - static void Initialize() - { - // Success case with a startup hook that is a private method. - Console.WriteLine("Hello from startup hook with non-public method!"); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithNonPublicMethod/StartupHookWithNonPublicMethod.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.cs deleted file mode 100644 index 69ca754984865..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - public static void Initialize() - { - // Success case with a startup hook that contains multiple - // Initialize methods. This is used to check that the startup - // hook provider doesn't get confused by the presence of an - // extra Initialize method with an incorrect signature. - Initialize(123); - } - - public static void Initialize(int input) - { - Console.WriteLine("Hello from startup hook with overload! Input: " + input); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithOverload/StartupHookWithOverload.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.cs deleted file mode 100644 index 76fa2a4a54c96..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - public static void Initialize(int input) - { - // This hook should not be called because it takes a - // parameter. Instead, the startup hook provider code should - // throw an exception. - Console.WriteLine("Hello from startup hook taking int! Input: " + input); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithParameter/StartupHookWithParameter.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.cs deleted file mode 100644 index e1e5172ef104e..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - public static int Initialize() - { - // This hook should not be called because it doesn't have a - // void return type. Instead, the startup hook provider code - // should throw an exception. - Console.WriteLine("Hello from startup hook returning int!"); - return 10; - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithReturnType/StartupHookWithReturnType.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.cs deleted file mode 100644 index f19b469dac718..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHook -{ - public static void Init() - { - // This hook should not be called because it doesn't have the - // correct name (Initialize). Instead, the startup hook - // provider code should throw an exception. - Console.WriteLine("Hello from startup hook!"); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithoutInitializeMethod/StartupHookWithoutInitializeMethod.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.cs b/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.cs deleted file mode 100644 index 547b1d6bd6daf..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -internal class StartupHookWrongType -{ - public static void Initialize() - { - // This hook should not be called because it doesn't have the - // correct type name (StartupHook). Instead, the startup hook - // provider code should throw an exception. - Console.WriteLine("Hello from startup hook!"); - } -} diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.csproj b/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.csproj deleted file mode 100644 index 637cbf46c656d..0000000000000 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithoutStartupHookType/StartupHookWithoutStartupHookType.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - $(NetCoreAppCurrent) - $(MNAVersion) - - - diff --git a/src/installer/tests/HostActivation.Tests/StartupHooks.cs b/src/installer/tests/HostActivation.Tests/StartupHooks.cs index 7f71bb83baa10..e6fbf6225283c 100644 --- a/src/installer/tests/HostActivation.Tests/StartupHooks.cs +++ b/src/installer/tests/HostActivation.Tests/StartupHooks.cs @@ -21,89 +21,6 @@ public StartupHooks(StartupHooks.SharedTestState fixture) sharedTestState = fixture; } - // Run the app with a startup hook - [Fact] - public void Muxer_activation_of_StartupHook_Succeeds() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHookWithNonPublicMethodFixture = sharedTestState.StartupHookWithNonPublicMethodFixture.Copy(); - var startupHookWithNonPublicMethodDll = startupHookWithNonPublicMethodFixture.TestProject.AppDll; - - // Simple startup hook - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello World"); - - // Non-public Initialize method - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookWithNonPublicMethodDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook with non-public method"); - - // Ensure startup hook tracing works - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookDll) - .EnableTracingAndCaptureOutputs() - .Execute() - .Should().Pass() - .And.HaveStdErrContaining("Property STARTUP_HOOKS = " + startupHookDll) - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello World"); - - // Startup hook in type that has an additional overload of Initialize with a different signature - startupHookFixture = sharedTestState.StartupHookWithOverloadFixture.Copy(); - startupHookDll = startupHookFixture.TestProject.AppDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook with overload! Input: 123") - .And.HaveStdOutContaining("Hello World"); - } - - // Run the app with multiple startup hooks - [Fact] - public void Muxer_activation_of_Multiple_StartupHooks_Succeeds() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHook2Fixture = sharedTestState.StartupHookWithDependencyFixture.Copy(); - var startupHook2Dll = startupHook2Fixture.TestProject.AppDll; - - // Multiple startup hooks - var startupHookVar = startupHookDll + Path.PathSeparator + startupHook2Dll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello from startup hook with dependency!") - .And.HaveStdOutContaining("Hello World"); - } - [Fact] public void Muxer_activation_of_RuntimeConfig_StartupHook_Succeeds() { @@ -120,10 +37,10 @@ public void Muxer_activation_of_RuntimeConfig_StartupHook_Succeeds() // RuntimeConfig defined startup hook dotnet.Exec(appDll) - .CaptureStdOut() - .CaptureStdErr() + .EnableTracingAndCaptureOutputs() .Execute() .Should().Pass() + .And.HaveStdErrContaining($"Property STARTUP_HOOKS = {startupHookDll}") .And.HaveStdOutContaining("Hello from startup hook!") .And.HaveStdOutContaining("Hello World"); } @@ -201,433 +118,6 @@ public void Muxer_activation_of_StartupHook_With_Missing_Dependencies_Fails() .And.HaveStdErrContaining("System.IO.FileNotFoundException: Could not load file or assembly 'Newtonsoft.Json"); } - // Different variants of the startup hook variable format - [Fact] - public void Muxer_activation_of_StartupHook_VariableVariants() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHook2Fixture = sharedTestState.StartupHookWithDependencyFixture.Copy(); - var startupHook2Dll = startupHook2Fixture.TestProject.AppDll; - - // Missing entries in the hook - var startupHookVar = startupHookDll + Path.PathSeparator + Path.PathSeparator + startupHook2Dll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello from startup hook with dependency!") - .And.HaveStdOutContaining("Hello World"); - - // Whitespace is invalid - startupHookVar = startupHookDll + Path.PathSeparator + " " + Path.PathSeparator + startupHook2Dll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining("System.ArgumentException: The startup hook simple assembly name ' ' is invalid."); - - // Leading separator - startupHookVar = Path.PathSeparator + startupHookDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello World"); - - // Trailing separator - startupHookVar = startupHookDll + Path.PathSeparator + startupHook2Dll + Path.PathSeparator; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello from startup hook with dependency!") - .And.HaveStdOutContaining("Hello World"); - } - - [Fact] - public void Muxer_activation_of_StartupHook_With_Invalid_Simple_Name_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var relativeAssemblyPath = $".{Path.DirectorySeparatorChar}Assembly"; - - var expectedError = "System.ArgumentException: The startup hook simple assembly name '{0}' is invalid."; - - // With directory separator - var startupHookVar = relativeAssemblyPath; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.NotHaveStdErrContaining("--->"); - - // With alternative directory separator - startupHookVar = $".{Path.AltDirectorySeparatorChar}Assembly"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.NotHaveStdErrContaining("--->"); - - // With comma - startupHookVar = $"Assembly,version=1.0.0.0"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.NotHaveStdErrContaining("--->"); - - // With space - startupHookVar = $"Assembly version"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.NotHaveStdErrContaining("--->"); - - // With .dll suffix - startupHookVar = $".{Path.AltDirectorySeparatorChar}Assembly.DLl"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.NotHaveStdErrContaining("--->"); - - // With invalid name - startupHookVar = $"Assembly=Name"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.HaveStdErrContaining("---> System.IO.FileLoadException: The given assembly name was invalid."); - - // Relative path error is caught before any hooks run - startupHookVar = startupHookDll + Path.PathSeparator + relativeAssemblyPath; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, relativeAssemblyPath)) - .And.NotHaveStdOutContaining("Hello from startup hook!"); - } - - [Fact] - public void Muxer_activation_of_StartupHook_With_Missing_Assembly_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var expectedError = "System.ArgumentException: Startup hook assembly '{0}' failed to load."; - - // With file path which doesn't exist - var startupHookVar = startupHookDll + ".missing.dll"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.HaveStdErrContaining($"---> System.IO.FileNotFoundException: Could not load file or assembly '{startupHookVar}'. The system cannot find the file specified."); - - // With simple name which won't resolve - startupHookVar = "MissingAssembly"; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar)) - .And.HaveStdErrContaining($"---> System.IO.FileNotFoundException: Could not load file or assembly '{startupHookVar}"); - } - - [Fact] - public void Muxer_activation_of_StartupHook_WithSimpleAssemblyName_Succeeds() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - var startupHookAssemblyName = Path.GetFileNameWithoutExtension(startupHookDll); - - File.Copy(startupHookDll, Path.Combine(fixture.TestProject.BuiltApp.Location, Path.GetFileName(startupHookDll))); - - SharedFramework.AddReferenceToDepsJson( - fixture.TestProject.DepsJson, - $"{fixture.TestProject.AssemblyName}/1.0.0", - startupHookAssemblyName, - "1.0.0"); - - fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll) - .EnvironmentVariable(startupHookVarName, startupHookAssemblyName) - .CaptureStdOut() - .CaptureStdErr() - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdOutContaining("Hello World"); - } - - // Run the app with missing startup hook assembly - [Fact] - public void Muxer_activation_of_Missing_StartupHook_Assembly_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - var startupHookMissingDll = Path.Combine(Path.GetDirectoryName(startupHookDll), "StartupHookMissing.dll"); - - var expectedError = "System.IO.FileNotFoundException: Could not load file or assembly '{0}'."; - - // Missing dll is detected with appropriate error - var startupHookVar = startupHookMissingDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, Path.GetFullPath(startupHookMissingDll))); - - // Missing dll is detected after previous hooks run - startupHookVar = startupHookDll + Path.PathSeparator + startupHookMissingDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdErrContaining(string.Format(expectedError, Path.GetFullPath((startupHookMissingDll)))); - } - - // Run the app with an invalid startup hook assembly - [Fact] - public void Muxer_activation_of_Invalid_StartupHook_Assembly_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHookInvalidAssembly = sharedTestState.StartupHookStartupHookInvalidAssemblyFixture.Copy(); - var startupHookInvalidAssemblyDll = Path.Combine(Path.GetDirectoryName(startupHookInvalidAssembly.TestProject.AppDll), "StartupHookInvalidAssembly.dll"); - - var expectedError = "System.BadImageFormatException"; - - // Dll load gives meaningful error message - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookInvalidAssemblyDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(expectedError); - - // Dll load error happens after previous hooks run - var startupHookVar = startupHookDll + Path.PathSeparator + startupHookInvalidAssemblyDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(expectedError); - } - - // Run the app with the startup hook type missing - [Fact] - public void Muxer_activation_of_Missing_StartupHook_Type_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHookMissingTypeFixture = sharedTestState.StartupHookWithoutStartupHookTypeFixture.Copy(); - var startupHookMissingTypeDll = startupHookMissingTypeFixture.TestProject.AppDll; - - // Missing type is detected - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookMissingTypeDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining("System.TypeLoadException: Could not load type 'StartupHook' from assembly 'StartupHook"); - - // Missing type is detected after previous hooks have run - var startupHookVar = startupHookDll + Path.PathSeparator + startupHookMissingTypeDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdErrContaining("System.TypeLoadException: Could not load type 'StartupHook' from assembly 'StartupHookWithoutStartupHookType"); - } - - - // Run the app with a startup hook that doesn't have any Initialize method - [Fact] - public void Muxer_activation_of_StartupHook_With_Missing_Method_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var startupHookMissingMethodFixture = sharedTestState.StartupHookWithoutInitializeMethodFixture.Copy(); - var startupHookMissingMethodDll = startupHookMissingMethodFixture.TestProject.AppDll; - - var expectedError = "System.MissingMethodException: Method 'StartupHook.Initialize' not found."; - - // No Initialize method - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookMissingMethodDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(expectedError); - - // Missing Initialize method is caught after previous hooks have run - var startupHookVar = startupHookDll + Path.PathSeparator + startupHookMissingMethodDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdErrContaining(expectedError); - } - - // Run the app with startup hook that has no static void Initialize() method - [Fact] - public void Muxer_activation_of_StartupHook_With_Incorrect_Method_Signature_Fails() - { - var fixture = sharedTestState.PortableAppFixture.Copy(); - var dotnet = fixture.BuiltDotnet; - var appDll = fixture.TestProject.AppDll; - - var startupHookFixture = sharedTestState.StartupHookFixture.Copy(); - var startupHookDll = startupHookFixture.TestProject.AppDll; - - var expectedError = "System.ArgumentException: The signature of the startup hook 'StartupHook.Initialize' in assembly '{0}' was invalid. It must be 'public static void Initialize()'."; - - // Initialize is an instance method - var startupHookWithInstanceMethodFixture = sharedTestState.StartupHookWithInstanceMethodFixture.Copy(); - var startupHookWithInstanceMethodDll = startupHookWithInstanceMethodFixture.TestProject.AppDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookWithInstanceMethodDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookWithInstanceMethodDll)); - - // Initialize method takes parameters - var startupHookWithParameterFixture = sharedTestState.StartupHookWithParameterFixture.Copy(); - var startupHookWithParameterDll = startupHookWithParameterFixture.TestProject.AppDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookWithParameterDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookWithParameterDll)); - - // Initialize method has non-void return type - var startupHookWithReturnTypeFixture = sharedTestState.StartupHookWithReturnTypeFixture.Copy(); - var startupHookWithReturnTypeDll = startupHookWithReturnTypeFixture.TestProject.AppDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookWithReturnTypeDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookWithReturnTypeDll)); - - // Initialize method that has multiple methods with an incorrect signature - var startupHookWithMultipleIncorrectSignaturesFixture = sharedTestState.StartupHookWithMultipleIncorrectSignaturesFixture.Copy(); - var startupHookWithMultipleIncorrectSignaturesDll = startupHookWithMultipleIncorrectSignaturesFixture.TestProject.AppDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookWithMultipleIncorrectSignaturesDll) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdErrContaining(string.Format(expectedError, startupHookWithMultipleIncorrectSignaturesDll)); - - // Signature problem is caught after previous hooks have run - var startupHookVar = startupHookDll + Path.PathSeparator + startupHookWithMultipleIncorrectSignaturesDll; - dotnet.Exec(appDll) - .EnvironmentVariable(startupHookVarName, startupHookVar) - .CaptureStdOut() - .CaptureStdErr() - .Execute(expectedToFail: true) - .Should().Fail() - .And.HaveStdOutContaining("Hello from startup hook!") - .And.HaveStdErrContaining(string.Format(expectedError, startupHookWithMultipleIncorrectSignaturesDll)); - } - private static void RemoveLibraryFromDepsJson(string depsJsonPath, string libraryName) { DependencyContext context; @@ -733,19 +223,7 @@ public class SharedTestState : IDisposable // Correct startup hooks public TestProjectFixture StartupHookFixture { get; } - public TestProjectFixture StartupHookWithOverloadFixture { get; } - // Missing startup hook type (no StartupHook type defined) - public TestProjectFixture StartupHookWithoutStartupHookTypeFixture { get; } - // Missing startup hook method (no Initialize method defined) - public TestProjectFixture StartupHookWithoutInitializeMethodFixture { get; } - // Invalid startup hook assembly - public TestProjectFixture StartupHookStartupHookInvalidAssemblyFixture { get; } - // Invalid startup hooks (incorrect signatures) - public TestProjectFixture StartupHookWithNonPublicMethodFixture { get; } - public TestProjectFixture StartupHookWithInstanceMethodFixture { get; } - public TestProjectFixture StartupHookWithParameterFixture { get; } - public TestProjectFixture StartupHookWithReturnTypeFixture { get; } - public TestProjectFixture StartupHookWithMultipleIncorrectSignaturesFixture { get; } + // Valid startup hooks with incorrect behavior public TestProjectFixture StartupHookWithDependencyFixture { get; } @@ -775,37 +253,7 @@ public SharedTestState() StartupHookFixture = new TestProjectFixture("StartupHook", RepoDirectories) .EnsureRestored() .PublishProject(); - StartupHookWithOverloadFixture = new TestProjectFixture("StartupHookWithOverload", RepoDirectories) - .EnsureRestored() - .PublishProject(); - // Missing startup hook type (no StartupHook type defined) - StartupHookWithoutStartupHookTypeFixture = new TestProjectFixture("StartupHookWithoutStartupHookType", RepoDirectories) - .EnsureRestored() - .PublishProject(); - // Missing startup hook method (no Initialize method defined) - StartupHookWithoutInitializeMethodFixture = new TestProjectFixture("StartupHookWithoutInitializeMethod", RepoDirectories) - .EnsureRestored() - .PublishProject(); - // Invalid startup hook assembly - StartupHookStartupHookInvalidAssemblyFixture = new TestProjectFixture("StartupHookFake", RepoDirectories) - .EnsureRestored() - .PublishProject(); - // Invalid startup hooks (incorrect signatures) - StartupHookWithNonPublicMethodFixture = new TestProjectFixture("StartupHookWithNonPublicMethod", RepoDirectories) - .EnsureRestored() - .PublishProject(); - StartupHookWithInstanceMethodFixture = new TestProjectFixture("StartupHookWithInstanceMethod", RepoDirectories) - .EnsureRestored() - .PublishProject(); - StartupHookWithParameterFixture = new TestProjectFixture("StartupHookWithParameter", RepoDirectories) - .EnsureRestored() - .PublishProject(); - StartupHookWithReturnTypeFixture = new TestProjectFixture("StartupHookWithReturnType", RepoDirectories) - .EnsureRestored() - .PublishProject(); - StartupHookWithMultipleIncorrectSignaturesFixture = new TestProjectFixture("StartupHookWithMultipleIncorrectSignatures", RepoDirectories) - .EnsureRestored() - .PublishProject(); + // Valid startup hooks with incorrect behavior StartupHookWithDependencyFixture = new TestProjectFixture("StartupHookWithDependency", RepoDirectories) .EnsureRestored() @@ -827,19 +275,7 @@ public void Dispose() // Correct startup hooks StartupHookFixture.Dispose(); - StartupHookWithOverloadFixture.Dispose(); - // Missing startup hook type (no StartupHook type defined) - StartupHookWithoutStartupHookTypeFixture.Dispose(); - // Missing startup hook method (no Initialize method defined) - StartupHookWithoutInitializeMethodFixture.Dispose(); - // Invalid startup hook assembly - StartupHookStartupHookInvalidAssemblyFixture.Dispose(); - // Invalid startup hooks (incorrect signatures) - StartupHookWithNonPublicMethodFixture.Dispose(); - StartupHookWithInstanceMethodFixture.Dispose(); - StartupHookWithParameterFixture.Dispose(); - StartupHookWithReturnTypeFixture.Dispose(); - StartupHookWithMultipleIncorrectSignaturesFixture.Dispose(); + // Valid startup hooks with incorrect behavior StartupHookWithDependencyFixture.Dispose(); diff --git a/src/tests/Loader/StartupHooks/Hook.cs b/src/tests/Loader/StartupHooks/Hook.cs new file mode 100644 index 0000000000000..409d84ee4b5d2 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hook.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection; + +public class Hook +{ + public static Hook Basic = new Hook(nameof(Basic), usePathAsValue: false); + public static Hook PrivateInitialize = new Hook(nameof(PrivateInitialize), Path.Combine(AppContext.BaseDirectory, "private")); + + public static Hook InstanceMethod = new Hook(nameof(InstanceMethod)); + public static Hook MultipleIncorrectSignatures = new Hook(nameof(MultipleIncorrectSignatures)); + public static Hook NoInitializeMethod = new Hook(nameof(NoInitializeMethod)); + public static Hook NonVoidReturn = new Hook(nameof(NonVoidReturn)); + public static Hook NotParameterless = new Hook(nameof(NotParameterless)); + + private string directory; + + public Hook(string name, bool usePathAsValue = true) + : this(name, AppContext.BaseDirectory, usePathAsValue) + { } + + public Hook(string name, string directory, bool usePathAsValue = true) + { + Name = name; + AssemblyPath = Path.Combine(directory, $"{name}.dll"); + Value = usePathAsValue ? AssemblyPath : Name; + } + + public string Name { get; } + public string Value { get; } + + private string AssemblyPath { get; } + + public unsafe int CallCount + { + get + { + if (TryGetCallCountProperty(out PropertyInfo callCount)) + { + delegate* getCallCount = (delegate*)callCount.GetMethod.MethodHandle.GetFunctionPointer(); + return getCallCount(); + } + + return 0; + } + set + { + if (TryGetCallCountProperty(out PropertyInfo callCount)) + { + delegate* setCallCount = (delegate*)callCount.SetMethod.MethodHandle.GetFunctionPointer(); + setCallCount(value); + } + } + } + + private bool TryGetCallCountProperty(out PropertyInfo callCount) + { + callCount = null; + Assembly asm = null; + foreach(Assembly loaded in AppDomain.CurrentDomain.GetAssemblies()) + { + if (loaded.GetName().Name == Name && loaded.Location == AssemblyPath) + { + asm = loaded; + break; + } + } + + if (asm == null) + return false; + + Type hook = asm.GetType("StartupHook"); + if (hook == null) + return false; + + callCount = hook.GetProperty(nameof(CallCount), BindingFlags.NonPublic | BindingFlags.Static); + return callCount != null; + } +} \ No newline at end of file diff --git a/src/tests/Loader/StartupHooks/Hooks/Basic.cs b/src/tests/Loader/StartupHooks/Hooks/Basic.cs new file mode 100644 index 0000000000000..3603c747737d1 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Basic.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +internal class StartupHook +{ + private static int CallCount { get; set; } + + public static void Initialize() + { + // Normal success case with a simple startup hook. + Initialize(123); + } + + public static void Initialize(int input) + { + CallCount++; + Console.WriteLine($"-- Hello from startup hook with overload! Call count: {CallCount}"); + } +} diff --git a/src/tests/Loader/StartupHooks/Hooks/Basic.csproj b/src/tests/Loader/StartupHooks/Hooks/Basic.csproj new file mode 100644 index 0000000000000..7caea741aa659 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Basic.csproj @@ -0,0 +1,9 @@ + + + Library + BuildOnly + + + + + diff --git a/src/tests/Loader/StartupHooks/Hooks/Invalid/InstanceMethod.csproj b/src/tests/Loader/StartupHooks/Hooks/Invalid/InstanceMethod.csproj new file mode 100644 index 0000000000000..ca2f21af1615a --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/InstanceMethod.csproj @@ -0,0 +1,10 @@ + + + Library + BuildOnly + $(DefineConstants);INSTANCE_METHOD + + + + + diff --git a/src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.cs b/src/tests/Loader/StartupHooks/Hooks/Invalid/InvalidHook.cs similarity index 71% rename from src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.cs rename to src/tests/Loader/StartupHooks/Hooks/Invalid/InvalidHook.cs index 79daf76ceca49..911458b606da9 100644 --- a/src/installer/tests/Assets/TestProjects/StartupHookWithMultipleIncorrectSignatures/StartupHookWithMultipleIncorrectSignatures.cs +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/InvalidHook.cs @@ -5,20 +5,32 @@ internal class StartupHook { - // Neither of these hooks should be called, because they have the + // None of these hooks should be called, because they have the // wrong signature (it should be static void Initialize()). This // is used to check that the provider code properly detects the // case where there are multiple incorrect Initialize // methods. Instead, the startup hook provider code should throw // an exception. - public void Initialize() +#if NONVOID_RETURN + public static int Initialize() { - Console.WriteLine("Hello from startup hook with instance method!"); + Console.WriteLine("Hello from startup hook returning int!"); + return 10; } +#endif +#if NOT_PARAMETERLESS public static void Initialize(int input) { Console.WriteLine("Hello from startup hook taking int! Input: " + input); } +#endif + +#if INSTANCE_METHOD + public void Initialize() + { + Console.WriteLine("Hello from startup hook with instance method!"); + } +#endif } diff --git a/src/tests/Loader/StartupHooks/Hooks/Invalid/MultipleIncorrectSignatures.csproj b/src/tests/Loader/StartupHooks/Hooks/Invalid/MultipleIncorrectSignatures.csproj new file mode 100644 index 0000000000000..c15f1858c05a5 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/MultipleIncorrectSignatures.csproj @@ -0,0 +1,10 @@ + + + Library + BuildOnly + $(DefineConstants);INSTANCE_METHOD;NOT_PARAMETERLESS + + + + + diff --git a/src/tests/Loader/StartupHooks/Hooks/Invalid/NoInitializeMethod.csproj b/src/tests/Loader/StartupHooks/Hooks/Invalid/NoInitializeMethod.csproj new file mode 100644 index 0000000000000..ed2d933058fba --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/NoInitializeMethod.csproj @@ -0,0 +1,9 @@ + + + Library + BuildOnly + + + + + diff --git a/src/tests/Loader/StartupHooks/Hooks/Invalid/NonVoidReturn.csproj b/src/tests/Loader/StartupHooks/Hooks/Invalid/NonVoidReturn.csproj new file mode 100644 index 0000000000000..ea5a5c7460165 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/NonVoidReturn.csproj @@ -0,0 +1,10 @@ + + + Library + BuildOnly + $(DefineConstants);NONVOID_RETURN + + + + + diff --git a/src/tests/Loader/StartupHooks/Hooks/Invalid/NotParameterless.csproj b/src/tests/Loader/StartupHooks/Hooks/Invalid/NotParameterless.csproj new file mode 100644 index 0000000000000..61692ff3141ae --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/Invalid/NotParameterless.csproj @@ -0,0 +1,10 @@ + + + Library + BuildOnly + $(DefineConstants);NOT_PARAMETERLESS + + + + + diff --git a/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.cs b/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.cs new file mode 100644 index 0000000000000..d31d13ec2e1ea --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +internal class StartupHook +{ + private static int CallCount { get; set; } + + private static void Initialize() + { + CallCount++; + Console.WriteLine($"-- Hello from startup hook with non-public method! Call count: {CallCount}"); + } +} diff --git a/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.csproj b/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.csproj new file mode 100644 index 0000000000000..e972d6c617be2 --- /dev/null +++ b/src/tests/Loader/StartupHooks/Hooks/PrivateInitialize.csproj @@ -0,0 +1,9 @@ + + + Library + BuildOnly + + + + + diff --git a/src/tests/Loader/StartupHooks/StartupHookTests.cs b/src/tests/Loader/StartupHooks/StartupHookTests.cs new file mode 100644 index 0000000000000..2f188bd43e188 --- /dev/null +++ b/src/tests/Loader/StartupHooks/StartupHookTests.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection; + +using Xunit; + +[ConditionalClass(typeof(StartupHookTests), nameof(StartupHookTests.IsSupported))] +public unsafe class StartupHookTests +{ + private const string StartupHookKey = "STARTUP_HOOKS"; + + private static Type s_startupHookProvider = typeof(object).Assembly.GetType("System.StartupHookProvider", throwOnError: true); + + private static delegate* ProcessStartupHooks = (delegate*)s_startupHookProvider.GetMethod("ProcessStartupHooks", BindingFlags.NonPublic | BindingFlags.Static).MethodHandle.GetFunctionPointer(); + + public static bool IsSupported = ((delegate*)s_startupHookProvider.GetProperty(nameof(IsSupported), BindingFlags.NonPublic | BindingFlags.Static).GetMethod.MethodHandle.GetFunctionPointer())(); + + [Fact] + public static void ValidHookName() + { + Console.WriteLine($"Running {nameof(ValidHookName)}..."); + + // Basic hook uses the simple name + Hook hook = Hook.Basic; + Assert.False(Path.IsPathRooted(hook.Value)); + AppContext.SetData(StartupHookKey, hook.Value); + hook.CallCount = 0; + + Assert.Equal(0, hook.CallCount); + ProcessStartupHooks(); + Assert.Equal(1, hook.CallCount); + } + + [Fact] + public static void ValidHookPath() + { + Console.WriteLine($"Running {nameof(ValidHookPath)}..."); + + // Private hook uses a path. It is in a subdirectory and would not be found via default probing. + Hook hook = Hook.PrivateInitialize; + Assert.True(Path.IsPathRooted(hook.Value)); + AppContext.SetData(StartupHookKey, hook.Value); + hook.CallCount = 0; + + Assert.Equal(0, hook.CallCount); + ProcessStartupHooks(); + Assert.Equal(1, hook.CallCount); + } + + [Fact] + public static void MultipleValidHooksAndSeparators() + { + Console.WriteLine($"Running {nameof(MultipleValidHooksAndSeparators)}..."); + + Hook hook1 = Hook.Basic; + Hook hook2 = Hook.PrivateInitialize; + + // Set multiple hooks with an empty entry and leading/trailing separators + AppContext.SetData(StartupHookKey, $"{Path.PathSeparator}{hook1.Value}{Path.PathSeparator}{Path.PathSeparator}{hook2.Value}{Path.PathSeparator}"); + hook1.CallCount = 0; + hook2.CallCount = 0; + + Assert.Equal(0, hook1.CallCount); + Assert.Equal(0, hook2.CallCount); + ProcessStartupHooks(); + Assert.Equal(1, hook1.CallCount); + Assert.Equal(1, hook2.CallCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void MissingAssembly(bool useAssemblyName) + { + Console.WriteLine($"Running {nameof(MissingAssembly)}..."); + + string hook = useAssemblyName ? "MissingAssembly" : Path.Combine(AppContext.BaseDirectory, "MissingAssembly.dll"); + AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{hook}"); + Hook.Basic.CallCount = 0; + + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message); + Assert.IsType(ex.InnerException); + + // Previous hooks should run before erroring on the missing assembly + Assert.Equal(1, Hook.Basic.CallCount); + } + + [Fact] + public static void InvalidAssembly() + { + Console.WriteLine($"Running {nameof(InvalidAssembly)}..."); + + string hook = Path.Combine(AppContext.BaseDirectory, "InvalidAssembly.dll"); + try + { + File.WriteAllText(hook, string.Empty); + AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{hook}"); + Hook.Basic.CallCount = 0; + + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message); + var innerEx = ex.InnerException; + Assert.IsType(ex.InnerException); + + // Previous hooks should run before erroring on the invalid assembly + Assert.Equal(1, Hook.Basic.CallCount); + } + finally + { + File.Delete(hook); + } + } + + public static System.Collections.Generic.IEnumerable InvalidSimpleAssemblyNameData() + { + yield return new object[] {$".{Path.DirectorySeparatorChar}Assembly", true }; // Directory separator + yield return new object[] {$".{Path.AltDirectorySeparatorChar}Assembly", true}; // Alternative directory separator + yield return new object[] {"Assembly,version=1.0.0.0", true}; // Comma + yield return new object[] {"Assembly version", true}; // Space + yield return new object[] {"Assembly.DLL", true}; // .dll suffix + yield return new object[] {"Assembly=Name", false}; // Invalid name + } + + [Theory] + [MemberData(nameof(InvalidSimpleAssemblyNameData))] + public static void InvalidSimpleAssemblyName(string name, bool failsSimpleNameCheck) + { + Console.WriteLine($"Running {nameof(InvalidSimpleAssemblyName)}({name}, {failsSimpleNameCheck})..."); + + AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{name}"); + Hook.Basic.CallCount = 0; + + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.StartsWith($"The startup hook simple assembly name '{name}' is invalid.", ex.Message); + if (failsSimpleNameCheck) + { + Assert.Null(ex.InnerException); + } + else + { + var innerEx = ex.InnerException; + Assert.IsType(innerEx); + Assert.Equal($"The given assembly name was invalid.", innerEx.Message); + } + + // Invalid assembly name should error early such that previous hooks are not run + Assert.Equal(0, Hook.Basic.CallCount); + } + + [Fact] + public static void MissingStartupHookType() + { + Console.WriteLine($"Running {nameof(MissingStartupHookType)}..."); + + var asm = typeof(StartupHookTests).Assembly; + string hook = asm.Location; + AppContext.SetData(StartupHookKey, hook); + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.StartsWith($"Could not load type 'StartupHook' from assembly '{asm.GetName().Name}", ex.Message); + } + + [Fact] + public static void MissingInitializeMethod() + { + Console.WriteLine($"Running {nameof(MissingInitializeMethod)}..."); + + AppContext.SetData(StartupHookKey, Hook.NoInitializeMethod.Value); + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.Equal($"Method 'StartupHook.Initialize' not found.", ex.Message); + } + + public static System.Collections.Generic.IEnumerable IncorrectInitializeSignatureData() + { + yield return new[] { Hook.InstanceMethod }; + yield return new[] { Hook.MultipleIncorrectSignatures }; + yield return new[] { Hook.NonVoidReturn }; + yield return new[] { Hook.NotParameterless }; + } + + [Theory] + [MemberData(nameof(IncorrectInitializeSignatureData))] + public static void IncorrectInitializeSignature(Hook hook) + { + Console.WriteLine($"Running {nameof(IncorrectInitializeSignature)}({hook.Name})..."); + + AppContext.SetData(StartupHookKey, hook.Value); + var ex = Assert.Throws(() => ProcessStartupHooks()); + Assert.Equal($"The signature of the startup hook 'StartupHook.Initialize' in assembly '{hook.Value}' was invalid. It must be 'public static void Initialize()'.", ex.Message); + } +} diff --git a/src/tests/Loader/StartupHooks/StartupHookTests.csproj b/src/tests/Loader/StartupHooks/StartupHookTests.csproj new file mode 100644 index 0000000000000..9f27d76504b70 --- /dev/null +++ b/src/tests/Loader/StartupHooks/StartupHookTests.csproj @@ -0,0 +1,29 @@ + + + true + true + + true + + + + + + + + false + Content + Always + + + false + Content + Always + + + false + Content + Always + + + diff --git a/src/tests/issues.targets b/src/tests/issues.targets index 11a0244ac8b3c..3460e5b2f8d2c 100644 --- a/src/tests/issues.targets +++ b/src/tests/issues.targets @@ -3838,6 +3838,9 @@ Loads an assembly from file + + Loads an assembly from file + System.Threading.Thread.UnsafeStart not supported