using System.Collections.Concurrent;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Observability;
///
/// ScadaBridge#56: which messages name the execution they serve, and what pushing one does to a
/// LogContext-enriched logger.
///
public class ExecutionLogContextTests
{
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
private static readonly Guid Id = Guid.NewGuid();
private sealed class CaptureSink : ILogEventSink
{
public ConcurrentQueue Events { get; } = new();
public void Emit(LogEvent logEvent) => Events.Enqueue(logEvent);
}
/// Every correlated message, built with on its execution field.
/// The messages.
public static TheoryData CorrelatedMessages() => new()
{
new SetStaticAttributeCommand("c", "i", "a", "v", Now, SourceExecutionId: Id),
new AttributeValueChanged("i", "p", "a", 1, "Good", Now, SourceExecutionId: Id),
new ScriptCallRequest("s", null, 0, "c", ParentExecutionId: Id),
new RouteToCallRequest("c", "i", "s", null, Now, ParentExecutionId: Id),
new RouteToGetAttributesRequest("c", "i", ["a"], Now, ParentExecutionId: Id),
new RouteToSetAttributesRequest("c", "i", new Dictionary(), Now, ParentExecutionId: Id),
new RouteToWaitForAttributeRequest("c", "i", "a", null, TimeSpan.FromSeconds(1), Now, ParentExecutionId: Id),
new GetAttributeRequest("c", "i", "a", Now, SourceExecutionId: Id),
new WaitForAttributeRequest("c", "i", "a", null, null, TimeSpan.FromSeconds(1), Now, SourceExecutionId: Id),
new WriteAttributeBatchRequest("c", "i", new Dictionary(), null, null, Now, SourceExecutionId: Id),
new WriteTagRequest("c", "conn", "tag", 1, Now, SourceExecutionId: Id),
new WriteTagBatchRequest("c", "conn", new Dictionary(), null, null, Now, SourceExecutionId: Id),
};
/// Each correlated message reports the execution id it was built with.
/// The message.
[Theory]
[MemberData(nameof(CorrelatedMessages))]
public void CorrelatedMessage_ReportsItsExecutionId(object message)
=> Assert.Equal(Id, Assert.IsAssignableFrom(message).CorrelatedExecutionId);
///
/// The correlation member is an explicit interface implementation: it adds nothing to the
/// serialized shape, so the wire is unchanged.
///
/// The message.
[Theory]
[MemberData(nameof(CorrelatedMessages))]
public void CorrelatedMessage_SerializesNoExtraMember(object message)
=> Assert.DoesNotContain(nameof(IExecutionCorrelated.CorrelatedExecutionId), JsonConvert.SerializeObject(message));
/// An inferred writer (the trace arm's device-echo heuristic) is not stamped onto log lines.
[Fact]
public void InferredValueChange_NamesNoExecution()
{
IExecutionCorrelated inferred = new AttributeValueChanged(
"i", "p", "a", 1, "Good", Now, SourceExecutionId: Id, Trace: true, SourceExecutionInferred: true);
Assert.Null(inferred.CorrelatedExecutionId);
}
/// A message that names no execution - or is not correlated at all - pushes nothing.
[Fact]
public void PushFor_NothingToStamp_ReturnsNull()
{
Assert.Null(ExecutionLogContext.PushFor(new WriteTagRequest("c", "conn", "tag", 1, Now)));
Assert.Null(ExecutionLogContext.PushFor("not a message"));
Assert.Null(ExecutionLogContext.PushFor(null));
Assert.Null(ExecutionLogContext.Push(null));
}
/// Lines inside the scope carry the id, lines after it do not, and a nested push wins.
[Fact]
public void PushFor_StampsLinesInsideTheScope_AndTheInnermostWins()
{
var sink = new CaptureSink();
var log = new LoggerConfiguration().Enrich.FromLogContext().WriteTo.Sink(sink).CreateLogger();
var child = Guid.NewGuid();
using (ExecutionLogContext.PushFor(new ScriptCallRequest("s", null, 0, "c", ParentExecutionId: Id)))
{
log.Information("parent");
using (ExecutionLogContext.Push(child))
log.Information("child");
}
log.Information("outside");
var events = sink.Events.ToList();
Assert.Equal(Id.ToString(), events[0].Properties[ExecutionLogContext.ExecutionIdProperty].ToString().Trim('"'));
Assert.Equal(child.ToString(), events[1].Properties[ExecutionLogContext.ExecutionIdProperty].ToString().Trim('"'));
Assert.False(events[2].Properties.ContainsKey(ExecutionLogContext.ExecutionIdProperty));
}
///
/// ScadaBridge#120: mirrors the pushed id, follows
/// nesting, and is restored when each push is disposed (twice is harmless).
///
[Fact]
public void Current_TracksPushes_AndIsRestoredOnDispose()
{
var outer = Guid.NewGuid();
var inner = Guid.NewGuid();
Assert.Null(ExecutionLogContext.Current);
var a = ExecutionLogContext.Push(outer);
Assert.Equal(outer, ExecutionLogContext.Current);
var b = ExecutionLogContext.Push(inner);
Assert.Equal(inner, ExecutionLogContext.Current);
Assert.Null(ExecutionLogContext.Push(null)); // a null push changes nothing
Assert.Equal(inner, ExecutionLogContext.Current);
b!.Dispose();
b.Dispose();
Assert.Equal(outer, ExecutionLogContext.Current);
a!.Dispose();
Assert.Null(ExecutionLogContext.Current);
}
}