Resolve all 46 low-severity architecture review findings
This commit is contained in:
+6
-4
@@ -127,18 +127,20 @@ public class AlarmCascadeParentExecutionTests : TestKit, IDisposable
|
||||
// Hop 3 — Alarm Actor → AlarmExecutionActor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ScriptWrittenValue_FiringAlarm_ParentsOnTriggerRunToTheWritingExecution()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ScriptWrittenValue_FiringAlarm_ParentsOnlyObservedWrites(bool inferred)
|
||||
{
|
||||
var writerExecutionId = Guid.NewGuid();
|
||||
var alarm = BuildAlarm(ValueMatchAlarm(), out var instanceProbe);
|
||||
|
||||
alarm.Tell(new AttributeValueChanged(
|
||||
"Pump1", "Status", "Status", "Critical", "Good", DateTimeOffset.UtcNow,
|
||||
SourceExecutionId: writerExecutionId));
|
||||
SourceExecutionId: writerExecutionId, SourceExecutionInferred: inferred));
|
||||
|
||||
instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(writerExecutionId, alarm.UnderlyingActor.LastOnTriggerParentExecutionId);
|
||||
Assert.Equal(inferred ? (Guid?)null : writerExecutionId, alarm.UnderlyingActor.LastOnTriggerParentExecutionId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -119,6 +119,7 @@ public class InstanceActorTraceArmTests : TestKit, IDisposable
|
||||
|
||||
Assert.Equal(writer, published.SourceExecutionId);
|
||||
Assert.True(published.Trace);
|
||||
Assert.True(published.SourceExecutionInferred);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -147,14 +147,16 @@ public class ScriptActorTraceTests : TestKit, IDisposable
|
||||
Assert.Empty(_writer.Events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValueChangeTrigger_TracedChange_RunParentsToSourceExecutionId()
|
||||
[Theory]
|
||||
[InlineData(false, "value-change")]
|
||||
[InlineData(true, "inferred-device-update")]
|
||||
public void ValueChangeTrigger_TracedChange_RunParentsToSourceExecutionId(bool inferred, string triggerSource)
|
||||
{
|
||||
var actor = CreateActor("OnStatus", "1", "{\"attribute\":\"Status\"}");
|
||||
var writer = Guid.NewGuid();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
actor.Tell(new AttributeValueChanged("Pump1", "Status", "Status", "Running", "Good", now, SourceExecutionId: writer, Trace: true));
|
||||
actor.Tell(new AttributeValueChanged("Pump1", "Status", "Status", "Running", "Good", now, SourceExecutionId: writer, Trace: true, SourceExecutionInferred: inferred));
|
||||
|
||||
var evt = _writer.WaitForKind("ScriptTrace.ScriptRun", TimeSpan.FromSeconds(10));
|
||||
Assert.NotNull(evt);
|
||||
@@ -165,7 +167,7 @@ public class ScriptActorTraceTests : TestKit, IDisposable
|
||||
Assert.Equal("valuechange", extra.TriggerKind);
|
||||
Assert.Equal("Status", extra.TriggerAttribute);
|
||||
Assert.Equal("Running", extra.TriggerValue);
|
||||
Assert.Equal("value-change", extra.TriggerSource);
|
||||
Assert.Equal(triggerSource, extra.TriggerSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+14
@@ -10,6 +10,20 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
/// </summary>
|
||||
public class ScriptExecutionSchedulerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Scheduler_DoesNotInlineOnAnotherSchedulersWorker()
|
||||
{
|
||||
using var first = new ScriptExecutionScheduler(1);
|
||||
using var second = new ScriptExecutionScheduler(1);
|
||||
await Task.Factory.StartNew(() =>
|
||||
{
|
||||
var firstThread = Environment.CurrentManagedThreadId;
|
||||
var nested = new Task<int>(() => Environment.CurrentManagedThreadId);
|
||||
nested.RunSynchronously(second);
|
||||
Assert.NotEqual(firstThread, nested.Result);
|
||||
}, CancellationToken.None, TaskCreationOptions.None, first);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scheduler_RunsWork_OffTheThreadPool()
|
||||
{
|
||||
|
||||
+22
-26
@@ -615,32 +615,31 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RuntimeWarning_InsideATracedWrite_IsAttributedToThatWriteStep()
|
||||
public async Task TraceWriteFailure_DoesNotCreateDiagnosticTraceRows()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var collector = new FlowTraceCollector();
|
||||
// The throwing writer makes EmitTrace log its best-effort "write failed" warning while the
|
||||
// AttributeWrite step scope is open — exactly the runtime line D6 exists to attribute.
|
||||
//
|
||||
// It also PINS THE ANTI-RECURSION PROPERTY: EmitLog's own write fails against this writer
|
||||
// too, so its "log-line write failed" warning must go to the UNDECORATED logger. Routed
|
||||
// through the capturing decorator instead, each warning would emit another row whose write
|
||||
// fails again — an unbounded loop, and this test would hang or blow the row count rather
|
||||
// than see exactly one ScriptLog row.
|
||||
var ctx = CreateContext(probe.Ref, new ThrowingAuditWriter(), traced: true, collector);
|
||||
|
||||
var call = ctx.SetAttribute("Setpoint", "42");
|
||||
var cmd = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
||||
probe.Reply(new SetStaticAttributeResponse(cmd.CorrelationId, InstanceName, "Setpoint", true, null, DateTimeOffset.UtcNow));
|
||||
await call;
|
||||
Assert.Single(collector.Snapshot());
|
||||
Assert.Equal(AuditKind.AttributeWrite, Row(collector.Snapshot().Single()).Kind);
|
||||
}
|
||||
|
||||
var rows = collector.Snapshot();
|
||||
var write = rows.Single(e => Row(e).Kind == AuditKind.AttributeWrite);
|
||||
var log = rows.Single(e => Row(e).Kind == AuditKind.ScriptLog);
|
||||
var extra = ScriptTraceExtra.TryParse(Row(log).Extra)!;
|
||||
Assert.Equal("runtime", extra.LogSource);
|
||||
Assert.Equal("warn", extra.LogLevel);
|
||||
Assert.Equal(write.EventId, extra.StepEventId);
|
||||
[Fact]
|
||||
public void Helpers_AreCachedPerContext()
|
||||
{
|
||||
var ctx = CreateContext(CreateTestProbe().Ref, null, traced: false, collector: null);
|
||||
Assert.Same(ctx.Scripts, ctx.Scripts);
|
||||
Assert.Same(ctx.ExternalSystem, ctx.ExternalSystem);
|
||||
Assert.Same(ctx.Database, ctx.Database);
|
||||
Assert.Same(ctx.Notify, ctx.Notify);
|
||||
Assert.Same(ctx.Alarms, ctx.Alarms);
|
||||
Assert.Same(ctx.Tracking, ctx.Tracking);
|
||||
var child = ctx.CreateChildContextForSharedScript(1);
|
||||
Assert.NotSame(ctx.Scripts, child.Scripts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -784,12 +783,11 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review item 4: step scopes NEST — the batch leg's lines name the batch row, the wait leg's
|
||||
/// name the wait row, and both unwind so a later line falls back to the run anchor. The
|
||||
/// throwing writer is again what provokes a runtime line inside each leg.
|
||||
/// The batch and wait scopes unwind even when trace writes fail; their internal
|
||||
/// diagnostics stay out of the collector, and the later script line belongs to the run.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task NestedStepScopes_AttributeEachLegSeparately_AndUnwind()
|
||||
public async Task NestedStepScopes_IgnoreTraceDiagnostics_AndUnwind()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var collector = new FlowTraceCollector();
|
||||
@@ -816,10 +814,8 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
var logs = rows.Where(e => Row(e).Kind == AuditKind.ScriptLog)
|
||||
.Select(e => ScriptTraceExtra.TryParse(Row(e).Extra)!)
|
||||
.ToList();
|
||||
Assert.Equal(3, logs.Count);
|
||||
Assert.Equal(write.EventId, logs[0].StepEventId);
|
||||
Assert.Equal(wait.EventId, logs[1].StepEventId);
|
||||
Assert.Null(logs[2].StepEventId);
|
||||
Assert.Equal(["runtime", "runtime", "script"], logs.Select(l => l.LogSource));
|
||||
Assert.Single(logs);
|
||||
Assert.Null(logs[0].StepEventId);
|
||||
Assert.Equal("script", logs[0].LogSource);
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
|
||||
public class TriggerEvalGateLifecycleTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SharedGate_IsProcessOwnedAndRemainsUsableAfterCallerDisposes()
|
||||
{
|
||||
var gate = TriggerEvalGate.Shared(new SiteRuntimeOptions());
|
||||
gate.Dispose();
|
||||
Assert.Same(gate, TriggerEvalGate.Shared(new SiteRuntimeOptions()));
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await gate.WaitAsync(deadline.Token);
|
||||
gate.Release();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnedGate_IsDisposedByCaller()
|
||||
{
|
||||
var gate = new TriggerEvalGate(1);
|
||||
gate.Dispose();
|
||||
Assert.Throws<ObjectDisposedException>(() => { _ = gate.WaitAsync(CancellationToken.None); });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user