Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/LongNameTests.cs
T
Joseph Doherty 8c6d6aff6a refactor(historian): extract Driver.Historian.Sql.Common from the SQL Server backend (no behaviour change) (1a)
Moves the database-independent logic of the SQL Server historian backend into a new
shared library for the upcoming QuestDB backend (#666): status codes and bits, value/column
mapping, StoredSample + quality clamp, the AtTime and aggregate evaluators, BucketPlan,
LiveWritePreparer and HistoryUpdatePlanner (both with a resolutionTicks parameter, default 1),
tag-name rules, the bootstrap failure cache, health counters, alarm event ids, the event
continuation and severity clamp. The SQL Server backend now calls it; its T-SQL, TVP
builders, connection, bootstrapper and catalog stay.

Tests: StatusCodeOracleTests, ValueWriterPrepareTests, the column half of SampleRowMapperTests
and ProcessedReadTests.Every_enum_member_is_either_served_or_refused move to the new
Sql.Common.Tests under the same names. SqlServer integration tests get mechanical renames
only. Test-name set across the three projects is identical to the baseline (160 tests).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-11 01:26:49 -04:00

89 lines
4.3 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
/// <summary>
/// Names longer than the 400-character <c>Tag.Name</c> / <c>Event.SourceName</c> columns are refused, never
/// truncated. Each test seeds a series under the 400-character PREFIX of the long name: a sized
/// <c>NVarChar(400)</c> parameter would silently cut the long name to that prefix and hit the wrong series.
/// </summary>
[Collection(SqlHistorianCollection.Name)]
public sealed class LongNameTests(SqlHistorianFixture fx)
{
private static readonly CancellationToken Ct = CancellationToken.None;
private static DateTime At(double s) => ReadTestDb.At(s);
private const uint BadInvalidArgument = 0x80AB0000u;
private const uint BadNoData = 0x809B0000u;
private static readonly string Prefix = new('p', 400);
private static readonly string Long = Prefix + "-x";
[Fact]
public async Task The_writer_refuses_a_long_name_as_a_per_tag_failure_and_writes_nothing()
{
var db = await ReadTestDb.CreateAsync(fx);
await db.SeedAsync(Prefix, (At(0), HistorianValue.FromDouble(1), ReadTestDb.Good));
var w = new SqlServerHistorianValueWriter(db.Conn, db.Boot, new SqlServerTagCatalog(db.Conn, db.Boot));
(await w.WriteLiveValuesAsync(Long, [new(At(5), HistorianValue.FromDouble(2), 0u)], Ct)).ShouldBeFalse();
(await db.ScalarAsync($"SELECT COUNT(*) FROM {db.Conn.Q("Sample")}")).ShouldBe(1);
(await db.ScalarAsync($"SELECT COUNT(*) FROM {db.Conn.Q("Tag")}")).ShouldBe(1);
}
[Fact]
public async Task The_updater_answers_BadInvalidArgument_for_every_item_of_a_long_name()
{
var db = await ReadTestDb.CreateAsync(fx);
await db.SeedAsync(Prefix, (At(0), HistorianValue.FromDouble(1), ReadTestDb.Good));
var u = new SqlServerHistorianDataUpdater(db.Conn, new SqlServerTagCatalog(db.Conn, db.Boot));
(await u.UpdateDataAsync(Long, HistoryUpdateMode.Update,
[new(At(0), HistorianValue.FromDouble(9), 0u), new(At(1), HistorianValue.FromDouble(9), 0u)], Ct))
.ShouldBe([BadInvalidArgument, BadInvalidArgument]);
(await u.DeleteRawAsync(Long, At(-1), At(10), Ct)).ShouldBe(BadInvalidArgument);
(await u.DeleteAtTimeAsync(Long, [At(0)], Ct)).ShouldBe([BadInvalidArgument]);
// The prefix series is untouched: not replaced, not deleted.
var raw = (await db.Source().ReadRawAsync(Prefix, At(-1), At(10), 0, Ct)).Samples;
raw.Select(s => s.Value).ShouldBe([(object)1d]);
}
[Fact]
public async Task Reads_of_a_long_name_are_not_found_rather_than_the_prefix_series()
{
var db = await ReadTestDb.CreateAsync(fx);
await db.SeedAsync(Prefix, (At(0), HistorianValue.FromDouble(1), ReadTestDb.Good));
var src = db.Source();
(await src.ReadRawAsync(Long, At(-1), At(10), 0, Ct)).Samples.ShouldBeEmpty();
(await src.ReadAtTimeAsync(Long, [At(0)], Ct)).Samples.Single().StatusCode.ShouldBe(BadNoData);
(await src.ReadProcessedAsync(Long, At(-1), At(10), TimeSpan.Zero, HistoryAggregateType.Count, Ct))
.Samples.Single().StatusCode.ShouldBe(HistorianInfoBits.WithCalculated(BadNoData));
src.GetHealthSnapshot().TotalFailures.ShouldBe(0);
}
[Fact]
public async Task An_event_read_filtered_by_a_long_source_matches_nothing()
{
var db = await ReadTestDb.CreateAsync(fx);
await using (var c = await db.Conn.OpenAsync(Ct))
await using (var cmd = db.Conn.CreateCommand(c,
$"INSERT {db.Conn.Q("Event")} (EventId, SourceName, AlarmId, AlarmName, AlarmTypeName, Severity, EventKind, Message, " +
"UserName, Comment, EventTimeUtc, ReceivedTimeUtc) VALUES ('e1', @src, 'a', 'n', 'T', 500, 'Activated', NULL, NULL, NULL, @t, @t)"))
{
cmd.Parameters.AddWithValue("@src", Prefix);
SqlHistorianConnection.AddDateTime2(cmd, "@t", At(0));
await cmd.ExecuteNonQueryAsync(Ct);
}
var src = db.Source();
(await src.ReadEventsAsync(Prefix, At(-1), At(10), 0, Ct)).Events.Count.ShouldBe(1); // control
(await src.ReadEventsAsync(Long, At(-1), At(10), 0, Ct)).Events.ShouldBeEmpty();
}
}