Files
lmxopcua/tests/ZB.MOM.WW.LmxOpcUa.Tests/Integration/IncrementalSyncTests.cs
Joseph Doherty 3c326e2d45 Replace full address space rebuild with incremental subtree sync
On Galaxy deploy changes, only the affected gobject subtrees are torn down
and rebuilt instead of destroying the entire address space. Unchanged nodes,
subscriptions, and alarm tracking continue uninterrupted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:23:11 -04:00

173 lines
7.0 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
public class IncrementalSyncTests
{
[Fact]
public async Task Sync_AddObject_NewNodeAppears()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Verify initial state
var children = await client.BrowseAsync(client.MakeNodeId("TestArea"));
children.Select(c => c.Name).ShouldContain("TestMachine_001");
children.Select(c => c.Name).ShouldNotContain("NewObject");
// Add a new object
fixture.GalaxyRepository!.Hierarchy.Add(new GalaxyObjectInfo
{
GobjectId = 100, TagName = "NewObject_001", ContainedName = "NewObject",
BrowseName = "NewObject", ParentGobjectId = 2, IsArea = false
});
fixture.GalaxyRepository.Attributes.Add(new GalaxyAttributeInfo
{
GobjectId = 100, TagName = "NewObject_001", AttributeName = "Status",
FullTagReference = "NewObject_001.Status", MxDataType = 5
});
fixture.Service.TriggerRebuild();
await Task.Delay(1000);
// Reconnect in case session was disrupted during rebuild
using var client2 = new OpcUaTestClient();
await client2.ConnectAsync(fixture.EndpointUrl);
// New object should appear when browsing parent
children = await client2.BrowseAsync(client2.MakeNodeId("TestArea"));
children.Select(c => c.Name).ShouldContain("NewObject",
$"Browse returned: [{string.Join(", ", children.Select(c => c.Name))}]");
// Original object should still be there
children.Select(c => c.Name).ShouldContain("TestMachine_001");
}
finally { await fixture.DisposeAsync(); }
}
[Fact]
public async Task Sync_RemoveObject_NodeDisappears()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Verify MESReceiver exists
var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
children.Select(c => c.Name).ShouldContain("MESReceiver");
// Remove MESReceiver (gobject_id 5) and its attributes
fixture.GalaxyRepository!.Hierarchy.RemoveAll(h => h.GobjectId == 5);
fixture.GalaxyRepository.Attributes.RemoveAll(a => a.GobjectId == 5);
fixture.Service.TriggerRebuild();
await Task.Delay(500);
// MESReceiver should be gone
children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
children.Select(c => c.Name).ShouldNotContain("MESReceiver");
// DelmiaReceiver should still be there
children.Select(c => c.Name).ShouldContain("DelmiaReceiver");
}
finally { await fixture.DisposeAsync(); }
}
[Fact]
public async Task Sync_AddAttribute_NewVariableAppears()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Add a new attribute to TestMachine_001
fixture.GalaxyRepository!.Attributes.Add(new GalaxyAttributeInfo
{
GobjectId = 3, TagName = "TestMachine_001", AttributeName = "NewAttr",
FullTagReference = "TestMachine_001.NewAttr", MxDataType = 5
});
fixture.Service.TriggerRebuild();
await Task.Delay(500);
var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
children.Select(c => c.Name).ShouldContain("NewAttr");
children.Select(c => c.Name).ShouldContain("MachineID");
}
finally { await fixture.DisposeAsync(); }
}
[Fact]
public async Task Sync_UnchangedObject_SubscriptionSurvives()
{
var mxClient = new FakeMxAccessClient();
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient);
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Subscribe to MachineID on TestMachine_001
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
var (sub, item) = await client.SubscribeAsync(nodeId, 250);
await Task.Delay(500);
// Modify a DIFFERENT object (MESReceiver) — TestMachine_001 should be unaffected
var mesAttr = fixture.GalaxyRepository!.Attributes
.First(a => a.GobjectId == 5 && a.AttributeName == "MoveInBatchID");
mesAttr.SecurityClassification = 2; // change something
fixture.Service.TriggerRebuild();
await Task.Delay(500);
// Push a value change through MXAccess — subscription should still deliver
mxClient.SimulateDataChange("TestMachine_001.MachineID", Vtq.Good("UPDATED"));
await Task.Delay(1000);
var lastValue = (item.LastValue as MonitoredItemNotification)?.Value?.Value;
lastValue.ShouldBe("UPDATED");
}
finally { await fixture.DisposeAsync(); }
}
[Fact]
public async Task Sync_NoChanges_NothingHappens()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
await fixture.InitializeAsync();
try
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
// Trigger rebuild with no changes
fixture.Service.TriggerRebuild();
await Task.Delay(500);
// Everything should still work
var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
children.Select(c => c.Name).ShouldContain("MachineID");
}
finally { await fixture.DisposeAsync(); }
}
}
}