| File: Infrastructure\TestReadinessHostingStartup.cs | Web Access |
| Project: src\aspnetcore\src\Components\Testing\src\Microsoft.AspNetCore.Components.Testing.csproj (Microsoft.AspNetCore.Components.Testing) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Components.Testing.Infrastructure; /// <summary> /// <see cref="IHostingStartup"/> that registers all E2E test infrastructure /// services into the Blazor app under test. Injected via the /// <c>ASPNETCORE_HOSTINGSTARTUPASSEMBLIES</c> environment variable. /// </summary> /// <remarks> /// <para>Registers:</para> /// <list type="bullet"> /// <item>Readiness notification service that POSTs to the test harness when the app starts</item> /// <item>Parent PID watcher for auto-termination on test crash</item> /// <item><see cref="TestSessionContext"/> + <see cref="TestLockProvider"/> + middleware for deterministic async state control</item> /// <item>Static method service overrides (when <c>E2E_TEST_SERVICES_TYPE</c> + <c>E2E_TEST_SERVICES_METHOD</c> are set)</item> /// </list> /// <para> /// The <c>[assembly: HostingStartup]</c> attribute is NOT in this file — it is emitted by the /// source generator into the consuming test assembly. /// </para> /// </remarks> public class TestReadinessHostingStartup : IHostingStartup { /// <inheritdoc /> public void Configure(IWebHostBuilder builder) { builder.ConfigureServices(services => { services.AddHostedService<ReadinessNotificationService>(); services.AddHostedService<ParentProcessWatcher>(); // Test infrastructure: session context, lock provider, and middleware. // Always registered; no-op when tests don't use them. services.AddScoped<TestSessionContext>(); services.AddSingleton<TestLockProvider>(); services.AddTransient<IStartupFilter, TestInfrastructureStartupFilter>(); }); // Wire up service overrides if the env vars are set var overrideTypeName = Environment.GetEnvironmentVariable("E2E_TEST_SERVICES_TYPE"); var overrideMethodName = Environment.GetEnvironmentVariable("E2E_TEST_SERVICES_METHOD"); if (!string.IsNullOrEmpty(overrideTypeName) && !string.IsNullOrEmpty(overrideMethodName)) { // Try the source-generated resolver first (no reflection on the target method) Action<IServiceCollection>? configureServices = TryResolveViaGeneratedResolver(overrideTypeName, overrideMethodName); // Fall back to reflection if the resolver didn't handle it if (configureServices is null) { var type = Type.GetType(overrideTypeName) ?? throw new InvalidOperationException( $"Could not resolve service override type '{overrideTypeName}'. " + "Ensure the type name is assembly-qualified and the assembly is loadable."); var method = type.GetMethod(overrideMethodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, [typeof(IServiceCollection)], null) ?? throw new InvalidOperationException( $"Could not find static method '{overrideMethodName}(IServiceCollection)' on type '{type.FullName}'."); configureServices = services => { method.Invoke(null, [services]); }; } DiagnosticListener.AllListeners.Subscribe( new TestServiceOverrideObserver(configureServices)); } } static Action<IServiceCollection>? TryResolveViaGeneratedResolver( string overrideTypeName, string overrideMethodName) { try { // The generated resolver lives in the test assembly, which is identified // by ASPNETCORE_HOSTINGSTARTUPASSEMBLIES (set by ServerInstance). var assemblyName = Environment.GetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES"); if (string.IsNullOrEmpty(assemblyName)) { return null; } Assembly? testAssembly = null; foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { if (asm.GetName().Name == assemblyName) { testAssembly = asm; break; } } if (testAssembly is null) { return null; } // The source generator emits this well-known type in every test assembly var resolverType = testAssembly.GetType("Microsoft.AspNetCore.Components.Testing.Generated.ServiceOverrideResolver"); if (resolverType is null) { return null; } var resolver = (IE2EServiceOverrideResolver)Activator.CreateInstance(resolverType)!; return resolver.TryResolve(overrideTypeName, overrideMethodName); } catch { // If anything goes wrong with the generated resolver, fall back to reflection return null; } } }