feat: migrate legacy playout workflow and scenes
This commit is contained in:
@@ -5,7 +5,7 @@ using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(3));
|
||||
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(10));
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
@@ -27,7 +27,8 @@ try
|
||||
|
||||
if (statuses.Any(status => status.State != DatabaseHealthState.Healthy))
|
||||
{
|
||||
Console.Error.WriteLine("Smoke test stopped because both database health probes must be healthy.");
|
||||
Console.Error.WriteLine(
|
||||
"Smoke test stopped because both database health probes must be healthy.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ try
|
||||
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;
|
||||
@@ -54,7 +56,8 @@ catch (DatabaseInfrastructureException exception)
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("REAL_DB_SMOKE: FAIL - An unexpected error occurred. No provider details were printed.");
|
||||
Console.Error.WriteLine(
|
||||
"REAL_DB_SMOKE: FAIL - An unexpected error occurred. No provider details were printed.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -65,12 +68,14 @@ static string? GetConfigurationPath(string[] arguments)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (arguments.Length == 2 && arguments[0].Equals("--config", StringComparison.OrdinalIgnoreCase))
|
||||
if (arguments.Length == 2 &&
|
||||
arguments[0].Equals("--config", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return arguments[1];
|
||||
}
|
||||
|
||||
throw new DatabaseConfigurationException("Usage: MBN_STOCK_WEBVIEW.DbSmoke [--config <path>]");
|
||||
throw new DatabaseConfigurationException(
|
||||
"Usage: MBN_STOCK_WEBVIEW.DbSmoke [--config <path>]");
|
||||
}
|
||||
|
||||
static async Task VerifyOracleServerVersionAsync(
|
||||
@@ -89,22 +94,33 @@ static async Task VerifyOracleServerVersionAsync(
|
||||
|
||||
if (table.Rows.Count == 0 || table.Columns.Count == 0)
|
||||
{
|
||||
throw new DatabaseConfigurationException("Oracle 서버 버전을 확인할 수 없습니다.");
|
||||
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);
|
||||
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))
|
||||
!int.TryParse(
|
||||
match.Groups["major"].Value,
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var major))
|
||||
{
|
||||
throw new DatabaseConfigurationException("Oracle 서버 주 버전을 판별할 수 없습니다.");
|
||||
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 23.26.200은 Oracle Database 19c 이상이 필요합니다.");
|
||||
"Oracle.ManagedDataAccess.Core requires Oracle Database 19c or newer.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,20 +129,26 @@ static async Task VerifySnapshotAsync(
|
||||
MarketDataView view,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = await service.GetAsync(view, maximumRowsPerTable: 5, cancellationToken);
|
||||
var snapshot = await service.GetAsync(
|
||||
view,
|
||||
maximumRowsPerTable: 5,
|
||||
cancellationToken);
|
||||
if (snapshot.Tables.Count == 0)
|
||||
{
|
||||
throw new DatabaseConfigurationException($"{view} 조회 결과에 데이터셋이 없습니다.");
|
||||
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} 결과 열이 없습니다.");
|
||||
throw new DatabaseConfigurationException(
|
||||
$"{view}/{table.Name} returned no columns.");
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"{view}/{table.Source}/{table.Name}: {table.TotalRowCount.ToString(CultureInfo.InvariantCulture)} rows");
|
||||
$"{view}/{table.Source}/{table.Name}: " +
|
||||
$"{table.TotalRowCount.ToString(CultureInfo.InvariantCulture)} rows");
|
||||
}
|
||||
}
|
||||
|
||||
811
tools/MBN_STOCK_WEBVIEW.DbSmoke/RealSceneDbSmoke.cs
Normal file
811
tools/MBN_STOCK_WEBVIEW.DbSmoke/RealSceneDbSmoke.cs
Normal file
@@ -0,0 +1,811 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
internal static class RealSceneDbSmoke
|
||||
{
|
||||
public static async Task RunAsync(
|
||||
IDataQueryExecutor executor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(executor);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var traced = new QueryTracingDataQueryExecutor(executor);
|
||||
var run = new SceneSmokeRun();
|
||||
|
||||
Console.WriteLine("DB_SMOKE_STEP: bounded scene selectors");
|
||||
var pieName = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_INPUT_PIE",
|
||||
"SELECT STOCK_NAME FROM INPUT_PIE WHERE STOCK_NAME IS NOT NULL AND ROWNUM = 1",
|
||||
"STOCK_NAME",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var profitName = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_INPUT_PROFIT",
|
||||
"SELECT STOCK_NAME FROM INPUT_PROFIT WHERE STOCK_NAME IS NOT NULL AND ROWNUM = 1",
|
||||
"STOCK_NAME",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var growthName = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_INPUT_GROW",
|
||||
"SELECT STOCK_NAME FROM INPUT_GROW WHERE STOCK_NAME IS NOT NULL AND ROWNUM = 1",
|
||||
"STOCK_NAME",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var salesName = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_INPUT_SELL",
|
||||
"SELECT STOCK_NAME FROM INPUT_SELL WHERE STOCK_NAME IS NOT NULL AND ROWNUM = 1",
|
||||
"STOCK_NAME",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var nxtStockCode = await SelectOneAsync(
|
||||
traced,
|
||||
DataSourceKind.MariaDb,
|
||||
"SELECTOR_NXT_STOCK",
|
||||
"""
|
||||
SELECT a.f_stock_code STOCK_CODE
|
||||
FROM n_online a
|
||||
JOIN n_stock b ON a.f_stock_code = b.f_stock_code
|
||||
JOIN n_batch_his c ON a.f_stock_code = c.f_stock_code
|
||||
WHERE a.f_curr_price <> 0
|
||||
AND c.f_data_day = (
|
||||
SELECT MAX(h.f_data_day)
|
||||
FROM n_batch_his h
|
||||
WHERE h.f_stock_code = a.f_stock_code)
|
||||
LIMIT 1
|
||||
""",
|
||||
"STOCK_CODE",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var domesticStock = await SelectStockAsync(
|
||||
traced,
|
||||
"SELECTOR_DOMESTIC_STOCK",
|
||||
"""
|
||||
SELECT STOCK_CODE, STOCK_NAME
|
||||
FROM (
|
||||
SELECT a.f_stock_code STOCK_CODE, b.f_stock_wanname STOCK_NAME
|
||||
FROM t_online1 a
|
||||
JOIN t_stock b ON a.f_stock_code = b.f_stock_code
|
||||
WHERE a.f_curr_price <> 0
|
||||
AND b.f_mkt_halt = 'N'
|
||||
AND b.f_stock_wanname IS NOT NULL
|
||||
ORDER BY a.f_stock_code
|
||||
)
|
||||
WHERE ROWNUM = 1
|
||||
""",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var comparisonNames = await SelectManyAsync(
|
||||
traced,
|
||||
"SELECTOR_COMPARISON_STOCKS",
|
||||
"""
|
||||
SELECT STOCK_NAME
|
||||
FROM (
|
||||
SELECT b.f_stock_wanname STOCK_NAME
|
||||
FROM t_online1 a
|
||||
JOIN t_stock b ON a.f_stock_code = b.f_stock_code
|
||||
WHERE a.f_curr_price <> 0
|
||||
AND b.f_mkt_halt = 'N'
|
||||
AND b.f_stock_wanname IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM t_candle_history h
|
||||
WHERE h.f_stock_code = a.f_stock_code)
|
||||
ORDER BY a.f_stock_code
|
||||
)
|
||||
WHERE ROWNUM <= 2
|
||||
""",
|
||||
"STOCK_NAME",
|
||||
maximumRows: 2,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var dealerStockName = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_DEALER_STOCK",
|
||||
"""
|
||||
SELECT STOCK_NAME
|
||||
FROM (
|
||||
SELECT b.f_stock_wanname STOCK_NAME
|
||||
FROM t_online1 a
|
||||
JOIN t_stock b ON a.f_stock_code = b.f_stock_code
|
||||
JOIN t_dealer d ON a.f_stock_code = d.f_stock_code
|
||||
WHERE a.f_curr_price <> 0
|
||||
AND b.f_mkt_halt = 'N'
|
||||
AND d.f_buy_dealer_no_1 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_buy_dealer_no_2 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_buy_dealer_no_3 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_buy_dealer_no_4 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_buy_dealer_no_5 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_sell_dealer_no_1 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_sell_dealer_no_2 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_sell_dealer_no_3 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_sell_dealer_no_4 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
AND d.f_sell_dealer_no_5 IN (SELECT f_sale_no FROM t_sale_member)
|
||||
ORDER BY a.f_stock_code
|
||||
)
|
||||
WHERE ROWNUM = 1
|
||||
""",
|
||||
"STOCK_NAME",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var orderBookStockCode = await SelectOneAsync(
|
||||
traced,
|
||||
"SELECTOR_ORDER_BOOK_STOCK",
|
||||
"""
|
||||
SELECT STOCK_CODE
|
||||
FROM (
|
||||
SELECT a.f_stock_code STOCK_CODE
|
||||
FROM t_online1 a
|
||||
JOIN t_stock b ON a.f_stock_code = b.f_stock_code
|
||||
JOIN t_online1_call c ON a.f_stock_code = c.f_stock_code
|
||||
WHERE a.f_curr_price > 0
|
||||
AND b.f_mkt_halt = 'N'
|
||||
AND c.f_call_sell_1 IS NOT NULL
|
||||
AND c.f_call_sell_2 IS NOT NULL
|
||||
AND c.f_call_sell_3 IS NOT NULL
|
||||
AND c.f_call_sell_jan_1 IS NOT NULL
|
||||
AND c.f_call_sell_jan_2 IS NOT NULL
|
||||
AND c.f_call_sell_jan_3 IS NOT NULL
|
||||
AND c.f_call_buy_1 IS NOT NULL
|
||||
AND c.f_call_buy_2 IS NOT NULL
|
||||
AND c.f_call_buy_3 IS NOT NULL
|
||||
AND c.f_call_buy_jan_1 IS NOT NULL
|
||||
AND c.f_call_buy_jan_2 IS NOT NULL
|
||||
AND c.f_call_buy_jan_3 IS NOT NULL
|
||||
AND c.f_t_call_sell_jan IS NOT NULL
|
||||
AND c.f_t_call_buy_jan IS NOT NULL
|
||||
ORDER BY a.f_stock_code
|
||||
)
|
||||
WHERE ROWNUM = 1
|
||||
""",
|
||||
"STOCK_CODE",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var domesticCode = domesticStock?.Code;
|
||||
var pageRequest = new DomesticPagedQuoteLoadRequest(
|
||||
PagedQuoteMarket.Kospi,
|
||||
DomesticPagedQuoteList.MarketCapitalization,
|
||||
DateOnly.FromDateTime(DateTime.Today));
|
||||
|
||||
await run.RequiredAsync(
|
||||
"s5001",
|
||||
"Oracle/BalticDryIndex",
|
||||
() => new S5001SceneDataLoader(traced).LoadAsync(
|
||||
new S5001BalticDryIndexLoadRequest(), cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5006",
|
||||
"Oracle/domestic",
|
||||
() => new S5006DomesticSceneDataLoader(traced).LoadAsync(
|
||||
new S5006DomesticSceneDataRequest(
|
||||
LegacyDomesticEquityMarket.Kospi,
|
||||
RequireSelector(domesticCode, "domestic stock code")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5006",
|
||||
"MariaDb/NXT",
|
||||
() => new S5006NxtSceneDataLoader(traced).LoadAsync(
|
||||
LegacyDomesticEquityMarket.NxtKospi,
|
||||
RequireSelector(nxtStockCode, "NXT stock code"),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5011",
|
||||
"Oracle/face-value",
|
||||
() => new S5011SceneDataLoader(traced).LoadAsync(
|
||||
new S5011SceneDataRequest(
|
||||
LegacyDomesticEquityMarket.Kospi,
|
||||
S5011DetailBranch.FaceValue,
|
||||
RequireSelector(domesticCode, "domestic stock code")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5016",
|
||||
"Oracle/US-indices",
|
||||
() => new S5016SceneDataLoader(traced).LoadAsync(
|
||||
S5016PanelTarget.UnitedStatesIndices,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s50160",
|
||||
"Oracle/cotton",
|
||||
() => new S50160SceneDataLoader(traced).LoadAsync(
|
||||
S50160PanelTarget.Cotton,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5023",
|
||||
"Oracle/Kospi",
|
||||
() => new S5023SceneDataLoader(traced).LoadAsync(
|
||||
new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5024",
|
||||
"Oracle/Kospi",
|
||||
() => new S5024SceneDataLoader(traced).LoadAsync(
|
||||
new S5024SceneLoadRequest(LegacyDomesticMarket.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
var manualDirectory = Environment.GetEnvironmentVariable(
|
||||
S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable);
|
||||
var manualConfigured = !string.IsNullOrWhiteSpace(manualDirectory);
|
||||
if (manualConfigured)
|
||||
{
|
||||
await run.RequiredAsync(
|
||||
"s5025",
|
||||
"trusted-external/manual-individual",
|
||||
() => new S5025SceneDataLoader(
|
||||
S5025TrustedManualFileDataSource.CreateFromEnvironment()).LoadAsync(
|
||||
new S5025ManualSceneLoadRequest(S5025ManualAudience.Individual),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5025",
|
||||
"trusted-external/manual-foreign",
|
||||
() => new S5025SceneDataLoader(
|
||||
S5025TrustedManualFileDataSource.CreateFromEnvironment()).LoadAsync(
|
||||
new S5025ManualSceneLoadRequest(S5025ManualAudience.Foreign),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5025",
|
||||
"trusted-external/manual-institution",
|
||||
() => new S5025SceneDataLoader(
|
||||
S5025TrustedManualFileDataSource.CreateFromEnvironment()).LoadAsync(
|
||||
new S5025ManualSceneLoadRequest(S5025ManualAudience.Institution),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
"SCENE_SMOKE:s5025:PREREQUISITE - trusted external manual-data directory is not configured");
|
||||
}
|
||||
|
||||
await run.RequiredAsync(
|
||||
"s5026",
|
||||
"Oracle/five-day-candle-pair",
|
||||
() => new S5026SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5029",
|
||||
"Oracle/five-day-yield-pair",
|
||||
() => new S5029SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5032",
|
||||
"Oracle/index-and-futures",
|
||||
() => new S5032SceneDataLoader(traced).LoadAsync(
|
||||
new S5032IndexAndFuturesLoadRequest(LegacyMarketQuoteTarget.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5037",
|
||||
"Oracle/dealer-quote",
|
||||
() => new S5037SceneDataLoader(traced).LoadAsync(
|
||||
new S5037SceneLoadRequest(
|
||||
LegacyDomesticMarket.Kospi,
|
||||
RequireSelector(dealerStockName, "dealer stock name")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5074",
|
||||
"Oracle/paged-5",
|
||||
() => new S5074SceneDataLoader(traced).LoadAsync(
|
||||
pageRequest,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5076",
|
||||
"Oracle/manual-pie",
|
||||
() => new S5076SceneDataLoader(traced).LoadAsync(
|
||||
RequireSelector(pieName, "INPUT_PIE stock name"),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5077",
|
||||
"Oracle/paged-6",
|
||||
() => new S5077SceneDataLoader(traced).LoadAsync(
|
||||
pageRequest,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5078",
|
||||
"Oracle/Kospi-sectors",
|
||||
() => new S5078SceneDataLoader(traced).LoadAsync(
|
||||
new S5078SceneDataRequest(SectorIndexMarket.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5079",
|
||||
"Oracle/manual-growth",
|
||||
() => new S5079SceneDataLoader(traced).LoadAsync(
|
||||
new S5079SceneDataRequest(RequireSelector(growthName, "INPUT_GROW stock name")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5080",
|
||||
"Oracle/manual-sales",
|
||||
() => new S5080SceneDataLoader(traced).LoadAsync(
|
||||
new S5080SceneDataRequest(RequireSelector(salesName, "INPUT_SELL stock name")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5081",
|
||||
"Oracle/manual-profit",
|
||||
() => new S5081SceneDataLoader(traced).LoadAsync(
|
||||
RequireSelector(profitName, "INPUT_PROFIT stock name"),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5082",
|
||||
"Oracle/grid",
|
||||
() => new S5082SceneDataLoader(traced).LoadAsync(cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5083",
|
||||
"Oracle/investor-flow",
|
||||
() => new S5083SceneDataLoader(traced).LoadAsync(
|
||||
new S5083SceneDataRequest(InvestorFlowParticipant.Individual),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5084",
|
||||
"Oracle/index-flow",
|
||||
() => new S5084SceneDataLoader(traced).LoadAsync(
|
||||
new S5084SceneDataRequest(InvestorFlowIndex.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5085",
|
||||
"Oracle/program-trading",
|
||||
() => new S5085SceneDataLoader(traced).LoadAsync(cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5086",
|
||||
"Oracle/five-day-FX-yield",
|
||||
() => new S5086SceneDataLoader(traced).LoadAsync(
|
||||
new YieldSceneLoadRequest(
|
||||
YieldGraphPeriod.FiveDays,
|
||||
new YieldGraphInstrument(YieldGraphInstrumentKind.WonDollar)),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s50860",
|
||||
"Oracle/five-day-quoted-FX-yield",
|
||||
() => new S50860SceneDataLoader(traced).LoadAsync(
|
||||
new YieldSceneLoadRequest(
|
||||
YieldGraphPeriod.FiveDays,
|
||||
new YieldGraphInstrument(YieldGraphInstrumentKind.WonDollar)),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5087",
|
||||
"Oracle/five-day-comparison",
|
||||
() => new S5087SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5088",
|
||||
"Oracle/paged-12",
|
||||
() => new S5088SceneDataLoader(traced).LoadAsync(
|
||||
pageRequest,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s6001",
|
||||
"Oracle/DowJones",
|
||||
() => new S6001SceneDataLoader(traced).LoadAsync(
|
||||
S6001QuoteTarget.DowJones,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s6067",
|
||||
"Oracle/institution-grid",
|
||||
() => new S6067SceneDataLoader(traced).LoadAsync(cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s8001",
|
||||
"Oracle/Kospi-sector-map",
|
||||
() => new S8001SceneDataLoader(traced).LoadAsync(
|
||||
new S8001SceneLoadRequest(S8001Market.Kospi),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s8003",
|
||||
"Oracle/order-book",
|
||||
() => new S8003SceneDataLoader(traced).LoadAsync(
|
||||
new S8003SceneLoadRequest(
|
||||
LegacyDomesticMarket.Kospi,
|
||||
RequireSelector(orderBookStockCode, "order-book stock code")),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s8010",
|
||||
"Oracle/Kospi-index-candle",
|
||||
() => new S8010SceneDataLoader(traced).LoadAsync(
|
||||
new S8010SceneDataRequest(
|
||||
CandleCutAlias.Cut8061,
|
||||
CandleMarketTarget.KospiIndex,
|
||||
CandleChartMode.Price,
|
||||
InstrumentName: null,
|
||||
ShowMovingAverage5: false,
|
||||
ShowMovingAverage20: false),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s8018",
|
||||
"Oracle/current-market-pair",
|
||||
() => new S8018SceneDataLoader(traced).LoadAsync(
|
||||
new S8018MarketPairLoadRequest(
|
||||
S8018MarketDataMode.Current,
|
||||
LegacyMarketQuoteTarget.Kospi,
|
||||
LegacyMarketQuoteTarget.Kosdaq),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s8067",
|
||||
"Oracle/world-map",
|
||||
() => new S8067SceneDataLoader(traced).LoadAsync(cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
await run.DiagnosticAsync(
|
||||
"s8086",
|
||||
"Oracle/unreachable-no-cut-alias",
|
||||
() => new S8086DiagnosticSceneDataLoader(traced).LoadAsync(
|
||||
S8086DiagnosticTarget.Gold,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
run.Complete(manualConfigured);
|
||||
}
|
||||
|
||||
private static async Task<string?> SelectOneAsync(
|
||||
IDataQueryExecutor executor,
|
||||
string tableName,
|
||||
string sql,
|
||||
string column,
|
||||
CancellationToken cancellationToken) =>
|
||||
await SelectOneAsync(
|
||||
executor,
|
||||
DataSourceKind.Oracle,
|
||||
tableName,
|
||||
sql,
|
||||
column,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
private static async Task<string?> SelectOneAsync(
|
||||
IDataQueryExecutor executor,
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string sql,
|
||||
string column,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var table = await executor.ExecuteAsync(
|
||||
source,
|
||||
tableName,
|
||||
new DataQuerySpec(sql),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var value = table.Rows.Count == 1 && table.Columns.Contains(column)
|
||||
? Convert.ToString(table.Rows[0][column], CultureInfo.InvariantCulture)?.Trim()
|
||||
: null;
|
||||
Console.WriteLine(
|
||||
string.IsNullOrEmpty(value)
|
||||
? $"DB_SMOKE_SELECTOR:{tableName}:NO_ELIGIBLE_ROW"
|
||||
: $"DB_SMOKE_SELECTOR:{tableName}:PASS");
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:FAIL:{SafeFailure(exception)}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<StockSelector?> SelectStockAsync(
|
||||
IDataQueryExecutor executor,
|
||||
string tableName,
|
||||
string sql,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var table = await executor.ExecuteAsync(
|
||||
DataSourceKind.Oracle,
|
||||
tableName,
|
||||
new DataQuerySpec(sql),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (table.Rows.Count != 1 ||
|
||||
!table.Columns.Contains("STOCK_CODE") ||
|
||||
!table.Columns.Contains("STOCK_NAME"))
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:NO_ELIGIBLE_ROW");
|
||||
return null;
|
||||
}
|
||||
|
||||
var code = Convert.ToString(table.Rows[0]["STOCK_CODE"], CultureInfo.InvariantCulture)?.Trim();
|
||||
var name = Convert.ToString(table.Rows[0]["STOCK_NAME"], CultureInfo.InvariantCulture)?.Trim();
|
||||
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(name))
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:NO_ELIGIBLE_ROW");
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:PASS");
|
||||
return new StockSelector(code, name);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:FAIL:{SafeFailure(exception)}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<string>> SelectManyAsync(
|
||||
IDataQueryExecutor executor,
|
||||
string tableName,
|
||||
string sql,
|
||||
string column,
|
||||
int maximumRows,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var table = await executor.ExecuteAsync(
|
||||
DataSourceKind.Oracle,
|
||||
tableName,
|
||||
new DataQuerySpec(sql),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!table.Columns.Contains(column) || table.Rows.Count > maximumRows)
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:INVALID_RESULT");
|
||||
return [];
|
||||
}
|
||||
|
||||
var values = table.Rows.Cast<DataRow>()
|
||||
.Select(row => Convert.ToString(row[column], CultureInfo.InvariantCulture)?.Trim())
|
||||
.Where(value => !string.IsNullOrEmpty(value))
|
||||
.Cast<string>()
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
Console.WriteLine(
|
||||
values.Length == 0
|
||||
? $"DB_SMOKE_SELECTOR:{tableName}:NO_ELIGIBLE_ROW"
|
||||
: $"DB_SMOKE_SELECTOR:{tableName}:PASS:{values.Length.ToString(CultureInfo.InvariantCulture)}");
|
||||
return values;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.WriteLine($"DB_SMOKE_SELECTOR:{tableName}:FAIL:{SafeFailure(exception)}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string RequireSelector(string? value, string selector)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new LegacySceneDataException(
|
||||
$"The {selector} selector returned no eligible database row.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string RequireComparisonSelector(
|
||||
IReadOnlyList<string> values,
|
||||
int index)
|
||||
{
|
||||
if (values.Count != 2 || index is < 0 or > 1)
|
||||
{
|
||||
return RequireSelector(null, "comparison stock pair");
|
||||
}
|
||||
|
||||
return values[index];
|
||||
}
|
||||
|
||||
private static string SafeFailure(Exception exception) => exception switch
|
||||
{
|
||||
LegacySceneDataException => "DATA_UNAVAILABLE_OR_INVALID",
|
||||
DatabaseOperationException operation when operation.ProviderErrorCode.HasValue =>
|
||||
"DATABASE_QUERY_FAILED_CODE_" +
|
||||
operation.ProviderErrorCode.Value.ToString(CultureInfo.InvariantCulture),
|
||||
DatabaseInfrastructureException => "DATABASE_QUERY_FAILED",
|
||||
S5025TrustedManualDataException => "TRUSTED_MANUAL_DATA_UNAVAILABLE",
|
||||
_ => exception.GetType().Name.ToUpperInvariant()
|
||||
};
|
||||
|
||||
private sealed record StockSelector(string Code, string Name);
|
||||
|
||||
private sealed class SceneSmokeRun
|
||||
{
|
||||
private readonly HashSet<string> _covered = new(StringComparer.Ordinal);
|
||||
private readonly List<string> _requiredFailures = [];
|
||||
|
||||
public async Task RequiredAsync<TData>(
|
||||
string builderKey,
|
||||
string caseName,
|
||||
Func<Task<TData>> loadAsync)
|
||||
where TData : ILegacySceneData =>
|
||||
await VerifyAsync(builderKey, caseName, loadAsync, required: true).ConfigureAwait(false);
|
||||
|
||||
public async Task DiagnosticAsync<TData>(
|
||||
string builderKey,
|
||||
string caseName,
|
||||
Func<Task<TData>> loadAsync)
|
||||
where TData : ILegacySceneData =>
|
||||
await VerifyAsync(builderKey, caseName, loadAsync, required: false).ConfigureAwait(false);
|
||||
|
||||
public void Complete(bool manualConfigured)
|
||||
{
|
||||
var expected = LegacySceneMutationBuilderRegistry.Default.Registrations
|
||||
.Select(registration => registration.BuilderKey)
|
||||
.Where(key => !string.Equals(key, "s8086", StringComparison.Ordinal))
|
||||
.Where(key => manualConfigured || !string.Equals(key, "s5025", StringComparison.Ordinal))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var missing = expected.Except(_covered, StringComparer.Ordinal)
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var requiredCovered = expected.Count(_covered.Contains);
|
||||
var diagnosticStatus = _covered.Contains("s8086")
|
||||
? "pass-unreachable-no-cut-alias"
|
||||
: "failed-unreachable-no-cut-alias";
|
||||
|
||||
Console.WriteLine(
|
||||
$"SCENE_SMOKE_SUMMARY: requiredCovered={requiredCovered.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"required={expected.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"requiredFailures={_requiredFailures.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"diagnostic={diagnosticStatus} " +
|
||||
$"manualS5025={(manualConfigured ? "configured" : "prerequisite")}");
|
||||
if (missing.Length > 0)
|
||||
{
|
||||
Console.WriteLine("SCENE_SMOKE_MISSING: " + string.Join(",", missing));
|
||||
}
|
||||
|
||||
if (_requiredFailures.Count > 0 || missing.Length > 0)
|
||||
{
|
||||
throw new DatabaseConfigurationException(
|
||||
"One or more required real-data scene loader cases did not produce validated mutations.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task VerifyAsync<TData>(
|
||||
string builderKey,
|
||||
string caseName,
|
||||
Func<Task<TData>> loadAsync,
|
||||
bool required)
|
||||
where TData : ILegacySceneData
|
||||
{
|
||||
Console.WriteLine($"SCENE_SMOKE:{builderKey}:{caseName}:START");
|
||||
try
|
||||
{
|
||||
var data = await loadAsync().ConfigureAwait(false);
|
||||
var mutations = LegacySceneMutationBuilderRegistry.Default.Build(builderKey, data);
|
||||
if (mutations.Count == 0)
|
||||
{
|
||||
throw new LegacySceneDataException(
|
||||
"The scene mutation builder produced no mutations.");
|
||||
}
|
||||
|
||||
_covered.Add(builderKey);
|
||||
Console.WriteLine(
|
||||
$"SCENE_SMOKE:{builderKey}:{caseName}:PASS:" +
|
||||
mutations.Count.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var failure = SafeFailure(exception);
|
||||
Console.WriteLine($"SCENE_SMOKE:{builderKey}:{caseName}:FAIL:{failure}");
|
||||
if (required)
|
||||
{
|
||||
_requiredFailures.Add(builderKey + "/" + caseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class QueryTracingDataQueryExecutor : IDataQueryExecutor
|
||||
{
|
||||
private readonly IDataQueryExecutor _inner;
|
||||
private int _sequence;
|
||||
|
||||
public QueryTracingDataQueryExecutor(IDataQueryExecutor inner)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
}
|
||||
|
||||
public DataTable Execute(DataSourceKind source, string tableName, string query)
|
||||
{
|
||||
var sequence = Start(source, tableName);
|
||||
try
|
||||
{
|
||||
var result = _inner.Execute(source, tableName, query);
|
||||
End(sequence, result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Fail(sequence, exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sequence = Start(source, tableName);
|
||||
try
|
||||
{
|
||||
var result = await _inner.ExecuteAsync(
|
||||
source,
|
||||
tableName,
|
||||
query,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
End(sequence, result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Fail(sequence, exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DataTable> ExecuteAsync(
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
DataQuerySpec query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sequence = Start(source, tableName);
|
||||
try
|
||||
{
|
||||
var result = await _inner.ExecuteAsync(
|
||||
source,
|
||||
tableName,
|
||||
query,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
End(sequence, result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Fail(sequence, exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private int Start(DataSourceKind source, string tableName)
|
||||
{
|
||||
var sequence = Interlocked.Increment(ref _sequence);
|
||||
Console.WriteLine(
|
||||
$"DB_SMOKE_QUERY:{sequence.ToString(CultureInfo.InvariantCulture)}:" +
|
||||
$"START:{source}:{tableName}");
|
||||
return sequence;
|
||||
}
|
||||
|
||||
private static void End(int sequence, DataTable result) =>
|
||||
Console.WriteLine(
|
||||
$"DB_SMOKE_QUERY:{sequence.ToString(CultureInfo.InvariantCulture)}:" +
|
||||
$"PASS:rows={result.Rows.Count.ToString(CultureInfo.InvariantCulture)}");
|
||||
|
||||
private static void Fail(int sequence, Exception exception) =>
|
||||
Console.WriteLine(
|
||||
$"DB_SMOKE_QUERY:{sequence.ToString(CultureInfo.InvariantCulture)}:" +
|
||||
$"FAIL:{SafeFailure(exception)}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user