using ZB.MOM.WW.ScadaBridge.SiteRuntime; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts; /// /// SiteRuntime-009: the dedicated script-execution scheduler must run script bodies on /// its own dedicated threads, not on the shared .NET thread pool, so blocking script /// I/O cannot starve the global pool. /// public class ScriptExecutionSchedulerTests { [Fact] public async Task Scheduler_RunsWork_OffTheThreadPool() { using var scheduler = new ScriptExecutionScheduler(2); bool wasThreadPoolThread = true; string? threadName = null; await Task.Factory.StartNew(() => { wasThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; threadName = Thread.CurrentThread.Name; }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, scheduler); Assert.False(wasThreadPoolThread, "Script work must not run on a shared thread-pool thread."); Assert.StartsWith("script-execution-", threadName); } [Fact] public void Scheduler_RespectsConfiguredThreadCount() { using var scheduler = new ScriptExecutionScheduler(4); Assert.Equal(4, scheduler.MaximumConcurrencyLevel); } [Fact] public void Scheduler_Shared_ReturnsSameInstanceForOptions() { var options = new SiteRuntimeOptions { ScriptExecutionThreadCount = 3 }; var a = ScriptExecutionScheduler.Shared(options); var b = ScriptExecutionScheduler.Shared(options); Assert.Same(a, b); } // SiteRuntime-S2/UA5: the scheduler exposes observability gauges so a stuck / // saturated script-execution pool is visible to operators via site health. [Fact] public async Task Gauges_ReflectBusyThreadAndQueueDepth() { using var scheduler = new ScriptExecutionScheduler(1); using var gate = new ManualResetEventSlim(false); var blocking = Task.Factory.StartNew(() => gate.Wait(), CancellationToken.None, TaskCreationOptions.None, scheduler); var queued = Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.None, scheduler); await WaitUntilAsync(() => scheduler.BusyThreadCount == 1); Assert.Equal(1, scheduler.QueueDepth); // second task waiting Assert.True(scheduler.OldestBusyAge > TimeSpan.Zero); gate.Set(); await Task.WhenAll(blocking, queued); await WaitUntilAsync(() => scheduler.BusyThreadCount == 0); Assert.Null(scheduler.OldestBusyAge); } private static async Task WaitUntilAsync(Func condition) { for (var i = 0; i < 100 && !condition(); i++) await Task.Delay(50); Assert.True(condition(), "condition not met within timeout"); } }