Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests/SqlServerHistorianProviderTests.cs
T
Joseph Doherty 821763a09d fix(historian): SQL quality contract, long-name refusal, info-bit masking, AtTime-after-now, bootstrap hardening
- 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
2026-10-09 14:16:31 -04:00

70 lines
2.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
public sealed class SqlServerHistorianProviderTests
{
private static SqlServerHistorianProvider Make() => new(new SqlServerHistorianOptions
{
ConnectionString = "Server=localhost;User Id=sa;Password=x;TrustServerCertificate=True",
});
[Fact]
public void Name_and_all_six_kinds()
{
var p = Make();
p.Name.ShouldBe("SqlServer");
p.SupportedKinds.ShouldBe(Enum.GetValues<HistorianValueKind>(), ignoreOrder: true);
p.SupportedKinds.Count.ShouldBe(6);
}
[Fact]
public void Creates_all_five_seams_without_touching_sql()
{
var sp = new NullSp();
var p = Make();
p.CreateDataSource(sp).ShouldBeOfType<SqlServerHistorianDataSource>();
p.CreateValueWriter(sp).ShouldBeOfType<SqlServerHistorianValueWriter>();
p.CreateAlarmWriter(sp).ShouldBeOfType<SqlServerAlarmHistorianWriter>();
p.CreateProvisioning(sp).ShouldBeOfType<SqlServerTagProvisioner>();
p.CreateUpdater(sp).ShouldBeOfType<SqlServerHistorianDataUpdater>();
}
[Fact]
public void Supported_kinds_are_a_frozen_set() =>
Make().SupportedKinds.ShouldBeAssignableTo<System.Collections.Frozen.FrozenSet<HistorianValueKind>>();
/// <summary>
/// An invalid schema must not fail provider construction (the host resolves the provider even when no
/// consumer is enabled); it surfaces only when a seam is actually built.
/// </summary>
[Fact]
public void An_invalid_schema_fails_only_when_a_seam_is_built()
{
var p = new SqlServerHistorianProvider(new SqlServerHistorianOptions { ConnectionString = "x", Schema = "bad;schema" });
p.Name.ShouldBe("SqlServer");
Should.Throw<ArgumentException>(() => p.CreateDataSource(new NullSp()));
}
[Fact]
public void Every_seam_shares_one_connection_bootstrapper_and_catalog()
{
var sp = new NullSp();
var p = Make();
var a = (SqlServerHistorianDataSource)p.CreateDataSource(sp);
var b = (SqlServerHistorianDataSource)p.CreateDataSource(sp);
Field(a, "_bootstrap").ShouldBeSameAs(Field(b, "_bootstrap"));
Field(a, "_catalog").ShouldBeSameAs(Field(b, "_catalog"));
}
private static object? Field(object o, string name) =>
o.GetType().GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.GetValue(o);
private sealed class NullSp : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
}