205 lines
8.7 KiB
C#
205 lines
8.7 KiB
C#
using ZB.MOM.WW.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
|
|
|
/// <summary>
|
|
/// Step-logs design 2026-09-09 D2: a <c>ScriptTrace.ScriptLog</c> row is never a node — it folds onto the
|
|
/// step its <c>StepEventId</c> names, else onto its execution's anchor (<c>ScriptRun</c> /
|
|
/// <c>InboundRequest</c> / <c>RouteCall</c>), else its execution's first non-<c>Response</c> step, else it is
|
|
/// dropped and counted in <see cref="FlowGraph.UnattachedLogLines"/>.
|
|
/// </summary>
|
|
public class FlowGraphBuilderLogLineTests
|
|
{
|
|
private static readonly DateTime T0 = new(2026, 9, 9, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
private static AuditEvent Run(Guid execution, Guid? parent = null, int ms = 0) =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "SomeScript", sourceNode: "node-a",
|
|
executionId: execution, parentExecutionId: parent, sourceSiteId: "site-a", sourceInstanceId: "M1",
|
|
durationMs: 100, extra: new ScriptTraceExtra { TriggerKind = "call" }.ToJson());
|
|
|
|
private static AuditEvent Write(Guid execution, Guid? eventId = null, int ms = 10) =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered,
|
|
eventId: eventId, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "M1.Flag",
|
|
sourceNode: "node-a", executionId: execution, sourceSiteId: "site-a", sourceInstanceId: "M1", durationMs: 2,
|
|
extra: new ScriptTraceExtra { Writes = [new AttributeWriteDetail("M1.Flag", "true", true, null)] }.ToJson());
|
|
|
|
private static AuditEvent Inbound(Guid execution, int ms = 0) =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiInbound, AuditKind.InboundRequest, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.AddMilliseconds(ms), actor: "some-key", target: "SomeMethod", sourceNode: "central-a",
|
|
executionId: execution, durationMs: 50, requestSummary: "{}", responseSummary: "{\"ok\":true}");
|
|
|
|
private static AuditEvent RouteCallRoot(Guid execution, int ms = 0) =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.RouteCall, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.AddMilliseconds(ms), actor: "flow-debugger", target: "M1.SomeScript", sourceNode: "central-a",
|
|
executionId: execution, durationMs: 120);
|
|
|
|
private static AuditEvent ApiOut(Guid execution, Guid eventId, int ms = 20) =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ApiOutbound, AuditKind.ApiCall, AuditStatus.Delivered,
|
|
eventId: eventId, occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "ERP/orders",
|
|
sourceNode: "node-a", executionId: execution, sourceSiteId: "site-a", durationMs: 30);
|
|
|
|
private static AuditEvent Line(Guid execution, Guid? step = null, int? seq = null, string level = "info",
|
|
int ms = 5, string message = "hello") =>
|
|
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered,
|
|
occurredAtUtc: T0.AddMilliseconds(ms), actor: "SomeScript", target: "SomeScript", sourceNode: "node-a",
|
|
executionId: execution, sourceSiteId: "site-a", sourceInstanceId: "M1", requestSummary: message,
|
|
extra: new ScriptTraceExtra { LogLevel = level, LogSource = "script", StepEventId = step, Seq = seq }.ToJson());
|
|
|
|
private static FlowStep StepOf(FlowGraph graph, FlowStepKind kind) => Assert.Single(graph.Steps, s => s.Kind == kind);
|
|
|
|
[Fact]
|
|
public void LogLine_WithStepEventId_AttachesToThatStep()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
var writeId = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution), Write(execution, writeId), Line(execution, step: writeId)]);
|
|
|
|
var write = StepOf(graph, FlowStepKind.AttributeWrite);
|
|
var line = Assert.Single(write.Logs!);
|
|
Assert.Equal("hello", line.Message);
|
|
Assert.Null(StepOf(graph, FlowStepKind.ScriptRun).Logs);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLine_WithoutStepId_AttachesToTheExecutionAnchor()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution), Write(execution), Line(execution)]);
|
|
|
|
Assert.Single(StepOf(graph, FlowStepKind.ScriptRun).Logs!);
|
|
Assert.Null(StepOf(graph, FlowStepKind.AttributeWrite).Logs);
|
|
}
|
|
|
|
[Fact]
|
|
public void CentralLine_AttachesToInboundRow_NotResponse()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Inbound(execution), Line(execution)]);
|
|
|
|
Assert.Single(StepOf(graph, FlowStepKind.InboundRequest).Logs!);
|
|
Assert.Null(StepOf(graph, FlowStepKind.Response).Logs);
|
|
}
|
|
|
|
[Fact]
|
|
public void ScriptEntryLine_AttachesToRouteCallRoot()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([RouteCallRoot(execution), Line(execution)]);
|
|
|
|
Assert.Single(StepOf(graph, FlowStepKind.RouteCall).Logs!);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLine_UnknownStepId_FallsBackToAnchor()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution), Line(execution, step: Guid.NewGuid())]);
|
|
|
|
Assert.Single(StepOf(graph, FlowStepKind.ScriptRun).Logs!);
|
|
Assert.Equal(0, graph.UnattachedLogLines);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLine_NoAnchor_FallsBackToFirstStep()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Write(execution), Line(execution)]);
|
|
|
|
Assert.Single(StepOf(graph, FlowStepKind.AttributeWrite).Logs!);
|
|
Assert.Equal(0, graph.UnattachedLogLines);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLine_NoStepInExecution_IsCountedUnattached()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution), Line(Guid.NewGuid())]);
|
|
|
|
Assert.Equal(1, graph.UnattachedLogLines);
|
|
Assert.All(graph.Steps, s => Assert.True(s.Logs is null or { Count: 0 }));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_LinesOnly_ReturnsEmptyGraphWithUnattachedCount()
|
|
{
|
|
var graph = FlowGraphBuilder.Build([Line(Guid.NewGuid()), Line(Guid.NewGuid())], logsTruncated: true);
|
|
|
|
Assert.Empty(graph.Steps);
|
|
Assert.Empty(graph.Edges);
|
|
Assert.Equal(2, graph.UnattachedLogLines);
|
|
Assert.True(graph.LogsTruncated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Lines_OrderBySeqWithinSameMillisecond_NullSeqLast()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build(
|
|
[
|
|
Run(execution),
|
|
Line(execution, seq: 3, ms: 5, message: "three"),
|
|
Line(execution, seq: 1, ms: 5, message: "one"),
|
|
Line(execution, seq: null, ms: 5, message: "null"),
|
|
Line(execution, seq: 2, ms: 5, message: "two"),
|
|
]);
|
|
|
|
var logs = StepOf(graph, FlowStepKind.ScriptRun).Logs!;
|
|
Assert.Equal(new int?[] { 1, 2, 3, null }, logs.Select(l => l.Seq).ToArray());
|
|
Assert.Equal(new[] { "one", "two", "three", "null" }, logs.Select(l => l.Message).ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLines_NeverBecomeSteps()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
var events = new List<AuditEvent> { Run(execution) };
|
|
for (var i = 0; i < 5; i++)
|
|
events.Add(Line(execution, seq: i, ms: 1 + i, message: $"line {i}"));
|
|
var lineIds = events.Skip(1).Select(e => e.EventId).ToHashSet();
|
|
|
|
var graph = FlowGraphBuilder.Build(events);
|
|
|
|
var step = Assert.Single(graph.Steps);
|
|
Assert.Equal(FlowStepKind.ScriptRun, step.Kind);
|
|
Assert.Equal(5, step.Logs!.Count);
|
|
Assert.DoesNotContain(graph.Edges, e => lineIds.Contains(e.FromEventId) || lineIds.Contains(e.ToEventId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_PassesLogsTruncatedThrough()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution)], truncated: false, logsTruncated: true);
|
|
|
|
Assert.True(graph.LogsTruncated);
|
|
Assert.False(graph.Truncated);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogLine_OnFoldedOutboundStep_AttachesByFirstRowId()
|
|
{
|
|
var execution = Guid.NewGuid();
|
|
var callId = Guid.NewGuid();
|
|
|
|
var graph = FlowGraphBuilder.Build([Run(execution), ApiOut(execution, callId), Line(execution, step: callId)]);
|
|
|
|
var call = StepOf(graph, FlowStepKind.ApiCall);
|
|
Assert.Equal(callId, call.EventId);
|
|
Assert.Single(call.Logs!);
|
|
Assert.Null(StepOf(graph, FlowStepKind.ScriptRun).Logs);
|
|
}
|
|
}
|