Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/SqlHistorianFixture.cs
T

80 lines
3.2 KiB
C#

using Microsoft.Data.SqlClient;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
/// <summary>
/// Env-gated throwaway database for the SQL historian backend. <c>HISTORIAN_SQL_TEST_CONNECTION</c> absent ⇒
/// <see cref="SkipReason"/> is set, nothing connects, and every test calls <c>Assert.Skip</c>. Present ⇒
/// creates <c>OtOpcUaHistorian_T_&lt;guid&gt;</c> (never touches another database) and drops it on dispose.
/// Tests isolate from each other with <see cref="UniqueSchema"/>.
/// </summary>
public sealed class SqlHistorianFixture : IAsyncLifetime
{
/// <summary>Env var holding a connection string with CREATE/DROP DATABASE rights.</summary>
public const string ConnectionEnvVar = "HISTORIAN_SQL_TEST_CONNECTION";
private string? _master;
private string? _dbName;
/// <summary>Why tests must skip, or null when the database is available.</summary>
public string? SkipReason { get; private set; }
/// <summary>Connection string targeting the throwaway database. Valid only when <see cref="SkipReason"/> is null.</summary>
public string ConnectionString { get; private set; } = "";
/// <summary>Skips the calling test when the gate is closed.</summary>
public void SkipIfUnavailable()
{
if (SkipReason is not null) Assert.Skip(SkipReason);
}
/// <summary>A fresh, valid schema name (nothing exists under it yet).</summary>
/// <returns>A schema identifier unique to this call.</returns>
public static string UniqueSchema() => "h" + Guid.NewGuid().ToString("N")[..16];
/// <inheritdoc/>
public async ValueTask InitializeAsync()
{
var cs = Environment.GetEnvironmentVariable(ConnectionEnvVar);
if (string.IsNullOrWhiteSpace(cs))
{
SkipReason = $"{ConnectionEnvVar} is not set; SQL Server historian integration tests skipped.";
return;
}
var b = new SqlConnectionStringBuilder(cs) { InitialCatalog = "master" };
_master = b.ConnectionString;
_dbName = "OtOpcUaHistorian_T_" + Guid.NewGuid().ToString("N");
await ExecAsync(_master, $"CREATE DATABASE [{_dbName}]").ConfigureAwait(false);
b.InitialCatalog = _dbName;
ConnectionString = b.ConnectionString;
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
if (_master is null || _dbName is null) return;
SqlConnection.ClearAllPools();
await ExecAsync(_master,
$"ALTER DATABASE [{_dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_dbName}]").ConfigureAwait(false);
}
private static async Task ExecAsync(string cs, string sql)
{
await using var c = new SqlConnection(cs);
await c.OpenAsync().ConfigureAwait(false);
await using var cmd = c.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
}
}
/// <summary>Shares one throwaway database across the integration tests.</summary>
[CollectionDefinition(Name)]
public sealed class SqlHistorianCollection : ICollectionFixture<SqlHistorianFixture>
{
/// <summary>Collection name.</summary>
public const string Name = "SqlHistorian";
}