Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScriptExecutionSchedulerTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

122 lines
5.2 KiB
C#

using ZB.MOM.WW.ScadaBridge.SiteRuntime;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
/// <summary>
/// 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.
/// </summary>
public class ScriptExecutionSchedulerTests
{
/// <summary>Verifies that the scheduler does not inline work on another scheduler's worker thread.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that the scheduler runs work off the shared thread pool.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that the scheduler respects the configured thread count.</summary>
[Fact]
public void Scheduler_RespectsConfiguredThreadCount()
{
using var scheduler = new ScriptExecutionScheduler(4);
Assert.Equal(4, scheduler.MaximumConcurrencyLevel);
}
/// <summary>Verifies that the shared scheduler returns the same instance for the same options.</summary>
[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);
}
// IsDisposed backs the Shared() recreate guard so a disposed scheduler
// can never be handed back and silently poison every later execution. Verified on a
// local instance — deliberately NOT by disposing the process-wide singleton, which
// would strand any script another test class queued on it (the very static hazard
// this test guards against).
/// <summary>Verifies that <c>IsDisposed</c> flips to true when the scheduler is disposed.</summary>
[Fact]
public void IsDisposed_FlipsOnDispose()
{
var scheduler = new ScriptExecutionScheduler(1);
Assert.False(scheduler.IsDisposed);
scheduler.Dispose();
Assert.True(scheduler.IsDisposed);
scheduler.Dispose(); // idempotent — stays disposed, does not throw
Assert.True(scheduler.IsDisposed);
}
// SiteRuntime-S2/UA5: the scheduler exposes observability gauges so a stuck /
// saturated script-execution pool is visible to operators via site health.
/// <summary>Verifies that the gauges reflect the busy thread count and the queue depth.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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
// A busy thread reports a non-null age. Not asserting a strictly-positive
// value: Environment.TickCount64 has ~15 ms granularity, so under load the
// busy-timestamp and this read can fall in the same tick (age == Zero).
Assert.NotNull(scheduler.OldestBusyAge);
gate.Set();
await Task.WhenAll(blocking, queued);
await WaitUntilAsync(() => scheduler.BusyThreadCount == 0);
Assert.Null(scheduler.OldestBusyAge);
}
private static async Task WaitUntilAsync(Func<bool> condition)
{
for (var i = 0; i < 100 && !condition(); i++)
await Task.Delay(50);
Assert.True(condition(), "condition not met within timeout");
}
}