Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -60,6 +60,166 @@ public class FlowDebugRunnerTests
|
||||
|
||||
private static JsonElement Json(string s) => JsonDocument.Parse(s).RootElement.Clone();
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-3600)]
|
||||
public async Task RunValueChange_FindsLateCompletionBehindLogCursorDespiteSiteClockSkew(int skewSeconds)
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
var child = Guid.NewGuid();
|
||||
var pollStarted = _clock;
|
||||
var siteStarted = DateTime.UtcNow.AddSeconds(skewSeconds);
|
||||
Guid root = default;
|
||||
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
root = ci.Arg<RouteToSetAttributesRequest>().ParentExecutionId!.Value;
|
||||
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
|
||||
});
|
||||
var logId = Guid.NewGuid();
|
||||
var runId = Guid.NewGuid();
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
var rows = new List<AuditEvent>
|
||||
{
|
||||
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog,
|
||||
AuditStatus.Delivered, eventId: logId, executionId: child, parentExecutionId: root,
|
||||
occurredAtUtc: siteStarted.AddSeconds(1)),
|
||||
};
|
||||
// Runtime stamps its closing ScriptRun row with the START time, after live log rows were already polled.
|
||||
if (_clock - pollStarted >= TimeSpan.FromSeconds(2))
|
||||
rows.Add(ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun,
|
||||
AuditStatus.Delivered, eventId: runId, executionId: child, parentExecutionId: root,
|
||||
occurredAtUtc: siteStarted));
|
||||
var since = ci.Arg<DateTime>();
|
||||
var after = ci.ArgAt<string?>(5);
|
||||
var ids = ci.Arg<IReadOnlyCollection<Guid>>();
|
||||
return (IReadOnlyList<AuditEvent>)rows.Where(e => (ids.Contains(root) || ids.Contains(child))
|
||||
&& (e.OccurredAtUtc.UtcDateTime > since || e.OccurredAtUtc.UtcDateTime == since
|
||||
&& (after is null || string.CompareOrdinal(e.EventId.ToString("D"), after) > 0)))
|
||||
.OrderBy(e => e.OccurredAtUtc).ThenBy(e => e.EventId.ToString("D"), StringComparer.Ordinal).ToArray();
|
||||
});
|
||||
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 5), Deployer, CancellationToken.None);
|
||||
Assert.True(result.Completed);
|
||||
Assert.Contains(result.Events, e => e.EventId == runId);
|
||||
Assert.Contains(result.Events, e => e.EventId == logId);
|
||||
Assert.Equal(3, result.Events.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunValueChange_PagesTiesAndQueriesOlderDescendantsBeyond64Ids()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
var children = Enumerable.Range(0, 70).Select(_ => Guid.NewGuid()).ToArray();
|
||||
var grandchildren = children.ToDictionary(id => id, _ => Guid.NewGuid());
|
||||
var rows = new List<AuditEvent>();
|
||||
Guid root = default;
|
||||
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
root = ci.Arg<RouteToSetAttributesRequest>().ParentExecutionId!.Value;
|
||||
var t = DateTime.UtcNow;
|
||||
rows.AddRange(Enumerable.Range(1, 2001).Select(n => ScadaBridgeAuditEventFactory.Create(
|
||||
AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered,
|
||||
eventId: Guid.Parse($"00000000-0000-0000-0000-{n:D12}"), occurredAtUtc: t, executionId: root)));
|
||||
rows.AddRange(children.Select((id, n) => ScadaBridgeAuditEventFactory.Create(
|
||||
AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
eventId: Guid.Parse($"ffffffff-0000-0000-0000-{n:D12}"), occurredAtUtc: t,
|
||||
executionId: id, parentExecutionId: root)));
|
||||
// These are older than the root's page cursor and only visible once child IDs are queried.
|
||||
rows.AddRange(children.Select(id => ScadaBridgeAuditEventFactory.Create(
|
||||
AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered,
|
||||
occurredAtUtc: t.AddSeconds(-1), executionId: grandchildren[id], parentExecutionId: id)));
|
||||
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
|
||||
});
|
||||
var queried = new HashSet<Guid>();
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
var ids = ci.Arg<IReadOnlyCollection<Guid>>();
|
||||
Assert.InRange(ids.Count, 1, 64);
|
||||
queried.UnionWith(ids);
|
||||
var since = ci.Arg<DateTime>();
|
||||
var after = ci.ArgAt<string?>(5);
|
||||
return (IReadOnlyList<AuditEvent>)rows.Where(e =>
|
||||
{
|
||||
var d = AuditDetailsCodec.Deserialize(e.DetailsJson);
|
||||
return (ids.Contains(d.ExecutionId ?? Guid.Empty) || ids.Contains(d.ParentExecutionId ?? Guid.Empty))
|
||||
&& (e.OccurredAtUtc.UtcDateTime > since || e.OccurredAtUtc.UtcDateTime == since
|
||||
&& (after is null || string.CompareOrdinal(e.EventId.ToString("D"), after) > 0));
|
||||
}).OrderBy(e => e.OccurredAtUtc).ThenBy(e => e.EventId.ToString("D"), StringComparer.Ordinal).Take(ci.Arg<int>()).ToArray();
|
||||
});
|
||||
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 4), Deployer, CancellationToken.None);
|
||||
Assert.True(result.Completed);
|
||||
Assert.True(result.LogsTruncated);
|
||||
Assert.All(children, id => Assert.Contains(id, queried));
|
||||
Assert.All(grandchildren.Values, id => Assert.Contains(id, queried));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replay_UsesChainAnchorEvenWhenChildClockPrecedesRoot()
|
||||
{
|
||||
var repo = Substitute.For<IAuditLogRepository>();
|
||||
var root = Guid.NewGuid();
|
||||
var child = Guid.NewGuid();
|
||||
var childRow = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun,
|
||||
AuditStatus.Delivered, executionId: child, parentExecutionId: root);
|
||||
repo.GetTraceEventsAsync(child, Arg.Any<CancellationToken>()).Returns(new TraceEventBatch(root, [childRow], false));
|
||||
_services.AddSingleton(repo);
|
||||
var result = await Runner().ReplayAsync(_services.BuildServiceProvider(), new FlowDebugReplayCommand(child), CancellationToken.None, Deployer);
|
||||
Assert.Equal(root, result.RootExecutionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunValueChange_TotalDeadlineIncludesInitialWrite()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
_services.Configure<ManagementServiceOptions>(o => o.LongRunningCommandTimeout = TimeSpan.FromSeconds(6));
|
||||
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
|
||||
.Returns(async ci =>
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, ci.Arg<CancellationToken>());
|
||||
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
|
||||
});
|
||||
var task = Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 300), Deployer, CancellationToken.None);
|
||||
var ex = await Assert.ThrowsAsync<ManagementCommandException>(() => task.WaitAsync(TimeSpan.FromSeconds(3)));
|
||||
Assert.Equal(FlowDebugRunner.TimeoutCode, ex.Code);
|
||||
Assert.Empty(puller.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunValueChange_TotalDeadlineIncludesAuditWrite()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
_services.Configure<ManagementServiceOptions>(o => o.LongRunningCommandTimeout = TimeSpan.FromSeconds(6));
|
||||
var writer = Substitute.For<ICentralAuditWriter>();
|
||||
writer.WriteAsync(Arg.Any<AuditEvent>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => Task.Delay(Timeout.InfiniteTimeSpan, ci.Arg<CancellationToken>()));
|
||||
_services.AddSingleton(writer);
|
||||
var ex = await Assert.ThrowsAsync<ManagementCommandException>(() => Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 300), Deployer, CancellationToken.None)
|
||||
.WaitAsync(TimeSpan.FromSeconds(3)));
|
||||
Assert.Equal(FlowDebugRunner.TimeoutCode, ex.Code);
|
||||
Assert.Empty(puller.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunValueChange_CapsReturnedEventsAndReportsTruncation()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
var events = Enumerable.Range(0, 700).Select(_ => ScadaBridgeAuditEventFactory.Create(
|
||||
AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered)).ToArray();
|
||||
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(events);
|
||||
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 1), Deployer, CancellationToken.None);
|
||||
Assert.True(result.Truncated);
|
||||
Assert.InRange(result.Events.Count, 1, FlowTraceCollector.DefaultMaxEvents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunMethod_OnStandby_RefusesWithStandbyCode_BeforeTouchingTheMethod()
|
||||
{
|
||||
@@ -207,7 +367,7 @@ public class FlowDebugRunnerTests
|
||||
_router.RouteToSetAttributesAsync("site-a", Arg.Do<RouteToSetAttributesRequest>(r => write = r), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => new RouteToSetAttributesResponse(((RouteToSetAttributesRequest)ci[1]).CorrelationId, true, null, DateTimeOffset.UtcNow));
|
||||
var calls = 0;
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
calls++;
|
||||
@@ -240,7 +400,7 @@ public class FlowDebugRunnerTests
|
||||
public async Task RunValueChange_NoSpawnWithinWindow_ReportsNoTriggerCode()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
puller.PullAsync(default!, default!, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
|
||||
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
|
||||
|
||||
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 3), Deployer, CancellationToken.None);
|
||||
@@ -260,7 +420,7 @@ public class FlowDebugRunnerTests
|
||||
// the site's own ceiling (SiteStreamGrpcServer.MaxTraceEventsPerPull) instead.
|
||||
var (_, _, puller) = WireValueChange();
|
||||
var maxima = new List<int>();
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Do<int>(maxima.Add), Arg.Any<CancellationToken>())
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Do<int>(maxima.Add), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(Array.Empty<AuditEvent>());
|
||||
|
||||
await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
@@ -476,7 +636,7 @@ public class FlowDebugRunnerTests
|
||||
public async Task RunValueChange_SynthesizedRootRow_IsRedacted_WhenARedactorIsRegistered()
|
||||
{
|
||||
var (_, _, puller) = WireValueChange();
|
||||
puller.PullAsync(default!, default!, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
|
||||
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
|
||||
_services.AddSingleton<IAuditRedactor>(new UpperRedactor());
|
||||
|
||||
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
|
||||
@@ -587,7 +747,7 @@ public class FlowDebugRunnerTests
|
||||
// poll, then the SAME row (fixed event id) from the second on — a fresh id per poll would
|
||||
// re-arm the settle window forever and the loop would only ever end on the deadline.
|
||||
var calls = 0;
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
calls++;
|
||||
@@ -662,7 +822,7 @@ public class FlowDebugRunnerTests
|
||||
// The write may fire several scripts. A sibling's run is still COLLECTED (the graph draws
|
||||
// it) but must not report the script the operator chose as having run.
|
||||
var (_, _, puller, _) = WireScript(WriteScript("OnLevel", "ValueChange", "Level"));
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
|
||||
.Returns(ci => (IReadOnlyList<AuditEvent>)[ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
|
||||
eventId: Guid.Parse("cccccccc-0000-0000-0000-000000000001"),
|
||||
executionId: Guid.NewGuid(), parentExecutionId: ((IReadOnlyCollection<Guid>)ci[1]).First(), target: "OtherScript")]);
|
||||
|
||||
Reference in New Issue
Block a user