Migrate historian from SQL to aahClientManaged SDK and resolve all OPC UA Part 11 gaps

Replace direct SQL queries against Historian Runtime database with the Wonderware
Historian managed SDK (ArchestrA.HistorianAccess). Add HistoryServerCapabilities node,
AggregateFunctions folder, continuation points, ReadAtTime interpolation, ReturnBounds,
ReadModified rejection, HistoricalDataConfiguration per node, historical event access,
and client-side StandardDeviation aggregate support. Remove screenshot tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-06 16:38:00 -04:00
parent 5c89a44255
commit 41f0e9ec4c
35 changed files with 1858 additions and 536 deletions

View File

@@ -28,6 +28,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
configuration.GetSection("GalaxyRepository").Bind(config.GalaxyRepository);
configuration.GetSection("Dashboard").Bind(config.Dashboard);
configuration.GetSection("Security").Bind(config.Security);
configuration.GetSection("Historian").Bind(config.Historian);
return config;
}
@@ -315,5 +316,75 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
configuration.GetSection("OpcUa").Bind(config);
config.ApplicationUri.ShouldBe("urn:test:app");
}
[Fact]
public void Historian_Section_BindsFromJson()
{
var config = LoadFromJson();
config.Historian.Enabled.ShouldBe(false);
config.Historian.ServerName.ShouldBe("localhost");
config.Historian.IntegratedSecurity.ShouldBe(true);
config.Historian.Port.ShouldBe(32568);
config.Historian.CommandTimeoutSeconds.ShouldBe(30);
config.Historian.MaxValuesPerRead.ShouldBe(10000);
}
[Fact]
public void Historian_Section_BindsCustomValues()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Historian:Enabled", "true"),
new KeyValuePair<string, string>("Historian:ServerName", "historian-server"),
new KeyValuePair<string, string>("Historian:IntegratedSecurity", "false"),
new KeyValuePair<string, string>("Historian:UserName", "testuser"),
new KeyValuePair<string, string>("Historian:Password", "testpass"),
new KeyValuePair<string, string>("Historian:Port", "12345"),
new KeyValuePair<string, string>("Historian:CommandTimeoutSeconds", "60"),
new KeyValuePair<string, string>("Historian:MaxValuesPerRead", "5000")
})
.Build();
var config = new HistorianConfiguration();
configuration.GetSection("Historian").Bind(config);
config.Enabled.ShouldBe(true);
config.ServerName.ShouldBe("historian-server");
config.IntegratedSecurity.ShouldBe(false);
config.UserName.ShouldBe("testuser");
config.Password.ShouldBe("testpass");
config.Port.ShouldBe(12345);
config.CommandTimeoutSeconds.ShouldBe(60);
config.MaxValuesPerRead.ShouldBe(5000);
}
[Fact]
public void Validator_HistorianEnabled_EmptyServerName_ReturnsFalse()
{
var config = new AppConfiguration();
config.Historian.Enabled = true;
config.Historian.ServerName = "";
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
}
[Fact]
public void Validator_HistorianEnabled_InvalidPort_ReturnsFalse()
{
var config = new AppConfiguration();
config.Historian.Enabled = true;
config.Historian.Port = 0;
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
}
[Fact]
public void Validator_HistorianEnabled_NoIntegratedSecurity_EmptyUserName_ReturnsFalse()
{
var config = new AppConfiguration();
config.Historian.Enabled = true;
config.Historian.IntegratedSecurity = false;
config.Historian.UserName = "";
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
}
}
}

View File

@@ -0,0 +1,65 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
{
public class HistorianConfigurationTests
{
[Fact]
public void DefaultConfig_Disabled()
{
var config = new HistorianConfiguration();
config.Enabled.ShouldBe(false);
}
[Fact]
public void DefaultConfig_ServerNameLocalhost()
{
var config = new HistorianConfiguration();
config.ServerName.ShouldBe("localhost");
}
[Fact]
public void DefaultConfig_IntegratedSecurityTrue()
{
var config = new HistorianConfiguration();
config.IntegratedSecurity.ShouldBe(true);
}
[Fact]
public void DefaultConfig_UserNameNull()
{
var config = new HistorianConfiguration();
config.UserName.ShouldBeNull();
}
[Fact]
public void DefaultConfig_PasswordNull()
{
var config = new HistorianConfiguration();
config.Password.ShouldBeNull();
}
[Fact]
public void DefaultConfig_Port32568()
{
var config = new HistorianConfiguration();
config.Port.ShouldBe(32568);
}
[Fact]
public void DefaultConfig_CommandTimeout30()
{
var config = new HistorianConfiguration();
config.CommandTimeoutSeconds.ShouldBe(30);
}
[Fact]
public void DefaultConfig_MaxValuesPerRead10000()
{
var config = new HistorianConfiguration();
config.MaxValuesPerRead.ShouldBe(10000);
}
}
}

View File

@@ -0,0 +1,59 @@
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Historian
{
public class HistorianDataSourceTests
{
[Fact]
public void MapAggregateToColumn_Average_ReturnsAverage()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_Average).ShouldBe("Average");
}
[Fact]
public void MapAggregateToColumn_Minimum_ReturnsMinimum()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_Minimum).ShouldBe("Minimum");
}
[Fact]
public void MapAggregateToColumn_Maximum_ReturnsMaximum()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_Maximum).ShouldBe("Maximum");
}
[Fact]
public void MapAggregateToColumn_Count_ReturnsValueCount()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_Count).ShouldBe("ValueCount");
}
[Fact]
public void MapAggregateToColumn_Start_ReturnsFirst()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_Start).ShouldBe("First");
}
[Fact]
public void MapAggregateToColumn_End_ReturnsLast()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_End).ShouldBe("Last");
}
[Fact]
public void MapAggregateToColumn_StdDev_ReturnsStdDev()
{
HistorianDataSource.MapAggregateToColumn(ObjectIds.AggregateFunction_StandardDeviationPopulation)
.ShouldBe("StdDev");
}
[Fact]
public void MapAggregateToColumn_Unsupported_ReturnsNull()
{
HistorianDataSource.MapAggregateToColumn(new NodeId(99999)).ShouldBeNull();
}
}
}

View File

@@ -0,0 +1,116 @@
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 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);
}
}
}