review(Core.VirtualTags): fix Good-null upstream blocking downstream (Medium)
Re-review at 7286d320. -014 (Medium): AreInputsReady gated on value!=null, so a script
returning null (Good quality) permanently blocked change-triggered dependents at
BadWaitingForInitialData; now gates on the StatusCode Good bit only + test. -015:
TimerTriggerScheduler.Start throws on double-call. -016: fix wrong status-code comment.
This commit is contained in:
@@ -167,4 +167,37 @@ public sealed class TimerTriggerSchedulerTests
|
||||
"T", DriverDataType.Int32, """return 1;""",
|
||||
TimerInterval: TimeSpan.FromMilliseconds(50))]));
|
||||
}
|
||||
|
||||
// ----- Core.VirtualTags-015: Start must not be callable twice on the same instance -----
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a second call to Start on the same (non-disposed) scheduler
|
||||
/// throws InvalidOperationException. Without this guard, calling Start twice
|
||||
/// appends a duplicate set of timers and TickGroups, causing every timer-group
|
||||
/// to fire and evaluate twice per interval with no diagnostic.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_called_twice_throws_InvalidOperationException()
|
||||
{
|
||||
// Regression for Core.VirtualTags-015: without the _started guard, two Start
|
||||
// calls silently double the timer count and the evaluation frequency.
|
||||
var up = new FakeUpstream();
|
||||
var logger = new LoggerConfiguration().CreateLogger();
|
||||
using var engine = new VirtualTagEngine(up,
|
||||
new ScriptLoggerFactory(logger), logger);
|
||||
engine.Load([new VirtualTagDefinition(
|
||||
"T", DriverDataType.Int32, """return 1;""",
|
||||
TimerInterval: TimeSpan.FromSeconds(10))]);
|
||||
|
||||
using var sched = new TimerTriggerScheduler(engine, logger);
|
||||
sched.Start([new VirtualTagDefinition(
|
||||
"T", DriverDataType.Int32, """return 1;""",
|
||||
TimerInterval: TimeSpan.FromSeconds(10))]);
|
||||
|
||||
// Second call on the same live instance must throw — not silently create duplicates.
|
||||
Should.Throw<InvalidOperationException>(() =>
|
||||
sched.Start([new VirtualTagDefinition(
|
||||
"T", DriverDataType.Int32, """return 1;""",
|
||||
TimerInterval: TimeSpan.FromSeconds(10))]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,6 +664,54 @@ public sealed class VirtualTagEngineTests
|
||||
fired.ShouldBeFalse("no evaluation has happened, observer must not fire");
|
||||
}
|
||||
|
||||
// ----- Core.VirtualTags-014: AreInputsReady must not block on Good-quality null value -----
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a Good-quality upstream snapshot with a null Value (e.g. a
|
||||
/// script returning null) does not permanently block downstream change-triggered
|
||||
/// dependents at BadWaitingForInitialData. A Good-quality null means "the
|
||||
/// upstream is online and returned null" — readiness is determined by StatusCode,
|
||||
/// not by the value's nullability.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AreInputsReady_Good_quality_null_upstream_does_not_block_downstream()
|
||||
{
|
||||
// Regression for Core.VirtualTags-014: before the fix, AreInputsReady checked
|
||||
// kv.Value.Value is null, so a Good-quality DataValueSnapshot(null, 0u, ...) —
|
||||
// produced by an upstream script returning null — kept every downstream stuck at
|
||||
// BadWaitingForInitialData forever. The fix gates only on StatusCode (bit 31).
|
||||
var up = new FakeUpstream();
|
||||
up.Set("In", 1);
|
||||
using var engine = Build(up);
|
||||
|
||||
engine.Load([
|
||||
// Producer script returns null — stores DataValueSnapshot(null, Good, ...).
|
||||
new VirtualTagDefinition("NullProducer", DriverDataType.String,
|
||||
"""return null;"""),
|
||||
// Consumer depends on NullProducer and is change-triggered.
|
||||
// With the bug: Consumer is stuck at BadWaitingForInitialData.
|
||||
// After the fix: Consumer evaluates and surfaces the NRE as BadInternalError
|
||||
// (since it casts null unconditionally), which is the correct sentinel for
|
||||
// "the script ran but produced an error" rather than "inputs not ready".
|
||||
new VirtualTagDefinition("Consumer", DriverDataType.String,
|
||||
"""return (string)ctx.GetTag("NullProducer").Value + "_suffix";""",
|
||||
ChangeTriggered: true),
|
||||
]);
|
||||
|
||||
await engine.EvaluateAllAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// NullProducer should be Good with null value (script returned null).
|
||||
var producerResult = engine.Read("NullProducer");
|
||||
producerResult.StatusCode.ShouldBe(0u, "Good quality: script returned null cleanly");
|
||||
producerResult.Value.ShouldBeNull();
|
||||
|
||||
// Consumer must NOT be stuck at BadWaitingForInitialData — it should have
|
||||
// evaluated (and produced BadInternalError from the cast of null + "_suffix").
|
||||
var consumerResult = engine.Read("Consumer");
|
||||
consumerResult.StatusCode.ShouldNotBe(0x80320000u,
|
||||
"Consumer must not be stuck at BadWaitingForInitialData when its upstream is Good-quality");
|
||||
}
|
||||
|
||||
private static async Task WaitForConditionAsync(Func<bool> cond, int timeoutMs = 2000)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
||||
|
||||
Reference in New Issue
Block a user