using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; Console.OutputEncoding = Encoding.UTF8; if (args.Length == 1 && args[0].Equals("--manual-import-audit", StringComparison.OrdinalIgnoreCase)) { return RunManualImportAudit(); } if (args.Length == 1 && args[0].Equals("--manual-import-fixture", StringComparison.OrdinalIgnoreCase)) { return await RunManualImportFixtureAsync(); } using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(10)); Console.CancelKeyPress += (_, eventArgs) => { eventArgs.Cancel = true; cancellation.Cancel(); }; try { var selection = CreateRuntimeSelection(args); var runtime = selection.Runtime; 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 NamedPlaylistReadOnlyDbAudit.RunAsync(runtime.Executor, cancellation.Token); await NamedManualRestoreReadOnlyAudit.RunAsync( runtime.Executor, NamedManualRestoreReadOnlyAudit.DefaultTrustedDirectory(), cancellation.Token); await NxtThemeRestoreDbAudit.RunAsync( runtime.Executor, runtime.Options.MariaDb, selection.ReferenceMariaDb, cancellation.Token); await VerifyLegacyComparisonImportAsync(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 DatabaseSmokeRuntimeSelection CreateRuntimeSelection(string[] arguments) { if (arguments.Length == 0) { return CreateJsonRuntimeSelection(configurationPath: null); } if (arguments.Length == 2 && arguments[0].Equals("--config", StringComparison.OrdinalIgnoreCase)) { return CreateJsonRuntimeSelection(arguments[1]); } if (arguments.Length == 2 && arguments[0].Equals("--legacy-runtime-root", StringComparison.OrdinalIgnoreCase)) { var root = Path.GetFullPath(arguments[1]); var iniPath = Path.Combine(root, "Res", "MmoneyCoder.ini"); if (!File.Exists(iniPath)) { throw new DatabaseConfigurationException( "The legacy runtime root does not contain Res\\MmoneyCoder.ini."); } // Keep the selected file's endpoint as the comparison reference before // environment overrides are applied. Credentials never leave the // loader-owned DatabaseOptions instance. var selectedOptions = new LegacyIniDatabaseOptionsLoader(_ => null).Load(iniPath); var runtime = DatabaseRuntime.CreateLegacyExecutableDefault( root, fallbackConfigurationPath: Path.Combine(root, "missing-database.json"), legacyOverridePath: Path.Combine(root, "missing-legacy.ini")); return new DatabaseSmokeRuntimeSelection( runtime, MariaDbEndpointReference.From(selectedOptions.MariaDb)); } throw new DatabaseConfigurationException( "Usage: MBN_STOCK_WEBVIEW.DbSmoke [--config |--legacy-runtime-root |--manual-import-audit|--manual-import-fixture]"); } static DatabaseSmokeRuntimeSelection CreateJsonRuntimeSelection(string? configurationPath) { var configuredOptions = new DatabaseOptionsLoader(_ => null).Load(configurationPath); var runtime = DatabaseRuntime.CreateDefault(configurationPath); var configuredMariaDb = configuredOptions.MariaDb; var reference = HasCompleteEndpointIdentity(configuredMariaDb) ? MariaDbEndpointReference.From(configuredMariaDb) : MariaDbEndpointReference.From(runtime.Options.MariaDb); return new DatabaseSmokeRuntimeSelection(runtime, reference); } static bool HasCompleteEndpointIdentity(MariaDbDatabaseOptions options) => !string.IsNullOrWhiteSpace(options.Host) && options.Port is >= 1 and <= 65_535 && !string.IsNullOrWhiteSpace(options.Database); static async Task VerifyLegacyComparisonImportAsync( IDataQueryExecutor executor, CancellationToken cancellationToken) { const string expectedSourceSha256 = "1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2"; var result = await new LegacyComparisonPairImportService( executor, TrustedLegacyComparisonFileSource.CreateDefault()) .ImportAsync(cancellationToken); if (!string.Equals( result.SourceSha256, expectedSourceSha256, StringComparison.Ordinal) || result.SourceRowCount != 8 || result.Pairs.Count != 8 || result.Pairs.Where((pair, index) => pair.RowNumber != index + 1).Any()) { throw new DatabaseConfigurationException( "The fixed legacy comparison import did not reproduce its audited 8-row identity set."); } Console.WriteLine( $"LegacyComparisonImport: rows={result.SourceRowCount.ToString(CultureInfo.InvariantCulture)}, " + $"sha256={result.SourceSha256}, identities=fresh-read-only"); } static async Task RunManualImportFixtureAsync() { const string expectedSourceSha256 = "fd39a3639e574a19f174225e8e65234d6bdd4c58700bb5d10230a779a03a2114"; using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var profile = Environment.GetFolderPath( Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify); var localAppData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify); if (string.IsNullOrWhiteSpace(profile) || string.IsNullOrWhiteSpace(localAppData)) { Console.Error.WriteLine("MANUAL_IMPORT_FIXTURE: FAIL - required profile roots are unavailable."); return 1; } var sourceDirectory = Path.Combine( profile, "source", "repos", FixedLegacyManualOperatorDataSource.FixedRelativeDirectory); var operatingDirectory = Path.Combine( localAppData, "MBN_STOCK_WEBVIEW", "OperatorData"); var temporaryDirectory = Path.GetFullPath(Path.Combine( Path.GetTempPath(), "MBN_STOCK_WEBVIEW-manual-import-fixture-" + Guid.NewGuid().ToString("N"))); var expectedDataFiles = new[] { "개인.dat", "외국인.dat", "기관.dat", "VI발동.dat" }; var trackedOperatingFiles = expectedDataFiles.Concat(new[] { LegacyManualOperatorDataImporter.MarkerFileName, LegacyManualOperatorDataImporter.IntentFileName }).ToArray(); var sourceBefore = SnapshotFiles(sourceDirectory, expectedDataFiles); var operatingBefore = SnapshotFiles(operatingDirectory, trackedOperatingFiles); try { var destination = TrustedManualDirectory.PreparePrivateWritableDirectory( temporaryDirectory); LegacyManualOperatorImportResult result; LegacyManualOperatorImportException? secondFailure = null; using (var importer = new LegacyManualOperatorDataImporter( destination, new FixedLegacyManualOperatorDataSource())) { result = await importer.ImportAsync(cancellation.Token); try { _ = await importer.ImportAsync(cancellation.Token); } catch (LegacyManualOperatorImportException exception) { secondFailure = exception; } } var sourceAfter = SnapshotFiles(sourceDirectory, expectedDataFiles); var operatingAfter = SnapshotFiles(operatingDirectory, trackedOperatingFiles); var fixtureFiles = SnapshotFiles(destination, trackedOperatingFiles); var dataHashesMatch = expectedDataFiles.All(name => sourceBefore.TryGetValue(name, out var sourceValue) && fixtureFiles.TryGetValue(name, out var fixtureValue) && string.Equals(sourceValue, fixtureValue, StringComparison.Ordinal)); var markerPresent = fixtureFiles.TryGetValue( LegacyManualOperatorDataImporter.MarkerFileName, out var markerValue) && markerValue != "missing"; var intentAbsent = fixtureFiles.TryGetValue( LegacyManualOperatorDataImporter.IntentFileName, out var intentValue) && intentValue == "missing"; var runtimeState = LegacyManualOperatorDataRuntimeGuard.Inspect(destination).State; if (!SnapshotsEqual(sourceBefore, sourceAfter) || !SnapshotsEqual(operatingBefore, operatingAfter) || !dataHashesMatch || !markerPresent || !intentAbsent || result.MarkerVersion != LegacyManualOperatorDataImporter.MarkerVersion || result.NetSellRowCount != 15 || result.ViItemCount != 9 || !string.Equals( result.SourceSha256, expectedSourceSha256, StringComparison.Ordinal) || runtimeState != LegacyManualOperatorDataRuntimeState.ReadyImportedData || secondFailure is null || secondFailure.Failure != LegacyManualOperatorImportFailure.AlreadyImported || secondFailure.OutcomeUnknown) { Console.Error.WriteLine( "MANUAL_IMPORT_FIXTURE: FAIL - an isolated import invariant was not reproduced."); return 1; } Console.WriteLine( $"MANUAL_IMPORT_FIXTURE: PASS rows={result.NetSellRowCount.ToString(CultureInfo.InvariantCulture)} " + $"vi={result.ViItemCount.ToString(CultureInfo.InvariantCulture)} " + $"sourceSha256={result.SourceSha256} state={runtimeState} " + "secondAttempt=AlreadyImported outcomeUnknown=false " + "sourceUnchanged=true operatingImportSetUnchanged=true"); return 0; } catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { Console.Error.WriteLine("MANUAL_IMPORT_FIXTURE: CANCELED_OR_TIMED_OUT"); return 3; } catch (LegacyManualOperatorImportException exception) { Console.Error.WriteLine( $"MANUAL_IMPORT_FIXTURE: FAIL - failure={exception.Failure} " + $"outcomeUnknown={exception.OutcomeUnknown.ToString().ToLowerInvariant()}"); return exception.OutcomeUnknown ? 5 : 1; } catch (Exception exception) { Console.Error.WriteLine( $"MANUAL_IMPORT_FIXTURE: FAIL - {exception.GetType().Name}"); return 1; } finally { RemoveOwnedFixtureDirectory(temporaryDirectory); } } static int RunManualImportAudit() { try { var profile = Environment.GetFolderPath( Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify); var localAppData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify); if (string.IsNullOrWhiteSpace(profile) || string.IsNullOrWhiteSpace(localAppData)) { Console.Error.WriteLine("MANUAL_IMPORT_AUDIT: FAIL - required profile roots are unavailable."); return 1; } var sourceDirectory = Path.Combine( profile, "source", "repos", FixedLegacyManualOperatorDataSource.FixedRelativeDirectory); var operatingDirectory = Path.Combine( localAppData, "MBN_STOCK_WEBVIEW", "OperatorData"); var expectedDataFiles = new[] { "개인.dat", "외국인.dat", "기관.dat", "VI발동.dat" }; var source = SnapshotFiles(sourceDirectory, expectedDataFiles); var operating = SnapshotFiles( operatingDirectory, expectedDataFiles.Concat(new[] { LegacyManualOperatorDataImporter.MarkerFileName, LegacyManualOperatorDataImporter.IntentFileName })); var markerPresent = operating[LegacyManualOperatorDataImporter.MarkerFileName] != "missing"; var intentPresent = operating[LegacyManualOperatorDataImporter.IntentFileName] != "missing"; var dataPresent = expectedDataFiles.Count(name => operating[name] != "missing"); var sourcePresent = expectedDataFiles.Count(name => source[name] != "missing"); var byteIdentical = expectedDataFiles.All(name => source[name] != "missing" && string.Equals(source[name], operating[name], StringComparison.Ordinal)); var stagedPresent = Directory.Exists(operatingDirectory) && Directory.EnumerateFileSystemEntries( operatingDirectory, ".legacy-manual-import-v1-*", SearchOption.TopDirectoryOnly).Any(); var runtimeState = LegacyManualOperatorDataRuntimeGuard .Inspect(operatingDirectory) .State; var (decision, outcomeUnknown) = runtimeState switch { LegacyManualOperatorDataRuntimeState.InterruptedImport => ("InterruptedImport", true), LegacyManualOperatorDataRuntimeState.InvalidImportedState => ("InvalidImportedState", false), LegacyManualOperatorDataRuntimeState.ReadyImportedData => ("AlreadyImported", false), _ when dataPresent > 0 || stagedPresent => ("DestinationNotEmpty", false), _ when sourcePresent == expectedDataFiles.Length => ("Eligible", false), _ => ("SourceUnavailable", false) }; Console.WriteLine( $"MANUAL_IMPORT_AUDIT: PASS sourceFiles={sourcePresent.ToString(CultureInfo.InvariantCulture)}/4 " + $"operatingFiles={dataPresent.ToString(CultureInfo.InvariantCulture)}/4 " + $"sourceByteIdentical={byteIdentical.ToString().ToLowerInvariant()} " + $"marker={markerPresent.ToString().ToLowerInvariant()} " + $"intent={intentPresent.ToString().ToLowerInvariant()} " + $"staged={stagedPresent.ToString().ToLowerInvariant()} " + $"runtime={runtimeState} " + $"decision={decision} outcomeUnknown={outcomeUnknown.ToString().ToLowerInvariant()} " + "writesAttempted=false"); return 0; } catch (Exception exception) { Console.Error.WriteLine( $"MANUAL_IMPORT_AUDIT: FAIL - {exception.GetType().Name}"); return 1; } } static IReadOnlyDictionary SnapshotFiles( string directory, IEnumerable fileNames) { const long maximumAuditFileBytes = 32 * 1024; var result = new Dictionary(StringComparer.Ordinal); foreach (var fileName in fileNames) { var path = Path.Combine(directory, fileName); if (!File.Exists(path)) { result.Add(fileName, "missing"); continue; } using var stream = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); if (stream.Length is < 0 or > maximumAuditFileBytes) { throw new InvalidDataException( "A manual-import audit file exceeded its closed size boundary."); } var bytes = new byte[checked((int)stream.Length)]; stream.ReadExactly(bytes); if (stream.ReadByte() != -1) { throw new InvalidDataException( "A manual-import audit file changed beyond its closed size boundary."); } var hash = Convert.ToHexString(SHA256.HashData(bytes)); result.Add( fileName, bytes.Length.ToString(CultureInfo.InvariantCulture) + ":" + hash); } return result; } static bool SnapshotsEqual( IReadOnlyDictionary first, IReadOnlyDictionary second) => first.Count == second.Count && first.All(pair => second.TryGetValue(pair.Key, out var value) && string.Equals(pair.Value, value, StringComparison.Ordinal)); static void RemoveOwnedFixtureDirectory(string directory) { if (!Directory.Exists(directory)) { return; } var temporaryRoot = Path.TrimEndingDirectorySeparator( Path.GetFullPath(Path.GetTempPath())); var resolved = Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory)); var prefix = temporaryRoot + Path.DirectorySeparatorChar; const string ownedNamePrefix = "MBN_STOCK_WEBVIEW-manual-import-fixture-"; var ownedName = Path.GetFileName(resolved); var ownedSuffix = ownedName.StartsWith(ownedNamePrefix, StringComparison.Ordinal) ? ownedName[ownedNamePrefix.Length..] : string.Empty; if (!resolved.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) || ownedSuffix.Length != 32 || ownedSuffix.Any(static value => !char.IsAsciiHexDigit(value)) || (File.GetAttributes(resolved) & FileAttributes.ReparsePoint) != 0) { throw new InvalidOperationException( "Refusing to remove a directory outside the owned fixture boundary."); } var pendingDirectories = new Stack(); pendingDirectories.Push(resolved); while (pendingDirectories.TryPop(out var current)) { foreach (var entry in Directory.EnumerateFileSystemEntries( current, "*", new EnumerationOptions { RecurseSubdirectories = false, ReturnSpecialDirectories = false, AttributesToSkip = 0 })) { var attributes = File.GetAttributes(entry); if ((attributes & FileAttributes.ReparsePoint) != 0) { throw new InvalidOperationException( "Refusing to recursively remove a fixture containing a reparse point."); } if ((attributes & FileAttributes.Directory) != 0) { pendingDirectories.Push(entry); } } } Directory.Delete(resolved, recursive: true); } 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"); } internal sealed record DatabaseSmokeRuntimeSelection( DatabaseRuntime Runtime, MariaDbEndpointReference ReferenceMariaDb);