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

109 lines
5.6 KiB
C#

using Microsoft.Data.SqlClient;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
[Collection(SqlHistorianCollection.Name)]
public sealed class ProvisioningTests(SqlHistorianFixture fx)
{
private (SqlServerTagProvisioner P, string S) Make(string? schema = null)
{
var s = schema ?? SqlHistorianFixture.UniqueSchema();
var o = new SqlServerHistorianOptions { ConnectionString = fx.ConnectionString, Schema = s };
var c = new SqlHistorianConnection(o);
return (new SqlServerTagProvisioner(c, new SqlServerSchemaBootstrapper(c, o)), s);
}
private async Task<(byte Type, string? Eu, string? Desc)> TagAsync(string s, string name)
{
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT DataType, EngineeringUnit, Description FROM [{s}].Tag WHERE Name=@n";
cmd.Parameters.AddWithValue("@n", name);
await using var r = await cmd.ExecuteReaderAsync();
(await r.ReadAsync()).ShouldBeTrue();
return (r.GetByte(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2));
}
[Fact]
public async Task Upsert_creates_then_changes_DataType_on_rerun_and_every_type_is_accepted()
{
fx.SkipIfUnavailable();
var (p, s) = Make();
var r1 = await p.EnsureTagsAsync([new("a", DriverDataType.Boolean, "degC", "desc"), new("s", DriverDataType.String, null, null)], CancellationToken.None);
r1.ShouldBe(new HistorianProvisionResult(2, 2, 0, 0));
(await TagAsync(s, "a")).ShouldBe(((byte)DriverDataType.Boolean, "degC", "desc"));
var r2 = await p.EnsureTagsAsync([new("a", DriverDataType.Float64, null, null)], CancellationToken.None);
r2.ShouldBe(new HistorianProvisionResult(1, 1, 0, 0));
(await TagAsync(s, "a")).ShouldBe(((byte)DriverDataType.Float64, "degC", "desc")); // null keeps stored metadata
}
[Fact]
public async Task Duplicate_names_and_bad_names_are_tallied_not_thrown()
{
fx.SkipIfUnavailable();
var (p, _) = Make();
var r = await p.EnsureTagsAsync([
new("d", DriverDataType.Int32, null, null), new("d", DriverDataType.Int64, null, null),
new("", DriverDataType.Int32, null, null), new(new string('x', 401), DriverDataType.Int32, null, null)], CancellationToken.None);
r.ShouldBe(new HistorianProvisionResult(4, 2, 0, 2));
}
[Fact]
public async Task More_tags_than_one_chunk_all_land()
{
fx.SkipIfUnavailable();
var (p, s) = Make();
var reqs = Enumerable.Range(0, 1000).Select(i => new HistorianTagProvisionRequest($"bulk{i}", DriverDataType.Float32, null, null)).ToList();
(await p.EnsureTagsAsync(reqs, CancellationToken.None)).ShouldBe(new HistorianProvisionResult(1000, 1000, 0, 0));
}
[Fact]
public async Task Two_concurrent_provisioners_of_the_same_new_tags_both_report_Ensured_with_no_Failed()
{
fx.SkipIfUnavailable();
var s = SqlHistorianFixture.UniqueSchema();
var (p0, _) = Make(s);
await p0.EnsureTagsAsync([new("warm", DriverDataType.Int32, null, null)], CancellationToken.None); // bootstrap once
for (var round = 0; round < 20; round++)
{
var ps = Enumerable.Range(0, 16).Select(_ => Make(s).P).ToList();
// Warm each instance (its own bootstrap-once) and release all together, else bootstrap serialisation staggers the race away.
foreach (var p in ps) await p.EnsureTagsAsync([new("warm", DriverDataType.Int32, null, null)], CancellationToken.None);
var gate = new TaskCompletionSource();
// Staggered windows over one pool: every worker's FIRST name differs, so a MERGE without HOLDLOCK cannot hide behind
// a lock convoy on a shared first key and the overlap really races (union per round = 6 names; the single/few-row shape is the one T8 proved races).
var tasks = ps.Select((p, k) => Task.Run(async () =>
{
var reqs = Enumerable.Range(k % 4, 3).Select(i => new HistorianTagProvisionRequest($"r{round}.t{i}", DriverDataType.Float64, "u", null)).ToList();
await gate.Task;
return await p.EnsureTagsAsync(reqs, CancellationToken.None);
})).ToList();
gate.SetResult();
var rs = await Task.WhenAll(tasks);
foreach (var r in rs) r.ShouldBe(new HistorianProvisionResult(3, 3, 0, 0));
// The production dup-key retry would mask a missing HOLDLOCK; HOLDLOCK must mean it never fires.
ps.Sum(p => p.DupKeyRetryCount).ShouldBe(0);
}
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Tag";
Convert.ToInt32(await cmd.ExecuteScalarAsync()).ShouldBe(20 * 6 + 1);
}
[Fact]
public async Task A_connection_failure_is_a_Failed_tally_not_an_exception()
{
fx.SkipIfUnavailable();
var o = new SqlServerHistorianOptions { ConnectionString = fx.ConnectionString + ";Connect Timeout=1", Schema = SqlHistorianFixture.UniqueSchema(), AutoCreateSchema = false };
var c = new SqlHistorianConnection(o);
var p = new SqlServerTagProvisioner(c, new SqlServerSchemaBootstrapper(c, o));
(await p.EnsureTagsAsync([new("x", DriverDataType.Int32, null, null)], CancellationToken.None)).ShouldBe(new HistorianProvisionResult(1, 0, 0, 1));
}
}