refactor(#655): delete the dead driver-side history capability
HistoryRead is served server-side by IHistorianDataSource; nothing ever dispatched to a driver's IHistoryProvider. Removes IHistoryProvider, the OpcUaClient history passthrough, DriverCapability.HistoryRead (+ resilience tier rows and AdminUI row), HistoryReadStatusException and its node-manager catches, and the tests that only covered them. Shared result types and HistoryAggregateNotSupportedException stay (moved to Historian/HistoryReadContracts.cs). A stored ResilienceConfig still carrying a HistoryRead key parses (diagnostic only; new test). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+1
-302
@@ -50,16 +50,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions {
|
||||
public interface IHostConnectivityProbe {
|
||||
IReadOnlyList<HostConnectivityStatus> GetHostStatuses();
|
||||
}
|
||||
public class HistoryReadResult { }
|
||||
public class HistoricalEventsResult { }
|
||||
public interface IHistoryProvider {
|
||||
Task<HistoryReadResult> ReadRawAsync(string fullRef, DateTime start, DateTime end, uint max, CancellationToken ct);
|
||||
Task<HistoryReadResult> ReadProcessedAsync(string fullRef, DateTime start, DateTime end, TimeSpan interval, CancellationToken ct);
|
||||
Task<HistoryReadResult> ReadAtTimeAsync(string fullRef, IReadOnlyList<DateTime> timestamps, CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
Task<HistoricalEventsResult> ReadEventsAsync(string? sourceName, DateTime start, DateTime end, int maxEvents, CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
public enum DriverCapability { Read, Write, Discover, AlarmSubscribe, AlarmAcknowledge }
|
||||
public interface IDriverCapabilityInvoker {
|
||||
ValueTask<T> ExecuteAsync<T>(DriverCapability c, string host, Func<CancellationToken, ValueTask<T>> call, CancellationToken ct);
|
||||
@@ -346,141 +336,6 @@ namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
diags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// IHistoryProvider — interface-typed receiver
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a direct ReadRawAsync call on an interface trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadRawAsync_OnInterface_TripsDiagnostic()
|
||||
{
|
||||
const string userSrc = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
public sealed class BadHistoryRead {
|
||||
public async Task DoIt(IHistoryProvider provider) {
|
||||
_ = await provider.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadRawAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct ReadProcessedAsync call on an interface trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadProcessedAsync_OnInterface_TripsDiagnostic()
|
||||
{
|
||||
const string userSrc = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
public sealed class BadHistoryProcessed {
|
||||
public async Task DoIt(IHistoryProvider provider) {
|
||||
_ = await provider.ReadProcessedAsync("tag", DateTime.MinValue, DateTime.MaxValue, TimeSpan.FromMinutes(1), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadProcessedAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct ReadAtTimeAsync call on an interface trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadAtTimeAsync_OnInterface_TripsDiagnostic()
|
||||
{
|
||||
// ReadAtTimeAsync is a default interface method — the analyzer must still flag it when
|
||||
// called through an interface-typed receiver.
|
||||
const string userSrc = """
|
||||
using System;
|
||||
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 BadHistoryAtTime {
|
||||
public async Task DoIt(IHistoryProvider provider) {
|
||||
_ = await provider.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a wrapped ReadRawAsync call inside a CapabilityInvoker lambda passes cleanly.</summary>
|
||||
[Fact]
|
||||
public async Task Wrapped_ReadRawAsync_InsideCapabilityInvokerLambda_PassesCleanly()
|
||||
{
|
||||
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 GoodHistoryRead {
|
||||
public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
|
||||
_ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
|
||||
async ct => await provider.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, ct),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// IHistoryProvider — concrete driver receiver (FindImplementationForInterfaceMember branch)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies that a direct HistoryRead call on a concrete driver trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_HistoryRead_OnConcreteDriver_TripsDiagnostic()
|
||||
{
|
||||
// ConcreteHistoryDriver implements IHistoryProvider with explicitly-named methods
|
||||
// (same names here, but the test exercises the FindImplementationForInterfaceMember
|
||||
// branch because the receiver type is the concrete class, not the interface).
|
||||
const string userSrc = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
public sealed class ConcreteHistoryDriver : IHistoryProvider {
|
||||
public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
|
||||
public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
|
||||
}
|
||||
public sealed class BadConcreteHistoryRead {
|
||||
public async Task DoIt(ConcreteHistoryDriver driver) {
|
||||
_ = await driver.ReadRawAsync("tag", DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadRawAsync");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// IHostConnectivityProbe — synchronous member must NOT be flagged
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -739,139 +594,9 @@ namespace SomeOther {
|
||||
diags.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Analyzers-005 — IHistoryProvider default-interface-method asymmetry
|
||||
// =======================================================================
|
||||
|
||||
/// <summary>Verifies that a direct ReadAtTimeAsync call on a concrete driver inheriting the DIM trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadAtTimeAsync_OnConcreteDriverInheritingDIM_TripsDiagnostic()
|
||||
{
|
||||
// ConcreteHistoryDriver does NOT override ReadAtTimeAsync (it inherits the DIM).
|
||||
// The call site has a concrete-receiver type — the analyzer must still flag the call
|
||||
// because the bound method (ReadAtTimeAsync) is the interface's own default impl.
|
||||
const string userSrc = """
|
||||
using System;
|
||||
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 PartialHistoryDriver : IHistoryProvider {
|
||||
public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
|
||||
public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
|
||||
// ReadAtTimeAsync + ReadEventsAsync NOT overridden — driver inherits the DIM throws.
|
||||
}
|
||||
public sealed class BadPartialHistoryCaller {
|
||||
public async Task DoIt(PartialHistoryDriver driver) {
|
||||
// Cast through the interface so the binder resolves to IHistoryProvider.ReadAtTimeAsync
|
||||
// (the DIM). FindImplementationForInterfaceMember returns the interface method itself
|
||||
// when nothing overrides it.
|
||||
var iface = (IHistoryProvider)driver;
|
||||
_ = await iface.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct ReadEventsAsync call on a concrete driver inheriting the DIM trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadEventsAsync_OnConcreteDriverInheritingDIM_TripsDiagnostic()
|
||||
{
|
||||
// Same as above but for ReadEventsAsync — confirms both DIMs are caught when the
|
||||
// driver doesn't override them.
|
||||
const string userSrc = """
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
public sealed class PartialHistoryDriver2 : IHistoryProvider {
|
||||
public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
|
||||
public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
|
||||
}
|
||||
public sealed class BadPartialHistoryEventsCaller {
|
||||
public async Task DoIt(PartialHistoryDriver2 driver) {
|
||||
var iface = (IHistoryProvider)driver;
|
||||
_ = await iface.ReadEventsAsync(null, DateTime.MinValue, DateTime.MaxValue, 100, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadEventsAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a direct ReadAtTimeAsync call on a concrete driver overriding the DIM trips the diagnostic.</summary>
|
||||
[Fact]
|
||||
public async Task Direct_ReadAtTimeAsync_OnConcreteDriverOverridingDIM_TripsDiagnostic()
|
||||
{
|
||||
// Driver explicitly overrides ReadAtTimeAsync — the concrete-receiver call goes through
|
||||
// the override; FindImplementationForInterfaceMember returns the override method, which
|
||||
// SymbolEqualityComparer matches against method.
|
||||
const string userSrc = """
|
||||
using System;
|
||||
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 FullHistoryDriver : IHistoryProvider {
|
||||
public Task<HistoryReadResult> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) => throw null!;
|
||||
public Task<HistoryReadResult> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, CancellationToken ct) => throw null!;
|
||||
public Task<HistoryReadResult> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> ts, CancellationToken ct) => throw null!;
|
||||
}
|
||||
public sealed class BadFullHistoryCaller {
|
||||
public async Task DoIt(FullHistoryDriver driver) {
|
||||
_ = await driver.ReadAtTimeAsync("tag", new List<DateTime>(), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
diags.Length.ShouldBe(1);
|
||||
diags[0].GetMessage().ShouldContain("ReadAtTimeAsync");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a wrapped ReadAtTimeAsync DIM call inside a CapabilityInvoker lambda passes cleanly.</summary>
|
||||
[Fact]
|
||||
public async Task Wrapped_ReadAtTimeAsync_DIM_InsideCapabilityInvokerLambda_PassesCleanly()
|
||||
{
|
||||
// DIM wrapped properly — must not trip.
|
||||
const string userSrc = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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 GoodHistoryAtTime {
|
||||
public async Task DoIt(IHistoryProvider provider, CapabilityInvoker invoker) {
|
||||
_ = await invoker.ExecuteAsync(DriverCapability.Read, "h1",
|
||||
async ct => await provider.ReadAtTimeAsync("tag", new List<DateTime>(), ct),
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var diags = await Compile(userSrc);
|
||||
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).
|
||||
// IAlarmSource.UnsubscribeAlarmsAsync, IAlarmSource.AcknowledgeAsync.
|
||||
// =======================================================================
|
||||
|
||||
/// <summary>Verifies that a direct UnsubscribeAsync call trips the diagnostic.</summary>
|
||||
@@ -965,32 +690,6 @@ namespace ZB.MOM.WW.OtOpcUa.Server {
|
||||
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) };
|
||||
|
||||
Reference in New Issue
Block a user