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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies Historian data source lifecycle behavior: dispose safety,
|
||||
/// post-dispose rejection, and double-dispose idempotency.
|
||||
/// </summary>
|
||||
public class HistorianDataSourceLifecycleTests
|
||||
{
|
||||
private static HistorianConfiguration DefaultConfig => new()
|
||||
{
|
||||
Enabled = true,
|
||||
ServerName = "test-historian",
|
||||
Port = 32568,
|
||||
IntegratedSecurity = true,
|
||||
CommandTimeoutSeconds = 5
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ReadRawAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
var ds = new HistorianDataSource(DefaultConfig);
|
||||
ds.Dispose();
|
||||
|
||||
Should.Throw<ObjectDisposedException>(() =>
|
||||
ds.ReadRawAsync("Tag1", DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, 100)
|
||||
.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAggregateAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
var ds = new HistorianDataSource(DefaultConfig);
|
||||
ds.Dispose();
|
||||
|
||||
Should.Throw<ObjectDisposedException>(() =>
|
||||
ds.ReadAggregateAsync("Tag1", DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, 60000, "Average")
|
||||
.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAtTimeAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
var ds = new HistorianDataSource(DefaultConfig);
|
||||
ds.Dispose();
|
||||
|
||||
Should.Throw<ObjectDisposedException>(() =>
|
||||
ds.ReadAtTimeAsync("Tag1", new[] { DateTime.UtcNow })
|
||||
.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadEventsAsync_AfterDispose_ThrowsObjectDisposedException()
|
||||
{
|
||||
var ds = new HistorianDataSource(DefaultConfig);
|
||||
ds.Dispose();
|
||||
|
||||
Should.Throw<ObjectDisposedException>(() =>
|
||||
ds.ReadEventsAsync(null, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, 100)
|
||||
.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_CalledTwice_DoesNotThrow()
|
||||
{
|
||||
var ds = new HistorianDataSource(DefaultConfig);
|
||||
ds.Dispose();
|
||||
Should.NotThrow(() => ds.Dispose());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractAggregateValue_UnknownColumn_ReturnsNull()
|
||||
{
|
||||
HistorianDataSource.MapAggregateToColumn(new Opc.Ua.NodeId(99999)).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user