fix(client): surface a Bad per-node HistoryRead status instead of '0 values returned'
DefaultSessionAdapter swallowed a Bad HistoryReadResult.StatusCode (raw: break; aggregate: skip) so Client.CLI/UI printed an empty result for BadHistoryOperationUnsupported / BadUserAccessDenied. It now throws a ServiceResultException naming the node and status; BadNoData stays empty. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
|
||||
|
||||
@@ -172,8 +173,9 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
break;
|
||||
|
||||
var result = results[0];
|
||||
if (StatusCode.IsBad(result.StatusCode))
|
||||
break;
|
||||
// A refused read (BadHistoryOperationUnsupported, BadUserAccessDenied, ...) must surface; only
|
||||
// BadNoData is an empty result. Swallowing it printed "0 values returned" for a refusal.
|
||||
HistoryReadStatus.ThrowIfRefused(result.StatusCode, nodeId);
|
||||
|
||||
if (result.HistoryData is ExtensionObject ext && ext.Body is HistoryData historyData)
|
||||
allValues.AddRange(historyData.DataValues);
|
||||
@@ -218,8 +220,8 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
if (results != null && results.Count > 0)
|
||||
{
|
||||
var result = results[0];
|
||||
if (!StatusCode.IsBad(result.StatusCode) &&
|
||||
result.HistoryData is ExtensionObject ext &&
|
||||
HistoryReadStatus.ThrowIfRefused(result.StatusCode, nodeId);
|
||||
if (result.HistoryData is ExtensionObject ext &&
|
||||
ext.Body is HistoryData historyData)
|
||||
allValues.AddRange(historyData.DataValues);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Opc.Ua;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Classifies the per-node <c>HistoryReadResult.StatusCode</c> a server returns for a HistoryRead.
|
||||
/// A Bad per-node status (<c>BadHistoryOperationUnsupported</c>, <c>BadUserAccessDenied</c>,
|
||||
/// <c>BadNodeIdUnknown</c>, ...) is a refused read, not an empty one, so it must reach the caller
|
||||
/// instead of collapsing to "0 values returned". <c>BadNoData</c> is the one Bad code that genuinely
|
||||
/// means "the window held nothing" and stays an empty result.
|
||||
/// </summary>
|
||||
internal static class HistoryReadStatus
|
||||
{
|
||||
/// <summary>Throws a <see cref="ServiceResultException"/> naming the node and status when the read was refused.</summary>
|
||||
/// <param name="status">The per-node result status.</param>
|
||||
/// <param name="nodeId">The node that was read, for the message.</param>
|
||||
/// <exception cref="ServiceResultException"><paramref name="status"/> is Bad and not <c>BadNoData</c>.</exception>
|
||||
public static void ThrowIfRefused(StatusCode status, NodeId nodeId)
|
||||
{
|
||||
if (!StatusCode.IsBad(status) || status.Code == StatusCodes.BadNoData) return;
|
||||
throw new ServiceResultException(status.Code,
|
||||
$"HistoryRead of {nodeId} failed: {StatusCodes.GetBrowseName(status.Code)} (0x{status.Code:X8}).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
|
||||
|
||||
public class HistoryReadStatusTests
|
||||
{
|
||||
/// <summary>A refused read must surface, carrying the status code, rather than read as an empty result.</summary>
|
||||
/// <param name="code">The Bad per-node status code.</param>
|
||||
[Theory]
|
||||
[InlineData(StatusCodes.BadHistoryOperationUnsupported)]
|
||||
[InlineData(StatusCodes.BadUserAccessDenied)]
|
||||
[InlineData(StatusCodes.BadNodeIdUnknown)]
|
||||
[InlineData(StatusCodes.BadTimeout)]
|
||||
public void Bad_status_throws_ServiceResultException_with_the_code(uint code)
|
||||
{
|
||||
var ex = Should.Throw<ServiceResultException>(
|
||||
() => HistoryReadStatus.ThrowIfRefused(new StatusCode(code), new NodeId("n", 2)));
|
||||
|
||||
ex.StatusCode.ShouldBe(code);
|
||||
ex.Message.ShouldContain("ns=2;s=n");
|
||||
}
|
||||
|
||||
/// <summary>Good, GoodNoData, Uncertain and BadNoData are all non-refusals (the window may simply be empty).</summary>
|
||||
/// <param name="code">The non-refusal status code.</param>
|
||||
[Theory]
|
||||
[InlineData(StatusCodes.Good)]
|
||||
[InlineData(StatusCodes.GoodNoData)]
|
||||
[InlineData(StatusCodes.GoodMoreData)]
|
||||
[InlineData(StatusCodes.UncertainNoCommunicationLastUsableValue)]
|
||||
[InlineData(StatusCodes.BadNoData)]
|
||||
public void Non_refusal_status_does_not_throw(uint code)
|
||||
{
|
||||
Should.NotThrow(() => HistoryReadStatus.ThrowIfRefused(new StatusCode(code), new NodeId("n", 2)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user