c782744b07
One Ask per attribute became one site-local InstanceGet/SetAttributesRequest answered in a single pass, keeping the per-attribute GetAttributeResponse / SetStaticAttributeResponse outcomes. The single-write path is refactored to a Task<SetStaticAttributeResponse> outcome shared by both paths; static writes and rejections still reply synchronously.
132 lines
5.8 KiB
C#
132 lines
5.8 KiB
C#
using System.Text.Json;
|
|
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Messages;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
using ZB.MOM.WW.ScadaBridge.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 01/SR-P4: <c>Route.To().GetAttributes()/SetAttributes()</c> used one Ask
|
|
/// per attribute. The Instance Actor now answers a whole batch in one message while keeping the
|
|
/// per-attribute outcome a single read/write would report.
|
|
/// </summary>
|
|
public class InstanceActorAttributeBatchTests : TestKit, IDisposable
|
|
{
|
|
private readonly SiteStorageService _storage;
|
|
private readonly ScriptCompilationService _compilationService;
|
|
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
|
private readonly TestLocalDb _localDb;
|
|
|
|
/// <summary>Creates a temporary LocalDb-backed site store per test.</summary>
|
|
public InstanceActorAttributeBatchTests()
|
|
{
|
|
_localDb = TestLocalDb.CreateTemp("instance-attr-batch");
|
|
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
|
|
_storage.InitializeAsync().GetAwaiter().GetResult();
|
|
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
|
|
_sharedScriptLibrary = new SharedScriptLibrary(
|
|
_compilationService, NullLogger<SharedScriptLibrary>.Instance);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IDisposable.Dispose()
|
|
{
|
|
Shutdown();
|
|
var path = _localDb.Path;
|
|
_localDb.Dispose();
|
|
TestLocalDb.DeleteFiles(path);
|
|
}
|
|
|
|
private IActorRef StartInstance(IActorRef? dcl = null)
|
|
{
|
|
var config = new FlattenedConfiguration
|
|
{
|
|
InstanceUniqueName = "Batch1",
|
|
Attributes =
|
|
[
|
|
new ResolvedAttribute { CanonicalName = "Mode", Value = "Manual", DataType = "String" },
|
|
new ResolvedAttribute { CanonicalName = "Count", Value = "3", DataType = "Int32" },
|
|
new ResolvedAttribute
|
|
{
|
|
CanonicalName = "Speed", Value = "0", DataType = "Int32",
|
|
DataSourceReference = "ns=2;s=Speed", BoundDataConnectionName = "conn-1"
|
|
}
|
|
]
|
|
};
|
|
var json = JsonSerializer.Serialize(config);
|
|
return ActorOf(Props.Create(() => new InstanceActor(
|
|
"Batch1", json, _storage, _compilationService, _sharedScriptLibrary, null,
|
|
new SiteRuntimeOptions(), NullLogger<InstanceActor>.Instance, dcl)));
|
|
}
|
|
|
|
private static SetStaticAttributeCommand Write(string attribute, string value) =>
|
|
new("corr", "Batch1", attribute, value, DateTimeOffset.UtcNow);
|
|
|
|
/// <summary>One reply, index-aligned, with found/quality per attribute.</summary>
|
|
[Fact]
|
|
public void GetAttributes_AnswersEveryNameInOneReply()
|
|
{
|
|
var instance = StartInstance();
|
|
|
|
instance.Tell(new InstanceGetAttributesRequest("corr", "Batch1", ["Count", "Missing", "Mode", "Speed"]));
|
|
var reply = ExpectMsg<InstanceGetAttributesResponse>(TimeSpan.FromSeconds(10));
|
|
|
|
Assert.Equal(["Count", "Missing", "Mode", "Speed"], reply.Results.Select(r => r.AttributeName));
|
|
Assert.Equal("3", reply.Results[0].Value?.ToString());
|
|
Assert.False(reply.Results[1].Found);
|
|
Assert.Equal("Manual", reply.Results[2].Value?.ToString());
|
|
Assert.Equal("Uncertain", reply.Results[3].Quality);
|
|
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
|
}
|
|
|
|
/// <summary>Static writes apply; an unknown attribute fails on its own row only.</summary>
|
|
[Fact]
|
|
public void SetAttributes_StaticAndUnknown_ReportsPerAttributeOutcome()
|
|
{
|
|
var instance = StartInstance();
|
|
|
|
instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Mode", "Auto"), Write("Nope", "1")]));
|
|
var reply = ExpectMsg<InstanceSetAttributesResponse>(TimeSpan.FromSeconds(10));
|
|
|
|
Assert.True(reply.Results[0].Success);
|
|
Assert.False(reply.Results[1].Success);
|
|
Assert.Contains("Unknown attribute 'Nope'", reply.Results[1].ErrorMessage);
|
|
|
|
instance.Tell(new GetAttributeRequest("g", "Batch1", "Mode", DateTimeOffset.UtcNow));
|
|
Assert.Equal("Auto", ExpectMsg<GetAttributeResponse>().Value?.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// A data-sourced write inside the batch goes to the DCL; the batch reply waits for the
|
|
/// device answer and carries its failure on that attribute's row.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SetAttributes_DataSourcedWrite_WaitsForTheDclAndCarriesItsOutcome()
|
|
{
|
|
var dcl = CreateTestProbe();
|
|
var instance = StartInstance(dcl.Ref);
|
|
dcl.ExpectMsg<SubscribeTagsRequest>(TimeSpan.FromSeconds(10));
|
|
|
|
instance.Tell(new InstanceSetAttributesRequest("corr", [Write("Count", "9"), Write("Speed", "42")]));
|
|
|
|
var write = dcl.ExpectMsg<WriteTagRequest>(TimeSpan.FromSeconds(10));
|
|
Assert.Equal("ns=2;s=Speed", write.TagPath);
|
|
ExpectNoMsg(TimeSpan.FromMilliseconds(300)); // the reply waits for the device
|
|
dcl.Reply(new WriteTagResponse(write.CorrelationId, false, "device rejected", DateTimeOffset.UtcNow));
|
|
|
|
var reply = ExpectMsg<InstanceSetAttributesResponse>(TimeSpan.FromSeconds(10));
|
|
Assert.True(reply.Results[0].Success);
|
|
Assert.False(reply.Results[1].Success);
|
|
Assert.Equal("Speed", reply.Results[1].AttributeName);
|
|
Assert.Equal("device rejected", reply.Results[1].ErrorMessage);
|
|
}
|
|
}
|