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>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user