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 f55862545f6a5e..00000000000000
--- 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 d487a97925e3d0..00000000000000
--- 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 cb39d711013c86..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 e5b3df4bbc3778..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 69ca754984865f..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 76fa2a4a54c96e..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 e1e5172ef104eb..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 f19b469dac7180..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 547b1d6bd6daf9..00000000000000
--- 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 637cbf46c656dd..00000000000000
--- 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 7f71bb83baa102..e6fbf6225283c2 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 00000000000000..409d84ee4b5d2b
--- /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 00000000000000..3603c747737d1c
--- /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 00000000000000..7caea741aa659e
--- /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 00000000000000..ca2f21af1615ac
--- /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 79daf76ceca49d..911458b606da9d 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 00000000000000..c15f1858c05a5f
--- /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 00000000000000..ed2d933058fbac
--- /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 00000000000000..ea5a5c7460165a
--- /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 00000000000000..61692ff3141ae4
--- /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 00000000000000..d31d13ec2e1ea2
--- /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 00000000000000..e972d6c617be2e
--- /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 00000000000000..2f188bd43e188f
--- /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