fix(drivers): serialize driver-config enums as strings in AdminUI pages + probes
AdminUI driver-instance pages serialized enum config fields (S7 CpuType, Modbus DataType/Region, AbCip PlcFamily, ...) as JSON *numbers* because each page's _jsonOpts lacked a JsonStringEnumConverter. The driver factories, however, deserialize into string-typed DTOs (+ lenient ParseEnum) and throw when binding a JSON number to a string? — so an AdminUI-authored config containing any enum field produced a blob the driver could not parse, faulting the driver on deploy. Proven end-to-end for S7 and Modbus; latent for AbCip/AbLegacy/TwinCAT/FOCAS/Galaxy/Historian. Only OpcUaClient was safe (its factory + probe already carried the converter). Add JsonStringEnumConverter to all 9 driver-instance pages' _jsonOpts and the 8 missing driver probes' _opts (factories unchanged — already string-via- ParseEnum; strictly more permissive, also lets pages load hand-seeded string-enum configs back into the form). Also fix DriverProbeHandshakeE2eTests.AbCip_Green_AgainstSim to probe a real sim tag (TestDINT) — the no-tags @raw_cpu_type fallback is rejected by the ab_server sim with ErrorBadParam (a real ControlLogix returns ErrorNotFound, which the probe treats as reachable; hardware-gated follow-up). Tests: reflection guard over all driver pages' _jsonOpts (AdminUI.Tests); factory round-trip + numeric-form-throws guards for S7 and Modbus. Found by running the never-before-run FB-9/FB-10 live verifies.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression guard for the systemic driver-config enum-serialization bug found 2026-06-19.
|
||||
/// Every <c>*DriverPage</c> serialised enum config fields (e.g. S7 <c>CpuType</c>, Modbus
|
||||
/// <c>DataType</c>/<c>Region</c>, AbCip <c>PlcFamily</c>) as JSON <em>numbers</em> because its
|
||||
/// private static <c>_jsonOpts</c> had no <see cref="JsonStringEnumConverter"/>. The driver
|
||||
/// factories, however, deserialise into string-typed DTOs (+ lenient <c>ParseEnum</c>) and
|
||||
/// <em>throw</em> when binding a JSON number to a <c>string?</c> — so an AdminUI-authored
|
||||
/// config that contained any enum field produced a blob the driver could not parse, faulting
|
||||
/// the driver on deploy. The fix makes every page serialise enums as strings (matching the
|
||||
/// factory + the long-correct OpcUaClient template). This test fails if any driver page loses
|
||||
/// its string-enum converter again.
|
||||
/// </summary>
|
||||
public sealed class DriverPageJsonConverterTests
|
||||
{
|
||||
/// <summary>Every concrete <c>*DriverPage</c> in the AdminUI assembly that declares a
|
||||
/// <c>_jsonOpts</c> config serializer.</summary>
|
||||
private static IReadOnlyList<Type> DriverPageTypes { get; } =
|
||||
typeof(S7DriverPage).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverPage", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
.Where(t => t.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static) is not null)
|
||||
.OrderBy(t => t.Name)
|
||||
.ToList();
|
||||
|
||||
/// <summary>xUnit theory source over the driver-page types discovered by reflection.</summary>
|
||||
public static TheoryData<Type> DriverPagesWithJsonOpts()
|
||||
{
|
||||
var data = new TheoryData<Type>();
|
||||
foreach (var t in DriverPageTypes)
|
||||
data.Add(t);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Verifies every driver page's config serializer registers a string-enum converter so
|
||||
/// enum config fields round-trip as strings (and the driver factory can parse the result).</summary>
|
||||
/// <param name="pageType">A driver page component type discovered by reflection.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(DriverPagesWithJsonOpts))]
|
||||
public void Driver_page_json_options_register_string_enum_converter(Type pageType)
|
||||
{
|
||||
var field = pageType.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var opts = (JsonSerializerOptions)field!.GetValue(null)!;
|
||||
opts.Converters.OfType<JsonStringEnumConverter>().ShouldNotBeEmpty(
|
||||
$"{pageType.Name}._jsonOpts must register a JsonStringEnumConverter; otherwise AdminUI-authored " +
|
||||
"enum config fields serialise as numbers and the string-typed driver factory throws on parse.");
|
||||
}
|
||||
|
||||
/// <summary>Sanity-check that reflection actually discovered the full driver-page fleet (so a
|
||||
/// rename/move can't silently shrink the guarded set to zero).</summary>
|
||||
[Fact]
|
||||
public void All_known_driver_pages_are_covered()
|
||||
=> DriverPageTypes.Count.ShouldBeGreaterThanOrEqualTo(9);
|
||||
}
|
||||
+9
-1
@@ -172,8 +172,16 @@ public sealed class DriverProbeHandshakeE2eTests
|
||||
public async Task AbCip_Green_AgainstSim()
|
||||
{
|
||||
SkipUnless(DockerHost, AbCipPort);
|
||||
// Probe an explicit tag that the ab_server ControlLogix sim actually defines
|
||||
// (`TestDINT:DINT[1]`). The no-tags fallback (`@raw_cpu_type`) is NOT exercised here:
|
||||
// ab_server answers an unknown/unsupported tag with libplctag ErrorBadParam (a REAL
|
||||
// ControlLogix instead returns ErrorNotFound, which the probe classifies as
|
||||
// reachable). Whether the `@raw_cpu_type` system-tag fallback is valid on a real
|
||||
// ControlLogix is a hardware-gated follow-up (AbCipDriverOptions.cs flags it deferred).
|
||||
var result = await new AbCipDriverProbe().ProbeAsync(
|
||||
$"{{\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]}}", Timeout, Ct);
|
||||
$"{{\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]," +
|
||||
$"\"Tags\":[{{\"DeviceHostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\",\"TagPath\":\"TestDINT\"}}]}}",
|
||||
Timeout, Ct);
|
||||
result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
|
||||
result.Message!.ShouldContain("CIP session OK");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user