17962d4233
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
80 lines
3.2 KiB
C#
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_<guid></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";
|
|
}
|