-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
Copy pathStartupHookTests.cs
249 lines (204 loc) · 9.99 KB
/
StartupHookTests.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// 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*<string, void> ProcessStartupHooks = (delegate*<string, void>)s_startupHookProvider.GetMethod("ProcessStartupHooks", BindingFlags.NonPublic | BindingFlags.Static).MethodHandle.GetFunctionPointer();
private static bool IsUnsupportedPlatform =
// these platforms need special setup for startup hooks
OperatingSystem.IsAndroid() ||
OperatingSystem.IsIOS() ||
OperatingSystem.IsTvOS() ||
OperatingSystem.IsBrowser() ||
OperatingSystem.IsWasi();
public static bool IsSupported = !IsUnsupportedPlatform && ((delegate*<bool>)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(string.Empty);
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(string.Empty);
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(string.Empty);
Assert.Equal(1, hook1.CallCount);
Assert.Equal(1, hook2.CallCount);
}
[Fact]
public static void MultipleValidDiagnosticHooksAndSeparators()
{
Console.WriteLine($"Running {nameof(MultipleValidDiagnosticHooksAndSeparators)}...");
Hook hook1 = Hook.Basic;
Hook hook2 = Hook.PrivateInitialize;
// Use multiple diagnostic hooks with an empty entry and leading/trailing separators
string diagnosticStartupHooks = $"{Path.PathSeparator}{hook1.Value}{Path.PathSeparator}{Path.PathSeparator}{hook2.Value}{Path.PathSeparator}";
AppContext.SetData(StartupHookKey, null);
hook1.CallCount = 0;
hook2.CallCount = 0;
Assert.Equal(0, hook1.CallCount);
Assert.Equal(0, hook2.CallCount);
ProcessStartupHooks(diagnosticStartupHooks);
Assert.Equal(1, hook1.CallCount);
Assert.Equal(1, hook2.CallCount);
}
[Fact]
public static void MultipleValidDiagnosticAndStandardHooks()
{
Console.WriteLine($"Running {nameof(MultipleValidDiagnosticAndStandardHooks)}...");
Hook hook1 = Hook.Basic;
Hook hook2 = Hook.PrivateInitialize;
AppContext.SetData(StartupHookKey, hook2.Value);
hook1.CallCount = 0;
hook2.CallCount = 0;
Assert.Equal(0, hook1.CallCount);
Assert.Equal(0, hook2.CallCount);
ProcessStartupHooks(hook1.Value);
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<ArgumentException>(() => ProcessStartupHooks(string.Empty));
Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message);
Assert.IsType<FileNotFoundException>(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<ArgumentException>(() => ProcessStartupHooks(string.Empty));
Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message);
var innerEx = ex.InnerException;
Assert.IsType<BadImageFormatException>(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<object[]> 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<ArgumentException>(() => ProcessStartupHooks(string.Empty));
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<FileLoadException>(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<TypeLoadException>(() => ProcessStartupHooks(string.Empty));
if (ex.Message.StartsWith("Could not load type")) // Mono-specific behavior, avoid dependency on TestLibrary.Utilities.IsMonoRuntime
{
Assert.StartsWith($"Could not load type 'StartupHook' from assembly '{asm.GetName().Name}", ex.Message);
}
else
{
Assert.StartsWith($"Could not resolve type 'StartupHook' in 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<MissingMethodException>(() => ProcessStartupHooks(string.Empty));
Assert.Equal($"Method 'StartupHook.Initialize' not found.", ex.Message);
}
public static System.Collections.Generic.IEnumerable<object[]> 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<ArgumentException>(() => ProcessStartupHooks(string.Empty));
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);
}
}