821763a09d
- I-1: an Empty sample is always Bad. The writer stores a non-Bad NaN/Inf as Empty+BadOutOfRange and any other non-Bad Empty as BadNoData; the updater refuses a non-Bad Empty with BadInvalidArgument; reads clamp a legacy non-Bad Empty row to BadNoData (Raw and AtTime). - Tag/source names over 400 chars are refused, never truncated: writer per-tag failure, updater BadInvalidArgument, reads not-found, events none. - HistorianInfoBits masks the historian bits before setting one. - AtTime past now, stepped from the last sample => UncertainDataSubNormal. - Bootstrapper: logger, 5 s negative cache, ClearPool + Warning when sp_releaseapplock fails. - Provider builds its shared state lazily (an invalid Schema no longer throws at Resolve); Kinds is a FrozenSet; Gateway reuses the recorder's DefaultSupportedKinds. - Cleanup: SqlHistorianDataTypes in its own file (updater dup removed), dead Invalidate and unused status consts deleted, named severity masks, alarm writer brace. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
88 lines
4.2 KiB
C#
88 lines
4.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
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();
|
|
}
|
|
}
|