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:
-21
@@ -1,21 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.Historian;
|
||||
|
||||
public sealed class HistoryReadStatusExceptionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0x801F0000u)] // BadUserAccessDenied
|
||||
[InlineData(0xC0000000u)] // severity 11 (reserved Bad)
|
||||
public void A_Bad_code_is_carried_verbatim(uint code) =>
|
||||
new HistoryReadStatusException(code).StatusCode.ShouldBe(code);
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x00000000u)] // Good
|
||||
[InlineData(0x00A70000u)] // GoodNoData
|
||||
[InlineData(0x40000000u)] // Uncertain
|
||||
public void A_non_Bad_code_is_rejected(uint code) =>
|
||||
Should.Throw<ArgumentException>(() => new HistoryReadStatusException(code));
|
||||
}
|
||||
-42
@@ -153,22 +153,6 @@ public sealed class IHistorianDataSourceContractTests
|
||||
parameter!.ParameterType.ShouldBe(expectedType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that history provider max parameters have the expected type.</summary>
|
||||
/// <param name="methodName">The name of the method to test.</param>
|
||||
/// <param name="parameterName">The name of the parameter to verify.</param>
|
||||
/// <param name="expectedType">The expected parameter type.</param>
|
||||
[Theory]
|
||||
[InlineData("ReadRawAsync", "maxValuesPerNode", typeof(uint))]
|
||||
[InlineData("ReadEventsAsync", "maxEvents", typeof(int))]
|
||||
public void HistoryProvider_MaxParameter_TypePinned(string methodName, string parameterName, Type expectedType)
|
||||
{
|
||||
var method = typeof(IHistoryProvider).GetMethod(methodName);
|
||||
method.ShouldNotBeNull();
|
||||
var parameter = method!.GetParameters().FirstOrDefault(p => p.Name == parameterName);
|
||||
parameter.ShouldNotBeNull($"Method {methodName} should expose a '{parameterName}' parameter.");
|
||||
parameter!.ParameterType.ShouldBe(expectedType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-009 (OpcUaServer-002): when <c>maxEvents <= 0</c> the contract
|
||||
/// requires implementations to set <see cref="HistoricalEventsResult.ContinuationPoint"/>
|
||||
@@ -195,30 +179,4 @@ public sealed class IHistorianDataSourceContractTests
|
||||
continuationProp!.PropertyType.ShouldBe(typeof(byte[]),
|
||||
"ContinuationPoint is byte[]? — null means no more pages, non-null means caller must page");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core.Abstractions-008: <see cref="IHistoryProvider.ReadAtTimeAsync"/> and
|
||||
/// <see cref="IHistoryProvider.ReadEventsAsync"/> are C# default interface methods
|
||||
/// (drivers opt in), whereas <see cref="IHistorianDataSource.ReadAtTimeAsync"/> and
|
||||
/// <see cref="IHistorianDataSource.ReadEventsAsync"/> are required (the server-side
|
||||
/// historian must implement them). This test pins the asymmetry so an implementer cannot
|
||||
/// accidentally collapse the two surfaces and so the documented rationale stays load-bearing.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The name of the optional method to check.</param>
|
||||
[Theory]
|
||||
[InlineData("ReadAtTimeAsync")]
|
||||
[InlineData("ReadEventsAsync")]
|
||||
public void HistoryProvider_OptionalMethods_HaveDefaultImplementation(string methodName)
|
||||
{
|
||||
var providerMethod = typeof(IHistoryProvider).GetMethod(methodName);
|
||||
providerMethod.ShouldNotBeNull();
|
||||
// Default interface methods carry a method body — IsAbstract is false.
|
||||
providerMethod!.IsAbstract.ShouldBeFalse(
|
||||
$"IHistoryProvider.{methodName} must remain a default-impl-throws method so legacy drivers continue to compile.");
|
||||
|
||||
var dataSourceMethod = typeof(IHistorianDataSource).GetMethod(methodName);
|
||||
dataSourceMethod.ShouldNotBeNull();
|
||||
dataSourceMethod!.IsAbstract.ShouldBeTrue(
|
||||
$"IHistorianDataSource.{methodName} must remain required — server-side historians own the full surface.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ public sealed class InterfaceIndependenceTests
|
||||
[InlineData(typeof(IWritable))]
|
||||
[InlineData(typeof(ISubscribable))]
|
||||
[InlineData(typeof(IAlarmSource))]
|
||||
[InlineData(typeof(IHistoryProvider))]
|
||||
[InlineData(typeof(IRediscoverable))]
|
||||
[InlineData(typeof(IHostConnectivityProbe))]
|
||||
[InlineData(typeof(ISkippedTagReporter))]
|
||||
@@ -73,4 +72,15 @@ public sealed class InterfaceIndependenceTests
|
||||
type.IsPublic.ShouldBeTrue($"{type.Name} must be public — drivers in separate assemblies implement it.");
|
||||
type.IsInterface.ShouldBeTrue($"{type.Name} must be an interface, not a class.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #655: HistoryRead is served server-side by <c>IHistorianDataSource</c>; no driver-side history
|
||||
/// capability exists. Re-adding one without also adding a dispatch path would recreate dead surface.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NoDriverSideHistoryCapability_Exists()
|
||||
{
|
||||
Assembly.GetType("ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHistoryProvider").ShouldBeNull();
|
||||
Enum.GetNames<DriverCapability>().ShouldNotContain("HistoryRead");
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -138,6 +138,33 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #655: the retired <c>HistoryRead</c> capability may still sit in stored
|
||||
/// <c>DriverInstance.ResilienceConfig</c> rows. It must parse (diagnostic only, never a throw), be skipped,
|
||||
/// and leave every other override in the same blob applied.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StoredConfig_WithRetiredHistoryReadKey_StillParses_AndOtherOverridesApply()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"capabilityPolicies": {
|
||||
"HistoryRead": { "timeoutSeconds": 45, "retryCount": 1, "breakerFailureThreshold": 3 },
|
||||
"Read": { "timeoutSeconds": 7 }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var options = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.A, json, out var diag);
|
||||
|
||||
diag.ShouldNotBeNull();
|
||||
diag.ShouldContain("HistoryRead");
|
||||
diag.ShouldContain("skipped");
|
||||
options.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(7);
|
||||
options.Resolve(DriverCapability.Write).ShouldBe(
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that top-level property names are case insensitive.</summary>
|
||||
[Fact]
|
||||
public void PropertyNames_AreCaseInsensitive()
|
||||
|
||||
@@ -116,7 +116,6 @@ public sealed class DriverResilienceOptionsTests
|
||||
/// <param name="capability">The driver capability to test.</param>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A, DriverCapability.Read)]
|
||||
[InlineData(DriverTier.A, DriverCapability.HistoryRead)]
|
||||
[InlineData(DriverTier.B, DriverCapability.Discover)]
|
||||
[InlineData(DriverTier.B, DriverCapability.Probe)]
|
||||
[InlineData(DriverTier.C, DriverCapability.AlarmSubscribe)]
|
||||
|
||||
Reference in New Issue
Block a user