using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests; /// /// The shared existence-query clustering (moved here, names unchanged, from the writer's WriteTests and the /// updater's UpdateTests when the two private copies were consolidated into ). /// public sealed class QuestDbTimeClustersTests { private static readonly DateTime T0 = new(2026, 3, 1, 12, 0, 0, DateTimeKind.Utc); [Fact] public void Cluster_splits_on_a_gap_over_the_limit_and_at_the_per_cluster_cap() { var g = TimeSpan.FromMinutes(1); QuestDbTimeClusters.Cluster([], g, 10).ShouldBeEmpty(); QuestDbTimeClusters.Cluster([T0], g, 10).ShouldBe([(T0, T0)]); // exactly the gap stays together; one tick over splits QuestDbTimeClusters.Cluster([T0, T0 + g], g, 10).ShouldBe([(T0, T0 + g)]); QuestDbTimeClusters.Cluster([T0, T0 + g + TimeSpan.FromTicks(10)], g, 10) .ShouldBe([(T0, T0), (T0 + g + TimeSpan.FromTicks(10), T0 + g + TimeSpan.FromTicks(10))]); // outlier gets its own range var far = T0.AddYears(1); QuestDbTimeClusters.Cluster([T0, T0.AddSeconds(1), far], g, 10).ShouldBe([(T0, T0.AddSeconds(1)), (far, far)]); // cap: 5 close times, max 2 per cluster var five = Enumerable.Range(0, 5).Select(i => T0.AddSeconds(i)).ToList(); QuestDbTimeClusters.Cluster(five, g, 2) .ShouldBe([(five[0], five[1]), (five[2], five[3]), (five[4], five[4])]); } [Fact] public void Cluster_splits_where_the_gap_exceeds_the_limit() { var gap = QuestDbTimeClusters.ExistenceGap; var c = QuestDbTimeClusters.Cluster( [T0.AddHours(2), T0, T0.AddSeconds(30), T0.AddHours(1)], gap); c.ShouldBe([(T0, T0.AddSeconds(30)), (T0.AddHours(1), T0.AddHours(1)), (T0.AddHours(2), T0.AddHours(2))]); QuestDbTimeClusters.Cluster([], gap).ShouldBeEmpty(); } [Fact] public void The_existence_gap_is_one_minute_and_the_default_cap_is_unbounded() { QuestDbTimeClusters.ExistenceGap.ShouldBe(TimeSpan.FromMinutes(1)); var many = Enumerable.Range(0, 5000).Select(i => T0.AddMilliseconds(i)).ToList(); QuestDbTimeClusters.Cluster(many, QuestDbTimeClusters.ExistenceGap).ShouldBe([(many[0], many[^1])]); } [Fact] public void Unsorted_input_is_sorted_before_capping() { var times = new[] { T0.AddSeconds(3), T0, T0.AddSeconds(2), T0.AddSeconds(1) }; QuestDbTimeClusters.Cluster(times, TimeSpan.FromMinutes(1), 2) .ShouldBe([(T0, T0.AddSeconds(1)), (T0.AddSeconds(2), T0.AddSeconds(3))]); } [Fact] public void A_cap_below_one_is_rejected() => Should.Throw(() => QuestDbTimeClusters.Cluster([T0], TimeSpan.FromMinutes(1), 0)); }