Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests/BucketPlanTests.cs
T
Joseph Doherty f0700571e8 test(historian): direct Sql.Common evaluator tests (1b)
51 new test cases (34 methods) in Sql.Common.Tests covering AtTimeEvaluator, AggregateEvaluator
(hand-built tag-A bucket rows, ProcessedRead literals), BucketPlan, LiveWritePreparer and
HistoryUpdatePlanner (incl. resolutionTicks=10 truncate-before-dedup and the superseded-duplicate
outcome), plus EventContinuation, HistorianHealthCounters, ClampSeverity and StoredSample.FromStored.
Each was falsified by reverting the rule it pins (37 mutations, all red, all restored).

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

72 lines
2.8 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using static ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests.Fixture;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
public sealed class BucketPlanTests
{
[Fact]
public void BucketPlan_rejects_more_than_100k_buckets()
{
var ex = Should.Throw<ArgumentOutOfRangeException>(() =>
BucketPlan.Create(DateTime.UnixEpoch, DateTime.UnixEpoch.AddSeconds(100_001), TimeSpan.FromSeconds(1)));
ex.ParamName.ShouldBe("interval");
}
[Fact]
public void Exactly_100k_one_tick_buckets_are_allowed()
{
var p = BucketPlan.Create(At(0), At(0).AddTicks(100_000), TimeSpan.FromTicks(1));
p.BucketCount.ShouldBe(100_000);
p.IntervalTicks.ShouldBe(1);
}
[Fact]
public void A_partial_last_interval_is_its_own_bucket()
{
var p = BucketPlan.Create(At(0), At(50), TimeSpan.FromSeconds(20));
p.BucketCount.ShouldBe(3);
p.BucketStart(2).ShouldBe(At(40));
p.BucketStart(2).Kind.ShouldBe(DateTimeKind.Utc);
}
[Fact]
public void Zero_or_oversized_interval_is_one_bucket_and_an_empty_window_none()
{
BucketPlan.Create(At(0), At(80), TimeSpan.Zero).BucketCount.ShouldBe(1);
var wide = BucketPlan.Create(At(0), At(80), TimeSpan.FromDays(1));
wide.BucketCount.ShouldBe(1);
wide.IntervalTicks.ShouldBe(80 * TimeSpan.TicksPerSecond);
BucketPlan.Create(At(0), At(0), TimeSpan.FromSeconds(1)).BucketCount.ShouldBe(0);
}
[Fact]
public void A_reverse_window_is_planned_forward_and_assembled_descending()
{
var p = BucketPlan.Create(At(80), At(0), TimeSpan.FromSeconds(20));
p.Reversed.ShouldBeTrue();
p.StartUtc.ShouldBe(At(0));
p.EndUtc.ShouldBe(At(80));
var s = p.Assemble(new Dictionary<long, DataValueSnapshot>(), t => new DataValueSnapshot(null, 0, t, t));
s.Select(x => x.SourceTimestampUtc).ShouldBe([At(60), At(40), At(20), At(0)]);
}
[Fact]
public void Assemble_fills_missing_buckets_with_the_empty_factory()
{
var p = BucketPlan.Create(At(0), At(60), TimeSpan.FromSeconds(20));
var hit = new DataValueSnapshot(7d, 0, At(20), At(20));
var s = p.Assemble(new Dictionary<long, DataValueSnapshot> { [1] = hit }, t => new DataValueSnapshot(null, 1u, t, t));
s.Select(x => x.Value).ShouldBe([null, (object?)7d, null]);
s[0].SourceTimestampUtc.ShouldBe(At(0));
s[2].SourceTimestampUtc.ShouldBe(At(40));
}
[Fact]
public void StartUnixTicks_is_ticks_since_the_unix_epoch()
=> BucketPlan.Create(DateTime.UnixEpoch.AddTicks(1234567), DateTime.UnixEpoch.AddSeconds(10), TimeSpan.Zero)
.StartUnixTicks.ShouldBe(1234567L);
}