fix(r2-04): AddressSpaceApplyOutcome carries RebuildFailed/FailedNodes; SafeRebuild returns bool (01/S-1)
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Guards the failure-visibility surface added for archreview 01/S-1: the applier must report a
|
||||
/// failed rebuild (<see cref="AddressSpaceApplyOutcome.RebuildFailed"/>) and tally swallowed per-node
|
||||
/// sink failures (<see cref="AddressSpaceApplyOutcome.FailedNodes"/> for Apply's own passes; the
|
||||
/// <c>Materialise*</c> passes return their own failed-node counts) instead of reporting optimistic
|
||||
/// success while the running address space is stale or partial.
|
||||
/// </summary>
|
||||
public sealed class AddressSpaceApplierFailureSurfaceTests
|
||||
{
|
||||
// ---------------- T1: RebuildFailed ----------------
|
||||
|
||||
/// <summary>A rebuild whose sink call throws is reported as attempted-but-failed, not optimistic success.</summary>
|
||||
[Fact]
|
||||
public void Apply_WhenRebuildThrows_ReportsRebuildFailed()
|
||||
{
|
||||
var sink = new ConfigurableThrowingSink { ThrowOnRebuild = true };
|
||||
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
||||
|
||||
var outcome = applier.Apply(AddedEquipmentPlan("new"));
|
||||
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
outcome.RebuildFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>A clean apply reports no failure on either channel — the regression guard for the new fields.</summary>
|
||||
[Fact]
|
||||
public void Apply_HappyPath_ReportsNoFailure()
|
||||
{
|
||||
var sink = new ConfigurableThrowingSink();
|
||||
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
||||
|
||||
var outcome = applier.Apply(AddedEquipmentPlan("new"));
|
||||
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
outcome.RebuildFailed.ShouldBeFalse();
|
||||
outcome.FailedNodes.ShouldBe(0);
|
||||
}
|
||||
|
||||
// ---------------- fixtures ----------------
|
||||
|
||||
private static AddressSpacePlan AddedEquipmentPlan(string id) => new(
|
||||
AddedEquipment: new[] { new EquipmentNode(id, id, "line-1") },
|
||||
RemovedEquipment: Array.Empty<EquipmentNode>(),
|
||||
ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
|
||||
AddedDrivers: Array.Empty<DriverInstancePlan>(),
|
||||
RemovedDrivers: Array.Empty<DriverInstancePlan>(),
|
||||
ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
|
||||
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
|
||||
|
||||
/// <summary>An <see cref="IOpcUaAddressSpaceSink"/> whose every sink call can be independently made to
|
||||
/// throw, so tests can drive each Safe* helper's catch branch. Non-throwing calls are no-ops.</summary>
|
||||
private sealed class ConfigurableThrowingSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
public bool ThrowOnRebuild { get; init; }
|
||||
public bool ThrowOnEnsureVariable { get; init; }
|
||||
public bool ThrowOnEnsureFolder { get; init; }
|
||||
public bool ThrowOnAlarmWrite { get; init; }
|
||||
public bool ThrowOnMaterialiseAlarm { get; init; }
|
||||
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
{
|
||||
if (ThrowOnAlarmWrite) throw new InvalidOperationException("simulated WriteAlarmCondition fault");
|
||||
}
|
||||
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
{
|
||||
if (ThrowOnMaterialiseAlarm) throw new InvalidOperationException("simulated MaterialiseAlarmCondition fault");
|
||||
}
|
||||
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
{
|
||||
if (ThrowOnEnsureFolder) throw new InvalidOperationException("simulated EnsureFolder fault");
|
||||
}
|
||||
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
{
|
||||
if (ThrowOnEnsureVariable) throw new InvalidOperationException("simulated EnsureVariable fault");
|
||||
}
|
||||
|
||||
public void RebuildAddressSpace()
|
||||
{
|
||||
if (ThrowOnRebuild) throw new InvalidOperationException("simulated RebuildAddressSpace fault");
|
||||
}
|
||||
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user