using System.Diagnostics.Metrics; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Observability; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; using ZB.MOM.WW.OtOpcUa.Runtime.Redundancy; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian; public sealed class HistorianRetentionServiceTests { private sealed class FakeRetention(Func>> run) : IHistorianRetention { public int Calls; public TimeSpan Interval => TimeSpan.FromHours(1); public Task> PurgeAsync(CancellationToken ct) { Interlocked.Increment(ref Calls); return run(); } } private sealed class FakeRole(bool record) : IHistorizationRoleView { public bool ShouldRecordHistorizedValues => record; public void PublishHistorizationRole(bool shouldRecordHistorizedValues) { } } private static Task> Rows(long sample, long ev) => Task.FromResult>( [new RetentionPurgeResult("Sample", sample), new RetentionPurgeResult("Event", ev)]); [Fact] public async Task Runs_when_role_is_unknown_and_when_recording() { var r1 = new FakeRetention(() => Rows(0, 0)); (await new HistorianRetentionService(r1, null).TickAsync(CancellationToken.None)).ShouldBeTrue(); var r2 = new FakeRetention(() => Rows(0, 0)); (await new HistorianRetentionService(r2, new FakeRole(true)).TickAsync(CancellationToken.None)).ShouldBeTrue(); r1.Calls.ShouldBe(1); r2.Calls.ShouldBe(1); } [Fact] public async Task Skips_when_not_the_recording_node() { var r = new FakeRetention(() => Rows(1, 1)); (await new HistorianRetentionService(r, new FakeRole(false)).TickAsync(CancellationToken.None)).ShouldBeFalse(); r.Calls.ShouldBe(0); } [Fact] public async Task Null_retention_is_inert() { (await new HistorianRetentionService(null, null).TickAsync(CancellationToken.None)).ShouldBeFalse(); } [Fact] public async Task A_throwing_purge_does_not_escape() { var r = new FakeRetention(() => throw new InvalidOperationException("boom")); (await new HistorianRetentionService(r, null).TickAsync(CancellationToken.None)).ShouldBeFalse(); r.Calls.ShouldBe(1); } [Fact] public async Task Emits_purged_counter_per_table_and_skips_zero_rows() { var marker = Guid.NewGuid().ToString("N"); var seen = new List<(string Table, long Value)>(); using var listener = new MeterListener(); listener.InstrumentPublished = (inst, l) => { if (inst.Meter.Name == OtOpcUaTelemetry.MeterName && inst.Name == HistorianRetentionService.PurgedCounterName) l.EnableMeasurementEvents(inst); }; listener.SetMeasurementEventCallback((_, value, tags, _) => { string? table = null; var mine = false; foreach (var t in tags) { if (t.Key == "table") table = (string?)t.Value; if (t.Key == "test" && (string?)t.Value == marker) mine = true; } if (mine) lock (seen) seen.Add((table!, value)); }); listener.Start(); var r = new FakeRetention(() => Rows(7, 0)); var svc = new HistorianRetentionService(r, null, null, new KeyValuePair("test", marker)); await svc.TickAsync(CancellationToken.None); lock (seen) seen.ShouldBe([("Sample", 7L)]); } }