513fa1aa16
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.
149 lines
7.8 KiB
C#
149 lines
7.8 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// Deferred-work register row 62 — hardening pin, not a bug fix.
|
|
///
|
|
/// <para>
|
|
/// Six SiteRuntime call sites reach <c>ISiteEventLogger</c> straight off the injected
|
|
/// <see cref="IServiceProvider"/>, with no scope in between:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description><c>SiteRuntime/Actors/AlarmActor.cs</c> — resolves once in the
|
|
/// constructor and CACHES it in <c>_siteEventLogger</c>, so it holds the instance for
|
|
/// the actor's whole lifetime.</description></item>
|
|
/// <item><description><c>SiteRuntime/Actors/ScriptActor.cs</c> (two sites) — per-call
|
|
/// resolve.</description></item>
|
|
/// <item><description><c>SiteRuntime/Actors/InstanceActor.cs</c> (two sites) — per-call
|
|
/// resolve.</description></item>
|
|
/// <item><description><c>SiteRuntime/Actors/NativeAlarmActor.cs</c> — per-call
|
|
/// resolve.</description></item>
|
|
/// <item><description><c>SiteRuntime/Actors/DeploymentManagerActor.cs</c> — per-call
|
|
/// resolve.</description></item>
|
|
/// <item><description><c>SiteRuntime/Scripts/ScriptRunLauncher.cs</c> — per-run resolve.
|
|
/// Not among the five the register row named: it is the run body that was lifted verbatim
|
|
/// out of the deleted ScriptExecutionActor/AlarmExecutionActor, so it inherited the
|
|
/// pattern rather than adding it.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>
|
|
/// Every one of those is CORRECT TODAY, and this test is the reason it stays that way.
|
|
/// They are correct for exactly one reason: <c>ISiteEventLogger</c> is registered
|
|
/// <see cref="ServiceLifetime.Singleton"/>, so resolving it from the root provider yields
|
|
/// the same instance a scope would. The day that registration changes lifetime, every one
|
|
/// of those call sites silently becomes register row 45 — a captive dependency /
|
|
/// resolved-outside-its-scope bug — and nothing else in the tree would notice:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description><c>ValidateScopes</c> catches a SCOPED service resolved from the
|
|
/// root provider — but the actors hold the root provider legitimately, and .NET's
|
|
/// check fires at the offending <c>GetService</c> call, i.e. at runtime on a site
|
|
/// node, on whatever code path happens to log an event first. Not at boot, and not
|
|
/// in CI.</description></item>
|
|
/// <item><description><c>ValidateOnBuild</c> walks CONSTRUCTOR graphs. These are
|
|
/// imperative <c>GetService</c> calls inside method bodies, invisible to it.</description></item>
|
|
/// <item><description><c>CaptiveDependencyGuard</c> catches the AlarmActor field case
|
|
/// (a cached resolve) by design, but cannot see the five per-call resolves at
|
|
/// all.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>
|
|
/// So the lifetime is a load-bearing invariant with no other enforcement. Pinning it here
|
|
/// means a future lifetime change fails loudly, in CI, with a message naming the call
|
|
/// sites that have to be fixed alongside it — rather than shipping and being found on the
|
|
/// rig months later, which is how row 45 was found.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// This deliberately asserts the REGISTRATION, not the call sites: the SiteRuntime actor
|
|
/// files are correct as written and need no change. The pin is a tripwire on the
|
|
/// assumption they rest on.
|
|
/// </para>
|
|
/// </summary>
|
|
public class SiteEventLoggerLifetimePinTests
|
|
{
|
|
/// <summary>
|
|
/// The failure message is the whole point of the test — it has to carry the reader
|
|
/// from "a lifetime assertion failed" to "here is what breaks and why".
|
|
/// </summary>
|
|
private const string Why =
|
|
"ISiteEventLogger MUST be registered Singleton in the site service graph " +
|
|
"(deferred-work register row 62).\n" +
|
|
"Six SiteRuntime call sites resolve it straight off the root IServiceProvider with " +
|
|
"no scope: AlarmActor (cached in a field for the actor's lifetime), ScriptActor (x2), " +
|
|
"InstanceActor (x2), NativeAlarmActor, DeploymentManagerActor, and " +
|
|
"Scripts/ScriptRunLauncher.\n" +
|
|
"Under any non-singleton lifetime every one of those becomes a captive-dependency / " +
|
|
"resolved-outside-its-scope bug (register row 45), and NOTHING else catches it: " +
|
|
"ValidateOnBuild only walks constructor graphs, ValidateScopes fires at runtime on a " +
|
|
"site node rather than at boot, and CaptiveDependencyGuard sees only the AlarmActor " +
|
|
"field case.\n" +
|
|
"If this lifetime change is intended, fix those six call sites to resolve through an " +
|
|
"explicit scope in the same change, then update this pin.";
|
|
|
|
/// <summary>
|
|
/// Builds the real Site service graph the way <c>Program.cs</c>'s Site branch does and
|
|
/// asserts the lifetime of the registration the SiteRuntime root-resolves depend on.
|
|
/// Registration only — the provider is never built, so this costs nothing and touches
|
|
/// no SQLite file.
|
|
/// </summary>
|
|
/// <returns>The configured site service collection.</returns>
|
|
private static IServiceCollection BuildSiteServiceGraph()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaBridge:Node:Role"] = "Site",
|
|
["ScadaBridge:Node:NodeName"] = "node-a",
|
|
["ScadaBridge:Node:NodeHostname"] = "test-site",
|
|
["ScadaBridge:Node:SiteId"] = "TestSite",
|
|
["ScadaBridge:Node:RemotingPort"] = "0",
|
|
["ScadaBridge:Node:GrpcPort"] = "0",
|
|
["LocalDb:Path"] = "/tmp/sb-row62-pin-never-opened.db",
|
|
["AuditLog:SiteWriter:DatabasePath"] = "/tmp/sb-row62-pin-never-opened-audit.db",
|
|
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
|
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552",
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
SiteServiceRegistration.Configure(services, configuration);
|
|
return services;
|
|
}
|
|
|
|
/// <summary>Verifies that <c>ISiteEventLogger</c> is registered as a singleton in the site service graph.</summary>
|
|
[Fact]
|
|
public void ISiteEventLogger_IsRegisteredSingleton_InTheSiteServiceGraph()
|
|
{
|
|
var services = BuildSiteServiceGraph();
|
|
|
|
var descriptor = Assert.Single(services, d => d.ServiceType == typeof(ISiteEventLogger));
|
|
|
|
Assert.True(descriptor.Lifetime == ServiceLifetime.Singleton,
|
|
$"{Why}\nActual lifetime: {descriptor.Lifetime}.");
|
|
}
|
|
|
|
/// <summary>Verifies that the concrete type the interface forwards to is also registered as a singleton.</summary>
|
|
[Fact]
|
|
public void SiteEventLogger_TheConcreteTypeTheInterfaceForwardsTo_IsAlsoSingleton()
|
|
{
|
|
// The interface registration is a FACTORY —
|
|
// AddSingleton<ISiteEventLogger>(sp => sp.GetRequiredService<SiteEventLogger>())
|
|
// — so a Singleton lifetime on ISiteEventLogger alone does not prove callers get a
|
|
// shared instance. Downgrading the concrete SiteEventLogger to Scoped would leave
|
|
// the assertion above passing while making the singleton factory resolve a scoped
|
|
// service from the root: the exact bug, with the pin still green. Both halves have
|
|
// to be pinned for the invariant to actually hold.
|
|
var services = BuildSiteServiceGraph();
|
|
|
|
var descriptor = Assert.Single(services, d => d.ServiceType == typeof(SiteEventLogger));
|
|
|
|
Assert.True(descriptor.Lifetime == ServiceLifetime.Singleton,
|
|
$"{Why}\nISiteEventLogger forwards to the concrete SiteEventLogger, whose actual " +
|
|
$"lifetime is: {descriptor.Lifetime}.");
|
|
}
|
|
}
|