Files
lmxopcua/tests/ZB.MOM.WW.LmxOpcUa.Tests/Historian/HistoryContinuationPointTests.cs
Joseph Doherty 95ad9c6866 Resolve 6 of 7 stability review findings and close test coverage gaps
Fixes P1 StaComThread hang (crash-path faulting via WorkItem queue), P1 subscription
fire-and-forget (block+log or ContinueWith on 5 call sites), P2 continuation point
leak (PurgeExpired on Retrieve/Release), P2 dashboard bind failure (localhost prefix,
bool Start), P3 background loop double-start (task handles + join on stop in 3 files),
and P3 config logging exposure (SqlConnectionStringBuilder password masking). Adds
FakeMxAccessClient fault injection and 12 new tests. Documents required runtime
assemblies in ServiceHosting.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:37:27 -04:00

143 lines
4.1 KiB
C#

using System;
using System.Collections.Generic;
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
{
public class HistoryContinuationPointTests
{
private static List<DataValue> CreateTestValues(int count)
{
var values = new List<DataValue>();
for (var i = 0; i < count; i++)
values.Add(new DataValue
{
Value = new Variant((double)i),
SourceTimestamp = DateTime.UtcNow.AddSeconds(i),
StatusCode = StatusCodes.Good
});
return values;
}
[Fact]
public void Store_ReturnsNonEmptyContinuationPoint()
{
var mgr = new HistoryContinuationPointManager();
var values = CreateTestValues(5);
var cp = mgr.Store(values);
cp.ShouldNotBeNull();
cp.Length.ShouldBe(16); // GUID = 16 bytes
}
[Fact]
public void Retrieve_ValidContinuationPoint_ReturnsStoredValues()
{
var mgr = new HistoryContinuationPointManager();
var values = CreateTestValues(5);
var cp = mgr.Store(values);
var retrieved = mgr.Retrieve(cp);
retrieved.ShouldNotBeNull();
retrieved!.Count.ShouldBe(5);
}
[Fact]
public void Retrieve_SameContinuationPointTwice_ReturnsNullSecondTime()
{
var mgr = new HistoryContinuationPointManager();
var values = CreateTestValues(3);
var cp = mgr.Store(values);
mgr.Retrieve(cp).ShouldNotBeNull();
mgr.Retrieve(cp).ShouldBeNull();
}
[Fact]
public void Retrieve_InvalidBytes_ReturnsNull()
{
var mgr = new HistoryContinuationPointManager();
mgr.Retrieve(new byte[] { 1, 2, 3 }).ShouldBeNull();
}
[Fact]
public void Retrieve_NullBytes_ReturnsNull()
{
var mgr = new HistoryContinuationPointManager();
mgr.Retrieve(null!).ShouldBeNull();
}
[Fact]
public void Retrieve_UnknownGuid_ReturnsNull()
{
var mgr = new HistoryContinuationPointManager();
mgr.Retrieve(Guid.NewGuid().ToByteArray()).ShouldBeNull();
}
[Fact]
public void Release_RemovesContinuationPoint()
{
var mgr = new HistoryContinuationPointManager();
var values = CreateTestValues(5);
var cp = mgr.Store(values);
mgr.Release(cp);
mgr.Retrieve(cp).ShouldBeNull();
}
[Fact]
public void Retrieve_ExpiredContinuationPoint_ReturnsNull()
{
var mgr = new HistoryContinuationPointManager(TimeSpan.FromMilliseconds(1));
var values = CreateTestValues(5);
var cp = mgr.Store(values);
System.Threading.Thread.Sleep(50);
mgr.Retrieve(cp).ShouldBeNull();
}
[Fact]
public void Release_PurgesExpiredEntries()
{
var mgr = new HistoryContinuationPointManager(TimeSpan.FromMilliseconds(1));
var cp1 = mgr.Store(CreateTestValues(3));
var cp2 = mgr.Store(CreateTestValues(5));
System.Threading.Thread.Sleep(50);
// Release one — purge should clean both expired entries
mgr.Release(cp1);
mgr.Retrieve(cp2).ShouldBeNull();
}
[Fact]
public void MultipleContinuationPoints_IndependentRetrieval()
{
var mgr = new HistoryContinuationPointManager();
var values1 = CreateTestValues(3);
var values2 = CreateTestValues(7);
var cp1 = mgr.Store(values1);
var cp2 = mgr.Store(values2);
var r1 = mgr.Retrieve(cp1);
var r2 = mgr.Retrieve(cp2);
r1.ShouldNotBeNull();
r1!.Count.ShouldBe(3);
r2.ShouldNotBeNull();
r2!.Count.ShouldBe(7);
}
}
}