ec9011429c
Review of Task 3: the sink invoke sat before WriteAsync/ForwardAsync inside the best-effort try, so a throwing sink would have silently dropped the durable row. GuardTraceSink wraps the delegate in every helper ctor (and Collect guards EmitTrace); pinned by a throwing-sink test. ScriptRunLauncher no longer allocates a collector for a traced run with no reply target (Nobody) — the list would never be read. FlowTraceCollector.Complete pinned at unit level; Snapshot documented as a non-closing peek. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
98 lines
2.8 KiB
C#
98 lines
2.8 KiB
C#
using ZB.MOM.WW.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
|
|
|
/// <summary>Flow Debugger design 2026-09-05 §5.4: the per-run inline trace sink.</summary>
|
|
public class FlowTraceCollectorTests
|
|
{
|
|
private static AuditEvent Evt(int i) => new()
|
|
{
|
|
EventId = Guid.NewGuid(),
|
|
OccurredAtUtc = DateTimeOffset.UtcNow,
|
|
Actor = "t",
|
|
Action = "ScriptTrace.AttributeWrite",
|
|
Outcome = AuditOutcome.Success,
|
|
Target = i.ToString(),
|
|
};
|
|
|
|
[Fact]
|
|
public void Add_BeyondCap_KeepsFirstN_AndFlagsTruncated()
|
|
{
|
|
var c = new FlowTraceCollector(maxEvents: 500);
|
|
Assert.False(c.Truncated);
|
|
|
|
for (var i = 0; i < 501; i++) c.Add(Evt(i));
|
|
|
|
var snap = c.Snapshot();
|
|
Assert.Equal(500, snap.Count);
|
|
Assert.True(c.Truncated);
|
|
Assert.Equal("0", snap[0].Target);
|
|
Assert.Equal("499", snap[499].Target);
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshot_IsACopy_InEmissionOrder()
|
|
{
|
|
var c = new FlowTraceCollector();
|
|
c.Add(Evt(1));
|
|
var first = c.Snapshot();
|
|
c.Add(Evt(2));
|
|
|
|
Assert.Single(first);
|
|
Assert.Equal(2, c.Count);
|
|
Assert.Equal(new[] { "1", "2" }, c.Snapshot().Select(e => e.Target));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentAdds_NeverThrow_AndNeverExceedCap()
|
|
{
|
|
var c = new FlowTraceCollector(maxEvents: 100);
|
|
var tasks = Enumerable.Range(0, 8).Select(t => Task.Run(() =>
|
|
{
|
|
for (var i = 0; i < 50; i++) c.Add(Evt(t * 100 + i));
|
|
}));
|
|
await Task.WhenAll(tasks);
|
|
|
|
Assert.Equal(100, c.Snapshot().Count);
|
|
Assert.True(c.Truncated);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultCap_Is500_AndNonPositiveCapIsRejected()
|
|
{
|
|
Assert.Equal(500, FlowTraceCollector.DefaultMaxEvents);
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new FlowTraceCollector(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Complete_ClosesTheCollector_LaterAddsAreIgnored_AndNeverTruncate()
|
|
{
|
|
var c = new FlowTraceCollector(maxEvents: 2);
|
|
var first = NewEvent();
|
|
c.Add(first);
|
|
Assert.False(c.Completed);
|
|
|
|
var snapshot = c.Complete();
|
|
|
|
Assert.True(c.Completed);
|
|
Assert.Same(first, Assert.Single(snapshot));
|
|
c.Add(NewEvent());
|
|
c.Add(NewEvent());
|
|
c.Add(NewEvent());
|
|
Assert.Equal(1, c.Count);
|
|
Assert.False(c.Truncated);
|
|
Assert.Equal(snapshot, c.Complete()); // idempotent
|
|
}
|
|
|
|
private static AuditEvent NewEvent() => new()
|
|
{
|
|
EventId = Guid.NewGuid(),
|
|
OccurredAtUtc = DateTimeOffset.UtcNow,
|
|
Actor = "t",
|
|
Action = "ScriptTrace.ScriptRun",
|
|
Outcome = AuditOutcome.Success,
|
|
DetailsJson = "{}",
|
|
};
|
|
}
|