using System.Collections.Concurrent; using Akka.Actor; using Akka.TestKit.Xunit2; using NSubstitute; using Serilog; using Serilog.Core; using Serilog.Events; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Observability; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors; using ZB.MOM.WW.ScadaBridge.HealthMonitoring; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Actors; /// /// ScadaBridge#56: a device write the DCL performs for an execution runs the protocol adapter /// inside that execution's log scope, so whatever the adapter logs while writing carries the /// execution's id - though the write crossed two actor hops (script, Instance Actor, DCL). /// public class ExecutionLogEnrichmentTests : TestKit { private sealed class CaptureSink : ILogEventSink { public ConcurrentQueue Events { get; } = new(); public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent); } private readonly CaptureSink _sink = new(); private readonly Serilog.ILogger _adapterLog; private readonly IDataConnection _adapter = Substitute.For(); /// Creates the adapter double; its writes log through a LogContext-enriched Serilog logger. public ExecutionLogEnrichmentTests() { _adapterLog = new LoggerConfiguration().Enrich.FromLogContext().WriteTo.Sink(_sink).CreateLogger(); _adapter.ConnectAsync(Arg.Any>(), Arg.Any()) .Returns(Task.CompletedTask); _adapter.WriteAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(ci => { _adapterLog.Information("adapter wrote {Tag}", ci.ArgAt(0)); return Task.FromResult(new WriteResult(true, null)); }); _adapter.WriteBatchAsync(Arg.Any>(), Arg.Any()) .Returns(ci => { _adapterLog.Information("adapter batch-wrote"); return Task.FromResult>( ci.ArgAt>(0).ToDictionary(kv => kv.Key, _ => new WriteResult(true, null))); }); } private async Task ConnectedActor(string name) { var options = new DataConnectionOptions { ReconnectInterval = TimeSpan.FromMilliseconds(100), WriteTimeout = TimeSpan.FromSeconds(5), SeedReadMaxAttempts = 1, SeedReadRetryDelay = TimeSpan.FromMilliseconds(10), }; var actor = Sys.ActorOf(Props.Create(() => new DataConnectionActor( name, _adapter, options, Substitute.For(), Substitute.For(), "OpcUa")), name); AwaitCondition(() => _adapter.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ConnectAsync"), TimeSpan.FromSeconds(3)); await Task.Delay(200); return actor; } private Guid? ExecutionIdOf(string fragment) { var evt = _sink.Events.First(e => e.RenderMessage().Contains(fragment, StringComparison.Ordinal)); return evt.Properties.TryGetValue(ExecutionLogContext.ExecutionIdProperty, out var v) ? Guid.Parse(v.ToString().Trim('"')) : null; } /// The adapter's line for a single write carries the execution id the request names. /// A task that represents the asynchronous operation. [Fact] public async Task SingleWrite_AdapterLinesCarryTheExecutionId() { var actor = await ConnectedActor("enrich-single"); var executionId = Guid.NewGuid(); actor.Tell(new WriteTagRequest("c1", "enrich-single", "tagA", 1, DateTimeOffset.UtcNow, executionId)); Assert.True(ExpectMsg(TimeSpan.FromSeconds(5)).Success); Assert.Equal(executionId, ExecutionIdOf("tagA")); } /// The adapter's line for a batch write carries the execution id the request names. /// A task that represents the asynchronous operation. [Fact] public async Task BatchWrite_AdapterLinesCarryTheExecutionId() { var actor = await ConnectedActor("enrich-batch"); var executionId = Guid.NewGuid(); actor.Tell(new WriteTagBatchRequest("c1", "enrich-batch", new Dictionary { ["tagB"] = 2 }, null, null, DateTimeOffset.UtcNow, executionId)); Assert.True(ExpectMsg(TimeSpan.FromSeconds(5)).Success); Assert.Equal(executionId, ExecutionIdOf("batch-wrote")); } /// A write that names no execution logs untagged (control for the two tests above). /// A task that represents the asynchronous operation. [Fact] public async Task UncorrelatedWrite_AdapterLinesAreUntagged() { var actor = await ConnectedActor("enrich-none"); actor.Tell(new WriteTagRequest("c1", "enrich-none", "tagC", 1, DateTimeOffset.UtcNow)); Assert.True(ExpectMsg(TimeSpan.FromSeconds(5)).Success); Assert.Null(ExecutionIdOf("tagC")); } }