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:
@@ -12,6 +12,7 @@
|
|||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||||
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
||||||
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Driver-instance options for the in-process .NET 10 Galaxy driver. Maps to the
|
||||||
|
/// <c>DriverConfig</c> JSON column on the central config DB. Decomposed into nested
|
||||||
|
/// records so the JSON structure mirrors the runtime shape and operators can target
|
||||||
|
/// individual sections (gateway endpoint, mxaccess client identity, reconnect policy)
|
||||||
|
/// without touching the rest.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Gateway">Connection details for the MxAccess gateway (mxaccessgw repo).</param>
|
||||||
|
/// <param name="MxAccess">MXAccess-specific knobs surfaced through gw — client name, publishing interval, write-user.</param>
|
||||||
|
/// <param name="Repository">Galaxy Repository browse options consumed by the discoverer.</param>
|
||||||
|
/// <param name="Reconnect">Backoff knobs for the in-driver reconnect supervisor (PR 4.5).</param>
|
||||||
|
public sealed record GalaxyDriverOptions(
|
||||||
|
GalaxyGatewayOptions Gateway,
|
||||||
|
GalaxyMxAccessOptions MxAccess,
|
||||||
|
GalaxyRepositoryOptions Repository,
|
||||||
|
GalaxyReconnectOptions Reconnect);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connection details for the MxAccess gateway. <see cref="ApiKeySecretRef"/> resolves
|
||||||
|
/// through the server-side secret store (DPAPI for production, environment override for
|
||||||
|
/// dev) — the API key never appears in cleartext config.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GalaxyGatewayOptions(
|
||||||
|
string Endpoint,
|
||||||
|
string ApiKeySecretRef,
|
||||||
|
bool UseTls = true,
|
||||||
|
string? CaCertificatePath = null,
|
||||||
|
int ConnectTimeoutSeconds = 10,
|
||||||
|
int DefaultCallTimeoutSeconds = 5,
|
||||||
|
int StreamTimeoutSeconds = 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MXAccess-specific knobs the gateway forwards to the worker process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ClientName">
|
||||||
|
/// Wonderware client identity. MUST be unique per OtOpcUa instance — when two instances
|
||||||
|
/// share a name, the older session loses subscription state. Redundancy pairs (decision
|
||||||
|
/// #149) enforce uniqueness via install scripts.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="PublishingIntervalMs">
|
||||||
|
/// Hint forwarded as <c>buffered_update_interval_ms</c> on subscribe; lets the worker
|
||||||
|
/// coalesce updates at the OPC UA publishing cadence rather than every COM tick.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="WriteUserId">
|
||||||
|
/// Reserved for ArchestrA secured-write user mapping; PR 4.3 wires <c>WriteSecured</c>
|
||||||
|
/// routing against this id. 0 = anonymous.
|
||||||
|
/// </param>
|
||||||
|
public sealed record GalaxyMxAccessOptions(
|
||||||
|
string ClientName,
|
||||||
|
int PublishingIntervalMs = 1000,
|
||||||
|
int WriteUserId = 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Galaxy Repository browse-side knobs consumed by PR 4.1's <c>GalaxyDiscoverer</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GalaxyRepositoryOptions(
|
||||||
|
int DiscoverPageSize = 5000,
|
||||||
|
bool WatchDeployEvents = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backoff knobs for the in-driver reconnect supervisor (PR 4.5). Replay-on-session-lost
|
||||||
|
/// calls the gw's <c>ReplaySubscriptions</c> RPC after reconnect rather than re-issuing
|
||||||
|
/// subscribe-bulk for every tag.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GalaxyReconnectOptions(
|
||||||
|
int InitialBackoffMs = 500,
|
||||||
|
int MaxBackoffMs = 30_000,
|
||||||
|
bool ReplayOnSessionLost = true);
|
||||||
102
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
Normal file
102
src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-process .NET 10 Galaxy driver — the v2 replacement for the Galaxy.Host /
|
||||||
|
/// Galaxy.Proxy pair. PR 4.0 ships the project skeleton with <see cref="IDriver"/>
|
||||||
|
/// bodies that wire to a future <c>IGalaxyGatewayClient</c> abstraction. Capability
|
||||||
|
/// interfaces (browse, read, write, subscribe, history routing, host probes) land in
|
||||||
|
/// PRs 4.1–4.7; the wiring sequence keeps every intermediate state buildable so the
|
||||||
|
/// <c>Galaxy:Backend</c> flag (PR 4.W) can flip between legacy-host and mxgateway
|
||||||
|
/// for parity testing.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This driver is registered as a Tier A in-process driver alongside Modbus / S7 / etc.
|
||||||
|
/// The legacy <c>GalaxyProxyDriver</c> (Driver.Galaxy.Proxy) coexists until PR 7.2;
|
||||||
|
/// <see cref="GalaxyDriverFactoryExtensions"/> registers under driver-type name
|
||||||
|
/// "GalaxyMxGateway" so both paths can be live simultaneously during parity testing.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class GalaxyDriver : IDriver, IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _driverInstanceId;
|
||||||
|
private readonly GalaxyDriverOptions _options;
|
||||||
|
private readonly ILogger<GalaxyDriver> _logger;
|
||||||
|
|
||||||
|
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public GalaxyDriver(
|
||||||
|
string driverInstanceId,
|
||||||
|
GalaxyDriverOptions options,
|
||||||
|
ILogger<GalaxyDriver>? logger = null)
|
||||||
|
{
|
||||||
|
_driverInstanceId = !string.IsNullOrWhiteSpace(driverInstanceId)
|
||||||
|
? driverInstanceId
|
||||||
|
: throw new ArgumentException("Driver instance id required.", nameof(driverInstanceId));
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_logger = logger ?? NullLogger<GalaxyDriver>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DriverInstanceId => _driverInstanceId;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName;
|
||||||
|
|
||||||
|
/// <summary>Test-visible options snapshot.</summary>
|
||||||
|
internal GalaxyDriverOptions Options => _options;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
// PR 4.0 skeleton — capability bodies (PRs 4.1-4.7) replace this stub with real
|
||||||
|
// MxGatewayClient session opening. The skeleton keeps the IDriver shape buildable
|
||||||
|
// so the Galaxy:Backend flag (PR 4.W) can register the driver factory now.
|
||||||
|
_logger.LogInformation(
|
||||||
|
"GalaxyDriver {InstanceId} initializing — endpoint={Endpoint} clientName={ClientName} (skeleton; real gateway connect in PR 4.1+)",
|
||||||
|
_driverInstanceId, _options.Gateway.Endpoint, _options.MxAccess.ClientName);
|
||||||
|
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// In-place config reapply. PR 4.5's reconnect supervisor will swap the
|
||||||
|
// gateway-client options under the lock; for the skeleton we just refresh health.
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task ShutdownAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_disposed) return Task.CompletedTask;
|
||||||
|
_logger.LogInformation("GalaxyDriver {InstanceId} shutting down", _driverInstanceId);
|
||||||
|
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DriverHealth GetHealth() => _health;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long GetMemoryFootprint() => 0; // PR 4.4 sets this from SubscriptionRegistry size.
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
// No owned IDisposables until PR 4.2's GalaxyMxSession lands.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static factory registration helper for <see cref="GalaxyDriver"/>. Mirrors
|
||||||
|
/// <c>GalaxyProxyDriverFactoryExtensions</c> / <c>ModbusDriverFactoryExtensions</c>.
|
||||||
|
/// Server's <c>Program.cs</c> calls <see cref="Register"/> once at startup; the driver
|
||||||
|
/// bootstrap pipeline materialises DriverInstance rows whose <c>DriverType</c> matches
|
||||||
|
/// <see cref="DriverTypeName"/> into live <see cref="GalaxyDriver"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The driver-type name <c>"GalaxyMxGateway"</c> is intentionally distinct from the
|
||||||
|
/// legacy proxy's <c>"Galaxy"</c> so both factories can be registered simultaneously
|
||||||
|
/// during parity testing (Phase 5). PR 4.W will add a server-side <c>Galaxy:Backend</c>
|
||||||
|
/// switch that materialises a Galaxy DriverInstance under one or the other type name.
|
||||||
|
/// </remarks>
|
||||||
|
public static class GalaxyDriverFactoryExtensions
|
||||||
|
{
|
||||||
|
public const string DriverTypeName = "GalaxyMxGateway";
|
||||||
|
|
||||||
|
public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(registry);
|
||||||
|
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Convenience for tests + standalone callers.</summary>
|
||||||
|
public static GalaxyDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||||
|
=> CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null);
|
||||||
|
|
||||||
|
public static GalaxyDriver CreateInstance(
|
||||||
|
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||||
|
|
||||||
|
var dto = JsonSerializer.Deserialize<GalaxyDriverConfigDto>(driverConfigJson, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Galaxy driver config for '{driverInstanceId}' deserialised to null");
|
||||||
|
|
||||||
|
var options = new GalaxyDriverOptions(
|
||||||
|
Gateway: new GalaxyGatewayOptions(
|
||||||
|
Endpoint: dto.Gateway?.Endpoint
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Galaxy driver '{driverInstanceId}' missing required Gateway.Endpoint"),
|
||||||
|
ApiKeySecretRef: dto.Gateway.ApiKeySecretRef
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Galaxy driver '{driverInstanceId}' missing required Gateway.ApiKeySecretRef"),
|
||||||
|
UseTls: dto.Gateway.UseTls ?? true,
|
||||||
|
CaCertificatePath: dto.Gateway.CaCertificatePath,
|
||||||
|
ConnectTimeoutSeconds: dto.Gateway.ConnectTimeoutSeconds ?? 10,
|
||||||
|
DefaultCallTimeoutSeconds: dto.Gateway.DefaultCallTimeoutSeconds ?? 5,
|
||||||
|
StreamTimeoutSeconds: dto.Gateway.StreamTimeoutSeconds ?? 0),
|
||||||
|
MxAccess: new GalaxyMxAccessOptions(
|
||||||
|
ClientName: dto.MxAccess?.ClientName
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Galaxy driver '{driverInstanceId}' missing required MxAccess.ClientName"),
|
||||||
|
PublishingIntervalMs: dto.MxAccess.PublishingIntervalMs ?? 1000,
|
||||||
|
WriteUserId: dto.MxAccess.WriteUserId ?? 0),
|
||||||
|
Repository: new GalaxyRepositoryOptions(
|
||||||
|
DiscoverPageSize: dto.Repository?.DiscoverPageSize ?? 5000,
|
||||||
|
WatchDeployEvents: dto.Repository?.WatchDeployEvents ?? true),
|
||||||
|
Reconnect: new GalaxyReconnectOptions(
|
||||||
|
InitialBackoffMs: dto.Reconnect?.InitialBackoffMs ?? 500,
|
||||||
|
MaxBackoffMs: dto.Reconnect?.MaxBackoffMs ?? 30_000,
|
||||||
|
ReplayOnSessionLost: dto.Reconnect?.ReplayOnSessionLost ?? true));
|
||||||
|
|
||||||
|
return new GalaxyDriver(driverInstanceId, options, loggerFactory?.CreateLogger<GalaxyDriver>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
internal sealed class GalaxyDriverConfigDto
|
||||||
|
{
|
||||||
|
public GatewayDto? Gateway { get; init; }
|
||||||
|
public MxAccessDto? MxAccess { get; init; }
|
||||||
|
public RepositoryDto? Repository { get; init; }
|
||||||
|
public ReconnectDto? Reconnect { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class GatewayDto
|
||||||
|
{
|
||||||
|
public string? Endpoint { get; init; }
|
||||||
|
public string? ApiKeySecretRef { get; init; }
|
||||||
|
public bool? UseTls { get; init; }
|
||||||
|
public string? CaCertificatePath { get; init; }
|
||||||
|
public int? ConnectTimeoutSeconds { get; init; }
|
||||||
|
public int? DefaultCallTimeoutSeconds { get; init; }
|
||||||
|
public int? StreamTimeoutSeconds { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MxAccessDto
|
||||||
|
{
|
||||||
|
public string? ClientName { get; init; }
|
||||||
|
public int? PublishingIntervalMs { get; init; }
|
||||||
|
public int? WriteUserId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RepositoryDto
|
||||||
|
{
|
||||||
|
public int? DiscoverPageSize { get; init; }
|
||||||
|
public bool? WatchDeployEvents { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ReconnectDto
|
||||||
|
{
|
||||||
|
public int? InitialBackoffMs { get; init; }
|
||||||
|
public int? MaxBackoffMs { get; init; }
|
||||||
|
public bool? ReplayOnSessionLost { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Galaxy</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-37gx-xxp4-5rgx"/>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-w3x6-4m5h-cxqf"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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