review(Analyzers): add trip-coverage for async guarded-interface methods

Re-review at 7286d320. -008: 5 regression tests for Unsubscribe/UnsubscribeAlarms/
Acknowledge/ReadEvents trip + suppression paths (analyzer source already correct).
Surfaced cross-module: Runtime DriverInstanceActor.HandleWriteAsync calls WriteAsync
directly (tracked for Runtime).
This commit is contained in:
Joseph Doherty
2026-06-19 11:21:35 -04:00
parent 6b4210cb17
commit 13c1215811
2 changed files with 164 additions and 2 deletions
@@ -833,6 +833,129 @@ namespace ZB.MOM.WW.OtOpcUa.Server {
diags.ShouldBeEmpty();
}
// =======================================================================
// Analyzers-008 — missing trip + pass-clean tests for ISubscribable.UnsubscribeAsync,
// IAlarmSource.UnsubscribeAlarmsAsync, IAlarmSource.AcknowledgeAsync, and the wrapped
// (pass-clean) path for IHistoryProvider.ReadEventsAsync (DIM).
// =======================================================================
/// <summary>Verifies that a direct UnsubscribeAsync call trips the diagnostic.</summary>
[Fact]
public async Task Direct_UnsubscribeAsync_Call_TripsDiagnostic()
{
const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Server {
public sealed class BadUnsubscribe {
public async Task DoIt(ISubscribable driver, ISubscriptionHandle handle) {
await driver.UnsubscribeAsync(handle, CancellationToken.None);
}
}
}
""";
var diags = await Compile(userSrc);
diags.Length.ShouldBe(1);
diags[0].GetMessage().ShouldContain("UnsubscribeAsync");
}
/// <summary>Verifies that a wrapped UnsubscribeAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
[Fact]
public async Task Wrapped_UnsubscribeAsync_InsideCapabilityInvokerLambda_PassesCleanly()
{
const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
namespace ZB.MOM.WW.OtOpcUa.Server {
public sealed class GoodUnsubscribe {
public async Task DoIt(ISubscribable driver, ISubscriptionHandle handle, CapabilityInvoker invoker) {
await invoker.ExecuteAsync(DriverCapability.AlarmSubscribe, "h1",
async ct => await driver.UnsubscribeAsync(handle, ct),
CancellationToken.None);
}
}
}
""";
var diags = await Compile(userSrc);
diags.ShouldBeEmpty();
}
/// <summary>Verifies that a direct UnsubscribeAlarmsAsync call trips the diagnostic.</summary>
[Fact]
public async Task Direct_UnsubscribeAlarmsAsync_Call_TripsDiagnostic()
{
const string userSrc = """
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Server {
public sealed class BadAlarmUnsubscribe {
public async Task DoIt(IAlarmSource source, IAlarmSubscriptionHandle handle) {
await source.UnsubscribeAlarmsAsync(handle, CancellationToken.None);
}
}
}
""";
var diags = await Compile(userSrc);
diags.Length.ShouldBe(1);
diags[0].GetMessage().ShouldContain("UnsubscribeAlarmsAsync");
}
/// <summary>Verifies that a direct AcknowledgeAsync call trips the diagnostic.</summary>
[Fact]
public async Task Direct_AcknowledgeAsync_Call_TripsDiagnostic()
{
const string userSrc = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Server {
public sealed class BadAcknowledge {
public async Task DoIt(IAlarmSource source) {
await source.AcknowledgeAsync(new List<string>(), CancellationToken.None);
}
}
}
""";
var diags = await Compile(userSrc);
diags.Length.ShouldBe(1);
diags[0].GetMessage().ShouldContain("AcknowledgeAsync");
}
/// <summary>Verifies that a wrapped ReadEventsAsync DIM call inside a CapabilityInvoker lambda passes cleanly.</summary>
[Fact]
public async Task Wrapped_ReadEventsAsync_DIM_InsideCapabilityInvokerLambda_PassesCleanly()
{
// ReadEventsAsync is a DIM — the wrapped path must not trip the diagnostic.
const string userSrc = """
using System;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
namespace ZB.MOM.WW.OtOpcUa.Server {
public sealed class GoodHistoryEvents {
public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
_ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
async ct => await provider.ReadEventsAsync(null, DateTime.MinValue, DateTime.MaxValue, 0, ct),
CancellationToken.None);
}
}
}
""";
var diags = await Compile(userSrc);
diags.ShouldBeEmpty();
}
private static async Task<ImmutableArray<Diagnostic>> CompileWithoutStubs(string userSource)
{
var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(userSource) };