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";
}