From f0082af5b9adfa5f00bd0728a037dc7cc989e65d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 8 Jul 2026 17:51:19 -0400 Subject: [PATCH] build(analyzers): wire OTOPCUA0001 tree-wide + triage the surfaced hits (arch-review 07/C-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inject the custom UnwrappedCapabilityCallAnalyzer as an OutputItemType=Analyzer ProjectReference from Directory.Build.props (excluding the analyzer + its test project) so OTOPCUA0001 runs on every src/ and tests/ compilation — it previously enforced its CapabilityInvoker-wrapping rule against nothing but its own 31 unit tests (the 'built-but-never-wired' failure mode). Triage of the ~280 surfaced hits, three categories: 1. RESILIENCE-DISPATCH-GAP (7 sites, DriverInstanceActor x6 + GenericDriverNodeManager x1): a REAL, previously-untracked gap the analyzer caught on first wiring — the Phase 6.1 CapabilityInvoker resilience pipeline (retry/breaker/bulkhead/telemetry) is constructed ONLY in tests and was never wired into the production dispatch layer. Scoped per-site #pragma with a greppable RESILIENCE-DISPATCH-GAP marker explicitly noting these are tracked-but-not-intentional, pending the dispatch-wiring remediation (filed as a follow-up). Keeps the analyzer live everywhere else in those projects so a NEW unwrapped call still fails the build. 2. Driver-INTERNAL self-calls (3 sites, AbCipAlarmProjection x2 + S7Driver x1): a driver's own poll/ack path calling its own capability method. The invoker wraps the driver from the dispatch layer OUTWARD; a driver re-wrapping its own internal calls would double-wrap. Genuinely intentional — scoped #pragma with that rationale. 3. Wire-level test suites + manual-testing CLIs (12 projects): invoke drivers directly by design — the analyzer's own documented intentional case. Project-level NoWarn with a comment. Verified: full solution build green, 0 OTOPCUA0001 hits; analyzer's 31 tests pass; negative control — dropping one dispatch-gap pragma re-fires OTOPCUA0001 and fails the Runtime build, proving the analyzer is genuinely live tree-wide, not disabled. --- Directory.Build.props | 19 +++++++++++++++++++ .../OpcUa/GenericDriverNodeManager.cs | 2 ++ .../ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj | 7 +++++++ ....MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj | 7 +++++++ .../ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj | 7 +++++++ ...ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj | 7 +++++++ .../ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj | 7 +++++++ ...B.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj | 7 +++++++ .../AbCipAlarmProjection.cs | 4 ++++ .../ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs | 2 ++ .../Drivers/DriverInstanceActor.cs | 12 ++++++++++++ ...W.OtOpcUa.Core.ScriptedAlarms.Tests.csproj | 5 +++++ ...M.WW.OtOpcUa.Core.VirtualTags.Tests.csproj | 5 +++++ ...OpcUa.Driver.AbCip.IntegrationTests.csproj | 7 +++++++ ...B.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj | 7 +++++++ ....OtOpcUa.Driver.S7.IntegrationTests.csproj | 7 +++++++ .../ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj | 7 +++++++ 17 files changed, 119 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 10a2a1d1..cbdfb25e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,4 +30,23 @@ + + + + + diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs index 6b9aaf89..c6118261 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs @@ -68,7 +68,9 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable throw new NotSupportedException($"Driver '{Driver.DriverInstanceId}' does not implement ITagDiscovery."); var capturing = new CapturingBuilder(builder, _alarmSinks); +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into GenericDriverNodeManager; tracked, NOT intentional. await discovery.DiscoverAsync(capturing, ct); +#pragma warning restore OTOPCUA0001 if (Driver is IAlarmSource alarmSource) { diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj index 094c2019..5dd39c15 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj index f71208a6..73724e3a 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj index 5cff6e6c..3f72bc5a 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj index 8b5a9516..5a2ec85f 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj index 38030f24..342d542f 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj index 0737e186..189cd784 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + Exe net10.0 diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs index c9c0e0d3..83629420 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs @@ -146,7 +146,9 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable // Best-effort — the driver's WriteAsync returns per-item status; individual ack // failures don't poison the batch. Swallow the return so a single faulted ack // doesn't bubble out of the caller's batch expectation. +#pragma warning disable OTOPCUA0001 // Driver-INTERNAL self-call: the projection is owned by AbCipDriver; CapabilityInvoker wraps the driver from the dispatch layer OUTWARD, so a driver's own internal ack path must NOT re-wrap. Intentional, not a dispatch gap. _ = await _driver.WriteAsync(requests, cancellationToken).ConfigureAwait(false); +#pragma warning restore OTOPCUA0001 } /// Releases all resources associated with this alarm projection. @@ -225,7 +227,9 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable { try { +#pragma warning disable OTOPCUA0001 // Driver-INTERNAL self-call: the projection is owned by AbCipDriver; CapabilityInvoker wraps the driver from the dispatch layer OUTWARD, so a driver's own internal poll path must NOT re-wrap. Intentional, not a dispatch gap. var results = await _driver.ReadAsync(refs, ct).ConfigureAwait(false); +#pragma warning restore OTOPCUA0001 Tick(sub, results); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index 717da6ad..20b95983 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -1294,7 +1294,9 @@ public sealed class S7Driver private async Task PollOnceAsync(SubscriptionState state, bool forceRaise, CancellationToken ct) { +#pragma warning disable OTOPCUA0001 // Driver-INTERNAL self-call: S7Driver's own poll loop reading via its own ReadAsync; CapabilityInvoker wraps the driver from the dispatch layer OUTWARD, so the driver's internal poll must NOT re-wrap. Intentional, not a dispatch gap. var snapshots = await ReadAsync(state.TagReferences, ct).ConfigureAwait(false); +#pragma warning restore OTOPCUA0001 for (var i = 0; i < state.TagReferences.Count; i++) { var tagRef = state.TagReferences[i]; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index 552f85b1..ef7fc99c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -572,7 +572,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. var results = await writable.WriteAsync(request, cts.Token); +#pragma warning restore OTOPCUA0001 if (results is { Count: 1 } && IsGoodStatus(results[0].StatusCode)) { replyTo.Tell(new WriteAttributeResult(true, null)); @@ -620,7 +622,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. await src.AcknowledgeAsync(request, cts.Token); +#pragma warning restore OTOPCUA0001 _log.Info("DriverInstance {Id}: acknowledged native condition {Cond} by {User}", _driverInstanceId, msg.ConditionId, msg.OperatorUser); } @@ -657,8 +661,10 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers subscribable.OnDataChange += _dataChangeHandler; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. _subscriptionHandle = await subscribable .SubscribeAsync(msg.FullReferences, msg.PublishingInterval, cts.Token); +#pragma warning restore OTOPCUA0001 replyTo.Tell(new SubscriptionEstablished(_subscriptionHandle.DiagnosticId, msg.FullReferences.Count)); _log.Info("DriverInstance {Id}: subscribed to {Count} refs ({Diag})", @@ -682,7 +688,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. await subscribable.UnsubscribeAsync(_subscriptionHandle, cts.Token); +#pragma warning restore OTOPCUA0001 } catch (Exception ex) { @@ -759,7 +767,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); try { +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. _alarmSubscriptionHandle = await src.SubscribeAlarmsAsync(refs, cts.Token); +#pragma warning restore OTOPCUA0001 _log.Info("DriverInstance {Id}: native-alarm subscription established for {Count} alarm ref(s) ({Diag})", _driverInstanceId, refs.Count, _alarmSubscriptionHandle.DiagnosticId); } @@ -818,7 +828,9 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers // resume on the actor task scheduler so the Context.Parent.Tell + Timers calls below run with a // live ActorContext. ConfigureAwait(false) would resume off-context and throw // NotSupportedException("no active ActorContext") — see the same warning on HandleSubscribeAsync. +#pragma warning disable OTOPCUA0001 // RESILIENCE-DISPATCH-GAP (07/C-1): dispatch bypasses CapabilityInvoker — Phase 6.1 pipeline built but never wired into DriverInstanceActor; tracked, NOT intentional. await discovery.DiscoverAsync(builder, cts.Token); +#pragma warning restore OTOPCUA0001 nodes = builder.Nodes.ToArray(); // immutable snapshot — never hand the builder's live list across actors } catch (Exception ex) diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj index 3e572cde..3c141d1e 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests.csproj @@ -7,6 +7,11 @@ false true ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests + + $(NoWarn);OTOPCUA0001 diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj index d692711b..4be9782d 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests.csproj @@ -7,6 +7,11 @@ false true ZB.MOM.WW.OtOpcUa.Core.VirtualTags.Tests + + $(NoWarn);OTOPCUA0001 diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj index 6c70bd94..7826e365 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + net10.0 enable diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj index fa847088..b35b6c83 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + net10.0 enable diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj index a5663e6e..36b0a7f9 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + net10.0 enable diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj index 76751b91..4720b2dc 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj @@ -1,5 +1,12 @@ + + + $(NoWarn);OTOPCUA0001 + + net10.0 enable