337 lines
11 KiB
C#
337 lines
11 KiB
C#
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 <path>]");
|
|
}
|
|
|
|
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)(?<major>\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");
|
|
}
|