diff --git a/Program.cs b/Program.cs index ea433fa..8309d27 100644 --- a/Program.cs +++ b/Program.cs @@ -40,6 +40,14 @@ internal static class Program return; } + // Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop + // und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster. + if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase)) + { + Environment.ExitCode = RunSmokeUi(); + return; + } + ApplicationConfiguration.Initialize(); var modules = new System.Collections.Generic.List @@ -47,7 +55,13 @@ internal static class Program new CopyTradingModule() }; - AppHost = Host.CreateDefaultBuilder().ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) + AppHost = Host.CreateDefaultBuilder() + // Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json + // + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die + // MySQL-Connection – daher hier explizit ergänzen, sonst bleibt sie leer. + .ConfigureAppConfiguration((context, config) => + config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) + .ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) { var databaseOptions = new DatabaseOptions { @@ -237,4 +251,87 @@ internal static class Program Services.ConfigMigrator.VerifyMySql(mySql); } + + /// + /// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung + /// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View + /// sowie den Launcher – ohne Message-Loop und ohne Trading-/WSS-Services zu starten. + /// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl. + /// + private static int RunSmokeUi() + { + ApplicationConfiguration.Initialize(); + + var modules = new System.Collections.Generic.List { new CopyTradingModule() }; + + using var host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false)) + .ConfigureServices((context, services) => + { + var databaseOptions = new DatabaseOptions + { + MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty + }; + services.Configure(context.Configuration.GetSection(DatabaseOptions.SectionName)); + services.AddCorePersistence(databaseOptions); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + foreach (var module in modules) + { + services.AddSingleton(module); + module.RegisterServices(services, context.Configuration); + } + + services.AddSingleton(); + services.AddTransient(); + }).Build(); + + // State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten. + try + { + host.Services.GetRequiredService() + .StartAsync(default).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}"); + } + + var uiHost = host.Services.GetRequiredService(); + foreach (var module in modules) + module.RegisterUi(uiHost, host.Services); + + int failures = 0; + Console.WriteLine("=== Smoke-UI: View-Konstruktion ==="); + foreach (var view in uiHost.Views) + { + try + { + using var form = view.CreateForm(); + Console.WriteLine($"[OK] {view.Id} ({view.Title})"); + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}"); + } + } + + try + { + using var launcher = host.Services.GetRequiredService(); + Console.WriteLine("[OK] LauncherForm konstruiert"); + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}"); + } + + Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); + return failures; + } }