Merge b647-opcua-nskind: retire OpcUaClient TargetNamespaceKind/UnsMappingTable (#648)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -218,7 +218,7 @@ DECLARE @OpcPlcAlarmSourceNodeId nvarchar(400) = N''; -- e.g. N'ns=3;s=<source
|
|||||||
IF NOT EXISTS (SELECT 1 FROM dbo.DriverInstance WHERE DriverInstanceId = 'MAIN-opcua')
|
IF NOT EXISTS (SELECT 1 FROM dbo.DriverInstance WHERE DriverInstanceId = 'MAIN-opcua')
|
||||||
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
INSERT dbo.DriverInstance(DriverInstanceId, ClusterId, RawFolderId, Name, DriverType, DriverConfig, Enabled)
|
||||||
VALUES ('MAIN-opcua', 'MAIN', NULL, 'opcplc', 'OpcUaClient',
|
VALUES ('MAIN-opcua', 'MAIN', NULL, 'opcplc', 'OpcUaClient',
|
||||||
N'{ "SecurityPolicy": "None", "SecurityMode": "None", "AuthType": "Anonymous", "AutoAcceptCertificates": true, "TargetNamespaceKind": "SystemPlatform" }', 1);
|
N'{ "SecurityPolicy": "None", "SecurityMode": "None", "AuthType": "Anonymous", "AutoAcceptCertificates": true }', 1);
|
||||||
IF NOT EXISTS (SELECT 1 FROM dbo.Device WHERE DeviceId = 'MAIN-opcua-dev')
|
IF NOT EXISTS (SELECT 1 FROM dbo.Device WHERE DeviceId = 'MAIN-opcua-dev')
|
||||||
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
INSERT dbo.Device(DeviceId, DriverInstanceId, Name, Enabled, DeviceConfig)
|
||||||
VALUES ('MAIN-opcua-dev', 'MAIN-opcua', 'plc', 1, N'{ "EndpointUrl": "opc.tcp://10.100.0.35:50000" }');
|
VALUES ('MAIN-opcua-dev', 'MAIN-opcua', 'plc', 1, N'{ "EndpointUrl": "opc.tcp://10.100.0.35:50000" }');
|
||||||
|
|||||||
+14
-13
@@ -38,8 +38,7 @@ out-of-process isolation.
|
|||||||
"EndpointUrl": "opc.tcp://plc.internal:4840",
|
"EndpointUrl": "opc.tcp://plc.internal:4840",
|
||||||
"SecurityPolicy": "None",
|
"SecurityPolicy": "None",
|
||||||
"SecurityMode": "None",
|
"SecurityMode": "None",
|
||||||
"AuthType": "Anonymous",
|
"AuthType": "Anonymous"
|
||||||
"TargetNamespaceKind": "Equipment"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,19 +61,21 @@ dangle. Stored NodeIds embed the server-stable namespace **URI** (not the
|
|||||||
session-relative `ns=N` index) so a remote namespace-table reorder across a
|
session-relative `ns=N` index) so a remote namespace-table reorder across a
|
||||||
restart doesn't silently re-point references at the wrong namespace.
|
restart doesn't silently re-point references at the wrong namespace.
|
||||||
|
|
||||||
### Namespace assignment
|
### Namespace assignment (retired, Gitea #648)
|
||||||
|
|
||||||
This is the only driver that gateways into **either** namespace kind, decided
|
Earlier versions required a per-instance `TargetNamespaceKind` (`Equipment` +
|
||||||
per instance via `TargetNamespaceKind`:
|
a mandatory `UnsMappingTable`, or `SystemPlatform` + an empty table). Both
|
||||||
|
options are **removed**: nothing ever read the mapping table beyond the startup
|
||||||
|
check, and in v3 the raw-to-UNS projection is explicit (`UnsTagReference`), so
|
||||||
|
the choice carried no behaviour — it only made a config without the key fail
|
||||||
|
`Initialize`. Remote nodes are authored as `/raw` tags bound by their upstream
|
||||||
|
`nodeId`; there is no namespace field on the driver, the AdminUI form or the
|
||||||
|
seed.
|
||||||
|
|
||||||
- `Equipment` — the remote server exposes raw equipment data; remote browse
|
Stored `DriverConfig` JSON that still carries `targetNamespaceKind` or
|
||||||
paths are remapped to UNS via a required `UnsMappingTable`.
|
`unsMappingTable` keeps working: the keys are unknown and ignored on parse and
|
||||||
- `SystemPlatform` — the remote server exposes processed/derived data; the
|
initialise (the AdminUI form drops them on the next save). Pinned by
|
||||||
remote hierarchy is preserved with no UNS conversion (and the mapping table
|
`OpcUaClientReinitConfigAdoptionTests.Legacy_namespace_keys_are_ignored_by_parse_and_initialise`.
|
||||||
must be empty).
|
|
||||||
|
|
||||||
The choice is enforced at startup so a misconfiguration fails draft validation
|
|
||||||
rather than surfacing as a runtime surprise.
|
|
||||||
|
|
||||||
## Capability surface
|
## Capability surface
|
||||||
|
|
||||||
|
|||||||
@@ -857,12 +857,7 @@ Tier A (pure managed, OPC Foundation reference SDK). Universal protections cover
|
|||||||
|
|
||||||
### Namespace Assignment
|
### Namespace Assignment
|
||||||
|
|
||||||
OPC UA Client is the only driver that supports **either** namespace kind, decided per driver instance via `DriverConfig.TargetNamespaceKind`:
|
> **Retired (Gitea #648).** The per-instance `TargetNamespaceKind` (Equipment + `UnsMappingTable` vs SystemPlatform) and its startup enforcement were removed as vestigial in v3: UNS projection is explicit via `UnsTagReference`, and nothing consumed the mapping table. Legacy `targetNamespaceKind` / `unsMappingTable` keys in stored config are ignored. See `docs/drivers/OpcUaClient.md`.
|
||||||
|
|
||||||
- **Equipment**: when gatewaying a remote OPC UA server that exposes raw equipment data (e.g. another vendor's OPC UA-native PLC stack). The driver must remap remote browse paths to UNS via a config-driven mapping table — remote nodes don't conform to UNS by default. Each remote node group → an `Equipment` row with its own UNS Area/Line/Name and stable UUID.
|
|
||||||
- **SystemPlatform**: when gatewaying a remote OPC UA server that exposes processed/derived data (e.g. another System Platform endpoint). Hierarchy is preserved via `Tag.FolderPath` mirroring the remote browse path; no UNS conversion.
|
|
||||||
|
|
||||||
The driver enforces the namespace choice at startup — a misconfigured remote (raw signals routed to SystemPlatform, or processed data routed to Equipment without a UNS mapping) fails draft validation, not runtime.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -877,6 +872,6 @@ The driver enforces the namespace choice at startup — a misconfigured remote (
|
|||||||
| Siemens S7 | S7netplus | MIT | B — in-process with guards | Equipment | .NET 10 x64 | No (polled) | Config DB | 3-30 per PLC | PUT/GET enabled (S7-1200/1500) |
|
| Siemens S7 | S7netplus | MIT | B — in-process with guards | Equipment | .NET 10 x64 | No (polled) | Config DB | 3-30 per PLC | PUT/GET enabled (S7-1200/1500) |
|
||||||
| TwinCAT | Beckhoff.TwinCAT.Ads 6.x | Proprietary | B — in-process with guards | Equipment | .NET 10 x64 | **Yes (ADS)** | Symbol upload | 64-128 | AMS route configured |
|
| TwinCAT | Beckhoff.TwinCAT.Ads 6.x | Proprietary | B — in-process with guards | Equipment | .NET 10 x64 | **Yes (ADS)** | Symbol upload | 64-128 | AMS route configured |
|
||||||
| FOCAS | Fwlib64.dll (P/Invoke) + MessagePack IPC | FANUC SDK license | **C — out-of-process** | Equipment | .NET 10 x64 (Host + Proxy) | No (polled) | Built-in + Config DB | **5-10 per CNC** | FANUC SDK license, CNC Ethernet option, Focas.Host Windows service |
|
| FOCAS | Fwlib64.dll (P/Invoke) + MessagePack IPC | FANUC SDK license | **C — out-of-process** | Equipment | .NET 10 x64 (Host + Proxy) | No (polled) | Built-in + Config DB | **5-10 per CNC** | FANUC SDK license, CNC Ethernet option, Focas.Host Windows service |
|
||||||
| OPC UA Client | OPC Foundation 1.5.x | GPL/RCL | A — in-process | Equipment OR SystemPlatform (per-instance) | .NET 10 x64 | **Yes (native)** | Browse remote | Varies | Certificate trust |
|
| OPC UA Client | OPC Foundation 1.5.x | GPL/RCL | A — in-process | Equipment | .NET 10 x64 | **Yes (native)** | Browse remote | Varies | Certificate trust |
|
||||||
|
|
||||||
Tier definitions and per-tier protections: see `driver-stability.md`. Namespace model and UNS naming rules: see `plan.md` §4 and `config-db-schema.md` (Equipment table). Equipment-namespace drivers populate `Equipment` rows whose UNS path comes from `ServerCluster.Enterprise/Site` + `Equipment.Area/Line/Name`; SystemPlatform-namespace drivers (Galaxy) preserve their own hierarchy via `Tag.FolderPath` as v1 LmxOpcUa expressed it.
|
Tier definitions and per-tier protections: see `driver-stability.md`. Namespace model and UNS naming rules: see `plan.md` §4 and `config-db-schema.md` (Equipment table). Equipment-namespace drivers populate `Equipment` rows whose UNS path comes from `ServerCluster.Enterprise/Site` + `Equipment.Area/Line/Name`; SystemPlatform-namespace drivers (Galaxy) preserve their own hierarchy via `Tag.FolderPath` as v1 LmxOpcUa expressed it.
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
|
|||||||
// Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions and
|
// Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions and
|
||||||
// OpcUaClientDriverProbe._opts so the browser parses a given DriverConfig JSON
|
// OpcUaClientDriverProbe._opts so the browser parses a given DriverConfig JSON
|
||||||
// the same way as the factory and the probe. The JsonStringEnumConverter lets
|
// the same way as the factory and the probe. The JsonStringEnumConverter lets
|
||||||
// enum-valued knobs (SecurityPolicy / SecurityMode / AuthType / TargetNamespaceKind)
|
// enum-valued knobs (SecurityPolicy / SecurityMode / AuthType)
|
||||||
// be authored as their string names — the natural form for AdminUI-emitted JSON.
|
// be authored as their string names — the natural form for AdminUI-emitted JSON.
|
||||||
// JsonStringEnumConverter also accepts numeric ordinals, so existing numeric configs
|
// JsonStringEnumConverter also accepts numeric ordinals, so existing numeric configs
|
||||||
// continue to work without migration.
|
// continue to work without migration.
|
||||||
|
|||||||
-49
@@ -153,36 +153,6 @@ public sealed record OpcUaClientDriverOptions
|
|||||||
[Range(1, int.MaxValue)]
|
[Range(1, int.MaxValue)]
|
||||||
public int MaxBrowseDepth { get; init; } = 10;
|
public int MaxBrowseDepth { get; init; } = 10;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The namespace kind this driver instance gateways into. Per
|
|
||||||
/// <c>docs/v2/driver-specs.md</c> §8 "Namespace Assignment", the OPC UA Client driver
|
|
||||||
/// is the only driver that supports <b>either</b> kind, decided per instance:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><see cref="OpcUaTargetNamespaceKind.Equipment"/> — gatewaying a remote
|
|
||||||
/// server that exposes raw equipment data; the driver remaps remote browse paths
|
|
||||||
/// to UNS.</item>
|
|
||||||
/// <item><see cref="OpcUaTargetNamespaceKind.SystemPlatform"/> — gatewaying a
|
|
||||||
/// remote server that exposes processed/derived data; hierarchy is preserved via
|
|
||||||
/// the remote browse path with no UNS conversion.</item>
|
|
||||||
/// </list>
|
|
||||||
/// The driver enforces the choice at startup — see
|
|
||||||
/// <c>OpcUaClientDriver.ValidateNamespaceKind</c> — so a misconfiguration fails
|
|
||||||
/// draft validation rather than surfacing as a runtime surprise.
|
|
||||||
/// </summary>
|
|
||||||
public OpcUaTargetNamespaceKind TargetNamespaceKind { get; init; } = OpcUaTargetNamespaceKind.Equipment;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional config-driven remote-to-UNS mapping table. Required when
|
|
||||||
/// <see cref="TargetNamespaceKind"/> is <see cref="OpcUaTargetNamespaceKind.Equipment"/>:
|
|
||||||
/// §8 mandates that a remote server gatewayed as Equipment carry a mapping table
|
|
||||||
/// because remote nodes do not conform to UNS by default. Keys are remote browse-path
|
|
||||||
/// prefixes; values are the UNS Area/Line/Name path the matching subtree maps onto.
|
|
||||||
/// For <see cref="OpcUaTargetNamespaceKind.SystemPlatform"/> the table must be empty —
|
|
||||||
/// processed data preserves its own hierarchy and a mapping table would be ambiguous.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyDictionary<string, string> UnsMappingTable { get; init; }
|
|
||||||
= new Dictionary<string, string>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
|
/// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
|
||||||
/// 60s server-side maximum; this default is what the form pre-fills for new instances.
|
/// 60s server-side maximum; this default is what the form pre-fills for new instances.
|
||||||
@@ -266,25 +236,6 @@ public sealed record OpcUaCertificateValidationOptions
|
|||||||
public string? IssuerStorePath { get; init; }
|
public string? IssuerStorePath { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The namespace kind an OPC UA Client driver instance gateways its remote server into.
|
|
||||||
/// See <c>docs/v2/driver-specs.md</c> §8 "Namespace Assignment".
|
|
||||||
/// </summary>
|
|
||||||
public enum OpcUaTargetNamespaceKind
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Remote server exposes raw equipment data; remote browse paths are remapped to UNS
|
|
||||||
/// via <see cref="OpcUaClientDriverOptions.UnsMappingTable"/>.
|
|
||||||
/// </summary>
|
|
||||||
Equipment,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remote server exposes processed/derived data; hierarchy is preserved via the
|
|
||||||
/// remote browse path with no UNS conversion.
|
|
||||||
/// </summary>
|
|
||||||
SystemPlatform,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>OPC UA message security mode.</summary>
|
/// <summary>OPC UA message security mode.</summary>
|
||||||
public enum OpcUaSecurityMode
|
public enum OpcUaSecurityMode
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -241,10 +241,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
|||||||
if (HasConfigBody(driverConfigJson))
|
if (HasConfigBody(driverConfigJson))
|
||||||
_options = OpcUaClientDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
|
_options = OpcUaClientDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
|
||||||
|
|
||||||
// Enforce the Equipment-vs-SystemPlatform choice at startup per driver-specs.md
|
|
||||||
// §8 "Namespace Assignment" — a misconfigured remote fails draft validation here,
|
|
||||||
// not as a runtime surprise.
|
|
||||||
ValidateNamespaceKind(_options);
|
|
||||||
ValidateTuning(_options);
|
ValidateTuning(_options);
|
||||||
|
|
||||||
var appConfig = await BuildApplicationConfigurationAsync(cancellationToken).ConfigureAwait(false);
|
var appConfig = await BuildApplicationConfigurationAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -643,42 +639,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
|||||||
return effective < TimeSpan.FromMilliseconds(50) ? 50 : (int)effective.TotalMilliseconds;
|
return effective < TimeSpan.FromMilliseconds(50) ? 50 : (int)effective.TotalMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enforce the §8 "Namespace Assignment" rule at startup. An <c>Equipment</c>-kind
|
|
||||||
/// instance gateways raw equipment data and therefore needs a config-driven UNS
|
|
||||||
/// mapping table (remote nodes don't conform to UNS); a <c>SystemPlatform</c>-kind
|
|
||||||
/// instance gateways processed data whose hierarchy is preserved verbatim, so a
|
|
||||||
/// UNS mapping table is meaningless and rejected. Throwing here surfaces the
|
|
||||||
/// misconfiguration as a draft-validation failure rather than a runtime surprise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="opts">Driver options containing namespace configuration.</param>
|
|
||||||
internal static void ValidateNamespaceKind(OpcUaClientDriverOptions opts)
|
|
||||||
{
|
|
||||||
switch (opts.TargetNamespaceKind)
|
|
||||||
{
|
|
||||||
case OpcUaTargetNamespaceKind.Equipment:
|
|
||||||
if (opts.UnsMappingTable is null || opts.UnsMappingTable.Count == 0)
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"OpcUaClient driver configured with TargetNamespaceKind=Equipment but no " +
|
|
||||||
"UnsMappingTable: §8 requires a config-driven remote-to-UNS mapping table " +
|
|
||||||
"because remote nodes do not conform to UNS by default. Provide a mapping " +
|
|
||||||
"table or set TargetNamespaceKind=SystemPlatform if the remote exposes " +
|
|
||||||
"processed data.");
|
|
||||||
break;
|
|
||||||
case OpcUaTargetNamespaceKind.SystemPlatform:
|
|
||||||
if (opts.UnsMappingTable is { Count: > 0 })
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"OpcUaClient driver configured with TargetNamespaceKind=SystemPlatform but " +
|
|
||||||
"a UnsMappingTable was supplied: processed data preserves its own hierarchy " +
|
|
||||||
"and a UNS mapping table is ambiguous here. Clear the mapping table or set " +
|
|
||||||
"TargetNamespaceKind=Equipment if the remote exposes raw equipment data.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(opts), opts.TargetNamespaceKind, "Unknown TargetNamespaceKind.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Build the user-identity token from the driver options. Split out of
|
/// Build the user-identity token from the driver options. Split out of
|
||||||
/// <see cref="InitializeAsync"/> so the failover sweep reuses one identity across
|
/// <see cref="InitializeAsync"/> so the failover sweep reuses one identity across
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ public static class OpcUaClientDriverFactoryExtensions
|
|||||||
|
|
||||||
// Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
|
// Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
|
||||||
// The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
|
// The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
|
||||||
// AuthType / TargetNamespaceKind) be authored as their string names — the natural form
|
// AuthType) be authored as their string names — the natural form
|
||||||
// for human-edited + AdminUI-emitted DriverConfig JSON.
|
// for human-edited + AdminUI-emitted DriverConfig JSON.
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
|
|||||||
-32
@@ -161,30 +161,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@* Namespace mapping *@
|
|
||||||
<section class="panel rise mt-3" style="animation-delay:.12s">
|
|
||||||
<div class="panel-head">Namespace mapping</div>
|
|
||||||
<div style="padding:1rem">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Target namespace kind</label>
|
|
||||||
<InputSelect @bind-Value="_form.OpcUa.TargetNamespaceKind" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
|
|
||||||
@foreach (var e in Enum.GetValues<OpcUaTargetNamespaceKind>())
|
|
||||||
{
|
|
||||||
<option value="@e">@e</option>
|
|
||||||
}
|
|
||||||
</InputSelect>
|
|
||||||
<div class="form-text">Equipment = raw data re-mapped to UNS. SystemPlatform = processed data; hierarchy preserved as-is.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label">UNS mapping table (read-only — edit via raw JSON import)</label>
|
|
||||||
<pre class="form-control form-control-sm mono" style="min-height:3rem;overflow:auto;white-space:pre-wrap;">@_unsMappingTableJson</pre>
|
|
||||||
<div class="form-text">Keys = remote browse-path prefixes; values = UNS Area/Line/Name paths. Required when TargetNamespaceKind = Equipment.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@* Subscriptions, limits, revocation, discovery (#273/#275/#277/#278) *@
|
@* Subscriptions, limits, revocation, discovery (#273/#275/#277/#278) *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.13s">
|
<section class="panel rise mt-3" style="animation-delay:.13s">
|
||||||
<div class="panel-head">Subscriptions, limits & discovery</div>
|
<div class="panel-head">Subscriptions, limits & discovery</div>
|
||||||
@@ -272,7 +248,6 @@
|
|||||||
|
|
||||||
private FormModel _form = new();
|
private FormModel _form = new();
|
||||||
private List<EndpointUrlRow> _endpoints = [];
|
private List<EndpointUrlRow> _endpoints = [];
|
||||||
private string _unsMappingTableJson = "{}";
|
|
||||||
private string? _lastParsed;
|
private string? _lastParsed;
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
@@ -282,7 +257,6 @@
|
|||||||
var opts = TryDeserialize(DriverConfigJson) ?? new OpcUaClientDriverOptions();
|
var opts = TryDeserialize(DriverConfigJson) ?? new OpcUaClientDriverOptions();
|
||||||
_form = new FormModel { OpcUa = OpcUaClientFormModel.FromRecord(opts) };
|
_form = new FormModel { OpcUa = OpcUaClientFormModel.FromRecord(opts) };
|
||||||
_endpoints = opts.EndpointUrls.Select(EndpointUrlRow.FromUrl).ToList();
|
_endpoints = opts.EndpointUrls.Select(EndpointUrlRow.FromUrl).ToList();
|
||||||
_unsMappingTableJson = JsonSerializer.Serialize(opts.UnsMappingTable, _jsonOpts);
|
|
||||||
_lastParsed = DriverConfigJson;
|
_lastParsed = DriverConfigJson;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +343,6 @@
|
|||||||
public string? UserCertificatePath { get; set; }
|
public string? UserCertificatePath { get; set; }
|
||||||
public string? UserCertificatePassword { get; set; }
|
public string? UserCertificatePassword { get; set; }
|
||||||
|
|
||||||
public OpcUaTargetNamespaceKind TargetNamespaceKind { get; set; } = OpcUaTargetNamespaceKind.Equipment;
|
|
||||||
|
|
||||||
public int ProbeTimeoutSeconds { get; set; } = 15;
|
public int ProbeTimeoutSeconds { get; set; } = 15;
|
||||||
|
|
||||||
@@ -385,7 +358,6 @@
|
|||||||
public string? DiscoveryUrl { get; set; }
|
public string? DiscoveryUrl { get; set; }
|
||||||
public string? DiscoveryServerUri { get; set; }
|
public string? DiscoveryServerUri { get; set; }
|
||||||
|
|
||||||
internal IReadOnlyDictionary<string, string> _unsMappingTable = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new()
|
public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new()
|
||||||
{
|
{
|
||||||
@@ -408,7 +380,6 @@
|
|||||||
Password = r.Password,
|
Password = r.Password,
|
||||||
UserCertificatePath = r.UserCertificatePath,
|
UserCertificatePath = r.UserCertificatePath,
|
||||||
UserCertificatePassword = r.UserCertificatePassword,
|
UserCertificatePassword = r.UserCertificatePassword,
|
||||||
TargetNamespaceKind = r.TargetNamespaceKind,
|
|
||||||
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
|
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
|
||||||
SubscriptionPublishingIntervalMs = r.SubscriptionPublishingInterval is { } spi ? (int)spi.TotalMilliseconds : null,
|
SubscriptionPublishingIntervalMs = r.SubscriptionPublishingInterval is { } spi ? (int)spi.TotalMilliseconds : null,
|
||||||
SubscriptionKeepAliveCount = (int)Math.Min(r.SubscriptionKeepAliveCount, int.MaxValue),
|
SubscriptionKeepAliveCount = (int)Math.Min(r.SubscriptionKeepAliveCount, int.MaxValue),
|
||||||
@@ -419,7 +390,6 @@
|
|||||||
IssuerStorePath = r.CertificateValidation.IssuerStorePath,
|
IssuerStorePath = r.CertificateValidation.IssuerStorePath,
|
||||||
DiscoveryUrl = r.DiscoveryUrl,
|
DiscoveryUrl = r.DiscoveryUrl,
|
||||||
DiscoveryServerUri = r.DiscoveryServerUri,
|
DiscoveryServerUri = r.DiscoveryServerUri,
|
||||||
_unsMappingTable = r.UnsMappingTable,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public OpcUaClientDriverOptions ToRecord(IReadOnlyList<string> endpointUrls) => new()
|
public OpcUaClientDriverOptions ToRecord(IReadOnlyList<string> endpointUrls) => new()
|
||||||
@@ -444,8 +414,6 @@
|
|||||||
Password = string.IsNullOrWhiteSpace(Password) ? null : Password,
|
Password = string.IsNullOrWhiteSpace(Password) ? null : Password,
|
||||||
UserCertificatePath = string.IsNullOrWhiteSpace(UserCertificatePath) ? null : UserCertificatePath,
|
UserCertificatePath = string.IsNullOrWhiteSpace(UserCertificatePath) ? null : UserCertificatePath,
|
||||||
UserCertificatePassword = string.IsNullOrWhiteSpace(UserCertificatePassword) ? null : UserCertificatePassword,
|
UserCertificatePassword = string.IsNullOrWhiteSpace(UserCertificatePassword) ? null : UserCertificatePassword,
|
||||||
TargetNamespaceKind = TargetNamespaceKind,
|
|
||||||
UnsMappingTable = _unsMappingTable,
|
|
||||||
ProbeTimeoutSeconds = ProbeTimeoutSeconds,
|
ProbeTimeoutSeconds = ProbeTimeoutSeconds,
|
||||||
SubscriptionPublishingInterval = SubscriptionPublishingIntervalMs is { } ms and > 0
|
SubscriptionPublishingInterval = SubscriptionPublishingIntervalMs is { } ms and > 0
|
||||||
? TimeSpan.FromMilliseconds(ms) : null,
|
? TimeSpan.FromMilliseconds(ms) : null,
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ public sealed class OpcUaClientDriverBrowserTests
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode,
|
/// AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode,
|
||||||
/// AuthType, TargetNamespaceKind) as their <b>string</b> names (e.g.
|
/// AuthType) as their <b>string</b> names (e.g.
|
||||||
/// <c>"SecurityPolicy":"Basic256Sha256"</c>). Without a
|
/// <c>"SecurityPolicy":"Basic256Sha256"</c>). Without a
|
||||||
/// <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/> on the browser's
|
/// <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/> on the browser's
|
||||||
/// <c>JsonSerializerOptions</c> the deserialization throws a
|
/// <c>JsonSerializerOptions</c> the deserialization throws a
|
||||||
|
|||||||
@@ -72,8 +72,5 @@ public static class OpcPlcProfile
|
|||||||
AutoAcceptCertificates = true,
|
AutoAcceptCertificates = true,
|
||||||
Timeout = TimeSpan.FromSeconds(10),
|
Timeout = TimeSpan.FromSeconds(10),
|
||||||
SessionTimeout = TimeSpan.FromSeconds(30),
|
SessionTimeout = TimeSpan.FromSeconds(30),
|
||||||
// opc-plc's standard address space mirrors verbatim — treat it as SystemPlatform so
|
|
||||||
// §8 namespace validation passes without requiring a UNS mapping table.
|
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,6 @@ public sealed class OpcUaClientFailoverTests
|
|||||||
PerEndpointConnectTimeout = TimeSpan.FromMilliseconds(500),
|
PerEndpointConnectTimeout = TimeSpan.FromMilliseconds(500),
|
||||||
Timeout = TimeSpan.FromMilliseconds(500),
|
Timeout = TimeSpan.FromMilliseconds(500),
|
||||||
AutoAcceptCertificates = true,
|
AutoAcceptCertificates = true,
|
||||||
// SystemPlatform kind needs no UNS mapping table — keeps this failover test
|
|
||||||
// focused on the endpoint sweep rather than §8 namespace validation.
|
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
};
|
};
|
||||||
using var drv = new OpcUaClientDriver(opts, "opcua-failover");
|
using var drv = new OpcUaClientDriver(opts, "opcua-failover");
|
||||||
|
|
||||||
|
|||||||
-4
@@ -79,10 +79,6 @@ public sealed class OpcUaClientMediumFindingsRegressionTests
|
|||||||
EndpointUrl = "opc.tcp://127.0.0.1:1",
|
EndpointUrl = "opc.tcp://127.0.0.1:1",
|
||||||
Timeout = TimeSpan.FromMilliseconds(300),
|
Timeout = TimeSpan.FromMilliseconds(300),
|
||||||
AutoAcceptCertificates = true,
|
AutoAcceptCertificates = true,
|
||||||
// Equipment is the default TargetNamespaceKind; it requires a mapping table to
|
|
||||||
// pass ValidateNamespaceKind, which runs before BuildApplicationConfigurationAsync
|
|
||||||
// where the warning is emitted.
|
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
};
|
};
|
||||||
using var drv = new OpcUaClientDriver(opts, "opcua-autocert", logger);
|
using var drv = new OpcUaClientDriver(opts, "opcua-autocert", logger);
|
||||||
|
|
||||||
|
|||||||
+1
-58
@@ -6,68 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regression coverage for driver-specs.md §8 namespace handling — the
|
/// Regression coverage for the server-stable NodeId encoding that <see cref="NamespaceMap"/> produces (findings Driver.OpcUaClient-004).
|
||||||
/// <c>TargetNamespaceKind</c> startup enforcement and the server-stable NodeId encoding
|
|
||||||
/// that <see cref="NamespaceMap"/> produces (findings Driver.OpcUaClient-004).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Trait("Category", "Unit")]
|
[Trait("Category", "Unit")]
|
||||||
public sealed class OpcUaClientNamespaceTests
|
public sealed class OpcUaClientNamespaceTests
|
||||||
{
|
{
|
||||||
// ---- Driver.OpcUaClient-004: TargetNamespaceKind startup enforcement ----
|
|
||||||
|
|
||||||
/// <summary>Verifies that Equipment namespace kind without mapping table is rejected.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void ValidateNamespaceKind_Equipment_without_mapping_table_is_rejected()
|
|
||||||
{
|
|
||||||
// §8: an Equipment-kind instance gateways raw equipment data and MUST carry a
|
|
||||||
// config-driven UNS mapping table — remote nodes don't conform to UNS by default.
|
|
||||||
var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment };
|
|
||||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
|
|
||||||
.Message.ShouldContain("UnsMappingTable");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Verifies that Equipment namespace kind with mapping table passes.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void ValidateNamespaceKind_Equipment_with_mapping_table_passes()
|
|
||||||
{
|
|
||||||
var opts = new OpcUaClientDriverOptions
|
|
||||||
{
|
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment,
|
|
||||||
UnsMappingTable = new Dictionary<string, string> { ["Line1"] = "Site/AreaA/Line1" },
|
|
||||||
};
|
|
||||||
Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Verifies that SystemPlatform namespace kind with mapping table is rejected.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void ValidateNamespaceKind_SystemPlatform_with_mapping_table_is_rejected()
|
|
||||||
{
|
|
||||||
// §8: processed data preserves its own hierarchy — a UNS mapping table is ambiguous.
|
|
||||||
var opts = new OpcUaClientDriverOptions
|
|
||||||
{
|
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
UnsMappingTable = new Dictionary<string, string> { ["x"] = "y" },
|
|
||||||
};
|
|
||||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
|
|
||||||
.Message.ShouldContain("SystemPlatform");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Verifies that SystemPlatform namespace kind without mapping table passes.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void ValidateNamespaceKind_SystemPlatform_without_mapping_table_passes()
|
|
||||||
{
|
|
||||||
var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform };
|
|
||||||
Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Verifies that the default target namespace kind is Equipment.</summary>
|
|
||||||
[Fact]
|
|
||||||
public void Default_TargetNamespaceKind_is_Equipment()
|
|
||||||
{
|
|
||||||
// §8 comparison table: OPC UA Client default lane is Equipment.
|
|
||||||
new OpcUaClientDriverOptions().TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.Equipment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Driver.OpcUaClient-004: server-stable NodeId encoding ----
|
// ---- Driver.OpcUaClient-004: server-stable NodeId encoding ----
|
||||||
|
|
||||||
/// <summary>Verifies that NamespaceMap.FromSession rejects null session.</summary>
|
/// <summary>Verifies that NamespaceMap.FromSession rejects null session.</summary>
|
||||||
|
|||||||
+42
-15
@@ -13,21 +13,21 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
|||||||
public sealed class OpcUaClientReinitConfigAdoptionTests
|
public sealed class OpcUaClientReinitConfigAdoptionTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>ValidateNamespaceKind</c> runs at the top of init and rejects <c>TargetNamespaceKind=Equipment</c>
|
/// <c>ValidateTuning</c> runs at the top of init and rejects a non-absolute <c>discoveryUrl</c>.
|
||||||
/// with an empty UNS mapping table. Reinitializing INTO that shape must therefore throw — a driver
|
/// Reinitializing INTO that shape must therefore throw — a driver still serving the constructor's
|
||||||
/// still serving the constructor's valid options would validate those and succeed, which is exactly
|
/// valid options would validate those and succeed, which is exactly how the discarded edit stayed
|
||||||
/// how the discarded edit stayed invisible.
|
/// invisible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reinitialize_adopts_a_changed_config()
|
public async Task Reinitialize_adopts_a_changed_config()
|
||||||
{
|
{
|
||||||
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance(
|
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance(
|
||||||
"opc1",
|
"opc1",
|
||||||
"""{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}""");
|
"""{"endpointUrl":"opc.tcp://127.0.0.1:1"}""");
|
||||||
|
|
||||||
await Should.ThrowAsync<InvalidOperationException>(
|
await Should.ThrowAsync<InvalidOperationException>(
|
||||||
() => driver.ReinitializeAsync(
|
() => driver.ReinitializeAsync(
|
||||||
"""{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{}}""",
|
"""{"endpointUrl":"opc.tcp://127.0.0.1:1","discoveryUrl":"not a uri"}""",
|
||||||
CancellationToken.None),
|
CancellationToken.None),
|
||||||
"OpcUaClientDriver kept its constructor-supplied options after a reinitialize with a changed config (#516)");
|
"OpcUaClientDriver kept its constructor-supplied options after a reinitialize with a changed config (#516)");
|
||||||
}
|
}
|
||||||
@@ -36,15 +36,42 @@ public sealed class OpcUaClientReinitConfigAdoptionTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_options()
|
public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_options()
|
||||||
{
|
{
|
||||||
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance(
|
// Built directly (the factory's parse would refuse the bad discoveryUrl up front).
|
||||||
"opc1",
|
using var driver = new OpcUaClientDriver(
|
||||||
"""{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}""");
|
new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://127.0.0.1:1", DiscoveryUrl = "not a uri" }, "opc1");
|
||||||
|
|
||||||
// "{}" keeps the (valid) constructor options, so ValidateNamespaceKind passes and the driver gets as
|
// The constructor options are invalid (bad discoveryUrl) but "{}" keeps them WITHOUT re-parsing;
|
||||||
// far as the connect — which fails against an unreachable endpoint. Anything BUT the validation
|
// a re-parse of an empty config would yield valid defaults. So tuning validation must still see
|
||||||
// throw proves the constructor options survived.
|
// the constructor's bad discoveryUrl and throw — a driver that re-parsed "{}" would not.
|
||||||
var ex = await Record.ExceptionAsync(() => driver.ReinitializeAsync("{}", CancellationToken.None));
|
var ex = await Should.ThrowAsync<InvalidOperationException>(
|
||||||
(ex is InvalidOperationException ioe && ioe.Message.Contains("UnsMappingTable", StringComparison.OrdinalIgnoreCase))
|
() => driver.ReinitializeAsync("{}", CancellationToken.None));
|
||||||
.ShouldBeFalse("an empty document must keep the constructor-supplied options, not re-validate an empty config");
|
ex.Message.ShouldContain("DiscoveryUrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #648 — <c>targetNamespaceKind</c> / <c>unsMappingTable</c> were removed (vestigial in v3). DriverConfig
|
||||||
|
/// JSON stored before the removal still carries them, in every shape the old validator used to refuse
|
||||||
|
/// (Equipment with no table, Equipment with a table, SystemPlatform with a table). None may fail the
|
||||||
|
/// parse or the initialise: the unknown keys are ignored. Falsifiable — it fails if a namespace-kind
|
||||||
|
/// validator is reinstated, because the Equipment-without-table blob would throw again.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"Equipment"}""")]
|
||||||
|
[InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}""")]
|
||||||
|
[InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"SystemPlatform","unsMappingTable":{"a":"b"}}""")]
|
||||||
|
[InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":1,"unsMappingTable":{}}""")]
|
||||||
|
public async Task Legacy_namespace_keys_are_ignored_by_parse_and_initialise(string legacyJson)
|
||||||
|
{
|
||||||
|
Should.NotThrow(() => OpcUaClientDriverFactoryExtensions.ParseOptions("opc1", legacyJson));
|
||||||
|
|
||||||
|
var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("opc1", legacyJson);
|
||||||
|
|
||||||
|
// Initialise gets past config validation and fails only at the connect (nothing listens on port 1).
|
||||||
|
// Anything other than a namespace-validation InvalidOperationException proves the keys were ignored.
|
||||||
|
var ex = await Record.ExceptionAsync(() => driver.InitializeAsync(legacyJson, CancellationToken.None));
|
||||||
|
(ex is InvalidOperationException ioe
|
||||||
|
&& (ioe.Message.Contains("UnsMappingTable", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| ioe.Message.Contains("TargetNamespaceKind", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ShouldBeFalse("legacy namespace keys must be ignored, not validated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-16
@@ -39,7 +39,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
BrowseRoot = "i=85",
|
BrowseRoot = "i=85",
|
||||||
MaxDiscoveredNodes = 5000,
|
MaxDiscoveredNodes = 5000,
|
||||||
MaxBrowseDepth = 6,
|
MaxBrowseDepth = 6,
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
ProbeTimeoutSeconds = 20,
|
ProbeTimeoutSeconds = 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +63,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
back.BrowseRoot.ShouldBe("i=85");
|
back.BrowseRoot.ShouldBe("i=85");
|
||||||
back.MaxDiscoveredNodes.ShouldBe(5000);
|
back.MaxDiscoveredNodes.ShouldBe(5000);
|
||||||
back.MaxBrowseDepth.ShouldBe(6);
|
back.MaxBrowseDepth.ShouldBe(6);
|
||||||
back.TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.SystemPlatform);
|
|
||||||
back.ProbeTimeoutSeconds.ShouldBe(20);
|
back.ProbeTimeoutSeconds.ShouldBe(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,16 +84,9 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void FormModel_RoundTrip_PreservesAllFields()
|
public void FormModel_RoundTrip_PreservesAllFields()
|
||||||
{
|
{
|
||||||
// Construct options with non-default values for every editable property plus a
|
// Construct options with non-default values for every editable property. EndpointUrls is now edited via the CollectionEditor on the page and is
|
||||||
// non-empty UnsMappingTable (read-only in the form, round-tripped via the original
|
|
||||||
// record). EndpointUrls is now edited via the CollectionEditor on the page and is
|
|
||||||
// threaded into ToRecord explicitly; see EndpointUrls_ListRoundTrip_PreservesOrder.
|
// threaded into ToRecord explicitly; see EndpointUrls_ListRoundTrip_PreservesOrder.
|
||||||
var endpointUrls = new List<string> { "opc.tcp://primary:4840", "opc.tcp://backup:4840" };
|
var endpointUrls = new List<string> { "opc.tcp://primary:4840", "opc.tcp://backup:4840" };
|
||||||
var unsMappingTable = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["Line1/"] = "Site/Area1/Line1",
|
|
||||||
["Line2/"] = "Site/Area1/Line2",
|
|
||||||
};
|
|
||||||
|
|
||||||
var original = new OpcUaClientDriverOptions
|
var original = new OpcUaClientDriverOptions
|
||||||
{
|
{
|
||||||
@@ -119,8 +110,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
BrowseRoot = "i=85",
|
BrowseRoot = "i=85",
|
||||||
MaxDiscoveredNodes = 3000,
|
MaxDiscoveredNodes = 3000,
|
||||||
MaxBrowseDepth = 5,
|
MaxBrowseDepth = 5,
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
UnsMappingTable = unsMappingTable,
|
|
||||||
ProbeTimeoutSeconds = 25,
|
ProbeTimeoutSeconds = 25,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,10 +138,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
result.BrowseRoot.ShouldBe("i=85");
|
result.BrowseRoot.ShouldBe("i=85");
|
||||||
result.MaxDiscoveredNodes.ShouldBe(3000);
|
result.MaxDiscoveredNodes.ShouldBe(3000);
|
||||||
result.MaxBrowseDepth.ShouldBe(5);
|
result.MaxBrowseDepth.ShouldBe(5);
|
||||||
result.TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.SystemPlatform);
|
|
||||||
result.UnsMappingTable.Count.ShouldBe(2);
|
|
||||||
result.UnsMappingTable["Line1/"].ShouldBe("Site/Area1/Line1");
|
|
||||||
result.UnsMappingTable["Line2/"].ShouldBe("Site/Area1/Line2");
|
|
||||||
result.ProbeTimeoutSeconds.ShouldBe(25);
|
result.ProbeTimeoutSeconds.ShouldBe(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +184,32 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
|||||||
def.DiscoveryUrl.ShouldBeNull();
|
def.DiscoveryUrl.ShouldBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #648: stored DriverConfig JSON from before <c>targetNamespaceKind</c>/<c>unsMappingTable</c> were removed
|
||||||
|
/// must still load into the form (the form's options use <c>UnmappedMemberHandling.Skip</c>) and the legacy
|
||||||
|
/// keys must NOT be re-emitted on save.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void LegacyNamespaceKeys_LoadIntoForm_AndAreNotReEmitted()
|
||||||
|
{
|
||||||
|
var formOpts = new JsonSerializerOptions(_opts)
|
||||||
|
{
|
||||||
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
||||||
|
Converters = { new JsonStringEnumConverter() },
|
||||||
|
};
|
||||||
|
const string legacy =
|
||||||
|
"""{"endpointUrl":"opc.tcp://plc:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"Line1/":"Site/Area1/Line1"}}""";
|
||||||
|
|
||||||
|
var opts = JsonSerializer.Deserialize<OpcUaClientDriverOptions>(legacy, formOpts);
|
||||||
|
opts.ShouldNotBeNull();
|
||||||
|
opts.EndpointUrl.ShouldBe("opc.tcp://plc:4840");
|
||||||
|
|
||||||
|
var saved = JsonSerializer.Serialize(
|
||||||
|
OpcUaClientDriverForm.OpcUaClientFormModel.FromRecord(opts).ToRecord([]), formOpts);
|
||||||
|
saved.ShouldNotContain("targetNamespaceKind", Case.Insensitive);
|
||||||
|
saved.ShouldNotContain("unsMappingTable", Case.Insensitive);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EndpointUrlRow_FromUrl_ToUrl_Trims()
|
public void EndpointUrlRow_FromUrl_ToUrl_Trims()
|
||||||
{
|
{
|
||||||
|
|||||||
-2
@@ -58,7 +58,6 @@ public sealed class OpcUaClientDriverWireTests
|
|||||||
Username = "apikey",
|
Username = "apikey",
|
||||||
Password = ServerEventRig.KeyPassword,
|
Password = ServerEventRig.KeyPassword,
|
||||||
AutoAcceptCertificates = true,
|
AutoAcceptCertificates = true,
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
Timeout = TimeSpan.FromSeconds(10),
|
Timeout = TimeSpan.FromSeconds(10),
|
||||||
SessionTimeout = TimeSpan.FromSeconds(30),
|
SessionTimeout = TimeSpan.FromSeconds(30),
|
||||||
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
||||||
@@ -281,7 +280,6 @@ public sealed class OpcUaClientDriverWireTests
|
|||||||
Username = "apikey",
|
Username = "apikey",
|
||||||
Password = ServerEventRig.KeyPassword,
|
Password = ServerEventRig.KeyPassword,
|
||||||
AutoAcceptCertificates = true,
|
AutoAcceptCertificates = true,
|
||||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform,
|
|
||||||
Timeout = TimeSpan.FromSeconds(10),
|
Timeout = TimeSpan.FromSeconds(10),
|
||||||
SessionTimeout = TimeSpan.FromSeconds(30),
|
SessionTimeout = TimeSpan.FromSeconds(30),
|
||||||
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
||||||
|
|||||||
Reference in New Issue
Block a user