using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests; [Collection(QuestDbCollection.Name)] public sealed class ProvisioningTests(QuestDbFixture fx) { private (QuestDbTagProvisioner P, QuestDbConnection C, QuestDbTagCatalog Cat) Make(string? prefix = null, string? connectionString = null, bool autoCreate = true) { var o = fx.Options(prefix ?? fx.UniquePrefix(), autoCreate: autoCreate); if (connectionString is not null) o.ConnectionString = connectionString; var c = new QuestDbConnection(o); var b = new QuestDbSchemaBootstrapper(c, o); var wal = new QuestDbWalVisibility(c, o.VisibilityTimeout); var cat = new QuestDbTagCatalog(c, b, wal); return (new QuestDbTagProvisioner(c, b, cat, wal), c, cat); } private static async Task<(byte Type, string? Eu, string? Desc)> TagAsync(QuestDbConnection c, string name) { await using var conn = await c.OpenAsync(default); await using var cmd = c.CreateCommand(conn, $"SELECT data_type, eng_unit, description FROM {c.TagTable} WHERE name = $1"); QuestDbConnection.BindText(cmd, name); await using var r = await cmd.ExecuteReaderAsync(); (await r.ReadAsync()).ShouldBeTrue(); return (QuestDbConnection.ReadKind(r, 0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2)); } private static async Task CountAsync(QuestDbConnection c) { await using var conn = await c.OpenAsync(default); await using var cmd = c.CreateCommand(conn, $"SELECT count() FROM {c.TagTable}"); return Convert.ToInt64(await cmd.ExecuteScalarAsync()); } [Fact] public async Task Upsert_creates_then_changes_DataType_on_rerun_and_every_type_is_accepted() { fx.SkipIfUnavailable(); var (p, c, _) = 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(c, "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(c, "a")).ShouldBe(((byte)DriverDataType.Float64, "degC", "desc")); // null keeps stored metadata (await CountAsync(c)).ShouldBe(2); // upsert by name, not a second row } [Fact] public async Task A_request_with_values_replaces_only_the_fields_it_carries() { fx.SkipIfUnavailable(); var (p, c, _) = Make(); await p.EnsureTagsAsync([new("m", DriverDataType.Int32, "A", "first")], CancellationToken.None); await p.EnsureTagsAsync([new("m", DriverDataType.Int32, "B", null)], CancellationToken.None); (await TagAsync(c, "m")).ShouldBe(((byte)DriverDataType.Int32, "B", "first")); } [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, c, _) = 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)); (await CountAsync(c)).ShouldBe(1000); } [Fact] public async Task A_later_catalog_Ensure_of_a_provisioned_tag_does_not_wipe_its_metadata() { fx.SkipIfUnavailable(); var (p, c, cat) = Make(); await p.EnsureTagsAsync([new("k", DriverDataType.Float64, "kg", "mass")], CancellationToken.None); (await cat.ExistsAsync("k", CancellationToken.None)).ShouldBeTrue(); await cat.EnsureAsync("k", 1, CancellationToken.None); (await TagAsync(c, "k")).ShouldBe(((byte)DriverDataType.Float64, "kg", "mass")); } [Fact] public async Task A_connection_failure_is_a_Failed_tally_not_an_exception() { fx.SkipIfUnavailable(); var (p, _, _) = Make(connectionString: "Host=localhost;Port=1;Username=admin;Password=quest;Database=qdb;ServerCompatibilityMode=NoTypeLoading;Timeout=2"); (await p.EnsureTagsAsync([new("x", DriverDataType.Int32, null, null)], CancellationToken.None)).ShouldBe(new HistorianProvisionResult(1, 0, 0, 1)); } }