Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests/GalaxyRepositoryLiveSmokeTests.cs
Joseph Doherty 549cd36662 Phase 2 — port GalaxyRepository to Galaxy.Host + DbBackedGalaxyBackend, smoke-tested against live ZB. Real Galaxy gobject hierarchy + dynamic attributes now flow through the IPC contract end-to-end without any MXAccess code involvement, so the OPC UA address-space build (Stream C.4 acceptance) becomes parity-testable today even before the COM client port lands. Backend/Galaxy/GalaxyRepository.cs is a byte-for-byte port of v1 GalaxyRepositoryService's HierarchySql + AttributesSql (the two SQL bodies, both ~50 lines of recursive CTE template-chain + deployed_package_chain logic, are identical to v1 so the row set is verifiably the same — extended-attributes + scope-filter queries from v1 are intentionally not ported yet, they're refinements not on the Phase 2 critical path); plus TestConnectionAsync (SELECT 1) and GetLastDeployTimeAsync (SELECT time_of_last_deploy FROM galaxy) for the ChangeDetection deploy-watermark path. Backend/Galaxy/GalaxyRepositoryOptions defaults to localhost ZB Integrated Security; runtime override comes from DriverConfig.Database section per plan.md §"Galaxy DriverConfig". Backend/Galaxy/GalaxyHierarchyRow + GalaxyAttributeRow are the row-shape DTOs (no required modifier — net48 lacks RequiredMemberAttribute and we'd need a polyfill shim like the existing IsExternalInit one; default-string init is simpler). System.Data.SqlClient 4.9.0 added (the same package the v1 Host uses; net48-compatible). Backend/DbBackedGalaxyBackend wraps the repository: DiscoverAsync builds a real DiscoverHierarchyResponse (groups attributes by gobject, resolves parent-by-tagname, maps category_id → human-readable template-category name mirroring v1 AlarmObjectFilter); ReadValuesAsync/WriteValuesAsync/HistoryReadAsync still surface "MXAccess code lift pending (Phase 2 Task B.1)" because runtime data values genuinely need the COM client; OpenSession/CloseSession/Subscribe/Unsubscribe/AlarmSubscribe/AlarmAck/Recycle return success without backend work (subscription ID is a synthetic counter for now). Live smoke tests (GalaxyRepositoryLiveSmokeTests) skip when localhost ZB is unreachable; when present they verify (1) TestConnection returns true, (2) GetHierarchy returns at least one deployed gobject with a non-empty TagName, (3) GetAttributes returns rows with FullTagReference matching the "tag.attribute" shape, (4) GetLastDeployTime returns a value, (5) DbBackedBackend.DiscoverAsync returns at least one gobject with attributes and a populated TemplateCategory. All 5 pass against the local Galaxy. Full solution 957 pass / 1 pre-existing Phase 0 baseline; the 494 v1 IntegrationTests + 6 v1 IntegrationTests-net48 tests still pass — legacy OtOpcUa.Host untouched. Remaining for the Phase 2 exit gate is the MXAccess COM client port itself (the v1 MxAccessClient partials + IMxProxy abstraction + StaPump-based Connect/Subscribe/Read/Write semantics) — Discover is now solved in DB-backed form, so the lift can focus exclusively on the runtime data-plane.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:14:09 -04:00

101 lines
3.8 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
{
/// <summary>
/// Live smoke against the Galaxy <c>ZB</c> repository. Skipped when ZB is unreachable so
/// CI / dev boxes without an AVEVA install still pass. Exercises the ported
/// <see cref="GalaxyRepository"/> + <see cref="DbBackedGalaxyBackend"/> against the same
/// SQL the v1 Host uses, proving the lift is byte-for-byte equivalent at the
/// <c>DiscoverHierarchyResponse</c> shape.
/// </summary>
[Trait("Category", "LiveGalaxy")]
public sealed class GalaxyRepositoryLiveSmokeTests
{
private static GalaxyRepositoryOptions DevZbOptions() => new()
{
ConnectionString =
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;Connect Timeout=2;",
CommandTimeoutSeconds = 10,
};
private static async Task<bool> ZbReachableAsync()
{
try
{
var repo = new GalaxyRepository(DevZbOptions());
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
return await repo.TestConnectionAsync(cts.Token);
}
catch { return false; }
}
[Fact]
public async Task TestConnection_returns_true_against_live_ZB()
{
if (!await ZbReachableAsync()) return;
var repo = new GalaxyRepository(DevZbOptions());
(await repo.TestConnectionAsync()).ShouldBeTrue();
}
[Fact]
public async Task GetHierarchy_returns_at_least_one_deployed_gobject()
{
if (!await ZbReachableAsync()) return;
var repo = new GalaxyRepository(DevZbOptions());
var rows = await repo.GetHierarchyAsync();
rows.Count.ShouldBeGreaterThan(0,
"the dev Galaxy has at least the WinPlatform + AppEngine deployed");
rows.ShouldAllBe(r => !string.IsNullOrEmpty(r.TagName));
}
[Fact]
public async Task GetAttributes_returns_attributes_for_deployed_objects()
{
if (!await ZbReachableAsync()) return;
var repo = new GalaxyRepository(DevZbOptions());
var attrs = await repo.GetAttributesAsync();
attrs.Count.ShouldBeGreaterThan(0);
attrs.ShouldAllBe(a => !string.IsNullOrEmpty(a.FullTagReference) && a.FullTagReference.Contains("."));
}
[Fact]
public async Task GetLastDeployTime_returns_a_value()
{
if (!await ZbReachableAsync()) return;
var repo = new GalaxyRepository(DevZbOptions());
var ts = await repo.GetLastDeployTimeAsync();
ts.ShouldNotBeNull();
}
[Fact]
public async Task DbBackedBackend_DiscoverAsync_returns_objects_with_attributes_and_categories()
{
if (!await ZbReachableAsync()) return;
var backend = new DbBackedGalaxyBackend(new GalaxyRepository(DevZbOptions()));
var resp = await backend.DiscoverAsync(new DiscoverHierarchyRequest { SessionId = 1 }, CancellationToken.None);
resp.Success.ShouldBeTrue(resp.Error);
resp.Objects.Length.ShouldBeGreaterThan(0);
var firstWithAttrs = System.Linq.Enumerable.FirstOrDefault(resp.Objects, o => o.Attributes.Length > 0);
firstWithAttrs.ShouldNotBeNull("at least one gobject in the dev Galaxy carries dynamic attributes");
firstWithAttrs!.TemplateCategory.ShouldNotBeNullOrEmpty();
}
}
}