using System.Globalization; using System.Text; using System.Text.RegularExpressions; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; Console.OutputEncoding = Encoding.UTF8; using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(10)); Console.CancelKeyPress += (_, eventArgs) => { eventArgs.Cancel = true; cancellation.Cancel(); }; var configurationPath = GetConfigurationPath(args); try { var runtime = DatabaseRuntime.CreateDefault(configurationPath); var statuses = await runtime.HealthService.CheckAllAsync(cancellation.Token); foreach (var status in statuses) { Console.WriteLine(FormatHealthStatus(status)); } if (statuses.Any(status => status.State != DatabaseHealthState.Healthy)) { Console.Error.WriteLine( "Smoke test stopped because both database health probes must be healthy."); return 2; } await VerifyOracleServerVersionAsync(runtime.Executor, cancellation.Token); await VerifyStockSearchAsync(runtime.Executor, cancellation.Token); await VerifyThemeSelectorAsync(runtime.Executor, cancellation.Token); await VerifyOverseasSelectorsAsync(runtime.Executor, cancellation.Token); await VerifyExpertSelectorAsync(runtime.Executor, cancellation.Token); await VerifyTradingHaltSelectorAsync(runtime.Executor, cancellation.Token); await VerifyOperatorCatalogSchemaAsync(runtime.Executor, cancellation.Token); await NxtThemeRestoreDbAudit.RunAsync(runtime.Executor, cancellation.Token); var service = new LegacyMarketDataService(runtime.Executor); await VerifySnapshotAsync(service, MarketDataView.Kospi, cancellation.Token); await VerifySnapshotAsync(service, MarketDataView.Kosdaq, cancellation.Token); await VerifySnapshotAsync(service, MarketDataView.Index, cancellation.Token); await VerifySnapshotAsync(service, MarketDataView.Overseas, cancellation.Token); await RealSceneDbSmoke.RunAsync(runtime.Executor, cancellation.Token); Console.WriteLine("REAL_DB_SMOKE: PASS"); return 0; } catch (OperationCanceledException) { Console.Error.WriteLine("REAL_DB_SMOKE: CANCELED_OR_TIMED_OUT"); return 3; } catch (DatabaseOperationException exception) { Console.Error.WriteLine( "REAL_DB_SMOKE: FAIL - " + $"Source={exception.DataSource} " + $"IsTransient={FormatNullableBoolean(exception.IsTransient)} " + $"ProviderErrorCode={FormatProviderErrorCode(exception.ProviderErrorCode)}"); return 1; } catch (DatabaseInfrastructureException) { Console.Error.WriteLine( "REAL_DB_SMOKE: FAIL - A database infrastructure error occurred. " + "No provider details were printed."); return 1; } catch (OverseasIndustryIndexSearchDataException exception) { Console.Error.WriteLine($"REAL_DB_SMOKE: FAIL - {exception.Message}"); return 1; } catch (Exception exception) { Console.Error.WriteLine( $"REAL_DB_SMOKE: FAIL - {exception.GetType().Name}. No provider details were printed."); return 1; } static string FormatHealthStatus(DatabaseHealthStatus status) { var summary = $"{status.Source}: {status.State} " + $"({status.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture)} ms)"; if (status.State == DatabaseHealthState.Healthy) { return summary; } return summary + $" IsTransient={FormatNullableBoolean(status.IsTransient)}" + $" ProviderErrorCode={FormatProviderErrorCode(status.ProviderErrorCode)}"; } static string FormatNullableBoolean(bool? value) => value switch { true => "true", false => "false", null => "none" }; static string FormatProviderErrorCode(int? value) => value?.ToString(CultureInfo.InvariantCulture) ?? "none"; static string? GetConfigurationPath(string[] arguments) { if (arguments.Length == 0) { return null; } if (arguments.Length == 2 && arguments[0].Equals("--config", StringComparison.OrdinalIgnoreCase)) { return arguments[1]; } throw new DatabaseConfigurationException( "Usage: MBN_STOCK_WEBVIEW.DbSmoke [--config ]"); } static async Task VerifyOracleServerVersionAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { const string query = "SELECT VERSION FROM PRODUCT_COMPONENT_VERSION " + "WHERE PRODUCT LIKE 'Oracle Database%' FETCH FIRST 1 ROWS ONLY"; var table = await executor.ExecuteAsync( DataSourceKind.Oracle, "OracleVersion", query, cancellationToken); if (table.Rows.Count == 0 || table.Columns.Count == 0) { throw new DatabaseConfigurationException( "The Oracle server version query returned no version."); } var version = Convert.ToString( table.Rows[0][0], CultureInfo.InvariantCulture) ?? string.Empty; var match = Regex.Match( version, @"(?\d{2})(?:\.\d+)", RegexOptions.CultureInvariant); if (!match.Success || !int.TryParse( match.Groups["major"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var major)) { throw new DatabaseConfigurationException( "The Oracle server major version could not be determined."); } Console.WriteLine($"Oracle server major version: {major}"); if (major < 19) { throw new DatabaseConfigurationException( "Oracle.ManagedDataAccess.Core requires Oracle Database 19c or newer."); } } static async Task VerifySnapshotAsync( IMarketDataService service, MarketDataView view, CancellationToken cancellationToken) { var snapshot = await service.GetAsync( view, maximumRowsPerTable: 5, cancellationToken); if (snapshot.Tables.Count == 0) { throw new DatabaseConfigurationException( $"{view} returned no database result set."); } foreach (var table in snapshot.Tables) { if (table.Columns.Count == 0) { throw new DatabaseConfigurationException( $"{view}/{table.Name} returned no columns."); } Console.WriteLine( $"{view}/{table.Source}/{table.Name}: " + $"{table.TotalRowCount.ToString(CultureInfo.InvariantCulture)} rows"); } } static async Task VerifyOperatorCatalogSchemaAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { var result = await new LegacyOperatorCatalogSchemaValidationService(executor) .ValidateAsync(cancellationToken); var themePersistence = new LegacyThemeCatalogPersistenceService(executor); _ = await themePersistence.SuggestNextCodeAsync( ThemeMarket.Krx, cancellationToken); _ = await themePersistence.SuggestNextCodeAsync( ThemeMarket.Nxt, cancellationToken); _ = await new LegacyExpertCatalogPersistenceService(executor) .SuggestNextCodeAsync(cancellationToken); Console.WriteLine( "Operator catalog schema: " + string.Join( ",", result.ValidatedSources.Select(static source => source.ToString())) + " (read-only zero-row preflight and code-range validation)"); } static async Task VerifyStockSearchAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { var result = await new LegacyStockSearchService(executor).SearchAsync( "삼성", maximumResults: 100, cancellationToken); if (result.Items.Count == 0 || !result.Items.Any(item => item.Name.Contains("삼성", StringComparison.OrdinalIgnoreCase))) { throw new DatabaseConfigurationException( "The production stock search returned no matching rows for the smoke selector."); } var counts = result.Items .GroupBy(item => item.Market) .OrderBy(group => group.Key) .Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}"); Console.WriteLine( $"StockSearch: {result.Items.Count.ToString(CultureInfo.InvariantCulture)} rows " + $"({string.Join(", ", counts)})"); } static async Task VerifyThemeSelectorAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { Console.WriteLine("ThemeSelector: BEGIN"); var service = new LegacyThemeSelectionService(executor); var themes = await service.SearchAsync( string.Empty, ThemeSession.PreMarket, maximumResults: 100, cancellationToken); if (themes.Items.Count == 0) { throw new DatabaseConfigurationException( "The production theme selector returned no unambiguous canonical rows."); } var selected = themes.Items[0]; var preview = await service.PreviewAsync( selected.Identity, maximumItems: 100, cancellationToken); Console.WriteLine( $"ThemeSelector: themes={themes.Items.Count.ToString(CultureInfo.InvariantCulture)}, " + $"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}"); } static async Task VerifyOverseasSelectorsAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { Console.WriteLine("OverseasIndustrySelector: BEGIN"); var industries = await new LegacyOverseasIndustryIndexSearchService(executor) .SearchAsync(string.Empty, maximumResults: 100, cancellationToken); Console.WriteLine("WorldStockSelector: BEGIN"); var stocks = await new LegacyWorldStockSearchService(executor) .SearchAsync(string.Empty, maximumResults: 100, cancellationToken); if (industries.Items.Count == 0 || stocks.Items.Count == 0) { throw new DatabaseConfigurationException( "The production overseas selectors returned no rows."); } Console.WriteLine( $"OverseasSelectors: industries={industries.Items.Count.ToString(CultureInfo.InvariantCulture)}, " + $"stocks={stocks.Items.Count.ToString(CultureInfo.InvariantCulture)}"); } static async Task VerifyExpertSelectorAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { Console.WriteLine("ExpertSelector: BEGIN"); var service = new LegacyExpertSelectionService(executor); var experts = await service.SearchAsync( string.Empty, maximumResults: 100, cancellationToken); if (experts.Items.Count == 0) { throw new DatabaseConfigurationException( "The production expert selector returned no rows."); } var preview = await service.PreviewAsync( experts.Items[0].Identity, maximumItems: 100, cancellationToken); Console.WriteLine( $"ExpertSelector: experts={experts.Items.Count.ToString(CultureInfo.InvariantCulture)}, " + $"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}"); } static async Task VerifyTradingHaltSelectorAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { Console.WriteLine("TradingHaltSelector: BEGIN"); var result = await new LegacyTradingHaltSelectionService(executor).SearchAsync( string.Empty, maximumResults: 500, cancellationToken); Console.WriteLine( $"TradingHaltSelector: {result.Items.Count.ToString(CultureInfo.InvariantCulture)} rows"); }