Merge pull request 'Driver-instance bootstrap pipeline (#248) — DriverInstance rows materialise as live IDriver instances' (#196) from phase-7-fu-248-driver-bootstrap into v2
This commit was merged in pull request #196.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ packages/
|
|||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
.local/
|
.local/
|
||||||
|
|
||||||
|
# LiteDB local config cache (Phase 6.1 Stream D — runtime artifact, not source)
|
||||||
|
src/ZB.MOM.WW.OtOpcUa.Server/config_cache.db
|
||||||
|
|||||||
64
src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs
Normal file
64
src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process-singleton registry of <see cref="IDriver"/> factories keyed by
|
||||||
|
/// <c>DriverInstance.DriverType</c> string. Each driver project ships a DI
|
||||||
|
/// extension (e.g. <c>services.AddGalaxyProxyDriverFactory()</c>) that registers
|
||||||
|
/// its factory at startup; the bootstrapper looks up the factory by
|
||||||
|
/// <c>DriverInstance.DriverType</c> + invokes it with the row's
|
||||||
|
/// <c>DriverInstanceId</c> + <c>DriverConfig</c> JSON.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Closes the gap surfaced by task #240 live smoke — DriverInstance rows in
|
||||||
|
/// the central config DB had no path to materialise as registered <see cref="IDriver"/>
|
||||||
|
/// instances. The factory registry is the seam.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class DriverFactoryRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Func<string, string, IDriver>> _factories
|
||||||
|
= new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a factory for <paramref name="driverType"/>. Throws if a factory is
|
||||||
|
/// already registered for that type — drivers are singletons by type-name in
|
||||||
|
/// this process.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="driverType">Matches <c>DriverInstance.DriverType</c>.</param>
|
||||||
|
/// <param name="factory">
|
||||||
|
/// Receives <c>(driverInstanceId, driverConfigJson)</c>; returns a new
|
||||||
|
/// <see cref="IDriver"/>. Must NOT call <see cref="IDriver.InitializeAsync"/>
|
||||||
|
/// itself — the bootstrapper calls it via <see cref="DriverHost.RegisterAsync"/>
|
||||||
|
/// so the host's per-driver retry semantics apply uniformly.
|
||||||
|
/// </param>
|
||||||
|
public void Register(string driverType, Func<string, string, IDriver> factory)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||||
|
ArgumentNullException.ThrowIfNull(factory);
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_factories.ContainsKey(driverType))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"DriverType '{driverType}' factory already registered for this process");
|
||||||
|
_factories[driverType] = factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to look up the factory for <paramref name="driverType"/>. Returns null
|
||||||
|
/// if no driver assembly registered one — bootstrapper logs + skips so a
|
||||||
|
/// missing-assembly deployment doesn't take down the whole server.
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, string, IDriver>? TryGet(string driverType)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||||
|
lock (_lock) return _factories.GetValueOrDefault(driverType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> RegisteredTypes
|
||||||
|
{
|
||||||
|
get { lock (_lock) return [.. _factories.Keys]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static factory registration helper for <see cref="GalaxyProxyDriver"/>. Server's
|
||||||
|
/// Program.cs calls <see cref="Register"/> once at startup; the bootstrapper (task #248)
|
||||||
|
/// then materialises Galaxy DriverInstance rows from the central config DB into live
|
||||||
|
/// driver instances. No dependency on Microsoft.Extensions.DependencyInjection so the
|
||||||
|
/// driver project stays free of DI machinery.
|
||||||
|
/// </summary>
|
||||||
|
public static class GalaxyProxyDriverFactoryExtensions
|
||||||
|
{
|
||||||
|
public const string DriverTypeName = "Galaxy";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register the Galaxy driver factory in the supplied <see cref="DriverFactoryRegistry"/>.
|
||||||
|
/// Throws if 'Galaxy' is already registered — single-instance per process.
|
||||||
|
/// </summary>
|
||||||
|
public static void Register(DriverFactoryRegistry registry)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(registry);
|
||||||
|
registry.Register(DriverTypeName, CreateInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static GalaxyProxyDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||||
|
|
||||||
|
// DriverConfig column is a JSON object that mirrors GalaxyProxyOptions.
|
||||||
|
// Required: PipeName, SharedSecret. Optional: ConnectTimeoutMs (defaults to 10s).
|
||||||
|
// The DriverInstanceId from the row wins over any value in the JSON — the row
|
||||||
|
// is the authoritative identity per the schema's UX_DriverInstance_Generation_LogicalId.
|
||||||
|
using var doc = JsonDocument.Parse(driverConfigJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
string pipeName = root.TryGetProperty("PipeName", out var p) && p.ValueKind == JsonValueKind.String
|
||||||
|
? p.GetString()!
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
$"GalaxyProxyDriver config for '{driverInstanceId}' missing required PipeName");
|
||||||
|
string sharedSecret = root.TryGetProperty("SharedSecret", out var s) && s.ValueKind == JsonValueKind.String
|
||||||
|
? s.GetString()!
|
||||||
|
: throw new InvalidOperationException(
|
||||||
|
$"GalaxyProxyDriver config for '{driverInstanceId}' missing required SharedSecret");
|
||||||
|
var connectTimeout = root.TryGetProperty("ConnectTimeoutMs", out var t) && t.ValueKind == JsonValueKind.Number
|
||||||
|
? TimeSpan.FromMilliseconds(t.GetInt32())
|
||||||
|
: TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
return new GalaxyProxyDriver(new GalaxyProxyOptions
|
||||||
|
{
|
||||||
|
DriverInstanceId = driverInstanceId,
|
||||||
|
PipeName = pipeName,
|
||||||
|
SharedSecret = sharedSecret,
|
||||||
|
ConnectTimeout = connectTimeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
<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"/>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
88
src/ZB.MOM.WW.OtOpcUa.Server/DriverInstanceBootstrapper.cs
Normal file
88
src/ZB.MOM.WW.OtOpcUa.Server/DriverInstanceBootstrapper.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Task #248 — bridges the gap surfaced by the Phase 7 live smoke (#240) where
|
||||||
|
/// <c>DriverInstance</c> rows in the central config DB had no path to materialise
|
||||||
|
/// as live <see cref="Core.Abstractions.IDriver"/> instances in <see cref="DriverHost"/>.
|
||||||
|
/// Called from <c>OpcUaServerService.ExecuteAsync</c> after the bootstrap loads
|
||||||
|
/// the published generation, before address-space build.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Per row: looks up the <c>DriverType</c> string in
|
||||||
|
/// <see cref="DriverFactoryRegistry"/>, calls the factory with the row's
|
||||||
|
/// <c>DriverInstanceId</c> + <c>DriverConfig</c> JSON to construct an
|
||||||
|
/// <see cref="Core.Abstractions.IDriver"/>, then registers via
|
||||||
|
/// <see cref="DriverHost.RegisterAsync"/> which invokes <c>InitializeAsync</c>
|
||||||
|
/// under the host's lifecycle semantics.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Unknown <c>DriverType</c> = factory not registered = log a warning and skip.
|
||||||
|
/// Per plan decision #12 (driver isolation), failure to construct or initialize
|
||||||
|
/// one driver doesn't prevent the rest from coming up — the Server keeps serving
|
||||||
|
/// the others' subtrees + the operator can fix the misconfigured row + republish
|
||||||
|
/// to retry.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class DriverInstanceBootstrapper(
|
||||||
|
DriverFactoryRegistry factories,
|
||||||
|
DriverHost driverHost,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<DriverInstanceBootstrapper> logger)
|
||||||
|
{
|
||||||
|
public async Task<int> RegisterDriversFromGenerationAsync(long generationId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
|
||||||
|
|
||||||
|
var rows = await db.DriverInstances.AsNoTracking()
|
||||||
|
.Where(d => d.GenerationId == generationId && d.Enabled)
|
||||||
|
.ToListAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var registered = 0;
|
||||||
|
var skippedUnknownType = 0;
|
||||||
|
var failedInit = 0;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var factory = factories.TryGet(row.DriverType);
|
||||||
|
if (factory is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"DriverInstance {Id} skipped — DriverType '{Type}' has no registered factory (known: {Known})",
|
||||||
|
row.DriverInstanceId, row.DriverType, string.Join(",", factories.RegisteredTypes));
|
||||||
|
skippedUnknownType++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var driver = factory(row.DriverInstanceId, row.DriverConfig);
|
||||||
|
await driverHost.RegisterAsync(driver, row.DriverConfig, ct).ConfigureAwait(false);
|
||||||
|
registered++;
|
||||||
|
logger.LogInformation(
|
||||||
|
"DriverInstance {Id} ({Type}) registered + initialized", row.DriverInstanceId, row.DriverType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Plan decision #12 — driver isolation. Log + continue so one bad row
|
||||||
|
// doesn't deny the OPC UA endpoint to the rest of the fleet.
|
||||||
|
logger.LogError(ex,
|
||||||
|
"DriverInstance {Id} ({Type}) failed to initialize — driver state will reflect Faulted; operator can republish to retry",
|
||||||
|
row.DriverInstanceId, row.DriverType);
|
||||||
|
failedInit++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"DriverInstanceBootstrapper: gen={Gen} registered={Registered} skippedUnknownType={Skipped} failedInit={Failed}",
|
||||||
|
generationId, registered, skippedUnknownType, failedInit);
|
||||||
|
return registered;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ public sealed class OpcUaServerService(
|
|||||||
DriverHost driverHost,
|
DriverHost driverHost,
|
||||||
OpcUaApplicationHost applicationHost,
|
OpcUaApplicationHost applicationHost,
|
||||||
DriverEquipmentContentRegistry equipmentContentRegistry,
|
DriverEquipmentContentRegistry equipmentContentRegistry,
|
||||||
|
DriverInstanceBootstrapper driverBootstrapper,
|
||||||
Phase7Composer phase7Composer,
|
Phase7Composer phase7Composer,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<OpcUaServerService> logger) : BackgroundService
|
ILogger<OpcUaServerService> logger) : BackgroundService
|
||||||
@@ -37,6 +38,13 @@ public sealed class OpcUaServerService(
|
|||||||
// address space until the first publish, then the registry fills on next restart.
|
// address space until the first publish, then the registry fills on next restart.
|
||||||
if (result.GenerationId is { } gen)
|
if (result.GenerationId is { } gen)
|
||||||
{
|
{
|
||||||
|
// Task #248 — register IDriver instances from the published DriverInstance
|
||||||
|
// rows BEFORE the equipment-content load + Phase 7 compose, so the rest of
|
||||||
|
// the pipeline sees a populated DriverHost. Without this step Phase 7's
|
||||||
|
// CachedTagUpstreamSource has no upstream feed + virtual-tag scripts read
|
||||||
|
// BadNodeIdUnknown for every tag path (gap surfaced by task #240 smoke).
|
||||||
|
await driverBootstrapper.RegisterDriversFromGenerationAsync(gen, stoppingToken);
|
||||||
|
|
||||||
await PopulateEquipmentContentAsync(gen, stoppingToken);
|
await PopulateEquipmentContentAsync(gen, stoppingToken);
|
||||||
|
|
||||||
// Phase 7 follow-up #246 — load Script + VirtualTag + ScriptedAlarm rows,
|
// Phase 7 follow-up #246 — load Script + VirtualTag + ScriptedAlarm rows,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy;
|
||||||
using ZB.MOM.WW.OtOpcUa.Server;
|
using ZB.MOM.WW.OtOpcUa.Server;
|
||||||
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||||
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
using ZB.MOM.WW.OtOpcUa.Server.Phase7;
|
||||||
@@ -89,6 +90,18 @@ builder.Services.AddSingleton<ILocalConfigCache>(_ => new LiteDbConfigCache(opti
|
|||||||
builder.Services.AddSingleton<DriverHost>();
|
builder.Services.AddSingleton<DriverHost>();
|
||||||
builder.Services.AddSingleton<NodeBootstrap>();
|
builder.Services.AddSingleton<NodeBootstrap>();
|
||||||
|
|
||||||
|
// Task #248 — driver-instance bootstrap pipeline. DriverFactoryRegistry is the
|
||||||
|
// type-name → factory map; each driver project's static Register call pre-loads
|
||||||
|
// its factory so the bootstrapper can materialise DriverInstance rows from the
|
||||||
|
// central DB into live IDriver instances.
|
||||||
|
builder.Services.AddSingleton<DriverFactoryRegistry>(_ =>
|
||||||
|
{
|
||||||
|
var registry = new DriverFactoryRegistry();
|
||||||
|
GalaxyProxyDriverFactoryExtensions.Register(registry);
|
||||||
|
return registry;
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<DriverInstanceBootstrapper>();
|
||||||
|
|
||||||
// ADR-001 Option A wiring — the registry is the handoff between OpcUaServerService's
|
// ADR-001 Option A wiring — the registry is the handoff between OpcUaServerService's
|
||||||
// bootstrap-time population pass + OpcUaApplicationHost's StartAsync walker invocation.
|
// bootstrap-time population pass + OpcUaApplicationHost's StartAsync walker invocation.
|
||||||
// DriverEquipmentContentRegistry.Get is the equipmentContentLookup delegate that PR #155
|
// DriverEquipmentContentRegistry.Get is the equipmentContentLookup delegate that PR #155
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.VirtualTags\ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.VirtualTags\ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"/>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Analyzers\ZB.MOM.WW.OtOpcUa.Analyzers.csproj"
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Analyzers\ZB.MOM.WW.OtOpcUa.Analyzers.csproj"
|
||||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Task #248 — covers the <see cref="DriverFactoryRegistry"/> contract that
|
||||||
|
/// <see cref="DriverInstanceBootstrapper"/> consumes.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class DriverFactoryRegistryTests
|
||||||
|
{
|
||||||
|
private static IDriver FakeDriver(string id, string config) => new FakeIDriver(id);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_then_TryGet_returns_factory()
|
||||||
|
{
|
||||||
|
var r = new DriverFactoryRegistry();
|
||||||
|
r.Register("MyDriver", FakeDriver);
|
||||||
|
|
||||||
|
r.TryGet("MyDriver").ShouldNotBeNull();
|
||||||
|
r.TryGet("Nope").ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_is_case_insensitive()
|
||||||
|
{
|
||||||
|
var r = new DriverFactoryRegistry();
|
||||||
|
r.Register("Galaxy", FakeDriver);
|
||||||
|
r.TryGet("galaxy").ShouldNotBeNull();
|
||||||
|
r.TryGet("GALAXY").ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_duplicate_type_throws()
|
||||||
|
{
|
||||||
|
var r = new DriverFactoryRegistry();
|
||||||
|
r.Register("Galaxy", FakeDriver);
|
||||||
|
Should.Throw<InvalidOperationException>(() => r.Register("Galaxy", FakeDriver));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_null_args_rejected()
|
||||||
|
{
|
||||||
|
var r = new DriverFactoryRegistry();
|
||||||
|
Should.Throw<ArgumentException>(() => r.Register("", FakeDriver));
|
||||||
|
Should.Throw<ArgumentNullException>(() => r.Register("X", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegisteredTypes_returns_snapshot()
|
||||||
|
{
|
||||||
|
var r = new DriverFactoryRegistry();
|
||||||
|
r.Register("A", FakeDriver);
|
||||||
|
r.Register("B", FakeDriver);
|
||||||
|
r.RegisteredTypes.ShouldContain("A");
|
||||||
|
r.RegisteredTypes.ShouldContain("B");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeIDriver(string id) : IDriver
|
||||||
|
{
|
||||||
|
public string DriverInstanceId => id;
|
||||||
|
public string DriverType => "Fake";
|
||||||
|
public Task InitializeAsync(string _, CancellationToken __) => Task.CompletedTask;
|
||||||
|
public Task ReinitializeAsync(string _, CancellationToken __) => Task.CompletedTask;
|
||||||
|
public Task ShutdownAsync(CancellationToken _) => Task.CompletedTask;
|
||||||
|
public Task FlushOptionalCachesAsync(CancellationToken _) => Task.CompletedTask;
|
||||||
|
public DriverHealth GetHealth() => new(DriverState.Healthy, null, null);
|
||||||
|
public long GetMemoryFootprint() => 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user