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>
84 lines
2.7 KiB
C#
84 lines
2.7 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|