// Copyright Epic Games, Inc. All Rights Reserved. using System.Diagnostics.Metrics; using EpicGames.Core; using EpicGames.Horde.Tools; using HordeCommon; using HordeServer.Accounts; using HordeServer.Acls; using HordeServer.Auditing; using HordeServer.Configuration; using HordeServer.Dashboard; using HordeServer.Plugins; using HordeServer.Server; using HordeServer.Tools; using HordeServer.Users; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenTelemetry.Trace; using Serilog; using Serilog.Exceptions; using Serilog.Exceptions.Core; using Serilog.Exceptions.Grpc.Destructurers; namespace HordeServer.Tests { public class ServerTestSetup : DatabaseIntegrationTest { public FakeClock Clock => ServiceProvider.GetRequiredService(); public IMemoryCache Cache => ServiceProvider.GetRequiredService(); public IAclService AclService => ServiceProvider.GetRequiredService(); public IMongoService MongoService => ServiceProvider.GetRequiredService(); public IDowntimeService DowntimeService => ServiceProvider.GetRequiredService(); public LifetimeService LifetimeService => ServiceProvider.GetRequiredService(); public ServerStatusService ServerStatusService => ServiceProvider.GetRequiredService(); public ServerSettings ServerSettings => ServiceProvider.GetRequiredService>().Value; public IOptionsMonitor ServerSettingsMon => ServiceProvider.GetRequiredService>(); public OpenTelemetry.Trace.Tracer Tracer => ServiceProvider.GetRequiredService(); public Meter Meter => ServiceProvider.GetRequiredService(); public ConfigService ConfigService => ServiceProvider.GetRequiredService(); public IOptionsMonitor GlobalConfig => ServiceProvider.GetRequiredService>(); public IOptionsSnapshot GlobalConfigSnapshot => ServiceProvider.GetRequiredService>(); public IPluginCollection PluginCollection => ServiceProvider.GetRequiredService(); public IEnumerable DefaultAclModifiers => ServiceProvider.GetRequiredService>(); private static bool s_datadogWriterPatched; readonly PluginCollection _pluginCollection; public ServerTestSetup() { _pluginCollection = new PluginCollection(); PatchDatadogWriter(); } static ServerTestSetup() { Serilog.Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .Enrich.WithExceptionDetails(new DestructuringOptionsBuilder() .WithDefaultDestructurers() .WithDestructurers(new[] { new RpcExceptionDestructurer() })) .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:w3}] {Indent}{Message:l}{NewLine}{Exception}") .CreateLogger(); } protected async Task SetConfigAsync(GlobalConfig globalConfig) { await globalConfig.PostLoadAsync(ServerSettings, _pluginCollection.LoadedPlugins, DefaultAclModifiers); ConfigService.OverrideConfig(globalConfig); } protected async Task UpdateConfigAsync(Action action) { GlobalConfig globalConfig = GlobalConfig.CurrentValue; action(globalConfig); await globalConfig.PostLoadAsync(ServerSettings, _pluginCollection.LoadedPlugins, DefaultAclModifiers); ConfigService.OverrideConfig(globalConfig); } protected override void ConfigureSettings(ServerSettings settings) { DirectoryReference baseDir = DirectoryReference.Combine(ServerApp.DataDir, "Tests"); try { FileUtils.ForceDeleteDirectoryContents(baseDir); } catch { } settings.ForceConfigUpdateOnStartup = true; } protected override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); IConfiguration configuration = new ConfigurationBuilder().Build(); ServerSettings settings = new ServerSettings(); ConfigureSettings(settings); services.Configure(x => x.WithAws = true); ServerInfo serverInfo = new ServerInfo(configuration, Options.Create(settings)); services.AddSingleton(serverInfo); services.AddSingleton(_pluginCollection); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddLogging(builder => builder.AddSerilog()); services.AddSingleton(sp => new MemoryCache(new MemoryCacheOptions { })); services.AddSingleton(sp => TracerProvider.Default.GetTracer("TestTracer")); services.AddSingleton(sp => new Meter("TestMeter")); services.AddSingleton(typeof(IAuditLogFactory<>), typeof(AuditLogFactory<>)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton>(sp => sp.GetRequiredService()); services.AddSingleton>(sp => sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(typeof(IHealthMonitor<>), typeof(HealthMonitor<>)); services.AddSingleton(); foreach (ILoadedPlugin plugin in _pluginCollection.LoadedPlugins) { plugin.ConfigureServices(configuration, serverInfo, services); } } protected void AddPlugin() where T : class, IPluginStartup { _pluginCollection.Add(); } /// /// Create a console logger for tests /// /// Type to instantiate /// A logger public static ILogger CreateConsoleLogger() { using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { builder.SetMinimumLevel(LogLevel.Debug); builder.AddSimpleConsole(options => { options.SingleLine = true; }); }); return loggerFactory.CreateLogger(); } /// /// Hack the Datadog tracing library to not block during shutdown of tests. /// Without this fix, the lib will try to send traces to a host that isn't running and block for +20 secs /// /// Since so many of the interfaces and classes in the lib are internal it was difficult to replace Tracer.Instance /// private static void PatchDatadogWriter() { if (s_datadogWriterPatched) { return; } s_datadogWriterPatched = true; /* string msg = "Unable to patch Datadog agent writer! Tests will still work, but shutdown will block for +20 seconds."; FieldInfo? agentWriterField = Datadog.Trace.Tracer.Instance.GetType().GetField("_agentWriter", BindingFlags.NonPublic | BindingFlags.Instance); if (agentWriterField == null) { Console.Error.WriteLine(msg); return; } object? agentWriterInstance = agentWriterField.GetValue(Datadog.Trace.Tracer.Instance); if (agentWriterInstance == null) { Console.Error.WriteLine(msg); return; } FieldInfo? processExitField = agentWriterInstance.GetType().GetField("_processExit", BindingFlags.NonPublic | BindingFlags.Instance); if (processExitField == null) { Console.Error.WriteLine(msg); return; } TaskCompletionSource? processExitInstance = (TaskCompletionSource?)processExitField.GetValue(agentWriterInstance); if (processExitInstance == null) { Console.Error.WriteLine(msg); return; } processExitInstance.TrySetResult(true); */ } } }