PR 4.0 — Driver.Galaxy project skeleton + factory
New in-process .NET 10 driver project at src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/. The Tier-A replacement for Driver.Galaxy.Host + Driver.Galaxy.Proxy. PR 4.0 ships only the IDriver shape + factory + options; capability bodies (browse, read, write, subscribe, deploy-watch, host probes) land in PRs 4.1–4.7. Files: - Driver.Galaxy.csproj — net10 x64, AnyCPU+x64 platforms, references Core.Abstractions + Core. No MxGatewayClient ProjectReference yet — that comes in PR 4.2 once the gw NuGet package is wired (the user is shipping mxaccessgw on a parallel track). - Config/GalaxyDriverOptions.cs — nested record hierarchy (Gateway/MxAccess/Repository/Reconnect) mirroring the JSON shape spelled out in lmx_mxgw_impl.md PR 4.0 acceptance section. - GalaxyDriver.cs — minimal IDriver impl. Initialize/Shutdown toggle DriverHealth between Healthy/Unknown; Reinitialize bumps the timestamp; GetMemoryFootprint=0 (PR 4.4 wires SubscriptionRegistry size); FlushOptionalCachesAsync no-op. Logs intent on lifecycle calls so partial deployments are diagnosable. - GalaxyDriverFactoryExtensions.cs — JSON parser, default fill-ins, validation throw on missing required fields. Driver type name "GalaxyMxGateway" intentionally distinct from legacy "Galaxy" so both factories coexist during parity testing (Phase 5). PR 4.W's Galaxy:Backend switch picks one or the other. Tests: - 10 tests in Driver.Galaxy.Tests covering minimal-config defaults, full override path, three required-field error cases, factory registration via DriverFactoryRegistry.TryGet, lifecycle health transitions (Init → Shutdown → Reinit), Dispose idempotency, and post-disposal ObjectDisposedException. slnx: registers the new Driver.Galaxy + Driver.Galaxy.Tests projects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Smoke tests for the PR 4.0 driver skeleton. The IDriver shape, factory parsing,
|
||||
/// and lifecycle methods land in PR 4.0; capability bodies (browse / read / write /
|
||||
/// subscribe / health forwarder / probe watcher) are tested in PRs 4.1–4.7 each
|
||||
/// against their own seam.
|
||||
/// </summary>
|
||||
public sealed class GalaxyDriverFactoryTests
|
||||
{
|
||||
private const string MinimalConfig = """
|
||||
{
|
||||
"Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "galaxy:apiKey" },
|
||||
"MxAccess": { "ClientName": "OtOpcUa-A" }
|
||||
}
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_ParsesMinimalConfig_AndAppliesDefaults()
|
||||
{
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-instance-a", MinimalConfig);
|
||||
|
||||
driver.DriverInstanceId.ShouldBe("galaxy-instance-a");
|
||||
driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
|
||||
driver.Options.Gateway.Endpoint.ShouldBe("https://mxgw.test:5001");
|
||||
driver.Options.Gateway.ApiKeySecretRef.ShouldBe("galaxy:apiKey");
|
||||
driver.Options.Gateway.UseTls.ShouldBeTrue();
|
||||
driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(10);
|
||||
driver.Options.MxAccess.ClientName.ShouldBe("OtOpcUa-A");
|
||||
driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(1000);
|
||||
driver.Options.Repository.DiscoverPageSize.ShouldBe(5000);
|
||||
driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_OverridesDefaults_FromFullConfig()
|
||||
{
|
||||
const string fullConfig = """
|
||||
{
|
||||
"Gateway": {
|
||||
"Endpoint": "https://mxgw.prod:5001",
|
||||
"ApiKeySecretRef": "secret:abc",
|
||||
"UseTls": false,
|
||||
"CaCertificatePath": "C:/certs/ca.crt",
|
||||
"ConnectTimeoutSeconds": 5,
|
||||
"DefaultCallTimeoutSeconds": 3,
|
||||
"StreamTimeoutSeconds": 60
|
||||
},
|
||||
"MxAccess": {
|
||||
"ClientName": "OtOpcUa-Prod",
|
||||
"PublishingIntervalMs": 250,
|
||||
"WriteUserId": 17
|
||||
},
|
||||
"Repository": { "DiscoverPageSize": 1000, "WatchDeployEvents": false },
|
||||
"Reconnect": { "InitialBackoffMs": 100, "MaxBackoffMs": 5000, "ReplayOnSessionLost": false }
|
||||
}
|
||||
""";
|
||||
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-prod", fullConfig);
|
||||
|
||||
driver.Options.Gateway.UseTls.ShouldBeFalse();
|
||||
driver.Options.Gateway.CaCertificatePath.ShouldBe("C:/certs/ca.crt");
|
||||
driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(5);
|
||||
driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(250);
|
||||
driver.Options.MxAccess.WriteUserId.ShouldBe(17);
|
||||
driver.Options.Repository.DiscoverPageSize.ShouldBe(1000);
|
||||
driver.Options.Repository.WatchDeployEvents.ShouldBeFalse();
|
||||
driver.Options.Reconnect.InitialBackoffMs.ShouldBe(100);
|
||||
driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_MissingEndpoint_Throws()
|
||||
{
|
||||
const string bad = """{"Gateway":{"ApiKeySecretRef":"x"},"MxAccess":{"ClientName":"y"}}""";
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("Gateway.Endpoint");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_MissingApiKey_Throws()
|
||||
{
|
||||
const string bad = """{"Gateway":{"Endpoint":"x"},"MxAccess":{"ClientName":"y"}}""";
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("ApiKeySecretRef");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInstance_MissingClientName_Throws()
|
||||
{
|
||||
const string bad = """{"Gateway":{"Endpoint":"x","ApiKeySecretRef":"y"}}""";
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("MxAccess.ClientName");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_AddsFactoryToRegistry()
|
||||
{
|
||||
var registry = new DriverFactoryRegistry();
|
||||
GalaxyDriverFactoryExtensions.Register(registry);
|
||||
|
||||
registry.RegisteredTypes.ShouldContain(GalaxyDriverFactoryExtensions.DriverTypeName);
|
||||
|
||||
var factory = registry.TryGet(GalaxyDriverFactoryExtensions.DriverTypeName);
|
||||
factory.ShouldNotBeNull();
|
||||
|
||||
var driver = factory!.Invoke("galaxy-x", MinimalConfig);
|
||||
driver.ShouldNotBeNull();
|
||||
driver.DriverInstanceId.ShouldBe("galaxy-x");
|
||||
driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DriverLifecycle_InitializeShutdown_ToggleHealth()
|
||||
{
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
|
||||
driver.GetHealth().State.ShouldBe(DriverState.Unknown);
|
||||
|
||||
await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
|
||||
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
|
||||
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
driver.GetHealth().State.ShouldBe(DriverState.Unknown);
|
||||
|
||||
driver.GetMemoryFootprint().ShouldBe(0);
|
||||
await driver.FlushOptionalCachesAsync(CancellationToken.None); // no-op shouldn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReinitializeAsync_RefreshesHealth()
|
||||
{
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
|
||||
await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
|
||||
var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value;
|
||||
|
||||
// Force a measurable clock delta so the comparison is stable on fast machines.
|
||||
await Task.Delay(20);
|
||||
await driver.ReinitializeAsync(MinimalConfig, CancellationToken.None);
|
||||
|
||||
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_IsIdempotent_AndShutdownAfterDisposeIsHarmless()
|
||||
{
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
|
||||
driver.Dispose();
|
||||
Should.NotThrow(() => driver.Dispose());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitializeAfterDispose_Throws()
|
||||
{
|
||||
var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
|
||||
driver.Dispose();
|
||||
await Should.ThrowAsync<ObjectDisposedException>(() =>
|
||||
driver.InitializeAsync(MinimalConfig, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user