using System.Collections.Concurrent; using Npgsql; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests; /// /// Env-gated QuestDB for the historian integration tests. HISTORIAN_QUESTDB_TEST_CONNECTION absent ⇒ /// is set, nothing connects and every test calls Assert.Skip. Present ⇒ tests isolate /// from each other with ; every prefix handed out is dropped on dispose (and a test may /// drop its own early with ). Never touches a table outside those prefixes. /// public sealed class QuestDbFixture : IAsyncLifetime { /// Env var holding an Npgsql connection string for QuestDB's PG-wire port. public const string ConnectionEnvVar = "HISTORIAN_QUESTDB_TEST_CONNECTION"; private readonly ConcurrentBag _prefixes = new(); /// Why tests must skip, or null when QuestDB is available. public string? SkipReason { get; private set; } /// The Npgsql connection string. Valid only when is null. public string ConnectionString { get; private set; } = ""; /// Skips the calling test when the gate is closed. public void SkipIfUnavailable() { if (SkipReason is not null) Assert.Skip(SkipReason); } /// A fresh, valid table prefix ("t" + 12 hex + "_") that nothing uses yet; dropped on dispose. /// The prefix. public string UniquePrefix() { var p = "t" + Guid.NewGuid().ToString("N")[..12] + "_"; _prefixes.Add(p); return p; } /// Options targeting this fixture's QuestDB under . /// Table prefix (normally from ). /// Optional TTL. /// AutoCreateSchema. /// Valid options. public QuestDbHistorianOptions Options(string prefix, int? retentionDays = null, bool autoCreate = true) => new() { ConnectionString = ConnectionString, TablePrefix = prefix, RetentionDays = retentionDays, AutoCreateSchema = autoCreate, }; /// Drops every table whose name starts with (found through tables()). /// Table prefix. /// A task completing when they are gone. public async Task DropPrefixAsync(string prefix) { if (SkipReason is not null) return; if (!QuestDbHistorianOptions.IsValidPrefix(prefix)) throw new ArgumentException("Invalid prefix.", nameof(prefix)); await using var ds = NpgsqlDataSource.Create(ConnectionString); await using var conn = await ds.OpenConnectionAsync().ConfigureAwait(false); var names = new List(); await using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT table_name FROM tables() WHERE starts_with(table_name, $1)"; // never LIKE: '_' is a wildcard cmd.Parameters.Add(new NpgsqlParameter { NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Varchar, Value = prefix }); await using var r = await cmd.ExecuteReaderAsync().ConfigureAwait(false); while (await r.ReadAsync().ConfigureAwait(false)) names.Add(r.GetString(0)); } foreach (var n in names) { await using var drop = conn.CreateCommand(); drop.CommandText = $"DROP TABLE IF EXISTS {n}"; await drop.ExecuteNonQueryAsync().ConfigureAwait(false); } } /// public ValueTask InitializeAsync() { var cs = Environment.GetEnvironmentVariable(ConnectionEnvVar); if (string.IsNullOrWhiteSpace(cs)) SkipReason = $"{ConnectionEnvVar} is not set; QuestDB historian integration tests skipped."; else ConnectionString = cs; return ValueTask.CompletedTask; } /// public async ValueTask DisposeAsync() { if (SkipReason is not null) return; foreach (var p in _prefixes.Distinct()) await DropPrefixAsync(p).ConfigureAwait(false); NpgsqlConnection.ClearAllPools(); } } /// Shares one fixture across the QuestDB integration tests. [CollectionDefinition(Name)] public sealed class QuestDbCollection : ICollectionFixture { /// Collection name. public const string Name = "QuestDb"; }